Keep late browser refusal terminal during context cleanup

A denial arriving as the browser closes must leave a failed bounded receipt and suppress recovery authentication. Exercise real late HTTP401/403 responses alongside the earlier navigation and API controls.

Contract-Neutral: Enforces the existing all-response refusal contract through final context disposal; no new acceptance route or permission.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-04 03:55:53 +01:00
parent d855d322e8
commit 30ca96816f
3 changed files with 25 additions and 2 deletions
@@ -490,11 +490,12 @@ def verify_browser_recovery_controls(controls_path: Path) -> None:
fixed = {(kind, "upgrade") for kind in ("success", "anonymous-probes", "restart-gap",
"wrong-preview", "silent-stream", "stuck-modal")}
fixed.add(("success", "recovery"))
fixed.update((f"cleanup-{status}", "upgrade") for status in (401, 403))
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 len(results) == len(fixed) == 31
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}"
@@ -243,8 +243,13 @@ async function runJourney(browser, options) {
throw error;
} finally {
await closeContext();
if (record.access_refused) {
record.status = 'failed';
record.failure_type ||= 'AssertionOrOperationError';
}
if (options.outputDir) fs.writeFileSync(path.join(options.outputDir, `browser-${options.mode}.json`), JSON.stringify(record, null, 2) + '\n');
assert.equal(record.context_closed, true, 'browser context cleanup failed');
requireAccess(); // A response received during cleanup cannot become a pass.
}
return record;
}
@@ -79,6 +79,7 @@ function fixture(kind) {
if (refuse('authenticated-api')) return;
return json(200, {});
}
if (url.pathname === '/api/cleanup-control') { refuse('cleanup'); return; }
if (url.pathname === '/api/updates/check') {
if (refuse('check')) return;
return json(200, { latestVersion: kind === 'wrong-preview' ? '6.5.0-rc.1' : to.slice(1), downloadUrl });
@@ -143,6 +144,7 @@ async function runControls(outputDir) {
['anonymous-probes', 'upgrade', true, 1, 1], ['restart-gap', 'upgrade', true, 1, 1],
['wrong-preview', 'upgrade', false, 0, 1], ['silent-stream', 'upgrade', false, 1, 1],
['stuck-modal', 'upgrade', false, 1, 1],
['cleanup-401', 'upgrade', false, 1, 1], ['cleanup-403', 'upgrade', false, 1, 1],
];
for (const endpoint of ['navigation', 'authenticated-navigation', 'login', 'initial-version',
'authenticated-api', 'check', 'apply', 'stream', 'version', 'health', 'read']) {
@@ -158,7 +160,22 @@ async function runControls(outputDir) {
const folder = path.resolve(outputDir, `${kind}-${mode}`); fs.mkdirSync(folder, { recursive: true });
let passed = false;
try {
await runJourney(browser, { origin: `http://127.0.0.1:${target.server.address().port}`, mode, from, to,
// A real denied HTTP response deliberately arrives during cleanup,
// after the main checks. The observer must not leave a passed receipt.
// Only this fixture delays context disposal; no response is fabricated.
const testBrowser = kind.startsWith('cleanup-') ? {
version: () => browser.version(),
newContext: async options => {
const context = await browser.newContext(options);
const close = context.close.bind(context);
context.close = async () => {
await context.pages()[0].evaluate(() => fetch('/api/cleanup-control'));
await close();
};
return context;
},
} : browser;
await runJourney(testBrowser, { origin: `http://127.0.0.1:${target.server.address().port}`, mode, from, to,
expected: mode === 'upgrade' ? to : from, auth: { username: 'synthetic', password: 'synthetic' },
timeout: 1500, readinessTimeout: 3000, outputDir: folder });
passed = true;