diff --git a/scripts/tests/test_release_lifecycle_rehearsal.py b/scripts/tests/test_release_lifecycle_rehearsal.py index dac7f9087c..97e5a7c7e6 100644 --- a/scripts/tests/test_release_lifecycle_rehearsal.py +++ b/scripts/tests/test_release_lifecycle_rehearsal.py @@ -477,5 +477,72 @@ class WorkflowContractTest(unittest.TestCase): self.assertNotIn("release-lifecycle-rehearsal", path.read_text(encoding="utf-8")) +def verify_browser_recovery_controls(controls_path: Path) -> None: + """Feed exact Chromium receipts into the real guard in a jq-equipped proof VM. + + The guard's permitted process is a local sentinel, not native recovery. + Source proof binds both this source and the browser input's exact hash. + """ + controls = json.loads(controls_path.read_text(encoding="utf-8")) + assert controls["schema_version"] == 1 + assert controls["scope"] == "offline adversarial observer controls only" + assert controls["client_version"] == "1.56.1" + fixed = {(kind, "upgrade") for kind in ("success", "anonymous-probes", "restart-gap", + "wrong-preview", "silent-stream", "stuck-modal")} + fixed.add(("success", "recovery")) + for endpoint in ("navigation", "authenticated-navigation", "login", "initial-version", + "authenticated-api", "check", "apply", "stream", "version", "health", "read"): + fixed.update((f"{endpoint}-{status}", "upgrade") for status in (401, 403)) + results = controls["results"] + assert len(results) == len(fixed) == 29 + assert {(row["fixture"], row["mode"]) for row in results} == fixed + source = HARNESS_PATH.read_text(encoding="utf-8") + function = "run_browser_journey() {" + source.split("run_browser_journey() {", 1)[1].split("\n}\n", 1)[0] + "\n}" + observed = [] + for row in results: + kind, mode, receipt = row["fixture"], row["mode"], row["receipt"] + refused = re.search(r"-(401|403)$", kind) is not None + assert receipt["mode"] == mode and receipt["context_closed"] is True + assert receipt["access_refused"] is refused + assert row["requests_after_denial"] == 0 + if refused: + assert receipt["status"] == "failed" and row["denials"] == 1 + assert receipt["access_refusals"][0]["status"] == int(kind[-3:]) + if mode != "upgrade": + continue + with tempfile.TemporaryDirectory() as tmp: + work = Path(tmp) + (work / "state").mkdir() + (work / "state/browser-upgrade.json").write_text(json.dumps(receipt), encoding="utf-8") + started = work / "process-started" + sentinel = work / "process-sentinel.sh" + sentinel.write_text("#!/bin/sh\nprintf started > " + json.dumps(str(started)) + "\n", encoding="utf-8") + sentinel.chmod(0o700) + result = subprocess.run(["bash", "-c", "set -euo pipefail\n" + function + + "\nrun_browser_journey recovery v6.4.5\n"], capture_output=True, text=True, timeout=5, + env={"PATH": "/usr/local/bin:/usr/bin:/bin", "WORK_DIR": tmp, "ROOT_DIR": str(REPO_ROOT), + "SEED_ADMIN_USER": "synthetic", "ADMIN_PASSWORD": "synthetic", "PULSE_REHEARSAL_NODE": str(sentinel), + "PULSE_REHEARSAL_BROWSER_ORIGIN": "https://ephemeral.tawny-powan.ts.net", + "FROM_TAG": "v6.4.5", "TO_TAG": "v6.4.6-rc.1"}) + assert result.returncode == (1 if refused else 0) and result.stderr == "" + assert started.exists() is (not refused) + auth_prepared = (work / "state/browser-auth.json").exists() + assert auth_prepared is (not refused) + if refused: + assert json.loads((work / "state/browser-recovery.json").read_text()) == { + "status": "not-executed", "reason": "stopped-access-no-reauthentication"} + observed.append({"fixture": kind, "guard_exit": result.returncode, + "browser_process_started": started.exists(), "auth_prepared": auth_prepared}) + print(json.dumps({"scope": "actual shell guard with bound offline Chromium receipts, not native recovery", + "browser_controls": len(results), "executed_recovery_guards": len(observed), "results": observed})) + + if __name__ == "__main__": - unittest.main() + if len(sys.argv) == 3 and sys.argv[1] == "--browser-controls": + try: + verify_browser_recovery_controls(Path(sys.argv[2])) + except Exception: + print("Bound browser receipts failed recovery controls; no installed proof.", file=sys.stderr) + sys.exit(1) + else: + unittest.main() diff --git a/tests/integration/scripts/release-browser-journey.cjs b/tests/integration/scripts/release-browser-journey.cjs index 2a93dcd04b..12fe55ce9b 100644 --- a/tests/integration/scripts/release-browser-journey.cjs +++ b/tests/integration/scripts/release-browser-journey.cjs @@ -50,7 +50,7 @@ async function runJourney(browser, options) { let closing; const closeContext = () => closing ||= context.close().then( () => { record.context_closed = true; }, - () => { record.context_closed = false; }, + () => { record.context_closed = false; record.status = 'failed'; record.failure_type = 'ContextCleanupError'; }, ); const requireAccess = () => assert.equal(record.access_refused, false, 'access refused; stop without another login, read or apply'); diff --git a/tests/integration/scripts/release-browser-journey.test.cjs b/tests/integration/scripts/release-browser-journey.test.cjs index 3dea8dfe04..dcca53da86 100644 --- a/tests/integration/scripts/release-browser-journey.test.cjs +++ b/tests/integration/scripts/release-browser-journey.test.cjs @@ -4,7 +4,6 @@ const assert = require('node:assert/strict'); const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); -const { spawnSync } = require('node:child_process'); const { chromium } = require('playwright'); const { validate, judge, runJourney } = require('./release-browser-journey.cjs'); const from = 'v6.4.5', to = 'v6.4.6-rc.1'; @@ -134,33 +133,6 @@ function fixture(kind) { } }; } -function recoveryGuard(folder, refused) { - // Exercise the ACTUAL shell guard with this browser's receipt. The permitted - // branch's process is a fixed local sentinel, not installation or recovery. - const state = path.join(folder, 'state'); fs.mkdirSync(state); - fs.copyFileSync(path.join(folder, 'browser-upgrade.json'), path.join(state, 'browser-upgrade.json')); - const invocation = path.join(folder, 'guard-process-started'); - const sentinel = path.join(folder, 'process-sentinel.sh'); - fs.writeFileSync(sentinel, `#!/bin/sh\nprintf started > ${JSON.stringify(invocation)}\n`, { mode: 0o700 }); - const harness = fs.readFileSync(path.resolve(__dirname, '../../../scripts/release_lifecycle_rehearsal.sh'), 'utf8'); - const body = 'run_browser_journey() {' + harness.split('run_browser_journey() {')[1].split('\n}\n')[0] + '\n}'; - const result = spawnSync('bash', ['-c', `set -euo pipefail\n${body}\nrun_browser_journey recovery v6.4.5\n`], { - env: { PATH: '/usr/local/bin:/usr/bin:/bin', WORK_DIR: folder, ROOT_DIR: path.resolve(__dirname, '../../..'), - SEED_ADMIN_USER: 'synthetic', ADMIN_PASSWORD: 'synthetic', PULSE_REHEARSAL_NODE: sentinel, - PULSE_REHEARSAL_BROWSER_ORIGIN: 'https://ephemeral.tawny-powan.ts.net', FROM_TAG: from, TO_TAG: to }, - encoding: 'utf8', timeout: 5000, - }); - assert.equal(result.error, undefined, 'shell guard must execute'); - assert.equal(result.stderr, '', 'no raw error/receipt contents'); - assert.equal(result.status, refused ? 1 : 0, 'actual recovery guard verdict'); - assert.equal(fs.existsSync(invocation), !refused, 'no later browser process after refusal'); - assert.equal(fs.existsSync(path.join(state, 'browser-auth.json')), !refused, 'no later auth preparation after refusal'); - if (refused) assert.deepEqual(JSON.parse(fs.readFileSync(path.join(state, 'browser-recovery.json'))), - { status: 'not-executed', reason: 'stopped-access-no-reauthentication' }); - else fs.writeFileSync(path.join(state, 'browser-auth.json'), ''); - return { executed_guard: true, exit: result.status, browser_process_started: !refused, auth_prepared: !refused }; -} - async function runControls(outputDir) { pureControls(); const browser = await chromium.launch({ headless: true, channel: 'chromium', args: ['--no-sandbox'] }); @@ -211,11 +183,10 @@ async function runControls(outputDir) { if (kind.startsWith('read-')) assert.equal(receipt.protected_read_status, Number(kind.slice(-3))); if (kind === 'anonymous-probes') assert.equal(counts.anonymous_probes, 2, 'normal pre-login denial controls ran'); if (kind === 'restart-gap') assert.equal(counts.transport_gaps, 1, 'restart transport control ran'); - const recovery = mode === 'upgrade' ? recoveryGuard(folder, refusal) : null; results.push({ fixture: kind, mode, expected_rejection: !expectedPass, observed_status: receipt.status, - active_step: receipt.active_step, ...counts, recovery_guard: recovery, receipt }); + active_step: receipt.active_step, ...counts, receipt }); } - fs.writeFileSync(path.join(outputDir, 'observer-controls.json'), JSON.stringify({ scope: 'offline adversarial observer controls only', + fs.writeFileSync(path.join(outputDir, 'observer-controls.json'), JSON.stringify({ schema_version: 1, scope: 'offline adversarial observer controls only', browser_version: browser.version(), client_version: require('playwright/package.json').version, results }, null, 2) + '\n'); console.log(JSON.stringify({ observer_controls: results.length, scope: 'fixtures, not native qualification', results })); } finally { await browser.close(); }