@@ -160,6 +160,47 @@ test("identical seed and DOM produce deterministic state and action graph identi
160160 assert . deepEqual ( stableGraph ( second . result ) , stableGraph ( first . result ) )
161161} )
162162
163+ test ( "volatile generated ids preserve semantic replay and distinct control identity" , async ( ) => {
164+ let render = 0
165+ const server = createServer ( ( _request , response ) => {
166+ render += 1
167+ const suffix = render . toString ( 16 ) . padStart ( 10 , "0" )
168+ response . setHeader ( "content-type" , "text/html" )
169+ response . end ( `<!doctype html>
170+ <style>button { display:block; width:180px; height:30px; margin:8px; }</style>
171+ <button id="choice-primary-dm${ suffix } ">Choose</button>
172+ <button id="choice-secondary-dm${ suffix } ">Choose</button>
173+ <output>Waiting</output>
174+ <script>
175+ document.querySelectorAll('button').forEach((button, index) => button.addEventListener('click', () => {
176+ document.querySelector('output').textContent = index === 0 ? 'Primary selected' : 'Secondary selected';
177+ }));
178+ </script>` )
179+ } )
180+ const startUrl = await listenLocalHttpServer ( server )
181+ try {
182+ const run = await runUrlFixture ( startUrl , {
183+ seed : "volatile-generated-ids" ,
184+ failOnFinding : false ,
185+ budgets : { maxActions : 12 , maxStates : 8 , maxTransitions : 8 , maxDurationMs : 15_000 } ,
186+ actionFamilies : [ "click" ] ,
187+ } )
188+ const initial = run . result . states [ 0 ] !
189+ const choices = initial . descriptors . filter ( ( descriptor ) => descriptor . label === "Choose" )
190+ assert . equal ( choices . length , 2 )
191+ assert . equal ( new Set ( choices . map ( ( descriptor ) => descriptor . id ) ) . size , 2 , "semantic occurrence keeps equivalent controls distinct" )
192+ assert . equal ( new Set ( choices . map ( ( descriptor ) => descriptor . selector ) ) . size , 2 , "execution retains each concrete selector" )
193+ const choiceTransitions = run . result . transitions . filter ( ( transition ) => transition . sourceDigest === initial . digest && transition . action . descriptorId && choices . some ( ( descriptor ) => descriptor . id === transition . action . descriptorId ) )
194+ assert . equal ( new Set ( choiceTransitions . map ( ( transition ) => transition . destinationDigest ) ) . size , 2 , "distinct controls reach distinct semantic states" )
195+ assert ( ! run . result . transitions . some ( ( transition ) => transition . diagnostic ?. code === "browser_adaptive_source_state_not_reproduced" ) )
196+ assert ( ! run . result . diagnostics . some ( ( diagnostic ) => diagnostic . code === "browser_adaptive_reset_replay_failed" ) )
197+ assert . notEqual ( run . result . summary . budgetExhausted , "maxDurationMs" )
198+ assert ( render > 2 , "the fixture must generate fresh ids across resets" )
199+ } finally {
200+ await closeHttpServer ( server )
201+ }
202+ } )
203+
163204test ( "partially failed actions retain evidence without creating unreplayable frontier paths" , async ( ) => {
164205 const input = {
165206 seed : "partial-repeat" ,
@@ -310,6 +351,26 @@ async function runFixture(html: string, input: Record<string, unknown>, signal?:
310351 }
311352}
312353
354+ async function runUrlFixture ( startUrl : string , input : Record < string , unknown > ) {
355+ const browser = await chromium . launch ( { headless : true } )
356+ const page = await browser . newPage ( )
357+ const consoleMessages : object [ ] = [ ]
358+ const errors : object [ ] = [ ]
359+ const network : object [ ] = [ ]
360+ page . on ( "console" , ( message ) => consoleMessages . push ( { type : message . type ( ) , text : message . text ( ) } ) )
361+ page . on ( "pageerror" , ( error ) => errors . push ( { message : error . message } ) )
362+ page . on ( "request" , ( request ) => network . push ( { url : request . url ( ) , method : request . method ( ) } ) )
363+ await page . addInitScript ( "globalThis.__name = value => value" )
364+ await page . goto ( startUrl , { waitUntil : "load" } )
365+ const contract = browserAdaptiveExplorationContract ( { startUrl, stabilization : { pollIntervalMs : 25 , quietWindowMs : 100 , maxWaitMs : 1500 , maxMutationRecords : 40 } , ...input } )
366+ try {
367+ const result = await exploreAdaptiveBrowserStateMachine ( { page, baseUrl : startUrl , contract, observations : { consoleMessages, errors, network } } )
368+ return { result, contract }
369+ } finally {
370+ await browser . close ( )
371+ }
372+ }
373+
313374async function runRoutedFixture ( topology : ReturnType < typeof browserPreviewTopology > , effectiveOrigin : string , initialHtml ?: string ) {
314375 const browser = await chromium . launch ( { headless : true } )
315376 const context = await browser . newContext ( )
0 commit comments