CROSSWIRE / tools /check_workbench.cjs
Kai9987kai's picture
Release crosswire-formal-synthesis-2026-11-03 from GitHub 91e7ac87c4e4
450ac2f verified
Raw History Blame Contribute Delete
33.7 kB
/* 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.','<img src=x onerror=alert(1)>']}}));
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;});