99 type BrowserAdaptiveExplorationResult ,
1010 type BrowserAdaptiveFinding ,
1111 type BrowserAdaptiveFrameIdentity ,
12+ type BrowserAdaptiveNetworkFailure ,
1213 type BrowserAdaptiveState ,
1314 type BrowserAdaptiveTransition ,
1415 type BrowserAccessibilityCollector ,
@@ -18,7 +19,7 @@ import { stableJson } from "@automattic/wp-codebox-core/internals"
1819import type { Frame , Page } from "playwright"
1920
2021import { discoverBrowserActionCorpusDescriptors } from "./browser-action-discovery.js"
21- import type { BrowserPreviewNavigationScope } from "./browser-preview-routing.js"
22+ import { browserPreviewNetworkDecision , type BrowserPreviewNavigationScope , type BrowserPreviewNetworkPolicy } from "./browser-preview-routing.js"
2223
2324interface AdaptiveObservationSources {
2425 consoleMessages : object [ ]
@@ -52,6 +53,7 @@ export async function exploreAdaptiveBrowserStateMachine({
5253 signal,
5354 now = Date . now ,
5455 navigationScope,
56+ networkPolicy,
5557 accessibilityCollector,
5658 onAccessibilityFindingEvidence,
5759} : {
@@ -62,6 +64,7 @@ export async function exploreAdaptiveBrowserStateMachine({
6264 signal ?: AbortSignal
6365 now ?: ( ) => number
6466 navigationScope ?: BrowserPreviewNavigationScope
67+ networkPolicy ?: BrowserPreviewNetworkPolicy
6568 accessibilityCollector ?: BrowserAccessibilityCollector
6669 onAccessibilityFindingEvidence ?: ( scan : Awaited < ReturnType < BrowserAccessibilityCollector [ "scan" ] > > ) => Promise < { screenshot ?: string ; domSnapshot ?: string } >
6770} ) : Promise < BrowserAdaptiveExplorationResult > {
@@ -138,9 +141,11 @@ export async function exploreAdaptiveBrowserStateMachine({
138141 const stabilized = await stabilizeAdaptiveState ( page , contract , source . path . length + 1 , now , navigationScope )
139142 appendDiagnostics ( diagnostics , stabilized . diagnostics , contract . descriptorLimits . maxDiagnostics )
140143 const scopeRejection = stabilized . diagnostics . find ( ( diagnostic ) => diagnostic . code === "browser_adaptive_redirect_scope_escape_rejected" )
141- const newConsoleErrors = consoleErrorMessages ( observations . consoleMessages . slice ( beforeConsole ) )
144+ const newConsoleRecords = consoleErrorRecords ( observations . consoleMessages . slice ( beforeConsole ) )
145+ const newConsoleErrors = newConsoleRecords . map ( recordMessage )
142146 const newPageErrors = errorMessages ( observations . errors . slice ( beforeErrors ) )
143- const fingerprints = [ ...new Set ( [ ...newConsoleErrors , ...newPageErrors ] . map ( ( message ) => browserAdaptiveDigest ( "oracle" , message ) ) ) ] . sort ( )
147+ const oracleEvidence = adaptiveOracleEvidence ( newConsoleRecords , observations . network . slice ( beforeNetwork ) , newPageErrors , contract , networkPolicy )
148+ const fingerprints = [ ...oracleEvidence . fingerprints ]
144149 const existing = states . get ( stabilized . state . digest )
145150 const newState = ! existing
146151 const transitionId = `transition-${ transitions . length } `
@@ -183,12 +188,13 @@ export async function exploreAdaptiveBrowserStateMachine({
183188 loadingBefore : source . state . loadingIndicators ,
184189 loadingAfter : stabilized . loading ,
185190 oracleFingerprints : fingerprints ,
191+ ...( oracleEvidence . networkFailures . length > 0 ? { networkFailures : oracleEvidence . networkFailures } : { } ) ,
186192 ...( accessibilityFingerprints . length > 0 ? { accessibilityFindingFingerprints : accessibilityFingerprints } : { } ) ,
187193 } ,
188194 status : actionError ? "error" : scopeRejection ? "rejected" : newState ? "ok" : "revisited" ,
189195 ...( actionError ? { diagnostic : { code : "browser_adaptive_action_error" , message : actionError } } : scopeRejection ? { diagnostic : { code : scopeRejection . code , message : scopeRejection . message } } : { } ) ,
190196 }
191- errors += newConsoleErrors . length + newPageErrors . length + ( actionError ? 1 : 0 )
197+ errors += oracleEvidence . errorCount + ( actionError ? 1 : 0 )
192198 if ( artifactBytes ( states , [ ...transitions , transition ] , diagnostics , findings ) + ( accessibilityCollector ? Buffer . byteLength ( stableJson ( accessibilityCollector . evidence ( ) ) ) : 0 ) > adaptiveJsonArtifactBudget ( contract , accessibilityCollector ) ) {
193199 if ( replayableDestination && newState ) states . delete ( stabilized . state . digest )
194200 exhausted = "maxArtifactBytes"
@@ -231,7 +237,7 @@ export async function exploreAdaptiveBrowserStateMachine({
231237
232238 if ( findings . length > 0 && ! signal ?. aborted ) {
233239 for ( const finding of findings ) {
234- const minimized = await minimizeAdaptiveFinding ( page , baseUrl , contract , finding , observations , Math . max ( 0 , contract . budgets . maxActions - actions ) , Math . max ( 0 , ( contract . accessibility ?. budgets . maxKeyboardActions ?? Number . POSITIVE_INFINITY ) - keyboardActions ) , started + contract . budgets . maxDurationMs , signal , now , navigationScope , accessibilityCollector )
240+ const minimized = await minimizeAdaptiveFinding ( page , baseUrl , contract , finding , observations , Math . max ( 0 , contract . budgets . maxActions - actions ) , Math . max ( 0 , ( contract . accessibility ?. budgets . maxKeyboardActions ?? Number . POSITIVE_INFINITY ) - keyboardActions ) , started + contract . budgets . maxDurationMs , signal , now , navigationScope , networkPolicy , accessibilityCollector )
235241 actions += minimized . executed
236242 keyboardActions += minimized . keyboardExecuted
237243 finding . minimizedPath = minimized . path
@@ -491,7 +497,7 @@ async function restoreAdaptivePath(page: Page, baseUrl: string, contract: Browse
491497 }
492498}
493499
494- async function minimizeAdaptiveFinding ( page : Page , baseUrl : string , contract : BrowserAdaptiveExplorationContract , finding : BrowserAdaptiveFinding , observations : AdaptiveObservationSources , maximumActions : number , maximumKeyboardActions : number , deadline : number , signal ?: AbortSignal , now : ( ) => number = Date . now , navigationScope ?: BrowserPreviewNavigationScope , accessibilityCollector ?: BrowserAccessibilityCollector ) : Promise < { path : BrowserAdaptiveAction [ ] ; executed : number ; keyboardExecuted : number ; exhausted ?: "maxActions" | "maxKeyboardActions" | "maxDurationMs" | "cancelled" } > {
500+ async function minimizeAdaptiveFinding ( page : Page , baseUrl : string , contract : BrowserAdaptiveExplorationContract , finding : BrowserAdaptiveFinding , observations : AdaptiveObservationSources , maximumActions : number , maximumKeyboardActions : number , deadline : number , signal ?: AbortSignal , now : ( ) => number = Date . now , navigationScope ?: BrowserPreviewNavigationScope , networkPolicy ?: BrowserPreviewNetworkPolicy , accessibilityCollector ?: BrowserAccessibilityCollector ) : Promise < { path : BrowserAdaptiveAction [ ] ; executed : number ; keyboardExecuted : number ; exhausted ?: "maxActions" | "maxKeyboardActions" | "maxDurationMs" | "cancelled" } > {
495501 let current = [ ...finding . originalPath ]
496502 let chunk = Math . max ( 1 , Math . floor ( current . length / 2 ) )
497503 let executed = 0
@@ -507,10 +513,11 @@ async function minimizeAdaptiveFinding(page: Page, baseUrl: string, contract: Br
507513 if ( keyboardExecuted + countKeyboardActions ( candidate ) > maximumKeyboardActions ) { exhausted = "maxKeyboardActions" ; break }
508514 const beforeConsole = observations . consoleMessages . length
509515 const beforeErrors = observations . errors . length
516+ const beforeNetwork = observations . network . length
510517 const replay = await restoreAdaptivePath ( page , baseUrl , contract , candidate , signal , navigationScope , accessibilityCollector )
511518 executed += replay . executed
512519 keyboardExecuted += replay . keyboardExecuted
513- const fingerprints = [ ... consoleErrorMessages ( observations . consoleMessages . slice ( beforeConsole ) ) , ... errorMessages ( observations . errors . slice ( beforeErrors ) ) ] . map ( ( message ) => browserAdaptiveDigest ( "oracle" , message ) ) . concat ( replay . finalAccessibilityFingerprints )
520+ const fingerprints = adaptiveOracleEvidence ( consoleErrorRecords ( observations . consoleMessages . slice ( beforeConsole ) ) , observations . network . slice ( beforeNetwork ) , errorMessages ( observations . errors . slice ( beforeErrors ) ) , contract , networkPolicy ) . fingerprints . concat ( replay . finalAccessibilityFingerprints )
514521 if ( replay . state && fingerprints . includes ( finding . fingerprint ) && ( ! finding . stateDigest || replay . state . digest === finding . stateDigest ) ) {
515522 current = candidate
516523 reduced = true
@@ -558,8 +565,47 @@ function rejectedTransition(state: BrowserAdaptiveState, action: BrowserAdaptive
558565 return { id : `transition-rejected-${ browserAdaptiveDigest ( "action" , `${ state . digest } :${ action . id } ` ) . slice ( 0 , 12 ) } ` , sourceDigest : state . digest , action, sourceUrl : state . url , destinationUrl, history : { before : state . historyLength , after : state . historyLength , beforeStateDigest : state . historyStateDigest , afterStateDigest : state . historyStateDigest } , timing : { durationMs : 0 , stabilizationMs : 0 , polls : 0 } , novelty : { newState : false , newDescriptors : 0 , mutationRecords : 0 , mutationEvidenceTruncated : false } , observations : { networkEvents : 0 , consoleErrors : [ ] , pageErrors : [ ] , loadingBefore : 0 , loadingAfter : 0 , oracleFingerprints : [ ] } , status : "rejected" , diagnostic : { code, message } }
559566}
560567
561- function consoleErrorMessages ( records : object [ ] ) : string [ ] {
562- return records . map ( ( record ) => record as Record < string , unknown > ) . filter ( ( record ) => record . type === "error" || record . level === "error" ) . map ( recordMessage ) . filter ( Boolean ) . slice ( 0 , 100 )
568+ function consoleErrorRecords ( records : object [ ] ) : Record < string , unknown > [ ] {
569+ return records . map ( ( record ) => record as Record < string , unknown > ) . filter ( ( record ) => record . type === "error" || record . level === "error" ) . slice ( 0 , 100 )
570+ }
571+
572+ function adaptiveOracleEvidence ( consoleRecords : Record < string , unknown > [ ] , networkRecords : object [ ] , pageErrors : string [ ] , contract : BrowserAdaptiveExplorationContract , networkPolicy ?: BrowserPreviewNetworkPolicy ) : { fingerprints : string [ ] ; networkFailures : BrowserAdaptiveNetworkFailure [ ] ; errorCount : number } {
573+ const failures = networkRecords . map ( ( record ) => record as Record < string , unknown > ) . filter ( ( record ) => record . type === "requestfailed" )
574+ const networkFailures = failures . map ( ( record ) : BrowserAdaptiveNetworkFailure => {
575+ const url = typeof record . url === "string" ? record . url : ""
576+ const decision = networkPolicy ? browserPreviewNetworkDecision ( url , networkPolicy ) : { url, urlClassification : "invalid" as const , policyDecision : "unknown" as const , policyReason : "network-policy-unavailable" }
577+ const failure = networkFailureMessage ( record )
578+ const expectedBlock = decision . policyDecision === "blocked" && / E R R _ B L O C K E D _ B Y _ C L I E N T | b l o c k e d b y c l i e n t / i. test ( failure )
579+ return { ...decision , ...( failure ? { failure } : { } ) , oracleFinding : ! expectedBlock || contract . oraclePolicy . policyBlocks === "finding" }
580+ } )
581+ const expectedBlockUrls = networkFailures . filter ( ( failure ) => ! failure . oracleFinding ) . map ( ( failure ) => failure . url )
582+ const findingConsoleRecords = consoleRecords . filter ( ( record ) => {
583+ if ( ! / E R R _ B L O C K E D _ B Y _ C L I E N T / i. test ( recordMessage ( record ) ) ) return true
584+ const location = objectRecord ( record . location )
585+ const match = typeof location . url === "string" && location . url
586+ ? expectedBlockUrls . indexOf ( location . url )
587+ : expectedBlockUrls . length > 0 ? 0 : - 1
588+ if ( match < 0 ) return true
589+ expectedBlockUrls . splice ( match , 1 )
590+ return false
591+ } )
592+ const consoleUrls = new Set ( findingConsoleRecords . map ( ( record ) => objectRecord ( record . location ) . url ) . filter ( ( url ) : url is string => typeof url === "string" ) )
593+ const networkMessages = networkFailures . filter ( ( failure ) => failure . oracleFinding && ! consoleUrls . has ( failure . url ) ) . map ( ( failure ) => stableJson ( { type : "requestfailed" , url : failure . url , failure : failure . failure , urlClassification : failure . urlClassification , policyDecision : failure . policyDecision , policyReason : failure . policyReason } ) )
594+ const messages = [ ...findingConsoleRecords . map ( recordMessage ) , ...pageErrors , ...networkMessages ]
595+ return {
596+ fingerprints : [ ...new Set ( messages . map ( ( message ) => browserAdaptiveDigest ( "oracle" , message ) ) ) ] . sort ( ) ,
597+ networkFailures,
598+ errorCount : findingConsoleRecords . length + pageErrors . length + networkMessages . length ,
599+ }
600+ }
601+
602+ function networkFailureMessage ( record : Record < string , unknown > ) : string {
603+ const failure = objectRecord ( record . failure )
604+ return typeof failure . errorText === "string" ? failure . errorText : typeof record . failure === "string" ? record . failure : ""
605+ }
606+
607+ function objectRecord ( value : unknown ) : Record < string , unknown > {
608+ return value && typeof value === "object" && ! Array . isArray ( value ) ? value as Record < string , unknown > : { }
563609}
564610
565611function errorMessages ( records : object [ ] ) : string [ ] {
0 commit comments