11#!/usr/bin/env node
22
3+ import { closeSync , existsSync , openSync , readSync , readdirSync , statSync } from 'node:fs' ;
4+ import { join } from 'node:path' ;
5+
36const baseUrl = ( process . env . BASE_URL || process . env . WINDSURFAPI_BASE_URL || 'http://127.0.0.1:3003' ) . replace ( / \/ + $ / , '' ) ;
47const apiKey = process . env . API_KEY || process . env . WINDSURFAPI_API_KEY || '' ;
58const model = process . env . MODEL || process . env . WINDSURFAPI_SMOKE_MODEL || 'claude-sonnet-4.6' ;
@@ -15,6 +18,10 @@ const requireNativeBridgeTool = process.env.NATIVE_BRIDGE_SMOKE_REQUIRE_NATIVE !
1518const validateToolArgs = process . env . NATIVE_BRIDGE_SMOKE_VALIDATE_ARGS !== '0' ;
1619const enforceLsBudget = process . env . NATIVE_BRIDGE_SMOKE_LS_BUDGET !== '0' ;
1720const requireNativeBridgeEnabled = process . env . NATIVE_BRIDGE_SMOKE_REQUIRE_BRIDGE_ENABLED !== '0' ;
21+ const includeProtoTraceSummary = process . env . NATIVE_BRIDGE_SMOKE_PROTO_TRACE_SUMMARY !== '0' ;
22+ const protoTraceDir = process . env . NATIVE_BRIDGE_SMOKE_PROTO_TRACE_DIR
23+ || process . env . WINDSURFAPI_PROTO_TRACE_DIR
24+ || '/data/proto-trace' ;
1825async function sha256Hex ( text ) {
1926 const bytes = new TextEncoder ( ) . encode ( String ( text || '' ) ) ;
2027 const digest = await crypto . subtle . digest ( 'SHA-256' , bytes ) ;
@@ -617,6 +624,84 @@ function nativeBridgeDecisionDelta(before, after) {
617624 } ;
618625}
619626
627+ function readTailText ( file , maxBytes = 2 * 1024 * 1024 ) {
628+ const stat = statSync ( file ) ;
629+ const size = stat . size ;
630+ const start = Math . max ( 0 , size - maxBytes ) ;
631+ const length = size - start ;
632+ const fd = openSync ( file , 'r' ) ;
633+ try {
634+ const buf = Buffer . alloc ( length ) ;
635+ readSync ( fd , buf , 0 , length , start ) ;
636+ return buf . toString ( 'utf8' ) ;
637+ } finally {
638+ closeSync ( fd ) ;
639+ }
640+ }
641+
642+ function summarizeWebFetchTraceDir ( dir = protoTraceDir ) {
643+ try {
644+ if ( ! includeProtoTraceSummary ) return null ;
645+ if ( ! dir || ! existsSync ( dir ) ) return { available : false , dir, reason : 'trace_dir_missing' } ;
646+ const files = readdirSync ( dir )
647+ . filter ( name => / G e t C a s c a d e T r a j e c t o r y S t e p s .* \. j s o n l $ / i. test ( name ) )
648+ . map ( name => {
649+ const path = join ( dir , name ) ;
650+ const stat = statSync ( path ) ;
651+ return { name, path, mtimeMs : stat . mtimeMs , size : stat . size } ;
652+ } )
653+ . sort ( ( a , b ) => b . mtimeMs - a . mtimeMs )
654+ . slice ( 0 , 6 ) ;
655+ const stateCounts = { } ;
656+ const recent = [ ] ;
657+ let records = 0 ;
658+ let parseErrors = 0 ;
659+ for ( const file of files ) {
660+ const lines = readTailText ( file . path ) . split ( '\n' ) . filter ( Boolean ) ;
661+ for ( const line of lines ) {
662+ let rec ;
663+ try {
664+ rec = JSON . parse ( line ) ;
665+ } catch {
666+ parseErrors ++ ;
667+ continue ;
668+ }
669+ records ++ ;
670+ const steps = rec ?. semantic ?. steps || [ ] ;
671+ for ( const step of steps ) {
672+ const trace = step ?. webFetchTrace ;
673+ if ( ! trace ?. state ) continue ;
674+ stateCounts [ trace . state ] = ( stateCounts [ trace . state ] || 0 ) + 1 ;
675+ recent . push ( {
676+ file : file . name ,
677+ method : rec . method || '' ,
678+ direction : rec . direction || '' ,
679+ stepIndex : step . index ,
680+ state : trace . state ,
681+ stepType : trace . stepType ,
682+ status : trace . status ,
683+ hasRequestedInteraction : ! ! trace . hasRequestedInteraction ,
684+ hasReadUrlOneof : ! ! trace . hasReadUrlOneof ,
685+ hasWebDocument : ! ! trace . hasWebDocument ,
686+ errorClassifications : trace . errorClassifications || { } ,
687+ } ) ;
688+ }
689+ }
690+ }
691+ return {
692+ available : true ,
693+ dir,
694+ files : files . map ( f => ( { name : f . name , size : f . size } ) ) ,
695+ records,
696+ parseErrors,
697+ stateCounts,
698+ recent : recent . slice ( - 12 ) ,
699+ } ;
700+ } catch ( error ) {
701+ return { available : false , dir, reason : 'trace_summary_failed' , error : String ( error ?. message || error ) } ;
702+ }
703+ }
704+
620705const selected = expandScenarios ( requestedScenarios ) ;
621706if ( ! selected . length ) {
622707 console . error ( `No valid scenarios selected. Use one or more of: ${ Object . keys ( SCENARIOS ) . join ( ',' ) } ,all` ) ;
@@ -655,6 +740,7 @@ if (!failures.length) {
655740 }
656741}
657742const healthAfter = await fetchHealthSnapshot ( 'after' ) ;
743+ const protoTraceSummary = summarizeWebFetchTraceDir ( ) ;
658744
659745console . log ( JSON . stringify ( {
660746 ok : failures . length === 0 ,
@@ -675,6 +761,7 @@ console.log(JSON.stringify({
675761 results,
676762 failures,
677763 nativeBridgeDecisionDelta : nativeBridgeDecisionDelta ( healthBefore , healthAfter ) ,
764+ protoTraceSummary,
678765 healthBefore,
679766 healthAfter,
680767} , null , 2 ) ) ;
0 commit comments