/* Real-browser regression check. Requires Playwright and its Chromium browser. */ 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const {spawn} = require('node:child_process'); const root = path.resolve(__dirname, '..'); let playwright; try { playwright = require(process.env.CROSSWIRE_PLAYWRIGHT || 'playwright'); } catch { playwright = require(path.join(process.env.USERPROFILE || process.env.HOME, '.codex/skills/develop-web-game/node_modules/playwright')); } const venvPython = path.join(root, '.venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python'); const python = process.env.CROSSWIRE_PYTHON || (fs.existsSync(venvPython) ? venvPython : 'python'); (async () => { const child = spawn(python, ['-m', 'crosswire', 'serve', '--port', '0'], {cwd:root, windowsHide:true, stdio:['ignore','pipe','pipe']}); let browser, page, output, base, serverErrors = ''; const errors = []; child.stderr.on('data', value => serverErrors += value); try { base = await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('Workbench server did not start')), 15000); child.stdout.once('data', value => {clearTimeout(timer);resolve(String(value).trim());}); child.once('error', reject); child.once('exit', code => {clearTimeout(timer);reject(new Error(`Server exited: ${code} ${serverErrors}`));}); }); browser = await playwright.chromium.launch({headless:true}); page = await browser.newPage({viewport:{width:1440,height:1000}}); output = path.join(root,'output/playwright'); fs.mkdirSync(output,{recursive:true}); page.on('pageerror', error => errors.push(error.message)); await page.goto(base); await page.waitForFunction(() => !document.querySelector('#run-button').disabled); assert.equal(await page.inputValue('#candidate_backend'), 'flat', 'Nominal runs must retain the exhaustive backend by default'); assert.match(await page.locator('label[for="candidate_backend"]').textContent(), /backend/i); await page.selectOption('#candidate_backend', 'factored'); await page.selectOption('#policy', 'lookahead'); assert.equal(await page.evaluate(() => config().candidate_backend), 'factored'); assert.deepEqual(await page.evaluate(() => studyConfig().candidate_backends), ['factored']); assert.match(await page.textContent('#study-description'), /factored/i); assert.match(await page.textContent('#policy-help'), /model.count.*not.*calibrated/i); await page.selectOption('#candidate_backend', 'flat'); await page.selectOption('#policy', 'task'); assert.equal(await page.locator('#instance_seed').count(), 1, 'Workbench must expose a distinct structural instance seed'); assert.equal(await page.isDisabled('#instance_seed'), true, 'Legacy families cannot set a structural instance seed'); assert.match(await page.locator('label[for="seed"]').textContent(), /label/i, 'The nuisance seed must be labeled as label variation'); assert.equal(await page.inputValue('#fault_type'), 'off', 'Fault injection must default to the nominal control'); for (const id of ['fault_step', 'fault_phase', 'fault_value']) assert.equal(await page.isDisabled(`#${id}`), true); assert.equal(await page.evaluate(() => config().fault_schedule), null); assert.deepEqual(await page.evaluate(() => studyConfig().fault_schedules), [null]); assert.match(await page.textContent('#fault-onset-help'), /zero-based/i); await page.selectOption('#fault_type', 'stuck_output'); for (const id of ['fault_step', 'fault_phase', 'fault_value']) assert.equal(await page.isEnabled(`#${id}`), true); for (const value of ['-1', '1.5', '']) { await page.fill('#fault_step', value); assert.equal(await page.locator('#fault_step').evaluate(el => el.checkValidity()), false, `Onset ${value} must fail native validation`); } await page.fill('#fault_step', '0'); for (const value of ['-1', '4', '1.5', '']) { await page.fill('#fault_value', value); assert.equal(await page.locator('#fault_value').evaluate(el => el.checkValidity()), false, `Stuck output ${value} must fail native validation`); } await page.fill('#fault_value', '3'); for (const phase of ['deployment', 'calibration', 'always']) { await page.selectOption('#fault_phase', phase); assert.deepEqual(await page.evaluate(() => config().fault_schedule), {type:'stuck_output',step:0,phase,value:3}); } for (const type of ['sensor_drift', 'actuator_swap', 'delay_spike', 'semantic_swap']) { await page.selectOption('#fault_type', type); assert.equal(await page.isDisabled('#fault_value'), true, 'Only a stuck output uses a configured symbol'); assert.deepEqual(await page.evaluate(() => config().fault_schedule), {type,step:0,phase:'always'}); } await page.fill('#fault_step', '-1'); await page.selectOption('#fault_type', 'off'); assert.equal(await page.evaluate(() => config().fault_schedule), null, 'Turning a fault off must omit retained inactive settings'); assert.deepEqual(await page.evaluate(() => studyConfig().fault_schedules), [null]); assert.equal(await page.locator('#run-form').evaluate(el => el.checkValidity()), true, 'Inactive invalid fault fields must not block a nominal run'); if (process.argv.includes('--controls-only')) { await page.evaluate(() => { const fixture = document.createElement('section');fixture.id='fixture-results';document.body.append(fixture); fixture.append(renderCertificate({valid:true,kind:'robust',model_diagnostics:{inclusion_status:'UNKNOWN',enabled_classes:['static'],classes:{static:{candidate_count:2,scored_symbols:1,unscored_symbols:0,events:[{probe_index:0,step:1,action:0,predicted_output_counts:{0:2},observed_output:0,survivors_before:2,survivors_after:2,prediction_status:'SUPPORTED'}],first_refutation:null}},reset_consistency:{status:'UNKNOWN',first_conflict:null},blind_spots:['Raw-equivalent semantic corruption remains unobservable.','']}})); fixture.append(renderCertificate({valid:true,kind:'unresolved',model_diagnostics:{inclusion_status:'REFUTED',enabled_classes:['static'],classes:{static:{candidate_count:0,scored_symbols:1,unscored_symbols:1,events:[{probe_index:1,step:1,action:0,predicted_output_counts:{0:2},observed_output:1,survivors_before:2,survivors_after:0,prediction_status:'UNSUPPORTED'}],first_refutation:{kind:'probe',probe_index:1,step:1,word_prefix:[0],observed_output:1,survivors_before:2}}},reset_consistency:{status:'REFUTED',first_conflict:{word_prefix:[0],previous_probe_index:0,probe_index:1}},blind_spots:[]}})); renderStudy({rows:[],summary:[],paired:[],protocol:{families:['generated_protocol'],instance_seeds:[0,1],seeds:[17,18],fault_schedules:[null,{type:'stuck_output',step:0,phase:'calibration',value:3}]},reliability:[{family:'generated_protocol',method:'task/robust',budget:0,fault_schedule:{type:'sensor_drift',step:2,phase:'always'},requests:4,parent_clusters:2,independent_parents:2,target_any_deployment_hazard:{events:0,targets:2,rate:0,interval:[0,0.84],unknown_targets:1},target_any_calibration_hazard:{events:1,targets:2,rate:0.5,interval:null,unknown_targets:0},target_average_useful_coverage:{estimate:0.5,interval:null},conditional_selective_harm:null,inference_eligible:false,inference_reason:'Insufficient independent targets',scope:'Conditional on independent synthetic structural parents.'}]},fixture); fixture.append(renderBackend({backend:'factored',cached_adapters:4,total_candidates:144,cached_predictions:12})); fixture.append(renderPlanning([{planning:{expanded_nodes:2048,node_limit:2048,depth_limit:2,search_complete:false,selection:'fallback',certificate:{valid:true,ready_models:3,candidate_count:12,worst_case_calls:5},tree:{private:'PRIVATE_TREE_SENTINEL'}}}])); }); const fixture = page.locator('#fixture-results'); assert.match(await fixture.textContent(), /Model inclusion: UNKNOWN/); assert.match(await fixture.textContent(), /Model inclusion: REFUTED/); assert.match(await fixture.textContent(), /First refutation: probe 2, step 1/); assert.match(await fixture.textContent(), /First reset-prefix conflict/); assert.match(await fixture.textContent(), /Prediction support timeline/); assert.match(await fixture.textContent(), /SUPPORTED/); assert.match(await fixture.textContent(), /Independent-target reliability/); assert.equal(await fixture.locator('.reliability-stratum').count(), 1, 'Each reliability condition must show its outcomes without scrolling a wide table'); assert.match(await fixture.textContent(), /0 \/ 2/); assert.match(await fixture.textContent(), /1 unknown/); assert.equal(await fixture.locator('img').count(), 0, 'Model diagnostic text must use safe DOM rendering'); assert.match(await fixture.textContent(), /0 to 0.84/); assert.match(await fixture.textContent(), /No injected fault/); assert.match(await fixture.textContent(), /stuck output.*zero-based step 0.*calibration.*output 3/); assert.match(await fixture.textContent(), /sensor drift.*zero-based step 2.*both phases/); assert.match(await fixture.textContent(), /backend flat/i, 'Legacy omitted backends must display the flat default'); assert.match(await fixture.textContent(), /Candidate backend/); assert.match(await fixture.textContent(), /cached adapters.*4/i); assert.match(await fixture.textContent(), /cached predictions.*12/i); assert.match(await fixture.textContent(), /Budgeted experiment planning/); assert.match(await fixture.textContent(), /3 \/ 12/); assert.match(await fixture.textContent(), /incomplete search/i); assert.match(await fixture.textContent(), /not a calibrated.*probability/i); assert.doesNotMatch(await fixture.textContent(), /PRIVATE_TREE_SENTINEL/); assert.deepEqual(errors, []); console.log('PASS: fault defaults and native validation, structural seed controls, legacy-family boundary, public diagnostics, target reliability, safe DOM'); return; } assert.equal(await page.locator('#probe_safety').count(), 1, 'Workbench must expose the calibration evidence rule'); assert.equal(await page.inputValue('#probe_safety'), 'unrestricted'); await page.selectOption('#probe_safety', 'model_safe'); await page.selectOption('#policy', 'aalpy'); assert.equal(await page.locator('#policy').evaluate(el => el.checkValidity()), false, 'AALpy must reject model-safe calibration'); await page.selectOption('#policy', 'task'); assert.equal(await page.isDisabled('#risk_tolerance'), true, 'Tolerance applies only to candidate-fraction admission'); await page.selectOption('#admission', 'risk_controlled'); assert.equal(await page.isEnabled('#risk_tolerance'), true); await page.fill('#risk_tolerance', '0.25'); assert.equal(await page.inputValue('#risk_tolerance'), '0.25'); await page.fill('#risk_tolerance', '1'); assert.equal(await page.locator('#risk_tolerance').evaluate(el => el.checkValidity()), false, 'Candidate tolerance must remain below one'); await page.fill('#risk_tolerance', '0.25'); await page.selectOption('#admission', 'robust'); assert.equal(await page.inputValue('#risk_tolerance'), '0'); await page.selectOption('#execution_mode', 'replan'); await page.selectOption('#admission', 'adaptive'); assert.equal(await page.inputValue('#execution_mode'), 'shielded', 'Adaptive selection must choose a compatible deployment mode'); assert.equal(await page.locator('#execution_mode option[value="replan"]').isDisabled(), true, 'Adaptive policy execution does not support replanning'); await page.selectOption('#execution_mode', 'recovery'); assert.equal(await page.inputValue('#execution_mode'), 'recovery', 'Adaptive mode must permit certified recovery'); await page.selectOption('#policy', 'ec2'); assert.equal(await page.inputValue('#policy'), 'ec2', 'Workbench must allow selecting EC2 active probing'); await page.selectOption('#policy', 'branching'); assert.equal(await page.inputValue('#policy'), 'branching', 'Workbench must allow selecting adaptive within-query branching calibration'); await page.selectOption('#family', 'robotics_airlock'); assert.equal(await page.inputValue('#family'), 'robotics_airlock', 'Workbench must expose realistic robotics_airlock benchmark'); await page.selectOption('#family', 'protocol_session'); assert.equal(await page.inputValue('#family'), 'protocol_session', 'Workbench must expose realistic protocol_session benchmark'); await page.selectOption('#policy', 'task'); await page.selectOption('#admission', 'risk_controlled'); assert.equal(await page.inputValue('#admission'), 'risk_controlled', 'Workbench must allow risk-controlled admission'); await page.selectOption('#admission', 'adaptive'); await page.check('#temporal'); await page.check('#escalate'); assert.equal(await page.isChecked('#temporal'), false); assert.equal(await page.isDisabled('#temporal'), true); await page.uncheck('#escalate'); assert.equal(await page.isDisabled('#temporal'), false); await page.selectOption('#family', 'routing'); await page.fill('#seed', '42'); await page.fill('#budget', '0'); await page.fill('#horizon', '3'); await page.selectOption('#split', 'evaluation'); assert.match(await page.textContent('#study-description'), /4 runs/); const submitted = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith('/api/studies')); await page.click('#study-button'); const spec = (await submitted).postDataJSON(); assert.deepEqual(spec, {families:['routing'],seeds:[42,43],budgets:[0],policies:['task'],admissions:['robust','adaptive'],implementations:['procedural'],anchors:false,split:'evaluation',horizon:3,temporal:false,escalate:false,execution_mode:'recovery',probe_safety:'model_safe',risk_tolerance:0,fault_schedules:[null],candidate_backends:['flat']}); await page.waitForFunction(() => !document.querySelector('#result-view').hidden && document.querySelector('#job-panel').hidden, null, {timeout:60000}); assert.match(await page.textContent('#result-content'), /parent[ _]clusters/i); assert.match(await page.textContent('#result-content'), /descriptive 95% interval/i); assert.match(await page.textContent('#result-content'), /model.safe/i); assert.match(await page.textContent('#result-content'), /Independent-target reliability/); await page.getByRole('button', {name:'Inspect run 1', exact:true}).click(); await page.waitForSelector('#study-run-detail:not([hidden])'); assert.match(await page.textContent('#study-run-detail'), /Calibration safety/); assert.match(await page.textContent('#study-run-detail'), /Public model evidence/); assert.equal(await page.textContent('#result-kind'), 'PAIRED STUDY RECEIPT', 'Inspecting a row preserves the parent receipt'); await page.setViewportSize({width:390,height:900}); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth), true, 'Study inspection must fit a mobile viewport'); await page.screenshot({path:path.join(output,'study-inspection-mobile.png'),fullPage:true}); await page.locator('#study-run-detail').evaluate(el => el.scrollIntoView({block:'start'})); await page.screenshot({path:path.join(output,'study-inspection-mobile-viewport.png')}); await page.setViewportSize({width:1440,height:1000}); const downloadPromise = page.waitForEvent('download'); await page.click('#export-button'); const download = await downloadPromise; const receipt = fs.readFileSync(await download.path(), 'utf8'); const original = JSON.parse(receipt); assert.equal(original.payload.rows.length, 4); assert.ok(original.payload.rows.every(row => row.configuration.fault_schedule === null), 'Nominal studies must retain null fault schedules'); for (const action of ['replay', 'import']) { const block = await page.request.post(base + 'api/studies', {data:{families:['latch'],seeds:[1,2,3,4,5,6,7,8],budgets:[16],policies:['fixed']}}); const blocking = await block.json(); assert.ok(blocking.id); let submittedJob; const jobResponse = page.waitForResponse(response => response.request().method() === 'POST' && (action === 'replay' ? response.url().endsWith('/replay') : response.url().endsWith('/api/import'))); if (action === 'replay') { await page.click('#replay-button'); } else { await page.setInputFiles('#receipt-file', {name:'receipt.json',mimeType:'application/json',buffer:Buffer.from(receipt)}); } submittedJob = await (await jobResponse).json(); await page.waitForSelector('#job-panel:not([hidden])'); assert.equal(await page.isEnabled('#cancel-button'), true, `${action} must expose cancellation`); const cancelRequest = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith(`/${submittedJob.id}/cancel`)); await page.click('#cancel-button'); await cancelRequest; await page.request.post(base + `api/runs/${blocking.id}/cancel`, {data:{}}); await page.waitForFunction(() => document.querySelector('#job-panel').hidden, null, {timeout:60000}); assert.match(await page.textContent('#notice'), /cancelled/i); assert.equal(await page.isVisible('#result-view'), true, 'Cancellation preserves previous complete evidence'); } await page.selectOption('#family', 'generated_protocol'); assert.equal(await page.isEnabled('#instance_seed'), true); await page.fill('#instance_seed', '1'); assert.match(await page.textContent('#study-description'), /8 runs/); assert.match(await page.textContent('#study-description'), /structural seeds 1 \/ 2/); const submittedGenerated = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith('/api/studies')); await page.click('#study-button'); assert.deepEqual((await submittedGenerated).postDataJSON(), {...spec,families:['generated_protocol'],instance_seeds:[1,2]}); await page.waitForFunction(() => document.querySelector('#result-content').textContent.includes('8 run results') && document.querySelector('#job-panel').hidden, null, {timeout:60000}); assert.match(await page.textContent('#result-content'), /Independent-target reliability/); assert.match(await page.textContent('#result-content'), /Independent parents/); assert.match(await page.textContent('#result-content'), /independent generator draws assumed/); assert.match(await page.textContent('#result-content'), /95% pointwise intervals/); await page.screenshot({path:path.join(output,'generated-study-desktop.png'),fullPage:true}); await page.getByRole('heading', {name:'Independent-target reliability',exact:true}).evaluate(el => el.scrollIntoView({block:'start'})); await page.screenshot({path:path.join(output,'generated-reliability-desktop-viewport.png')}); await page.getByRole('button', {name:'Inspect run 1', exact:true}).click(); assert.match(await page.textContent('#study-run-detail'), /Structural instance design/); assert.match(await page.textContent('#study-run-detail'), /Model inclusion: UNKNOWN/); assert.match(await page.textContent('#study-run-detail'), /Prediction support timeline/); await page.setViewportSize({width:390,height:900}); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth), true, 'Structural diagnostics and reliability must fit a mobile viewport'); await page.screenshot({path:path.join(output,'generated-study-mobile.png'),fullPage:true}); await page.getByRole('heading', {name:'Independent-target reliability',exact:true}).evaluate(el => el.scrollIntoView({block:'start'})); await page.screenshot({path:path.join(output,'generated-reliability-mobile-viewport.png')}); await page.getByText('Model inclusion: UNKNOWN', {exact:true}).evaluate(el => el.scrollIntoView({block:'start'})); await page.screenshot({path:path.join(output,'generated-diagnostics-mobile-viewport.png')}); await page.setViewportSize({width:1440,height:1000}); await page.selectOption('#family', 'symmetric'); assert.equal(await page.isDisabled('#instance_seed'), true); await page.selectOption('#admission', 'risk_controlled'); await page.fill('#risk_tolerance', '0.25'); const submittedRisk = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith('/api/runs')); await page.click('#run-button'); assert.equal((await submittedRisk).postDataJSON().risk_tolerance, 0.25, 'The selected candidate fraction must reach the scientific worker'); await page.waitForFunction(() => document.querySelector('#result-kind').textContent === 'EXPERIMENT RECEIPT' && document.querySelector('#job-panel').hidden, null, {timeout:60000}); assert.match(await page.textContent('#result-content'), /no plan selected; candidate rates are undefined/i); assert.match(await page.textContent('#result-content'), /Configured tolerance 0.25/); assert.doesNotMatch(await page.textContent('#result-content'), /Uniformly weighted candidate fractions: hazardous/); await page.selectOption('#family', 'symmetric'); await page.selectOption('#admission', 'robust'); await page.click('#run-button'); await page.waitForFunction(() => document.querySelector('#result-content').textContent.includes('Admission: Robust sequence across survivors') && document.querySelector('#job-panel').hidden, null, {timeout:60000}); assert.match(await page.textContent('#result-content'), /Public model evidence/); assert.match(await page.textContent('#result-content'), /Calibration safety/); assert.match(await page.textContent('#result-content'), /Structural factors among surviving adapters:/); assert.match(await page.textContent('#result-content'), /does not authenticate/i); await page.selectOption('#family', 'latch'); await page.fill('#seed', '42'); await page.fill('#budget', '16'); await page.fill('#horizon', '4'); await page.selectOption('#execution_mode', 'shielded'); await page.selectOption('#probe_safety', 'unrestricted'); await page.selectOption('#fault_type', 'sensor_drift'); await page.fill('#fault_step', '1'); await page.selectOption('#fault_phase', 'deployment'); const schedule = {type:'sensor_drift',step:1,phase:'deployment'}; assert.match(await page.textContent('#study-description'), /sensor drift.*zero-based step 1.*deployment/i); const submittedFault = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith('/api/runs')); await page.click('#run-button'); assert.deepEqual((await submittedFault).postDataJSON().fault_schedule, schedule); await page.waitForFunction(() => document.querySelector('#result-content').textContent.includes('Change-point fault') && document.querySelector('#job-panel').hidden, null, {timeout:60000}); assert.deepEqual(await page.evaluate(() => state.completed.result.configuration.fault_schedule), schedule); assert.equal(await page.evaluate(() => state.completed.result.deployment.change_point.detected), true); assert.match(await page.textContent('#result-content'), /sensor drift.*zero-based step 1.*deployment/i); await page.click('#replay-button'); await page.waitForFunction(() => !state.active && !state.busy && document.querySelector('#notice').textContent.includes('Replay verified') && document.querySelector('#job-panel').hidden, null, {timeout:60000}); await page.selectOption('#family', 'routing'); await page.fill('#budget', '0'); await page.selectOption('#admission', 'adaptive'); const submittedFaultStudy = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith('/api/studies')); await page.click('#study-button'); const faultSpec = (await submittedFaultStudy).postDataJSON(); assert.deepEqual(faultSpec.fault_schedules, [schedule]); await page.waitForFunction(() => document.querySelector('#result-kind').textContent === 'PAIRED STUDY RECEIPT' && document.querySelector('#job-panel').hidden, null, {timeout:60000}); assert.match(await page.textContent('#result-content'), /sensor drift.*zero-based step 1.*deployment/i); const faultDownloadPromise = page.waitForEvent('download'); await page.click('#export-button'); const faultDownload = await faultDownloadPromise; const faultReceipt = JSON.parse(fs.readFileSync(await faultDownload.path(), 'utf8')); assert.equal(faultReceipt.payload.rows.length, 4); assert.deepEqual(faultReceipt.payload.protocol.fault_schedules, [schedule]); for (const row of faultReceipt.payload.rows) assert.deepEqual(row.configuration.fault_schedule, schedule); for (const row of faultReceipt.payload.summary) assert.deepEqual(row.fault_schedule, schedule); await page.getByRole('button', {name:'Inspect run 1', exact:true}).click(); assert.match(await page.textContent('#study-run-detail'), /sensor drift.*zero-based step 1.*deployment/i); await page.click('#replay-button'); await page.waitForFunction(() => !state.active && !state.busy && document.querySelector('#notice').textContent.includes('Replay verified') && document.querySelector('#job-panel').hidden, null, {timeout:60000}); await page.selectOption('#fault_type', 'off'); await page.selectOption('#candidate_backend', 'factored'); await page.selectOption('#policy', 'lookahead'); await page.selectOption('#admission', 'robust'); await page.selectOption('#family', 'latch'); await page.fill('#budget', '4'); const submittedLookahead = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith('/api/runs')); await page.click('#run-button'); const lookaheadConfiguration = (await submittedLookahead).postDataJSON(); assert.equal(lookaheadConfiguration.policy, 'lookahead'); assert.equal(lookaheadConfiguration.candidate_backend, 'factored'); assert.equal(lookaheadConfiguration.fault_schedule, null); await page.waitForFunction(() => !state.active && !state.busy && state.completed?.result.configuration?.policy === 'lookahead', null, {timeout:60000}); assert.match(await page.textContent('#result-content'), /Candidate backend/); assert.match(await page.textContent('#result-content'), /backend factored/i); assert.match(await page.textContent('#result-content'), /Budgeted experiment planning/); assert.match(await page.textContent('#result-content'), /not a calibrated.*probability/i); const lookaheadDownloadPromise = page.waitForEvent('download'); await page.click('#export-button'); const lookaheadDownload = await lookaheadDownloadPromise; const lookaheadReceipt = JSON.parse(fs.readFileSync(await lookaheadDownload.path(), 'utf8')); assert.equal(lookaheadReceipt.payload.configuration.candidate_backend, 'factored'); assert.ok(lookaheadReceipt.payload.backend); const plans = lookaheadReceipt.payload.probes.filter(probe => probe.planning); assert.ok(plans.length > 0, 'The actual lookahead run must retain its checked experiment proposal'); assert.ok(plans.every(probe => probe.planning.certificate.valid === true)); await page.click('#replay-button'); await page.waitForFunction(() => !state.active && !state.busy && document.querySelector('#notice').textContent.includes('Replay verified'), null, {timeout:60000}); await page.fill('#budget', '2'); const submittedLookaheadStudy = page.waitForRequest(req => req.method() === 'POST' && req.url().endsWith('/api/studies')); await page.click('#study-button'); const lookaheadSpec = (await submittedLookaheadStudy).postDataJSON(); assert.deepEqual(lookaheadSpec.candidate_backends, ['factored']); assert.deepEqual(lookaheadSpec.policies, ['lookahead','fixed']); await page.waitForFunction(() => !state.active && !state.busy && state.completed?.result.schema === 'crosswire-study-v1' && state.completed.result.protocol?.policies.includes('lookahead'), null, {timeout:60000}); const lookaheadStudyDownloadPromise = page.waitForEvent('download'); await page.click('#export-button'); const lookaheadStudyDownload = await lookaheadStudyDownloadPromise; const lookaheadStudy = JSON.parse(fs.readFileSync(await lookaheadStudyDownload.path(), 'utf8')).payload; assert.equal(lookaheadStudy.rows.length, 8); assert.deepEqual(lookaheadStudy.protocol.candidate_backends, ['factored']); assert.ok(lookaheadStudy.rows.every(row => row.configuration.candidate_backend === 'factored')); assert.ok(lookaheadStudy.summary.every(row => row.candidate_backend === 'factored')); const plannedRow = lookaheadStudy.rows.findIndex(row => row.probes.some(probe => probe.planning)); assert.ok(plannedRow >= 0, 'The small study must include executed lookahead proposals'); await page.getByRole('button', {name:`Inspect run ${plannedRow+1}`, exact:true}).click(); assert.match(await page.textContent('#study-run-detail'), /Budgeted experiment planning/); assert.match(await page.textContent('#study-run-detail'), /backend factored/i); await page.click('#replay-button'); await page.waitForFunction(() => !state.active && !state.busy && document.querySelector('#notice').textContent.includes('Replay verified'), null, {timeout:60000}); await page.screenshot({path:path.join(output,'evidence-workbench-desktop.png'),fullPage:true}); await page.setViewportSize({width:390,height:900}); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth), true, 'Mobile workbench must not overflow horizontally'); await page.screenshot({path:path.join(output,'evidence-workbench-mobile.png'),fullPage:true}); assert.deepEqual(errors, []); assert.equal(serverErrors, ''); console.log('PASS: backend/lookahead controls and actual factored run/study/export/replay, checked experiment planning, fault controls, native validation, actual fault run/study/export/replay, calibration and tolerance controls, structural instance seeds, independent-target reliability, public model diagnostics, row inspection, cancellable replay/import, responsive layout'); } catch (error) { if (page && output) { await page.screenshot({path:path.join(output,'failure.png'),fullPage:true}).catch(() => {}); const context = await page.evaluate(() => ({ notice:document.querySelector('#notice')?.textContent, job:document.querySelector('#job-message')?.textContent, status:document.querySelector('#job-status')?.textContent, active:state.active, completedSchema:state.completed?.result?.schema })).catch(() => ({})); if(context.active?.id&&base){ context.serverJob=await page.request.get(base+`api/runs/${context.active.id}`).then(r=>r.json()).then(j=>({status:j.status,error:j.error,progress:j.progress})).catch(e=>({error:String(e)})); } fs.writeFileSync(path.join(output,'failure-context.json'),JSON.stringify({error:String(error),serverErrors,pageErrors:errors,...context},null,2)); console.error('Workbench failure context:',context); } throw error; } finally { if (browser) await browser.close(); child.kill(); } })().catch(error => {console.error(error);process.exitCode=1;});