mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(ci): reject false browser environment cancellation passes
Require typed cancellation and confirmed external-browser cleanup in the real environment verifier. Close its provider and CDP observer while preserving primary and cleanup failures. Cover six controlled outcomes through the verifier's actual cancellation path. Fixes #4033 Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { AnalysisCancelledError } from "../../dist/domain/analysisErrorCore.js";
|
||||
import { runScenarioCli } from "./browser-scenario-verifier.mjs";
|
||||
import { requireMcpEvidenceResult } from "./mcp-verifier-results.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
|
||||
const expected = {
|
||||
timezone: "Pacific/Honolulu",
|
||||
@@ -113,19 +114,21 @@ export async function verifyScenarioEnvironment(endpoint, targetId, origin) {
|
||||
assert.equal(value.result.type, "string");
|
||||
return JSON.parse(value.result.value);
|
||||
};
|
||||
const baseline = await readEnvironment();
|
||||
const scenario = environmentScenario({
|
||||
endpoint,
|
||||
targetId,
|
||||
origin,
|
||||
values: expected,
|
||||
});
|
||||
const assertRestored = async () => {
|
||||
assert.deepEqual(await readEnvironment(), baseline);
|
||||
const remaining = await (await fetch(`${endpoint}/json/list`)).json();
|
||||
assert.ok(remaining.some(({ id }) => id === targetId));
|
||||
};
|
||||
const provider = createBrowserScenarioProvider(process.env);
|
||||
let verificationError;
|
||||
try {
|
||||
const baseline = await readEnvironment();
|
||||
const scenario = environmentScenario({
|
||||
endpoint,
|
||||
targetId,
|
||||
origin,
|
||||
values: expected,
|
||||
});
|
||||
const assertRestored = async () => {
|
||||
assert.deepEqual(await readEnvironment(), baseline);
|
||||
const remaining = await (await fetch(`${endpoint}/json/list`)).json();
|
||||
assert.ok(remaining.some(({ id }) => id === targetId));
|
||||
};
|
||||
assertCapture((await runScenarioCli(scenario)).normalized_result);
|
||||
await assertRestored();
|
||||
const transport = new StdioClientTransport({
|
||||
@@ -167,7 +170,6 @@ export async function verifyScenarioEnvironment(endpoint, targetId, origin) {
|
||||
await transport.close();
|
||||
}
|
||||
await assertRestored();
|
||||
const provider = createBrowserScenarioProvider(process.env);
|
||||
const initializationFailure = await provider.captureScenario({
|
||||
...scenario,
|
||||
environment: { ...scenario.environment, timezone: "REA/Invalid" },
|
||||
@@ -204,8 +206,14 @@ export async function verifyScenarioEnvironment(endpoint, targetId, origin) {
|
||||
cancellation_cleanup: true,
|
||||
external_target_preserved: true,
|
||||
};
|
||||
} catch (cause) {
|
||||
verificationError = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await connection.close();
|
||||
await closeVerifierResources(
|
||||
[() => provider.close(), () => connection.close()],
|
||||
verificationError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +364,11 @@ const releaseScenario = async (connection) => {
|
||||
});
|
||||
};
|
||||
|
||||
async function verifyCancelledEnvironment(provider, connection, scenario) {
|
||||
export async function verifyCancelledEnvironment(
|
||||
provider,
|
||||
connection,
|
||||
scenario,
|
||||
) {
|
||||
const controller = new AbortController();
|
||||
const priorSnapshot = await readScenarioSnapshot(connection);
|
||||
const pending = provider.captureScenario(
|
||||
@@ -378,9 +390,15 @@ async function verifyCancelledEnvironment(provider, connection, scenario) {
|
||||
await settlement;
|
||||
}
|
||||
const cancelled = await pending;
|
||||
assert.ok(
|
||||
!cancelled.ok ||
|
||||
if (!cancelled.ok) {
|
||||
if (!(cancelled.error instanceof AnalysisCancelledError))
|
||||
throw cancelled.error;
|
||||
assert.equal(cancelled.error.cleanupIncomplete, false);
|
||||
} else {
|
||||
assert.ok(
|
||||
cancelled.value.steps.some(({ status }) => status === "cancelled"),
|
||||
"Cancellation unexpectedly completed every action",
|
||||
);
|
||||
"Cancellation unexpectedly completed every action",
|
||||
);
|
||||
assert.equal(cancelled.value.browser.cleanup, "disconnected-external");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
it("requires actual cancellation and confirmed cleanup before the environment verifier passes", async () => {
|
||||
// Execute the verifier's actual oracle with controlled provider outcomes.
|
||||
// The real-browser lane separately proves emulation and restoration in Chrome.
|
||||
const { stdout } = await execute(process.execPath, [
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
String.raw`
|
||||
const { verifyCancelledEnvironment } = await import(process.argv[1]);
|
||||
const { AnalysisCancelledError, AnalysisProtocolError } = await import(
|
||||
new URL("../../dist/domain/analysisErrorCore.js", process.argv[1])
|
||||
);
|
||||
const outcomes = [];
|
||||
for (const kind of ["cancelled", "protocol-error", "cleanup-failed", "completed", "cancelled-step", "incomplete-step"]) {
|
||||
let snapshots = 0;
|
||||
let aborted = false;
|
||||
const connection = {
|
||||
send: async () => ({ result: {
|
||||
type: "string",
|
||||
value: JSON.stringify({
|
||||
timeOrigin: ++snapshots,
|
||||
environment: { timezone: "Pacific/Honolulu", locale: "fr-FR", dpr: 2, width: 900, height: 600 },
|
||||
}),
|
||||
} }),
|
||||
};
|
||||
const error = kind === "protocol-error"
|
||||
? new AnalysisProtocolError("controlled protocol failure")
|
||||
: new AnalysisCancelledError("capture_browser_scenario", kind === "cleanup-failed"
|
||||
? { cleanup: { reason: "controlled cleanup failure", resources: ["browser_transport"] } }
|
||||
: undefined);
|
||||
const result = ["completed", "cancelled-step", "incomplete-step"].includes(kind)
|
||||
? { ok: true, value: {
|
||||
steps: [{ status: kind === "completed" ? "completed" : "cancelled" }],
|
||||
browser: { cleanup: kind === "incomplete-step" ? "incomplete" : "disconnected-external" },
|
||||
} }
|
||||
: { ok: false, error };
|
||||
const provider = {
|
||||
captureScenario: (_scenario, { signal }) => new Promise((resolve) => {
|
||||
signal.addEventListener("abort", () => { aborted = true; resolve(result); }, { once: true });
|
||||
}),
|
||||
};
|
||||
let accepted = false;
|
||||
try {
|
||||
await verifyCancelledEnvironment(provider, connection, { actions: [{}] });
|
||||
accepted = true;
|
||||
} catch {}
|
||||
outcomes.push({ kind, accepted, aborted, snapshots });
|
||||
}
|
||||
console.log(JSON.stringify(outcomes));
|
||||
`,
|
||||
new URL(
|
||||
"../../../scripts/lib/browser-scenario-environment-e2e.mjs",
|
||||
import.meta.url,
|
||||
).href,
|
||||
]);
|
||||
expect(JSON.parse(stdout)).toEqual(
|
||||
[
|
||||
["cancelled", true],
|
||||
["protocol-error", false],
|
||||
["cleanup-failed", false],
|
||||
["completed", false],
|
||||
["cancelled-step", true],
|
||||
["incomplete-step", false],
|
||||
].map(([kind, accepted]) => ({
|
||||
kind,
|
||||
accepted,
|
||||
aborted: true,
|
||||
snapshots: 2,
|
||||
})),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user