diff --git a/docs/adb-device-analysis.md b/docs/adb-device-analysis.md index 634a004b0a..1dfcb826da 100644 --- a/docs/adb-device-analysis.md +++ b/docs/adb-device-analysis.md @@ -257,12 +257,12 @@ beyond already-connected devices (`adb connect`) are not exposed yet. ## Real verification -`npm run verify:adb` exercises every operation against a live device. It +`npm run verify:adb` exercises observation and transfer operations against a live device. It requires `adb` (or `REA_ADB_PATH`) and any attached emulator or phone; `--serial` selects one when several are attached, `--pull PACKAGE` pulls a -real package and verifies the returned digests against the files on disk, -and `--install-apk PATH` drives a full install → resolve → uninstall -lifecycle with a lane-owned probe. +real package and verifies the returned digests against the files on disk. +The optional `--install-apk` lifecycle is unavailable until the selected APK +identity and device cleanup ownership can be established safely. Record: verified against adb 34.0.5 on Linux against an Android 14 (API 34) x86_64 emulator — all operations above, a 224-package inventory, diff --git a/docs/testing.md b/docs/testing.md index d619d375c3..19e3c60d5e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1423,12 +1423,12 @@ budgets. ## ADB device acquisition -`npm run verify:adb` exercises every ADB operation against a live device or +`npm run verify:adb` exercises ADB observation and transfer operations against a live device or emulator through the caller's adb binary (`REA_ADB_PATH` or PATH). No SDK -installation or emulator management is involved; the device-mutating tools -run only in the explicit lifecycle section (`--install-apk` drives a -lane-owned install → resolve → start → observe → force-stop → uninstall -probe). `--pull` acquires a real package and re-digests the pulled files on +installation or emulator management is involved. `--install-apk` is unavailable +until the selected APK identity and device cleanup ownership can be established +safely. The transfer probe creates and removes a temporary device file. `--pull` +acquires a real package and re-digests the pulled files on disk against the returned SHA-256 values; `--serial` selects a device when several are attached. diff --git a/scripts/lib/test-discovery.d.mts b/scripts/lib/test-discovery.d.mts index 259a21fbb8..54b6d1f3ee 100644 --- a/scripts/lib/test-discovery.d.mts +++ b/scripts/lib/test-discovery.d.mts @@ -10,5 +10,10 @@ export function compareTestDiscovery( readonly file: string; readonly projects: readonly string[]; }[]; + readonly wrongProjects: readonly { + readonly file: string; + readonly expected: string; + readonly actual: string; + }[]; readonly discovered: number; }; diff --git a/scripts/lib/test-discovery.mjs b/scripts/lib/test-discovery.mjs index dbcafb220a..99af5bc6b7 100644 --- a/scripts/lib/test-discovery.mjs +++ b/scripts/lib/test-discovery.mjs @@ -1,4 +1,5 @@ import { relative, resolve } from "node:path"; +import { projectForTest } from "./test-projects.mjs"; /** Compare actual Vitest discovery with repository-owned test paths. */ export const compareTestDiscovery = (expectedPaths, discovery, root) => { @@ -36,6 +37,14 @@ export const compareTestDiscovery = (expectedPaths, discovery, root) => { const projects = projectsByFile.get(file); return projects.length > 1 ? [{ file, projects: sorted(projects) }] : []; }), + wrongProjects: sorted(expected).flatMap((file) => { + const projects = projectsByFile.get(file); + if (!projects || projects.length !== 1) return []; + const expectedProject = projectForTest(file).name; + return projects[0] === expectedProject + ? [] + : [{ file, expected: expectedProject, actual: projects[0] }]; + }), discovered: projectsByFile.size, }; }; diff --git a/scripts/test-website.py b/scripts/test-website.py index 54b4679585..227dd4b122 100644 --- a/scripts/test-website.py +++ b/scripts/test-website.py @@ -539,11 +539,13 @@ class WebsiteMaintenanceChecks(unittest.TestCase): self.assertIn(b"color: black", response.read()) with self.assertRaises(HTTPError) as error: urlopen(base + prefix + "missing") - self.assertEqual(error.exception.code, 404) + with contextlib.closing(error.exception) as response: + self.assertEqual(response.code, 404) if prefix != "/": with self.assertRaises(HTTPError) as error: urlopen(base + "/guide/") - self.assertEqual(error.exception.code, 404) + with contextlib.closing(error.exception) as response: + self.assertEqual(response.code, 404) finally: server.shutdown() worker.join() diff --git a/scripts/verify-adb.mjs b/scripts/verify-adb.mjs index 58183f04a1..8d0685c908 100644 --- a/scripts/verify-adb.mjs +++ b/scripts/verify-adb.mjs @@ -15,8 +15,10 @@ if (options.help) { process.stdout.write( "Usage: npm run verify:adb [-- --serial SERIAL --pull PACKAGE]\n" + "Requires a caller-selected adb binary (REA_ADB_PATH or PATH) and a\n" + - "connected device or emulator. Verifies all five operations against\n" + - "the live device; --pull acquires a real package and re-digests the\n" + + "connected device or emulator. Verifies observation and transfer operations\n" + + "against the live device. --install-apk is unavailable until APK identity\n" + + "and device cleanup ownership can be established safely.\n" + + "--pull acquires a real package and re-digests the\n" + "files on disk against the returned SHA-256 values.\n", ); process.exit(0); @@ -356,79 +358,7 @@ try { `versionCode=${String(details.version_code)}`, ); } - if (options.installApk !== null && options.installApk !== undefined) { - const probePackage = "com.rea.adb.probe"; - const installedProbe = await run("install_adb_package", { - serial, - apk_path: options.installApk, - replace: false, - }); - if (installedProbe !== null) { - check( - "install reports the APK digest", - typeof installedProbe.sha256 === "string" && - installedProbe.sha256.length === 64, - `${installedProbe.bytes} bytes`, - ); - const resolved = await run("resolve_adb_packages", { - serial, - query: "rea.adb.probe", - }); - if (resolved !== null) - check( - "resolve finds the installed probe uniquely", - resolved.matches.length === 1 && - resolved.matches[0]?.package_name === probePackage, - `${resolved.matches.length} match(es)`, - ); - const started = await run("start_adb_app", { - serial, - package: probePackage, - }); - if (started !== null) - check( - "app start resolves and launches the launcher activity", - typeof started.component === "string" && - started.component.startsWith(probePackage), - String(started.component), - ); - const observedProcesses = await run("list_adb_processes", { serial }); - if (observedProcesses !== null) - check( - "started app appears in the process listing", - observedProcesses.processes.some( - (process) => process.name === probePackage, - ), - ); - const stopped = await run("stop_adb_app", { - serial, - package: probePackage, - }); - if (stopped !== null) - check( - "force-stop completes the dynamic-analysis run", - stopped.package_name === probePackage, - ); - const removed = await run("uninstall_adb_package", { - serial, - package: probePackage, - }); - if (removed !== null) - check( - "uninstall completes the lifecycle", - removed.package_name === probePackage, - ); - const afterRemoval = await run("resolve_adb_packages", { - serial, - query: "rea.adb.probe", - }); - if (afterRemoval !== null) - check( - "resolve reports zero matches after removal", - afterRemoval.matches.length === 0, - ); - } - } + } finally { await rm(transferRoot, { recursive: true, force: true }); } @@ -459,7 +389,7 @@ if (failures > 0) { } function parseArguments(argv) { - const parsed = { help: false, serial: null, pull: null, installApk: null }; + const parsed = { help: false, serial: null, pull: null }; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--help" || argument === "-h") parsed.help = true; @@ -470,8 +400,10 @@ function parseArguments(argv) { parsed.pull = argv[index + 1] ?? null; index += 1; } else if (argument === "--install-apk") { - parsed.installApk = argv[index + 1] ?? null; - index += 1; + process.stderr.write( + "--install-apk is unavailable: APK identity and device cleanup ownership cannot be established safely\n", + ); + process.exit(1); } else { process.stderr.write(`unknown argument: ${argument}\n`); process.exit(2); diff --git a/scripts/verify-bun-standalone.mjs b/scripts/verify-bun-standalone.mjs index 19d8e0024c..82b6f7edda 100644 --- a/scripts/verify-bun-standalone.mjs +++ b/scripts/verify-bun-standalone.mjs @@ -8,7 +8,7 @@ import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; const root = await PrivateRuntimeRoot.create({ prefix: "rea-bun-verifier-" }); -const commands = await createVerifierCommands(); +let commands; const run = (command, args, options = {}) => commands.exec(command, args, { timeout: 120_000, @@ -19,6 +19,7 @@ const run = (command, args, options = {}) => // only after every owned command family has settled. let commandsClosed = false; await withVerifierCleanup(async () => { + commands = await createVerifierCommands(); const source = join(root.path, "hello.ts"); await writeFile(source, 'console.log("REA_BUN_VERIFICATION");\n'); const docker = process.argv.includes("--docker"); @@ -104,7 +105,7 @@ await withVerifierCleanup(async () => { ); }, [ async () => { - await commands.close(); + if (commands !== undefined) await commands.close(); commandsClosed = true; }, async () => { diff --git a/scripts/verify-claude-client.mjs b/scripts/verify-claude-client.mjs index 4ed2a7356d..94a4366bb5 100644 --- a/scripts/verify-claude-client.mjs +++ b/scripts/verify-claude-client.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { createCatalogIdentity } from "../dist/catalogIdentity.js"; @@ -15,6 +15,7 @@ import { TOOL_CONTRACTS, toolContract, } from "../dist/contracts/toolContracts.js"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; @@ -306,7 +307,9 @@ await withVerifierCleanup(async () => { assert( parsed.plannedActions.every( (a) => - a.target === configPath || a.target.startsWith(skillDirectory), + a.target === configPath || + (isAbsolute(a.target) && + isPathWithinRoot(skillDirectory, a.target)), ), ); } diff --git a/scripts/verify-commandcode-client.mjs b/scripts/verify-commandcode-client.mjs index e88fa3ee9f..70d6f5f565 100644 --- a/scripts/verify-commandcode-client.mjs +++ b/scripts/verify-commandcode-client.mjs @@ -6,6 +6,7 @@ import { isAbsolute, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; @@ -54,6 +55,7 @@ const expectedNames = TOOL_CONTRACTS.map( ).sort(); let clientVersion = null; let expectedSkill; +let skillCallId; let inventoryChecked = false; let catalogSize = null; let skillLoaded = false; @@ -180,10 +182,24 @@ await withVerifierCleanup(async () => { finalSeen = true; return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" }; } - if (stage === 0) - return call("activate_skill", { name: "reverse-engineer-anything" }); + if (stage === 0) { + const request = call("activate_skill", { + name: "reverse-engineer-anything", + }); + skillCallId = request.tool_calls[0].id; + return request; + } assert( - strings.some((text) => text.includes(expectedSkill)), + skillCallId !== undefined && + expectedSkill !== undefined && + body.messages.some( + (message) => + message.role === "tool" && + message.tool_call_id === skillCallId && + textValues(message.content).some((text) => + text.includes(expectedSkill), + ), + ), "The model must receive the complete activated skill body", ); skillLoaded = true; @@ -322,8 +338,8 @@ await withVerifierCleanup(async () => { result.plannedActions.every( (action) => action.target === configPath || - action.target === skillDirectory || - action.target.startsWith(skillDirectory + "/"), + (isAbsolute(action.target) && + isPathWithinRoot(skillDirectory, action.target)), ), "Refuse setup writes outside the disposable account's configuration and skill", ); @@ -342,9 +358,16 @@ await withVerifierCleanup(async () => { assert.deepEqual(repeated.appliedActions, []); assert.equal(await readFile(configPath, "utf8"), configured); assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original); - expectedSkill = (await readFile(join(skillDirectory, "SKILL.md"), "utf8")) - .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "") - .trim(); + const installedSkill = await readFile( + join(skillDirectory, "SKILL.md"), + "utf8", + ); + const frontmatter = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec( + installedSkill, + ); + assert(frontmatter, "Installed skill must have authored frontmatter"); + expectedSkill = installedSkill.slice(frontmatter[0].length).trim(); + assert(expectedSkill.length > 0, "Installed skill body must not be empty"); const response = await saveVerifierCommandOutput( () => commands.exec( diff --git a/scripts/verify-copilot-client.mjs b/scripts/verify-copilot-client.mjs index acf9f9eba3..3f61f89d2a 100644 --- a/scripts/verify-copilot-client.mjs +++ b/scripts/verify-copilot-client.mjs @@ -12,6 +12,7 @@ import { import { advertisedInputJsonSchema } from "../dist/contracts/toolSchemaMetadata.js"; import { presentAdvertisedInputSchema } from "../dist/contracts/compactInputSchema.js"; import { compactPresentationForMcpInputSchemaProfile } from "../dist/config/mcpInputSchemaProfile.js"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; @@ -300,8 +301,8 @@ await withVerifierCleanup(async () => { parsed.plannedActions.every( (a) => a.target === configPath || - a.target === skillDirectory || - a.target.startsWith(skillDirectory + "/"), + (isAbsolute(a.target) && + isPathWithinRoot(skillDirectory, a.target)), ), ); } diff --git a/scripts/verify-deepseek-client.mjs b/scripts/verify-deepseek-client.mjs index 9b800422f9..1d960a0ec0 100644 --- a/scripts/verify-deepseek-client.mjs +++ b/scripts/verify-deepseek-client.mjs @@ -56,6 +56,7 @@ let catalog; let patternCount = 0; let skillRequested = false; let skillLoaded = false; +let expectedSkillBody; let evidenceSeen = false; let finalSeen = false; let artifactDigest; @@ -169,11 +170,34 @@ await withVerifierCleanup(async () => { const toolMessages = (body.messages ?? []).filter( (message) => message.role === "tool", ); - const serialized = JSON.stringify(toolMessages); - skillLoaded ||= - serialized.includes( - '', - ) && serialized.includes(dirname(skillPath)); + skillLoaded ||= toolMessages.some((message) => { + if ( + typeof message.content !== "string" || + expectedSkillBody === undefined + ) + return false; + const open = + '\n\n'; + const start = message.content.indexOf(open); + if (start < 0) return false; + const resourcesStart = start + open.length; + const instructions = `\n\n\n\n${expectedSkillBody}\n\n`; + const resourcesEnd = message.content.indexOf( + instructions, + resourcesStart, + ); + if (resourcesEnd < 0) return false; + const expectedPath = dirname(skillPath) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + const resources = message.content.slice(resourcesStart, resourcesEnd); + return ( + !resources.includes("") && + !resources.includes("") && + resources.includes(expectedPath) + ); + }); for (const message of toolMessages) { const found = findClientResult( message.content, @@ -281,11 +305,15 @@ await withVerifierCleanup(async () => { const configured = await readFile(configPath, "utf8"); assert.deepEqual(withoutRea(configured), withoutRea(original)); assert(configured.includes("!!js process.cwd()")); - assert( - (await readFile(skillPath, "utf8")).includes( - "name: reverse-engineer-anything", - ), + const installedSkill = await readFile(skillPath, "utf8"); + assert(installedSkill.includes("name: reverse-engineer-anything")); + const frontmatter = /^---\r?\n[\s\S]*?^---(?:\r?\n|$)/mu.exec( + installedSkill, ); + assert(frontmatter, "Installed skill must have its authored frontmatter"); + expectedSkillBody = installedSkill.slice(frontmatter[0].length).trim(); + assert(expectedSkillBody.length > 0); + lifecycle.push("setup"); const repeated = await runRea( ["setup", "--client", "dsh", "--yes"], diff --git a/scripts/verify-flutter.mjs b/scripts/verify-flutter.mjs index b20d76d72b..0425711fda 100644 --- a/scripts/verify-flutter.mjs +++ b/scripts/verify-flutter.mjs @@ -104,6 +104,10 @@ await withVerifierCleanup( } // The same checks apply to the default selection and each remaining ABI. const preferred = await run("inspect_dart_aot", { path: apk }); + assert.ok( + abis.some((abi) => abi.abi === preferred.abi), + "Default AOT selection must be one of the identified ABIs", + ); for (const abi of abis) { const aot = abi.abi === preferred.abi diff --git a/scripts/verify-frida.mjs b/scripts/verify-frida.mjs index e42bd4a805..396ddf4f88 100644 --- a/scripts/verify-frida.mjs +++ b/scripts/verify-frida.mjs @@ -2,7 +2,7 @@ import { basename } from "node:path"; import { FridaInstrumentationService } from "../dist/application/frida/FridaInstrumentationService.js"; import { FridaInstrumentationManager } from "../dist/frida/FridaInstrumentationManager.js"; -import { isValidFridaRemoteAddress } from "../dist/contracts/fridaRemoteAddress.js"; +import { isValidFridaRemoteAddress } from "../dist/domain/fridaRemoteAddress.js"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; const options = parseArguments(process.argv.slice(2)); @@ -80,6 +80,10 @@ try { !Array.isArray(observed.messages) ) throw new Error("Frida returned no message observation"); + if (!Number.isSafeInteger(observed.pid) || observed.pid <= 0) + throw new Error("Frida returned no valid target PID"); + if (remote !== undefined && observed.pid !== options.pid) + throw new Error("Frida observed a different target PID"); const messageObserved = observed.messages.some( (message) => typeof message === "object" && diff --git a/scripts/verify-gemini-client.mjs b/scripts/verify-gemini-client.mjs index 9837ad53dd..eab017d6b4 100644 --- a/scripts/verify-gemini-client.mjs +++ b/scripts/verify-gemini-client.mjs @@ -2,13 +2,14 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join, resolve, sep } from "node:path"; +import { isAbsolute, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { TOOL_CONTRACTS, toolContract, } from "../dist/contracts/toolContracts.js"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; @@ -65,6 +66,7 @@ let fixture; let passed = false; let evidenceSeen = false; let skillLoaded = false; +let expectedSkillBody; let skillRequested = false; let finalSeen = false; let catalogSize = null; @@ -150,13 +152,21 @@ await withVerifierCleanup(async () => { if (args.includes("--dry-run")) { assert(parsed.plannedActions.some((a) => a.target === configPath)); assert( - parsed.plannedActions.some((a) => - a.target.startsWith(join(profile, ".agents", "skills") + sep), - ), + parsed.plannedActions.some((a) => { + const skills = join(profile, ".agents", "skills"); + return ( + isAbsolute(a.target) && + relative(skills, a.target) !== "" && + isPathWithinRoot(skills, a.target) + ); + }), ); assert( - parsed.plannedActions.every((a) => - a.target.startsWith(profile + sep), + parsed.plannedActions.every( + (a) => + isAbsolute(a.target) && + relative(profile, a.target) !== "" && + isPathWithinRoot(profile, a.target), ), ); } @@ -189,11 +199,15 @@ await withVerifierCleanup(async () => { assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []); assert.equal(await readFile(configPath, "utf8"), configured); assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original); - assert( - (await readFile(skillPath, "utf8")).includes( - "name: reverse-engineer-anything", - ), + const installedSkill = await readFile(skillPath, "utf8"); + assert(installedSkill.includes("name: reverse-engineer-anything")); + const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?/u.exec( + installedSkill, ); + assert(frontmatter, "Installed skill must have authored frontmatter"); + expectedSkillBody = (frontmatter[2] ?? "").trim(); + assert(expectedSkillBody.length > 0); + const target = join(lab, "fixture α测试"); await mkdir(target); await writeFile(join(target, "app.js"), CLIENT_JAVASCRIPT_FIXTURE_SOURCE); @@ -268,16 +282,32 @@ await withVerifierCleanup(async () => { .flatMap((c) => c.parts ?? []) .filter((p) => p.functionResponse) .map((p) => p.functionResponse); - skillLoaded ||= responses.some( - (r) => - r.name === "activate_skill" && - r.response?.output?.includes( - ``, - ) && - r.response.output.includes( + skillLoaded ||= responses.some((response) => { + const output = response.response?.output; + if ( + response.name !== "activate_skill" || + typeof output !== "string" || + expectedSkillBody === undefined + ) + return false; + const start = `\n \n ${expectedSkillBody}\n \n\n \n `; + const at = output.indexOf(start); + if (at < 0) return false; + const resourcesStart = at + start.length; + const resourcesEnd = output.indexOf( + "\n \n", + resourcesStart, + ); + if (resourcesEnd < 0) return false; + const resources = output.slice(resourcesStart, resourcesEnd); + return ( + !resources.includes("") && + !resources.includes("") && + resources.includes( join(profile, ".agents", "skills", "reverse-engineer-anything"), - ), - ); + ) + ); + }); const observed = findClientResult( responses, isClientJavaScriptEvidence, diff --git a/scripts/verify-grok-client.mjs b/scripts/verify-grok-client.mjs index 753c8ae998..0ea587e45f 100644 --- a/scripts/verify-grok-client.mjs +++ b/scripts/verify-grok-client.mjs @@ -6,6 +6,7 @@ import { isAbsolute, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; @@ -402,8 +403,8 @@ enabled = false parsed.plannedActions.every( (action) => action.target === configPath || - action.target === skillDirectory || - action.target.startsWith(skillDirectory + "/"), + (isAbsolute(action.target) && + isPathWithinRoot(skillDirectory, action.target)), ), "Refuse writes outside the owned profile/skill", ); diff --git a/scripts/verify-http3-network-captures.mjs b/scripts/verify-http3-network-captures.mjs index 55997a95dc..130a55802b 100644 --- a/scripts/verify-http3-network-captures.mjs +++ b/scripts/verify-http3-network-captures.mjs @@ -373,6 +373,7 @@ try { } const report = { run_id: run.run_id, + input_mode: inspectExisting ? "existing-captures" : "producer-attempt", status: primaryError || cleanupFailures.length ? "failed" : "passed", finished_at: new Date().toISOString(), ...(primaryError @@ -386,8 +387,11 @@ const report = { verifier, producer_audit: producerAudit, scope: - "Real Linux reverse HTTP/3, mitmproxy 12.2.3, aioquic 1.2.0 and guest cURL", + "Linux reverse HTTP/3 capture verification for mitmproxy 12.2.3, aioquic 1.2.0 and guest cURL", limitations: [ + ...(inspectExisting + ? ["Existing capture producer provenance was not authenticated by this attempt."] + : []), "No local/WireGuard, browser, QUIC v2, migration, 0-RTT, mTLS or replay claim.", "No HAR, interruption, certificate-failure or body-completeness coverage in this profile.", ], diff --git a/scripts/verify-il2cpp-metadata.mjs b/scripts/verify-il2cpp-metadata.mjs index 5bc7d1d6f8..362208cd09 100644 --- a/scripts/verify-il2cpp-metadata.mjs +++ b/scripts/verify-il2cpp-metadata.mjs @@ -17,7 +17,7 @@ import { hashVerifierFile } from "./lib/verifier-file-resources.mjs"; const args = process.argv.slice(2); if (args.includes("--help") || args.includes("-h")) { console.log( - "Usage: npm run verify:il2cpp:metadata -- [--metadata PATH]\nPATH is a caller-owned, unencrypted global-metadata.dat (version 24, 27, 29, 31 or 39). Without --metadata, only the advertised contract is verified.", + "Usage: npm run verify:il2cpp:metadata -- [--metadata PATH]\nPATH is a caller-owned, unencrypted global-metadata.dat (version 24, 27, 29, 31 or 39). Without --metadata, only the MCP tool name is checked.", ); process.exit(0); } @@ -178,4 +178,8 @@ await withVerifierCleanup(async () => { ); } }, [() => client.close(), () => transport.close(), () => resources.close()]); -console.log("All asserted IL2CPP metadata CLI/MCP checks passed"); +console.log( + path === null + ? "IL2CPP metadata MCP tool presence check passed" + : "All asserted IL2CPP metadata CLI/MCP checks passed", +); diff --git a/scripts/verify-jeb.mjs b/scripts/verify-jeb.mjs index 812917d73c..6abd439a5d 100644 --- a/scripts/verify-jeb.mjs +++ b/scripts/verify-jeb.mjs @@ -68,7 +68,9 @@ try { }); if (!decompiled.ok) throw new Error(decompiled.error.message); const text = decompiled.value.normalized_result.text; - if (!text.includes(target.marker)) + if (text.trim().length === 0) + throw new Error("Decompiled pseudo-code was empty"); + if (target.marker !== undefined && !text.includes(target.marker)) throw new Error( "Decompiled pseudo-code did not contain the fixture marker", ); @@ -83,7 +85,7 @@ try { open_project_observed: openRefusal === null, ...(openRefusal === null ? {} : { open_project_refusal: openRefusal }), unit_count: units.value.normalized_result.units.length, - decompiled_marker_observed: true, + decompiled_marker_observed: target.marker !== undefined, }; }, [ async () => { @@ -110,7 +112,7 @@ async function resolveTarget(selected, directory) { return { path: "/engine-side/target", itemAddress: selected.itemAddress, - marker: selected.marker ?? "", + marker: selected.marker, }; await writeFile( join(directory, "Hello.java"), @@ -153,6 +155,8 @@ function parseArguments(args) { values.set(flag, value); index += 1; } + if (values.get("--marker") === "") + throw new Error("--marker must not be empty"); return { help: false, ...(values.has("--item-address") diff --git a/scripts/verify-omp-client.mjs b/scripts/verify-omp-client.mjs index bb2599e828..0cd3d0227b 100644 --- a/scripts/verify-omp-client.mjs +++ b/scripts/verify-omp-client.mjs @@ -2,9 +2,10 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { homedir, tmpdir } from "node:os"; -import { join, relative } from "node:path"; +import { isAbsolute, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; @@ -314,8 +315,8 @@ await withVerifierCleanup(async () => { parsed.plannedActions.every( (action) => action.target === configPath || - action.target === skillDirectory || - action.target.startsWith(skillDirectory + "/"), + (isAbsolute(action.target) && + isPathWithinRoot(skillDirectory, action.target)), ), "Refuse setup writes outside the owned config/skill", ); diff --git a/scripts/verify-opencode-client.mjs b/scripts/verify-opencode-client.mjs index acf39a2494..8877b4664d 100644 --- a/scripts/verify-opencode-client.mjs +++ b/scripts/verify-opencode-client.mjs @@ -2,10 +2,11 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { isAbsolute, join, relative, sep } from "node:path"; +import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; @@ -49,19 +50,15 @@ const skillDirectory = join( "skills", "reverse-engineer-anything", ); -const isManagedSkillTarget = (target) => { - const value = relative(skillDirectory, target); - return ( - value === "" || - (value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value)) - ); -}; assert.equal( - isManagedSkillTarget(`${skillDirectory}-sibling/AGENTS.md`), + isPathWithinRoot(skillDirectory, `${skillDirectory}-sibling/AGENTS.md`), false, ); assert.equal( - isManagedSkillTarget(join(skillDirectory, "..", "outside", "AGENTS.md")), + isPathWithinRoot( + skillDirectory, + join(skillDirectory, "..", "outside", "AGENTS.md"), + ), false, ); let clientVersion = null; @@ -271,7 +268,10 @@ await withVerifierCleanup(async () => { ); assert( parsed.plannedActions.every( - (a) => a.target === configPath || isManagedSkillTarget(a.target), + (a) => + a.target === configPath || + (isAbsolute(a.target) && + isPathWithinRoot(skillDirectory, a.target)), ), ); } diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index 847a092667..0e7bbf41a1 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -266,7 +266,6 @@ await withVerifierCleanup(async () => { extractedPluginMcp: true, targetFree: true, targetLifecycle: true, - boundedRegexBridge: true, update, mcpStartup, mcpModuleLoading, diff --git a/scripts/verify-qwen-client.mjs b/scripts/verify-qwen-client.mjs index a47e0d6373..6011c6d711 100644 --- a/scripts/verify-qwen-client.mjs +++ b/scripts/verify-qwen-client.mjs @@ -9,9 +9,10 @@ import { symlink, } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join, resolve, sep } from "node:path"; +import { isAbsolute, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { createHash } from "node:crypto"; +import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; @@ -58,9 +59,10 @@ let clientVersion = null; let fixture; let passed = false; let evidenceSeen = false; -let skillSeen = false; let finalSeen = false; let skillRequested = false; +let skillCallId; +let expectedSkillBody; let searchCallId; let analysisCallId; let artifactReadByAgent = false; @@ -133,8 +135,11 @@ await withVerifierCleanup(async () => { assert( result.plannedActions.every( (a) => - a.target.startsWith(profile + sep) || - a.target.startsWith(sharedSkills + sep), + isAbsolute(a.target) && + ((relative(profile, a.target) !== "" && + isPathWithinRoot(profile, a.target)) || + (relative(sharedSkills, a.target) !== "" && + isPathWithinRoot(sharedSkills, a.target))), ), ); await writeFile( @@ -162,6 +167,20 @@ await withVerifierCleanup(async () => { command: "unrelated-server", disabled: true, }); + const skillFile = join( + sharedSkills, + "reverse-engineer-anything", + "SKILL.md", + ); + const normalizedSkill = (await readFile(skillFile, "utf8")) + .replace(/^\uFEFF/u, "") + .replace(/\r\n|\r/gu, "\n"); + const frontmatter = /^---\n([\s\S]*?)\n---(?:\n|$)([\s\S]*)$/u.exec( + normalizedSkill, + ); + assert(frontmatter, "Installed skill must have authored frontmatter"); + expectedSkillBody = frontmatter[2].trim(); + assert(expectedSkillBody.length > 0); clientPath = baseEnvironment.PATH; if (reducedPath) { assert.equal( @@ -200,9 +219,6 @@ await withVerifierCleanup(async () => { directory: lab, prefix: `${client}-${mode}`, onRequest: async ({ body, tools, requestIndex }) => { - skillSeen ||= JSON.stringify(body.messages ?? []).includes( - "reverse-engineer-anything", - ); const messages = (body.messages ?? []).map((message) => ({ role: message.role, callId: message.tool_call_id, @@ -213,14 +229,14 @@ await withVerifierCleanup(async () => { .map((part) => part.text ?? "") .join("\n"), })); - const messageText = messages.map((message) => message.text).join("\n"); const toolResponseText = (callId) => messages.find( (message) => message.role === "tool" && message.callId === callId, )?.text ?? ""; - selectedSkillLoaded ||= messageText.includes( - join(sharedSkills, "reverse-engineer-anything"), - ); + const expectedSkill = `Base directory for this skill: ${join(sharedSkills, "reverse-engineer-anything")}\nImportant: ALWAYS resolve absolute paths from this base directory when working with skills.\n\n${expectedSkillBody}\n`; + selectedSkillLoaded ||= + skillCallId !== undefined && + toolResponseText(skillCallId).includes(expectedSkill); const toolText = JSON.stringify( (body.messages ?? []).filter((m) => m.role === "tool"), ); @@ -298,6 +314,7 @@ await withVerifierCleanup(async () => { } else if (skill && !skillRequested) { skillRequested = true; message = call(skill.name, { skill: "reverse-engineer-anything" }); + skillCallId = message.tool_calls[0].id; } else if (persisted && shell) { artifactPath = persisted[1]; assert( diff --git a/scripts/verify-real-cutter.mjs b/scripts/verify-real-cutter.mjs index 705bd902d2..aa7ee19bab 100644 --- a/scripts/verify-real-cutter.mjs +++ b/scripts/verify-real-cutter.mjs @@ -32,8 +32,6 @@ const runCli = async (...arguments_) => { return JSON.parse(stdout); }; -const projectRoot = await mkdtemp(join(tmpdir(), "rea-cutter-smoke-")); -const projectPath = join(projectRoot, "smoke.rzdb"); const rizinQuote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; const unavailableAnalysis = { @@ -44,16 +42,21 @@ const unavailableAnalysis = { }, }; const expectedPlatform = { + linux: "linux", Linux: "linux", macOS: "darwin", Windows: "win32", }[process.env.EXPECTED_PLATFORM]; -if (expectedPlatform !== undefined) +if (process.env.EXPECTED_PLATFORM !== undefined) { + assert.ok(expectedPlatform, "Unrecognized Cutter runner platform."); assert.equal( process.platform, expectedPlatform, "Cutter runner did not match its declared platform.", ); +} +const projectRoot = await mkdtemp(join(tmpdir(), "rea-cutter-smoke-")); +const projectPath = join(projectRoot, "smoke.rzdb"); try { const discovery = await runCli("list-cutter-sessions"); assert.equal( diff --git a/scripts/verify-real-electron.mjs b/scripts/verify-real-electron.mjs index 45306de604..192b9d3805 100644 --- a/scripts/verify-real-electron.mjs +++ b/scripts/verify-real-electron.mjs @@ -79,7 +79,11 @@ const output = { windows: capture.windows, verified: capture.coverage.status === "partial_attach" && - capture.actions.every(({ status }) => status === "completed") && + capture.actions.length === input.actions.length && + capture.actions.every( + ({ step_id, status }, index) => + step_id === input.actions[index].step_id && status === "completed", + ) && capture.actions.some( ({ kind, window_index }) => kind === "renderer-reload" && window_index === 1, diff --git a/scripts/verify-real-ghidra-assertions.mjs b/scripts/verify-real-ghidra-assertions.mjs index f79f287df9..c5fed71b0a 100644 --- a/scripts/verify-real-ghidra-assertions.mjs +++ b/scripts/verify-real-ghidra-assertions.mjs @@ -1,6 +1,6 @@ import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.mjs"; -import { access } from "node:fs/promises"; +import { lstat } from "node:fs/promises"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js"; @@ -437,10 +437,11 @@ export function summary(observed) { async function exists(path) { try { - await access(path); + await lstat(path); return true; - } catch { - return false; + } catch (cause) { + if (cause?.code === "ENOENT") return false; + throw cause; } } diff --git a/scripts/verify-real-ghidra-function.mjs b/scripts/verify-real-ghidra-function.mjs index dbf4e77b7f..1e4f2c2cf9 100644 --- a/scripts/verify-real-ghidra-function.mjs +++ b/scripts/verify-real-ghidra-function.mjs @@ -415,7 +415,7 @@ async function verifyRequestCancellationAndTimeout(client, entry) { } async function verifyConcurrentRequests(client, entry, leaf) { - const concurrent = await Promise.all([ + await Promise.all([ functionCall(client, "procedure_info", { document: null, procedure: entry.address, @@ -425,9 +425,7 @@ async function verifyConcurrentRequests(client, entry, leaf) { address: leaf.address, }), ]); - if (concurrent.length !== 2) - throw new Error("Ghidra serial request queue lost a concurrent result"); - return "serialized-two-results"; + return "two-concurrent-requests-completed"; } /** Prove dense jump-table completeness on the optimized jump-table fixture. */ diff --git a/scripts/verify-real-ghidra-nativeaot.mjs b/scripts/verify-real-ghidra-nativeaot.mjs index fb5064613f..6b0bb5851a 100644 --- a/scripts/verify-real-ghidra-nativeaot.mjs +++ b/scripts/verify-real-ghidra-nativeaot.mjs @@ -375,8 +375,13 @@ try { JSON.stringify(report, null, 2) + "\n", ); } + const outcome = report.typed_failure + ? "MCP refusal" + : mode === "default-native" + ? "native load image without metadata recovery" + : "MCP metadata"; console.log( - `PASS real Ghidra NativeAOT ${oracle.runtime}/${mode}: MCP metadata, source identity and owned cleanup${report?.compute ? ", independent type/slot oracle and pseudocode" : ""}${report?.cli_metadata ? ", equivalent CLI" : ""}`, + `PASS real Ghidra NativeAOT ${oracle.runtime}/${mode}: ${outcome}, source unchanged and runtime directory empty${report?.compute ? ", independent type/slot oracle and pseudocode" : ""}${report?.cli_metadata ? ", equivalent CLI" : ""}`, ); } catch (error) { console.error(stderr); diff --git a/scripts/verify-test-discovery.mjs b/scripts/verify-test-discovery.mjs index 46a6aced87..5db6905852 100644 --- a/scripts/verify-test-discovery.mjs +++ b/scripts/verify-test-discovery.mjs @@ -55,7 +55,8 @@ try { if ( report.missing.length || report.unexpected.length || - report.duplicates.length + report.duplicates.length || + report.wrongProjects.length ) { console.error(JSON.stringify(report, null, 2)); process.exitCode = 1; diff --git a/scripts/verify/javascript/application-workflows.mjs b/scripts/verify/javascript/application-workflows.mjs index 725e48a9ab..95b861b039 100644 --- a/scripts/verify/javascript/application-workflows.mjs +++ b/scripts/verify/javascript/application-workflows.mjs @@ -41,10 +41,18 @@ const [leftPath, rightPath] = await Promise.all([ ]); if (leftPath === rightPath) throw new Error("--left and --right must resolve to distinct paths"); -const [left, right] = await Promise.all([ +const analyses = await Promise.allSettled([ analyze(options.left), analyze(options.right), ]); +const failures = analyses.filter((result) => result.status === "rejected"); +if (failures.length === 1) throw failures[0].reason; +if (failures.length > 1) + throw new AggregateError( + failures.map((result) => result.reason), + "Application analyses failed", + ); +const [left, right] = analyses.map((result) => result.value); const comparison = compareApplicationVersionsEvidence({ left, right }); if (!comparison.ok) throw comparison.error; const comparisonResult = comparison.value.result; diff --git a/scripts/verify/javascript/runtime-observation.mjs b/scripts/verify/javascript/runtime-observation.mjs index 8587e5341e..bd1781ca52 100644 --- a/scripts/verify/javascript/runtime-observation.mjs +++ b/scripts/verify/javascript/runtime-observation.mjs @@ -1,8 +1,17 @@ #!/usr/bin/env node -import { readFile, realpath } from "node:fs/promises"; +import { realpath } from "node:fs/promises"; import { parseArgs } from "node:util"; +import { readStableArtifact } from "../../../dist/artifacts/readStableArtifact.js"; +import { ArtifactReaderFailure } from "../../../dist/artifacts/ArtifactReader.js"; +import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; +import { + ArtifactFileCleanupFailure, + artifactFileCompletion, + artifactFilePrimaryFacts, +} from "../../../dist/artifacts/ArtifactFileOperation.js"; + import { reconcileJavaScriptRuntimeEvidence } from "../../../dist/application/javascript/JavaScriptRuntimeReconciliationService.js"; import { analyzeJavaScriptApplicationWithOwnedScope } from "../../lib/javascript-application-analysis.mjs"; import { @@ -52,9 +61,11 @@ for (const encoded of options.mapping) { const mapping = parseMapping(encoded, staticLayers.length); staticLayers[mapping.layer].runtime_mappings.push(mapping.value); } -const runtimeObservations = ( - await Promise.all(options["runtime-evidence"].map(readEvidenceFile)) -).flat(); +const runtimeObservations = []; +for (const path of options["runtime-evidence"]) { + const observations = await readEvidenceFile(path); + for (const observation of observations) runtimeObservations.push(observation); +} const reconciled = reconcileJavaScriptRuntimeEvidence({ static_layers: staticLayers, runtime_observations: runtimeObservations, @@ -95,10 +106,47 @@ process.stdout.write( ); async function readEvidenceFile(path) { - const bytes = await readFile(await realpath(path)); - if (bytes.byteLength > 64 * 1024 * 1024) - throw new Error(`Runtime Evidence exceeds 64 MiB: ${path}`); - const value = JSON.parse(bytes.toString("utf8")); + const scope = new ArtifactResourceScope(); + let outcome; + try { + outcome = { + ok: true, + value: await readStableArtifact( + await realpath(path), + 64 * 1024 * 1024, + undefined, + undefined, + scope, + ), + }; + } catch (cause) { + outcome = { ok: false, cause }; + } + try { + await scope.close(); + } catch (cleanupCause) { + if (!outcome.ok) + throw new AggregateError( + [outcome.cause, cleanupCause], + "Verifier cleanup failed", + { cause: outcome.cause }, + ); + throw cleanupCause; + } + if (!outcome.ok && outcome.cause instanceof ArtifactFileCleanupFailure) { + const completion = artifactFileCompletion(outcome.cause); + outcome = + completion === undefined + ? { ok: false, cause: artifactFilePrimaryFacts(outcome.cause).cause } + : { ok: true, value: completion.value }; + } + if (!outcome.ok) { + const cause = outcome.cause; + if (cause instanceof ArtifactReaderFailure && cause.reason === "limit") + throw new Error(`Runtime Evidence exceeds 64 MiB: ${path}`, { cause }); + throw cause; + } + const value = JSON.parse(outcome.value.bytes.toString("utf8")); return Array.isArray(value) ? value : [value]; } diff --git a/src/application/BrowserScenarioCaptureService.test.ts b/src/application/BrowserScenarioCaptureService.test.ts index 34a3c588cb..c370e77ffa 100644 --- a/src/application/BrowserScenarioCaptureService.test.ts +++ b/src/application/BrowserScenarioCaptureService.test.ts @@ -114,6 +114,38 @@ it.each([ }, ); +it("retains completed scenario Evidence when cancellation arrives during capture", async () => { + const caller = new AbortController(); + const reason = { requested: "stop after capture" }; + const provider: BrowserScenarioCapturePort = { + identity: () => scenarioIdentity, + captureScenario: async (received, options) => { + expect(received).toBe(scenario); + expect(options?.signal).toBe(caller.signal); + caller.abort(reason); + return ok(capture); + }, + }; + const result = await captureBrowserScenario(provider, scenario, { + signal: caller.signal, + }); + if (result.ok) throw new Error("Expected late cancellation"); + expect(result.error).toBeInstanceOf(AnalysisCancelledError); + expect(result.error.cause).toBe(reason); + expect(parseEvidence(result.error.partialObservation)).toMatchObject({ + operation: "capture_browser_scenario", + provider: scenarioIdentity, + confidence: "observed", + authority: "controlled-replay", + parameters: { + browser: scenario.browser, + start_url: scenario.start_url, + actions: scenario.actions, + }, + normalized_result: capture, + }); +}); + it("keeps startup cancellation and its incomplete capture", async () => { const reason = { requested: "stop during initial navigation" }; const completed = scriptScenarioFixture(); diff --git a/src/application/WebRuntimeService.test.ts b/src/application/WebRuntimeService.test.ts index 63fe30f8c2..600b64aa7d 100644 --- a/src/application/WebRuntimeService.test.ts +++ b/src/application/WebRuntimeService.test.ts @@ -139,6 +139,13 @@ it("retains completed execution Evidence on late abort and keeps invalid output expect(completed.error.cause).toBe(cancellationReason); expect(parseEvidence(completed.error.partialObservation)).toMatchObject({ operation: "observe_web_execution", + provider: { id: "web-runtime", name: "Runtime fixture", version: "1" }, + parameters: input, + confidence: "observed", + authority: "external-service", + limitations: expect.arrayContaining( + webExecutionFixture(input.target_id, input.observation_ms).limitations, + ), normalized_result: webExecutionFixture( input.target_id, input.observation_ms, diff --git a/src/application/WebSourceLocationService.test.ts b/src/application/WebSourceLocationService.test.ts index 7faa175c3a..86f436a855 100644 --- a/src/application/WebSourceLocationService.test.ts +++ b/src/application/WebSourceLocationService.test.ts @@ -220,7 +220,8 @@ it("honors cancellation both before acquisition and across decoder completion", if (!response.ok) expect(response.error.cause).toBe(firstReason); const controller = new AbortController(); const lateReason = { caller: "after source-map provider completion" }; - const late = await service(undefined, { + const fixture = webSourceLocationFixture(); + const late = await service(fixture, { trace: (input) => { controller.abort(lateReason); return decoder.trace(input); @@ -232,6 +233,28 @@ it("honors cancellation both before acquisition and across decoder completion", expect(parseEvidence(late.error.partialObservation)).toMatchObject({ operation: "trace_web_source_location", confidence: "derived", + authority: "historical-reference", + subject: { + local_path: fixture.sourceFile.path, + digest: { sha256: fixture.sourceFile.sha256 }, + }, + parameters: args, + raw_result: { source_map_text: fixture.sourceMap.text }, + normalized_result: { + source_map: { + ...fixture.sourceMap.file, + url: args.source_map.url, + association: "caller-selected", + }, + lookup: { requested: args.generated_position }, + source_authenticity: "unknown", + execution: "unknown", + }, + locations: [ + fixture.sourceFile.path, + fixture.manifestFile.path, + fixture.sourceMap.file.path, + ].map((path) => ({ kind: "artifact-path", path })), }); const malformedDecoder: WebSourceMapPort = { diff --git a/src/application/electron/ElectronFuseService.test.ts b/src/application/electron/ElectronFuseService.test.ts index 1a501fee62..aba6d285ac 100644 --- a/src/application/electron/ElectronFuseService.test.ts +++ b/src/application/electron/ElectronFuseService.test.ts @@ -30,11 +30,12 @@ const report: ElectronFuseReport = { it("retains completed fuse Evidence when the provider completes after abort", async () => { const controller = new AbortController(); + const reason = { requested: "stop after fuse inspection" }; const result = await new ElectronFuseService({ identity, close: async () => {}, inspect: async () => { - controller.abort(); + controller.abort(reason); return ok(report); }, }).inspect( @@ -44,8 +45,19 @@ it("retains completed fuse Evidence when the provider completes after abort", as expect(result.ok).toBe(false); if (!result.ok) { expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); expect(parseEvidence(result.error.partialObservation)).toMatchObject({ operation: "inspect_electron_fuses", + provider: identity, + subject: { + local_path: report.artifact.resolved_path, + digest: { sha256: report.artifact.sha256 }, + format: report.artifact.format, + }, + parameters: { path: report.artifact.selected_path }, + confidence: "observed", + limitations: expect.arrayContaining(report.limitations), + locations: [{ kind: "artifact-path", path: report.artifact.selected_path }], normalized_result: report, }); } diff --git a/src/application/javascript/ElectronObservationService.test.ts b/src/application/javascript/ElectronObservationService.test.ts index 01e9e8c0e2..5393957b0c 100644 --- a/src/application/javascript/ElectronObservationService.test.ts +++ b/src/application/javascript/ElectronObservationService.test.ts @@ -150,6 +150,7 @@ it("returns a typed output error for malformed Electron target output after abor it("retains completed target facts when cancellation arrives before Evidence publication", async () => { const controller = new AbortController(); + const reason = { requested: "stop after listing targets" }; const targets = electronTargetListSchema.parse({ browser: inspection.browser, targets: [], @@ -159,7 +160,7 @@ it("retains completed target facts when cancellation arrives before Evidence pub const provider: ElectronObservationPort = { identity: () => identity, listTargets: async () => { - controller.abort(); + controller.abort(reason); return ok(targets); }, inspectPage: async () => @@ -173,8 +174,22 @@ it("retains completed target facts when cancellation arrives before Evidence pub expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBeInstanceOf(AnalysisCancelledError); + expect(result.error.cause).toBe(reason); expect(parseEvidence(result.error.partialObservation)).toMatchObject({ operation: "list_electron_targets", + provider: identity, + parameters: { cdp_endpoint: input.cdp_endpoint }, + confidence: "observed", + authority: "external-service", + environment: { + id: `${inspection.browser.product}@${inspection.browser.revision}`, + platform: "unknown", + architecture: "unknown", + isolation: "none", + }, + limitations: expect.arrayContaining([ + "Browser host platform and architecture were not observed.", + ]), normalized_result: targets, }); } diff --git a/src/application/javascript/JavaScriptAnalysisControl.test.ts b/src/application/javascript/JavaScriptAnalysisControl.test.ts index a16892ef9a..0dc60c78a0 100644 --- a/src/application/javascript/JavaScriptAnalysisControl.test.ts +++ b/src/application/javascript/JavaScriptAnalysisControl.test.ts @@ -1,5 +1,6 @@ import { setImmediate } from "node:timers/promises"; import { describe, expect, it } from "vitest"; +import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js"; describe("JavaScript cooperative computation lifecycle", () => { @@ -15,9 +16,19 @@ describe("JavaScript cooperative computation lifecycle", () => { } } const cancellation = setImmediate().then(() => controller.abort(reason)); - await expect( - completeJavaScriptAnalysisSteps(work(), controller.signal), - ).rejects.toMatchObject({ reason: "cancelled", cause: reason }); + const failure = await completeJavaScriptAnalysisSteps( + work(), + controller.signal, + ).then( + () => { + throw new Error("Expected cancellation"); + }, + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) throw failure; + expect(failure.reason).toBe("cancelled"); + expect(failure.cause).toBe(reason); await cancellation; expect(released).toBe(true); }); @@ -32,9 +43,19 @@ describe("JavaScript cooperative computation lifecycle", () => { yield; return 42; } - await expect( - completeJavaScriptAnalysisSteps(work(), controller.signal), - ).rejects.toMatchObject({ reason: "cancelled", cause: reason }); + const failure = await completeJavaScriptAnalysisSteps( + work(), + controller.signal, + ).then( + () => { + throw new Error("Expected cancellation"); + }, + (cause: unknown) => cause, + ); + expect(failure).toBeInstanceOf(ArtifactReaderFailure); + if (!(failure instanceof ArtifactReaderFailure)) throw failure; + expect(failure.reason).toBe("cancelled"); + expect(failure.cause).toBe(reason); expect(started).toBe(false); expect(await completeJavaScriptAnalysisSteps(work())).toBe(42); }); diff --git a/src/application/javascript/JavaScriptRecoveryService.test.ts b/src/application/javascript/JavaScriptRecoveryService.test.ts index fbd6ae9a60..1977b6031b 100644 --- a/src/application/javascript/JavaScriptRecoveryService.test.ts +++ b/src/application/javascript/JavaScriptRecoveryService.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; import { err, ok } from "../../domain/result.js"; +import { parseEvidence } from "../../domain/evidence.js"; import type { JavaScriptRecoveryResult } from "../../domain/javascript/javascriptRecovery.js"; import { createAnalysisExecution } from "../AnalysisProvider.js"; import { JavaScriptRecoveryService } from "./JavaScriptRecoveryService.js"; @@ -179,7 +180,17 @@ it("returns cancellation with the validated Evidence when the caller aborts afte if (result.ok) throw new Error("expected cancellation"); expect(result.error._tag).toBe("AnalysisCancelledError"); expect(result.error.cause).toBe(reason); - expect(result.error.partialObservation).toMatchObject({ + expect(parseEvidence(result.error.partialObservation)).toMatchObject({ operation: "recover_javascript_sources", + provider: providerIdentity, + parameters: input, + subject: { + local_path: artifact.path, + digest: { sha256: artifact.sha256 }, + format: "javascript", + }, + confidence: "derived", + authority: "shipped-artifact", + normalized_result: recoveryResult(), }); }); diff --git a/src/application/managed/ManagedReconstructionService.test.ts b/src/application/managed/ManagedReconstructionService.test.ts index cf489058d4..81a3786238 100644 --- a/src/application/managed/ManagedReconstructionService.test.ts +++ b/src/application/managed/ManagedReconstructionService.test.ts @@ -41,36 +41,6 @@ it("managed reconstruction reports UTF-8 byte totals while preserving UTF-16 tex }); }); -it("managed reconstruction rejects text hash drift", () => { - const rejected = resolveManagedReconstructionImport({ - ...exampleInput(), - methods: [ - { - ...exampleMethod(), - reconstruction: { - ...exampleMethod().reconstruction, - text_sha256: "0".repeat(64), - }, - }, - ], - }); - expect(rejected).toMatchObject({ - ok: false, - error: { - _tag: "AnalysisInputError", - operation: "import_managed_reconstruction", - issues: [ - { - path: ["methods", 0, "reconstruction", "text_sha256"], - reason: "invalid_value", - message: expect.stringContaining("Reconstruction text hash mismatch"), - }, - ], - }, - }); - if (!rejected.ok) expect(rejected.error).toBeInstanceOf(AnalysisInputError); -}); - it("managed reconstruction rejects stale method locks", () => { const staleInput = { ...exampleInput(), @@ -234,13 +204,15 @@ describe("managed reconstruction input diagnostics", () => { }); expect(result.ok).toBe(false); if (result.ok) return; + expect(result.error).toBeInstanceOf(AnalysisInputError); expect(result.error).toMatchObject({ _tag: "AnalysisInputError", + operation: "import_managed_reconstruction", issues: [ { path: ["methods", 0, "reconstruction", "text_sha256"], reason: "invalid_value", - message: expect.stringMatching(/text hash mismatch/u), + message: expect.stringContaining("Reconstruction text hash mismatch"), }, ], }); diff --git a/src/artifacts/ArtifactPaths.test.ts b/src/artifacts/ArtifactPaths.test.ts index 26c4814d01..6a203ee234 100644 --- a/src/artifacts/ArtifactPaths.test.ts +++ b/src/artifacts/ArtifactPaths.test.ts @@ -181,7 +181,7 @@ it("retains both source spellings and the native failure in a destination collis expect(collision?.cause).toBe(cause); }); -it.each([["app.js"], ["app.js", "App.js"], ["other.js"]])( +it.each([["app.js", "App.js"], ["other.js"]])( "does not invent a destination alias from listing %j", (...names) => { expect(destinationCaseCollision("res/app.js", names)).toBeUndefined(); diff --git a/src/artifacts/BundleResourceArtifactClient.test.ts b/src/artifacts/BundleResourceArtifactClient.test.ts deleted file mode 100644 index 90fd7ef12a..0000000000 --- a/src/artifacts/BundleResourceArtifactClient.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { build } from "plist"; -import { afterEach, expect, it } from "vitest"; - -import { ArtifactProvider } from "./ArtifactProvider.js"; -import type { ResolvedAppBundleResourceRoot } from "../application/AppBundleExecutable.js"; - -const temporaryDirectories: string[] = []; - -afterEach(async () => { - await Promise.all( - temporaryDirectories - .splice(0) - .map((path) => rm(path, { recursive: true, force: true })), - ); -}); - -it("returns keyed archive evidence with root provenance and unknown executable identity", async () => { - const bundlePath = await mkdtemp(join(tmpdir(), "rea-resource-reader-")); - temporaryDirectories.push(bundlePath); - await mkdir(join(bundlePath, "Resources")); - await writeFile( - join(bundlePath, "Info.plist"), - '', - ); - const archive = build({ - $archiver: "NSKeyedArchiver", - $version: 100000, - $objects: ["$null", "sample"], - $top: { root: { UID: 1 } }, - }); - await writeFile(join(bundlePath, "Resources", "Archive.plist"), archive); - const root: ResolvedAppBundleResourceRoot = { - callerPath: bundlePath, - canonicalPath: bundlePath, - infoPlistPath: join(bundlePath, "Info.plist"), - layout: "root", - executableSha256: null, - }; - const provider = new ArtifactProvider({}, "linux"); - const client = provider.createBundleResourceClient(root); - const execution = await client.execute("inspect_keyed_archive", { - path: "Resources/Archive.plist", - }); - expect(execution.ok).toBe(true); - if (execution.ok) { - expect(execution.value.result).toMatchObject({ - target_sha256: null, - resource_root: { - caller_path: bundlePath, - canonical_path: bundlePath, - executable_sha256: null, - }, - archive_path: "Resources/Archive.plist", - }); - } - expect((await client.close()).ok).toBe(true); -}); diff --git a/src/artifacts/FitArtifactReader.test.ts b/src/artifacts/FitArtifactReader.test.ts index 2090ecc960..10c44f41e5 100644 --- a/src/artifacts/FitArtifactReader.test.ts +++ b/src/artifacts/FitArtifactReader.test.ts @@ -55,6 +55,41 @@ const firstEntry = async (reader: FitArtifactReader) => { return entry; }; +it.each(["close", "cancel"] as const)( + "refuses to finish a paused FIT inventory after %s", + async (action) => { + const reader = new FitArtifactReader( + await writeFit("paused-inventory.itb", FIT_EXTERNAL), + ); + const controller = new AbortController(); + const reason = { stage: "after final entry" }; + const iterator = reader.entries(controller.signal)[Symbol.asyncIterator](); + try { + expect((await iterator.next()).done).toBe(false); + expect((await iterator.next()).done).toBe(false); + if (action === "close") await reader.close(); + else controller.abort(reason); + const failure: unknown = await iterator.next().then( + () => { + throw new Error("Expected the paused inventory to fail"); + }, + (cause: unknown) => cause, + ); + expect(failure).toMatchObject( + action === "close" + ? { reason: "unavailable" } + : { reason: "cancelled" }, + ); + if (action === "cancel") { + if (!(failure instanceof ArtifactReaderFailure)) throw failure; + expect(failure.cause).toBe(reason); + } + } finally { + await reader.close(); + } + }, +); + it.each([ ["embedded", FIT_EMBEDDED, "embedded"], ["external", FIT_EXTERNAL, "data-offset"], diff --git a/src/artifacts/FitArtifactReader.ts b/src/artifacts/FitArtifactReader.ts index 1dfb0931ae..e3dea26293 100644 --- a/src/artifacts/FitArtifactReader.ts +++ b/src/artifacts/FitArtifactReader.ts @@ -82,6 +82,7 @@ export class FitArtifactReader implements ArtifactReader { this.#assertOpen(signal); yield copyArtifactEntry(entry); } + this.#assertOpen(signal); } async open(entry: ArtifactEntry, signal?: AbortSignal): Promise { diff --git a/src/artifacts/GodotPckArtifactReader.test.ts b/src/artifacts/GodotPckArtifactReader.test.ts index 4340130faf..b7a9a81a15 100644 --- a/src/artifacts/GodotPckArtifactReader.test.ts +++ b/src/artifacts/GodotPckArtifactReader.test.ts @@ -184,21 +184,6 @@ describe("GodotPckArtifactReader", () => { }, ); - it("preserves declared MD5 for the shared inventory integrity verifier", async () => { - const bytes = buildGodotPck(3, [ - { path: "a.gd", data: "real", md5: Buffer.alloc(16, 1) }, - ]); - await withReader(bytes, async (reader) => { - const [entry] = await list(reader); - if (entry === undefined) throw new Error("expected one entry"); - expect(entry.declaredIntegrity).toEqual({ - algorithm: "md5", - digest: "01".repeat(16), - }); - expect(await text(await reader.open(entry))).toBe("real"); - }); - }); - it("reports encrypted members without reading them", async () => { const bytes = buildGodotPck(3, [ { path: "secret.gd", data: "x", flags: 1 }, diff --git a/src/artifacts/UImageArtifactReader.boundaries.test.ts b/src/artifacts/UImageArtifactReader.boundaries.test.ts index d4a711dbc3..74de505d2a 100644 --- a/src/artifacts/UImageArtifactReader.boundaries.test.ts +++ b/src/artifacts/UImageArtifactReader.boundaries.test.ts @@ -10,6 +10,8 @@ import { buildUImage, buildMultiPayload } from "../../tests/fixtures/uimage.js"; import { parseUImageHeaderFields, parseMultiImageTable, + UIMAGE_HEADER_SIZE, + UIMAGE_MULTI_TABLE_WINDOW, } from "../domain/uimageFormat.js"; const fixture = async (bytes: Buffer): Promise => { @@ -110,7 +112,31 @@ it("admits a multi-image count before scanning the remaining payload", async () reason: "limit", boundary: "capacity", }); - expect(reads).toHaveBeenCalledTimes(2); // header and bounded table, no payload scan + expect(reads.mock.calls.length).toBeGreaterThan(0); + for (const call of reads.mock.calls) { + const [, , length, position] = call as unknown as [ + Buffer, + number, + number, + number, + ]; + expect(Number.isSafeInteger(position)).toBe(true); + expect(Number.isSafeInteger(length)).toBe(true); + expect(position).toBeGreaterThanOrEqual(0); + expect(length).toBeGreaterThanOrEqual(0); + if (length > 0) + expect(position + length).toBeLessThanOrEqual( + UIMAGE_HEADER_SIZE + UIMAGE_MULTI_TABLE_WINDOW, + ); + } + const results = await Promise.all( + reads.mock.results.map( + ({ value }) => value as Promise<{ bytesRead: number }>, + ), + ); + expect( + results.reduce((sum, result) => sum + result.bytesRead, 0), + ).toBeLessThanOrEqual(UIMAGE_HEADER_SIZE + UIMAGE_MULTI_TABLE_WINDOW); } finally { await reader.close(); reads.mockRestore(); diff --git a/src/artifacts/pyinstaller/CArchive.fixture.ts b/src/artifacts/pyinstaller/CArchive.fixture.ts index 2732420858..481eb82cf1 100644 --- a/src/artifacts/pyinstaller/CArchive.fixture.ts +++ b/src/artifacts/pyinstaller/CArchive.fixture.ts @@ -5,7 +5,8 @@ import { pythonPycHeader, } from "../../python/PythonPyc.fixture.js"; -import { PYINSTALLER_COOKIE_MAGIC } from "./CArchiveLayout.js"; +// PyInstaller CArchiveWriter's on-disk cookie signature, independent of REA's reader. +const CARCHIVE_COOKIE_MAGIC = Buffer.from("4d45490c0b0a0b0e", "hex"); /** Final CPython 3.9 bytecode magic, little-endian with the CR LF trailer. */ export const PYTHON_39_MAGIC = Buffer.from("610d0d0a", "hex"); @@ -172,7 +173,7 @@ export const cArchiveBytecodeExecutable = () => { /** Cookie whose package range starts before the file. */ export const cookieRangeOutsideFile = (): Buffer => { const cookie = Buffer.alloc(88); - PYINSTALLER_COOKIE_MAGIC.copy(cookie, 0); + CARCHIVE_COOKIE_MAGIC.copy(cookie, 0); cookie.writeUInt32BE(200, 8); cookie.writeUInt32BE(80, 12); cookie.writeUInt32BE(32, 16); @@ -255,7 +256,7 @@ const cookieBytes = (input: { }): Buffer => { const size = input.layout === "2.1" ? 88 : 24; const bytes = Buffer.alloc(size); - PYINSTALLER_COOKIE_MAGIC.copy(bytes, 0); + CARCHIVE_COOKIE_MAGIC.copy(bytes, 0); bytes.writeUInt32BE(input.tocOffset + input.tocLength + size, 8); bytes.writeUInt32BE(input.tocOffset, 12); bytes.writeUInt32BE(input.tocLength, 16); diff --git a/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts b/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts index a92b2e2a52..221255522f 100644 --- a/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts +++ b/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts @@ -909,67 +909,6 @@ it("retains the descriptor owner when admission cleanup fails", async () => { } }); -it("inventories and extracts a Windows member name under its canonical path", async () => { - const storedName = String.raw`pkg\data.bin`; - const pyzName = String.raw`res\icon.bin`; - const payload = Buffer.from("nested-bytes"); - const icon = Buffer.from("icon-bytes"); - const path = await writeArchive( - "windows", - cArchiveExecutable({ - carrier: "pe", - layout: "2.1", - pythonVersion: 309, - pythonLibrary: "python39.dll", - members: [ - { name: storedName, typeCode: "x", payload, compress: false }, - { - name: "PYZ.pyz", - typeCode: "z", - payload: pyzArchive({ - magic: PYTHON_39_MAGIC, - modules: [{ name: pyzName, typeCode: 2, body: icon }], - }), - compress: false, - }, - ], - }), - ); - const snapshot = await scanCanonicalArtifactInventory(path); - const member = snapshot.occurrences.find( - (occurrence) => occurrence.logical_path === "pkg/data.bin", - ); - const resource = snapshot.occurrences.find( - (occurrence) => occurrence.logical_path === "PYZ.pyz/res/icon.bin", - ); - expect(member?.format_metadata).toMatchObject({ source_name: storedName }); - expect(resource?.format_metadata).toMatchObject({ source_name: pyzName }); - expect(member?.source_path).toBe(storedName); - expect(resource?.source_path).toBe(pyzName); - const directory = await createTestTempDirectory("rea-pyinstaller-out-"); - const output = join(directory, "extracted"); - await materializeArtifactInventory( - { - inputPath: path, - inputFormat: "pe", - outputRoot: output, - environment: process.env, - integrityPolicy: "fail", - }, - path, - snapshot, - ); - expect( - Buffer.compare(await readFile(join(output, "pkg", "data.bin")), payload), - ).toBe(0); - expect( - Buffer.compare( - await readFile(join(output, "PYZ.pyz", "res", "icon.bin")), - icon, - ), - ).toBe(0); -}); - const posixPathFixture = async () => { const backslashName = String.raw`a\b.txt`; const slashName = "a/b.txt"; diff --git a/src/browser/BrowserScenario.test.ts b/src/browser/BrowserScenario.test.ts index 16a7b00293..9684839ffb 100644 --- a/src/browser/BrowserScenario.test.ts +++ b/src/browser/BrowserScenario.test.ts @@ -227,9 +227,6 @@ describe("browserScenarioSchema", () => { at_end: ["dom", "storage"], events: ["console", "page-errors", "network", "websockets"], }); - expect(parsed.capture.limits).toEqual( - DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS, - ); }); it("allows callers to choose visible browsers and service workers", () => { diff --git a/src/browser/PlaywrightElectronActiveActions.test.ts b/src/browser/PlaywrightElectronActiveActions.test.ts index 16618377db..c9c4ec8302 100644 --- a/src/browser/PlaywrightElectronActiveActions.test.ts +++ b/src/browser/PlaywrightElectronActiveActions.test.ts @@ -188,8 +188,6 @@ it("preserves local Playwright action diagnostics", async () => { status: "failed", error: `locator ${selector} failed with token=raw-secret`, }); - expect(result[0]?.error).toContain(selector); - expect(result[0]?.error).toContain("raw-secret"); }); it("retains complete action errors and Electron window text", async () => { diff --git a/src/config.test.ts b/src/config.test.ts index 7abcf113eb..5b6264bd97 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -359,6 +359,12 @@ describe("configuration failure diagnostics", () => { it.each(["1", "90000", "2147483647"])( "accepts Swift compile deadline %s without changing Hopper's deadline", (value) => { + expect( + parseEnvironmentValues({ REA_SWIFTC_COMPILE_TIMEOUT_MS: value }), + ).toMatchObject({ + ok: true, + value: { REA_SWIFTC_COMPILE_TIMEOUT_MS: Number(value) }, + }); expect( parseConfig({ REA_SWIFTC_COMPILE_TIMEOUT_MS: value }), ).toMatchObject({ diff --git a/src/domain/bundleComparison.test.ts b/src/domain/bundleComparison.test.ts index f91aff856e..406389608d 100644 --- a/src/domain/bundleComparison.test.ts +++ b/src/domain/bundleComparison.test.ts @@ -77,39 +77,6 @@ describe("bundle comparison explicit cross-pairs", () => { }); describe("bundle comparison", () => { - it("does not report display-only bundle differences as unexplained changes", () => { - const target = { - path: "/left/artifact.elf", - sha256: "a".repeat(64), - format: "elf" as const, - architecture: "x86_64" as const, - }; - const record = createEvidence(target, PROVIDER, { - operation: "observe", - parameters: {}, - result: { observed: true }, - confidence: "observed", - authority: "shipped-artifact", - }); - const left = createEvidenceBundle([record]); - const rightRecord = { - ...record, - subject: { - ...record.subject!, - name: "renamed.elf", - local_path: "/right/renamed.elf", - }, - }; - const right = { ...left, records: [rightRecord] }; - const result = compareBundles(left, right); - expect(result).toMatchObject({ - status: "unchanged", - summary: { records_unchanged: 1, unresolved: 0 }, - changes: [], - }); - expect(result.left_bundle_sha256).not.toBe(result.right_bundle_sha256); - }); - it("accepts long caller-selected bundle paths", () => { const path = `/${"deep/".repeat(1_000)}bundle.json`; expect( diff --git a/src/domain/changedBehavior.test.ts b/src/domain/changedBehavior.test.ts index 4142623361..07c31f4852 100644 --- a/src/domain/changedBehavior.test.ts +++ b/src/domain/changedBehavior.test.ts @@ -229,14 +229,6 @@ describe("changed behavior", () => { expect(aggregate([changed, unknown]).result).toEqual(result); }); - it("uses complete inline artifact deltas as static candidates", () => { - const complete = comparison("compare_artifacts", artifactResult()); - expect(aggregate([complete]).result.findings.items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ dimension: "artifact:main.js" }), - ]), - ); - }); }); describe("changed-behavior input and Evidence validation", () => { diff --git a/src/domain/ghidraValues.fixture.ts b/src/domain/ghidraValues.fixture.ts index 255947a1f4..904de4e462 100644 --- a/src/domain/ghidraValues.fixture.ts +++ b/src/domain/ghidraValues.fixture.ts @@ -6,7 +6,7 @@ export const ghidraFunctionClassification = () => ({ }); /** Wire-boundary fixture, not evidence that a real Ghidra run was performed. */ -export const ghidraFunctionBody = () => ({ +const ghidraFunctionBody = () => ({ available: true as const, provenance: "ghidra-function-body-address-set", ranges: [{ start: "0x401000", end: "0x401005" }], diff --git a/src/domain/managed/managedNativeVerificationSource.test.ts b/src/domain/managed/managedNativeVerificationSource.test.ts index 05889d68e6..e7489739b7 100644 --- a/src/domain/managed/managedNativeVerificationSource.test.ts +++ b/src/domain/managed/managedNativeVerificationSource.test.ts @@ -31,7 +31,7 @@ describe("managed/native verification source", () => { const [observation] = source.value.nativeObservations; expect(observation?.supported).toBe(true); expect(observation?.symbols.map(({ name }) => name)).toEqual( - legacy.exports.items.map(({ name }) => name), + ["open_native"], ); }); }); diff --git a/src/domain/native/peImports.test.ts b/src/domain/native/peImports.test.ts deleted file mode 100644 index ffdbc7786e..0000000000 --- a/src/domain/native/peImports.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect, it } from "vitest"; -import { inspectPeImportsInputSchema, peImportsSchema } from "./peImports.js"; - -it("rejects extra caller arguments and admits an empty import inventory", () => { - expect( - inspectPeImportsInputSchema.safeParse({ - path: "/artifacts/app.exe", - extra: true, - }).success, - ).toBe(false); - const report = peImportsSchema.parse({ - artifact: { - path: "/artifacts/app.exe", - sha256: "a".repeat(64), - bytes: 512, - }, - format: "pe32-plus", - machine: 0x8664, - bits: 64, - architecture: "amd64", - import_directory: null, - delay_import_directory: null, - imports: [], - delay_imports: [], - imphash: { - algorithm: "pefile", - md5: null, - reason: "IMAGE_DIRECTORY_ENTRY_IMPORT is absent.", - }, - coverage: { - status: "complete", - reason: null, - examined_dlls: 0, - imports: 0, - delay_imports: 0, - }, - issues: [], - limitations: [ - "IMAGE_DIRECTORY_ENTRY_IMPORT is absent. That is not proof the image never imported APIs through another table or overlay.", - ], - }); - expect(report.imports).toEqual([]); - expect(report.imphash.md5).toBeNull(); -}); diff --git a/src/dotnet/ManagedArtifactInspector.test.ts b/src/dotnet/ManagedArtifactInspector.test.ts index 249fc507af..d66ab29c08 100644 --- a/src/dotnet/ManagedArtifactInspector.test.ts +++ b/src/dotnet/ManagedArtifactInspector.test.ts @@ -613,23 +613,6 @@ it.each([false, true])( ); describe("managed artifact reference inventory", () => { - it("qualifies local custom-attribute names with their enclosing types", () => { - const bytes = buildManagedPeFixture({ - additionalEmptyTypes: 1, - customAttributeTypeRaw: (1 << 3) | 2, - nestedTypes: [[2, 1]], - typeName: "InnerAttribute", - typeNamespace: "", - }); - const result = inspectManagedArtifactBytes( - bytes, - managedPeFixtureTarget(bytes), - ); - - expect(result.attributes[0]?.type_name).toBe("Empty0+InnerAttribute"); - expect(result.coverage).toEqual({ state: "complete", issues: [] }); - }); - it("qualifies referenced custom-attribute names from TypeRef scopes", () => { const bytes = buildManagedPeFixture({ customAttributeTypeRaw: (1 << 3) | 3, diff --git a/src/dotnet/ManagedCilDecoding.test.ts b/src/dotnet/ManagedCilDecoding.test.ts index eb2e283170..ade7734cc5 100644 --- a/src/dotnet/ManagedCilDecoding.test.ts +++ b/src/dotnet/ManagedCilDecoding.test.ts @@ -473,14 +473,6 @@ describe("managed complete operand decoding", () => { }); }); - it("reports a switch whose target count is itself truncated", () => { - expect(fatBody(Buffer.from([0x45, 0x02]))).toMatchObject({ - status: "malformed", - decoded_instruction_count: 0, - issue: "Instruction switch at IL offset 0 leaves method body", - }); - }); - it("preserves instructions decoded before a truncated operand", () => { expect(fatBody(Buffer.from([0x00, 0x20]))).toMatchObject({ status: "malformed", diff --git a/src/dotnet/ManagedFieldConstants.test.ts b/src/dotnet/ManagedFieldConstants.test.ts index cbce7aab96..902c6a6efb 100644 --- a/src/dotnet/ManagedFieldConstants.test.ts +++ b/src/dotnet/ManagedFieldConstants.test.ts @@ -192,9 +192,12 @@ it("does not invent an inspected constant on historical member Evidence", () => expect(historical.fields[0]).not.toHaveProperty("constant"); }); -it.each([7, 8])( - "retains the common coded-index diagnostic for invalid parent %s", - (parentRaw) => { +it.each([ + { parentRaw: 7, reason: "tag 3" }, + { parentRaw: 8, reason: "row 2" }, +])( + "retains the coded-index diagnostic for invalid parent $parentRaw", + ({ parentRaw, reason }) => { const bytes = buildManagedPeFixture({ fields: [{ name: "Default", flags: 0x8056 }], constants: [ @@ -226,7 +229,7 @@ it.each([7, 8])( expect.objectContaining({ code: "invalid-row", scope: "metadata.Constant", - detail: parent.reason, + detail: expect.stringContaining(reason), }), ]), ); diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index bd1045a837..43807fc523 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -101,14 +101,6 @@ it.each([ ); describe("managed compressed integer framing", () => { - it.each([[0x80], [0xc0], [0xc0, 1], [0xc0, 1, 2]])( - "keeps an incomplete width %j distinct from a reserved prefix", - (...value) => { - expect(() => readCompressed(Buffer.from(value), 0)).toThrow( - "truncated compressed integer", - ); - }, - ); it("preserves complete widths and rejects reserved leading bits", () => { expect(readCompressed(Buffer.from([0x80, 1]), 0)).toEqual({ value: 1, @@ -2418,6 +2410,9 @@ it.each([ it.each([ [[], "truncated compressed integer"], [[0x80], "truncated compressed integer"], + [[0xc0], "truncated compressed integer"], + [[0xc0, 1], "truncated compressed integer"], + [[0xc0, 1, 2], "truncated compressed integer"], [[0xc0, 0, 0], "truncated compressed integer"], [[0xe0], "reserved compressed integer"], ] as const)( diff --git a/src/dotnet/ManagedNativeBoundaryInspector.test.ts b/src/dotnet/ManagedNativeBoundaryInspector.test.ts index 9788ef5eab..adce48d8ad 100644 --- a/src/dotnet/ManagedNativeBoundaryInspector.test.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.test.ts @@ -228,14 +228,7 @@ it("reports field imports as declarations without inventing method implementatio }); describe("managed native boundary inspection", () => { - it("returns complete member inventories inline and reports unavailable metadata", () => { - const bytes = buildManagedPeFixture(); - const paged = inspectManagedMembersBytes( - bytes, - managedPeFixtureTarget(bytes), - ); - expect(paged.methods).toHaveLength(1); - + it("reports absent or malformed member metadata", () => { const nativeBytes = buildNativePeFixture(); const native = inspectManagedMembersBytes( nativeBytes, diff --git a/src/dotnet/ManagedSignatureResolution.test.ts b/src/dotnet/ManagedSignatureResolution.test.ts index d33066ba91..21dfbc45b8 100644 --- a/src/dotnet/ManagedSignatureResolution.test.ts +++ b/src/dotnet/ManagedSignatureResolution.test.ts @@ -610,7 +610,7 @@ describe("signature display name validity", () => { const result = inspectDisplayFixture({ fieldSignature: blob }); expect(result.fields[0]?.signature.parse_status).toBe(status); expect(result.fields[0]?.signature.raw_sha256).toBe( - signature(blob).raw_sha256, + createHash("sha256").update(blob).digest("hex"), ); }); }); diff --git a/src/electron/ElectronFuseProvider.test.ts b/src/electron/ElectronFuseProvider.test.ts index af36e1696c..774f2ec3ad 100644 --- a/src/electron/ElectronFuseProvider.test.ts +++ b/src/electron/ElectronFuseProvider.test.ts @@ -266,11 +266,11 @@ describe("ELF embedded ASAR integrity applicability", () => { it("preserves an enabled ASAR integrity fuse without claiming enforcement on ELF", async () => { const root = await createTestTempDirectory("rea-electron-fuses-elf-asar-"); const fuses = [ - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.enabled, + 0x30, + 0x30, + 0x30, + 0x30, + 0x31, ]; const image = elfFuseImage(fuses); const path = join(root, "electron"); @@ -281,7 +281,7 @@ describe("ELF embedded ASAR integrity applicability", () => { index: 4, name: "EnableEmbeddedAsarIntegrityValidation", state: "enabled", - raw_byte: ELECTRON_FUSE_STATE_BYTES.enabled, + raw_byte: 0x31, }); expect( report.runtime_implications.find( diff --git a/src/flutter/FlutterPayloadScan.test.ts b/src/flutter/FlutterPayloadScan.test.ts index 13bb4f743f..4e36d78efa 100644 --- a/src/flutter/FlutterPayloadScan.test.ts +++ b/src/flutter/FlutterPayloadScan.test.ts @@ -136,7 +136,7 @@ describe("LabeledStringScanner", () => { }; it("collects a full clang line within the 500-character bound", () => { assertSingleToolchainLine( - "Android (5900059 based on r365631c1) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)", + "Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)", ); }); @@ -155,12 +155,6 @@ describe("LabeledStringScanner", () => { ); }); - it("collects the labeled Android clang line", () => { - assertSingleToolchainLine( - "Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc) (based on LLVM 9.0.8svn)", - ); - }); - it("does not claim a toolchain cap for unrelated overlong strings", () => { const scanner = new LabeledStringScanner(); const license = `Apache License Version 2.0 ${"x".repeat(700)}`; diff --git a/src/ghidra/GhidraFunctionValues.test.ts b/src/ghidra/GhidraFunctionValues.test.ts index 9c33de190a..e711c2461a 100644 --- a/src/ghidra/GhidraFunctionValues.test.ts +++ b/src/ghidra/GhidraFunctionValues.test.ts @@ -803,6 +803,15 @@ const malformedOutputs = (): Array< length: 1, signature: null, locals: [], + body: { + available: true, + provenance: "ghidra-function-body-address-set", + ranges: [{ start: "0x401000", end: "0x401000" }], + total_bytes: 1, + span_bytes: 1, + non_contiguous: false, + contains_entry: true, + }, }, ], [ @@ -814,6 +823,8 @@ const malformedOutputs = (): Array< references: [ { ...edge, kind: { available: false, reason: "unknown" } }, ], + reference_kinds_available: true, + unresolved_calls: [], }, ], [ diff --git a/src/ida/IdaContainment.test.ts b/src/ida/IdaContainment.test.ts index 045df2e778..7562afda45 100644 --- a/src/ida/IdaContainment.test.ts +++ b/src/ida/IdaContainment.test.ts @@ -458,7 +458,10 @@ describe("headless IDA containment lookup failures", () => { }); expect(result.ok).toBe(false); - if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError"); + if (!result.ok) { + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(controller.signal.reason); + } expect( producer.calls.some(({ name }) => name === "survey_binary"), "nothing is dispatched after cancellation", @@ -606,7 +609,10 @@ describe("headless IDA document identity", () => { ); expect(result.ok).toBe(false); - if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError"); + if (!result.ok) { + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(controller.signal.reason); + } expect(producer.calls).toEqual([]); } finally { expect((await client.close()).ok).toBe(true); diff --git a/src/ida/IdaOperationFailures.test.ts b/src/ida/IdaOperationFailures.test.ts index 350af553cd..e107c4b622 100644 --- a/src/ida/IdaOperationFailures.test.ts +++ b/src/ida/IdaOperationFailures.test.ts @@ -302,6 +302,7 @@ describe("headless IDA partial operation evidence", () => { expect(result.ok).toBe(false); if (!result.ok) { expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(controller.signal.reason); expect(result.error.partialObservation).toMatchObject({ analysis_profile: null, normalized_result: { diff --git a/src/kuna/KunaProjectCache.ts b/src/kuna/KunaProjectCache.ts index 2070c2ff5a..673c82125c 100644 --- a/src/kuna/KunaProjectCache.ts +++ b/src/kuna/KunaProjectCache.ts @@ -556,7 +556,11 @@ export class KunaProjectCache { request: KunaProjectRequest, ): Promise> { if (request.signal?.aborted === true) - return err(new AnalysisCancelledError(KUNA_EXPORT_OPERATION)); + return err( + new AnalysisCancelledError(KUNA_EXPORT_OPERATION, { + cause: request.signal.reason, + }), + ); const key = kunaProjectKey({ targetSha256: request.targetSha256, targetBasename: basename(request.targetPath), @@ -878,7 +882,13 @@ export class KunaProjectCache { settled = true; leave(); if (entry.waiters === 0) entry.controller.abort(); - resolve(err(new AnalysisCancelledError(KUNA_EXPORT_OPERATION))); + resolve( + err( + new AnalysisCancelledError(KUNA_EXPORT_OPERATION, { + cause: request.signal?.reason, + }), + ), + ); }; request.signal?.addEventListener("abort", onAbort, { once: true }); void entry.promise.then((result) => { diff --git a/src/kuna/KunaTargetCommands.ts b/src/kuna/KunaTargetCommands.ts index 14306dbc59..c151dde167 100644 --- a/src/kuna/KunaTargetCommands.ts +++ b/src/kuna/KunaTargetCommands.ts @@ -97,7 +97,14 @@ export class KunaTargetCommands { if (this.#closing || request.signal.aborted) { request.releaseRun?.(); return Promise.resolve( - err(new AnalysisCancelledError(request.operation)), + err( + new AnalysisCancelledError( + request.operation, + request.signal.aborted + ? { cause: request.signal.reason } + : undefined, + ), + ), ); } return this.#run(request); @@ -106,7 +113,16 @@ export class KunaTargetCommands { /** Establish the immutable input before waiting for a foreground run slot. */ prepare(): Promise> { if (this.#closing || this.lifetime.signal.aborted) - return Promise.resolve(err(new AnalysisCancelledError("kuna:workspace"))); + return Promise.resolve( + err( + new AnalysisCancelledError( + "kuna:workspace", + this.lifetime.signal.aborted + ? { cause: this.lifetime.signal.reason } + : undefined, + ), + ), + ); return this.#admit("kuna:workspace", () => this.#ensureWorkspace().then((workspace) => workspace.ok ? ok(null) : workspace, diff --git a/src/native/elf/KernelModule.fixture.ts b/src/native/elf/KernelModule.fixture.ts index d3b35a60a3..9c34b8a103 100644 --- a/src/native/elf/KernelModule.fixture.ts +++ b/src/native/elf/KernelModule.fixture.ts @@ -1,5 +1,3 @@ -import { MODULE_SIGNATURE_MAGIC } from "../../domain/native/kernelModule.js"; - const concat = (...parts: readonly Buffer[]): Buffer => Buffer.concat(parts); const write16 = ( @@ -75,7 +73,13 @@ export const moduleSignatureTrailer = ( const info = Buffer.alloc(12); info[2] = 2; info.writeUInt32BE(sigLen >>> 0, 8); - return concat(pkcs7, info, Buffer.from(MODULE_SIGNATURE_MAGIC, "latin1")); + // Linux MODULE_SIG_STRING includes the final newline: + // https://code.googlesource.com/linux/torvalds/linux/+/78273df7f646f8daf2604ec714bea0897cd03aae/include/linux/module_signature.h + return concat( + pkcs7, + info, + Buffer.from("~Module signature appended~\n", "latin1"), + ); }; export interface KernelModuleFixtureOptions { diff --git a/src/process/ProcessOwnership.nativeExit.test.ts b/src/process/ProcessOwnership.nativeExit.test.ts index 115e520ba4..6d691ada0e 100644 --- a/src/process/ProcessOwnership.nativeExit.test.ts +++ b/src/process/ProcessOwnership.nativeExit.test.ts @@ -551,13 +551,14 @@ it("honors cancellation during token-open exit settlement", async () => { lifecycle: { state: "observed", lifecycle: "non-zombie" }, }; const controller = new AbortController(); + const reason = new Error("cancelled"); const host: ProcessOwnershipHost = { platform: "linux", listProcesses: async () => [row], environment: async () => ({}), signalGroup: vi.fn(), processIdentities: async () => { - controller.abort(new Error("cancelled")); + controller.abort(reason); return new Map([[row.pid, identity]]); }, }; @@ -576,7 +577,7 @@ it("honors cancellation during token-open exit settlement", async () => { { deadline: undefined }, controller.signal, ), - ).rejects.toThrow("cancelled"); + ).rejects.toBe(reason); }); it("does not signal a stale group after its last member exits during revalidation", async () => { diff --git a/src/process/RegexMatcher.test.ts b/src/process/RegexMatcher.test.ts index 3fe5c8eb5d..26e2c60fca 100644 --- a/src/process/RegexMatcher.test.ts +++ b/src/process/RegexMatcher.test.ts @@ -416,9 +416,6 @@ it("describes the effective heap and the configurable matching deadline", () => regexWorkerHeapRequestRecovery(DEFAULT_REGEX_WORKER_HEAP_REQUEST), ); if (heap.kind === "unknown") { - expect(projected.remediation.action).toContain( - "--max-old-space-size-percentage", - ); expect(projected.remediation.action).toContain( "--experimental-config-file", ); diff --git a/src/reference/ReferenceSourceReader.test.ts b/src/reference/ReferenceSourceReader.test.ts index 8cb73cae9b..27cf4c7a4b 100644 --- a/src/reference/ReferenceSourceReader.test.ts +++ b/src/reference/ReferenceSourceReader.test.ts @@ -57,7 +57,7 @@ describe("unobserved internal symlink reconciliation", () => { ]); }); - it("retains unreadable targets below the nearest failed directory", () => { + it("retains unreadable targets below a failed directory", () => { const entries: ReferenceSourceEntry[] = [ internalLink("alias.txt", "hidden/target.txt"), failedDirectory("hidden", "io"), diff --git a/src/rust/RustBinaryImage.machoAlignment.test.ts b/src/rust/RustBinaryImage.machoAlignment.test.ts deleted file mode 100644 index 35ca20844d..0000000000 --- a/src/rust/RustBinaryImage.machoAlignment.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect, it } from "vitest"; -import { machoImageWithUnknownCommand } from "../artifacts/apple/MachoImage.fixture.js"; -import { readRustBinaryImage } from "./RustBinaryImage.js"; -import { RustAuditableFormatFailure } from "./RustAuditableReader.js"; - -it("maps malformed 64-bit Mach-O load-command alignment to Rust input failure", () => { - expect(() => - readRustBinaryImage(Buffer.from(machoImageWithUnknownCommand(true, 12))), - ).toThrow(RustAuditableFormatFailure); - expect(() => - readRustBinaryImage(Buffer.from(machoImageWithUnknownCommand(true, 12))), - ).toThrow(expect.objectContaining({ kind: "malformed" })); -}); - -it.each([ - { wide: true, size: 16 }, - { wide: false, size: 12 }, -])( - "accepts $size-byte Rust Mach-O command for wide=$wide", - ({ wide, size }) => { - expect( - readRustBinaryImage(Buffer.from(machoImageWithUnknownCommand(wide, size))) - .format, - ).toBe("macho"); - }, -); diff --git a/src/server/BundleResourcesMcp.test.ts b/src/server/BundleResourcesMcp.test.ts index 377cb71d02..1da3c2d37f 100644 --- a/src/server/BundleResourcesMcp.test.ts +++ b/src/server/BundleResourcesMcp.test.ts @@ -2,7 +2,7 @@ import type { Client } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import type { AnySchema } from "ajv"; import { expect, it } from "vitest"; -import { mkdir, writeFile } from "node:fs/promises"; +import { mkdir, realpath, symlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { z } from "zod"; @@ -178,7 +178,8 @@ it("exposes the production resource session through the MCP SDK for a keyless bu const parent = await createTestTempDirectory( "rea-bundle-resource-production-mcp-", ); - const bundle = join(parent, "Fixture.app"); + const bundle = join(parent, "Fixture"); + const alias = join(parent, "Caller"); const resourceDirectory = join(bundle, "Resources"); await mkdir(resourceDirectory, { recursive: true }); await writeFile(join(bundle, "Info.plist"), ""); @@ -192,6 +193,9 @@ it("exposes the production resource session through the MCP SDK for a keyless bu }), ); + await symlink(bundle, alias, "junction"); + const canonical = await realpath(bundle); + const session = await createArtifactBinarySession({}, process.platform); const { client } = await createApplicationMcpHarness({ session, @@ -200,7 +204,7 @@ it("exposes the production resource session through the MCP SDK for a keyless bu const opened = await client.callTool({ name: "open_bundle_resources", - arguments: { path: bundle }, + arguments: { path: alias }, }); expect(opened.isError).not.toBe(true); expect( @@ -208,7 +212,11 @@ it("exposes the production resource session through the MCP SDK for a keyless bu opened.structuredContent, ), ).toMatchObject({ - result: { canonical_path: bundle, executable_sha256: null }, + result: { + caller_path: alias, + canonical_path: canonical, + executable_sha256: null, + }, }); const decoded = await client.callTool({ @@ -223,7 +231,11 @@ it("exposes the production resource session through the MCP SDK for a keyless bu subject: null, normalized_result: { target_sha256: null, - resource_root: { canonical_path: bundle, executable_sha256: null }, + resource_root: { + caller_path: alias, + canonical_path: canonical, + executable_sha256: null, + }, }, }); const decodedEvidenceId = z @@ -252,10 +264,10 @@ it("exposes the production resource session through the MCP SDK for a keyless bu ), ).toMatchObject({ normalized_result: { - artifact: { path: bundle, sha256: null }, + artifact: { path: canonical, sha256: null }, resource_root: { - caller_path: bundle, - canonical_path: bundle, + caller_path: alias, + canonical_path: canonical, executable_sha256: null, }, }, @@ -275,7 +287,11 @@ it("exposes the production resource session through the MCP SDK for a keyless bu normalized_result: { archive_path: "Resources/Archive.plist", target_sha256: null, - resource_root: { canonical_path: bundle, executable_sha256: null }, + resource_root: { + caller_path: alias, + canonical_path: canonical, + executable_sha256: null, + }, objects: expect.arrayContaining([ expect.objectContaining({ value: "resource-only-value" }), ]), @@ -288,9 +304,24 @@ it("exposes the production resource session through the MCP SDK for a keyless bu subject: null, operation: "inspect_keyed_archive", normalized_result: { - resource_root: { canonical_path: bundle, executable_sha256: null }, + resource_root: { + caller_path: alias, + canonical_path: canonical, + executable_sha256: null, + }, }, }); + const closed = await client.callTool({ name: "close_binary", arguments: {} }); + expect(closed.isError).not.toBe(true); + const statusAfterClose = await client.callTool({ + name: "binary_session", + arguments: {}, + }); + expect( + toolContract("binary_session").outputSchema.parse( + statusAfterClose.structuredContent, + ), + ).toMatchObject({ result: { open: false } }); }); const expectResourceSchemaBranches = async (client: Client): Promise => { diff --git a/src/server/runDerivedOperation.test.ts b/src/server/runDerivedOperation.test.ts index 152c70752e..e6bb67dcd0 100644 --- a/src/server/runDerivedOperation.test.ts +++ b/src/server/runDerivedOperation.test.ts @@ -125,23 +125,6 @@ describe("derived MCP operation progress and cancellation", () => { expect(result.ok ? undefined : result.error.cause).toBe(reason); }); - it("lets cancellation win immediately before result publication", async () => { - const controller = new AbortController(); - const result = await runDerivedOperation( - context(controller), - "compare", - () => { - controller.abort(); - return "must-not-publish"; - }, - ); - - expect(result).toMatchObject({ - ok: false, - error: { _tag: "AnalysisCancelledError" }, - }); - }); - it("lets cancellation win while terminal progress delivery is pending", async () => { const controller = new AbortController(); let terminalReached = false; diff --git a/tests/acceptance/applications/electronBlankPathCli.test.ts b/tests/acceptance/applications/electronBlankPathCli.test.ts index a03968ca11..a485add76c 100644 --- a/tests/acceptance/applications/electronBlankPathCli.test.ts +++ b/tests/acceptance/applications/electronBlankPathCli.test.ts @@ -3,7 +3,7 @@ import { expect } from "vitest"; import { cliTest } from "../../support/cli/cliFixture.js"; cliTest.for(["", " \t "])( - "rejects blank Electron application_root (%j) before filesystem or provider work", + "rejects blank Electron application_root (%j) as invalid input", async (applicationRoot, { cli }) => { const response = await cli.run({ arguments: [ diff --git a/tests/acceptance/applications/electronDynamicIpcHandlersCli.test.ts b/tests/acceptance/applications/electronDynamicIpcHandlersCli.test.ts index d9b9005998..5d013fa275 100644 --- a/tests/acceptance/applications/electronDynamicIpcHandlersCli.test.ts +++ b/tests/acceptance/applications/electronDynamicIpcHandlersCli.test.ts @@ -79,7 +79,7 @@ ipcRenderer.send("app:log", 1);`, for (const surface of ["CLI", "stdio MCP"] as const) cliTest.for(cases)( - `keeps literal sends unknown beside $shape through ${surface}`, + `classifies literal sends beside $shape through ${surface}`, async ({ main, preload, pairing }, { cli }) => { const root = await createTestTempDirectory("rea-electron-dynamic-ipc-"); await writeFixtureFiles(root, { diff --git a/tests/acceptance/asarSelectedCompanionCli.test.ts b/tests/acceptance/asarSelectedCompanionCli.test.ts index 0026292553..a31d60d31b 100644 --- a/tests/acceptance/asarSelectedCompanionCli.test.ts +++ b/tests/acceptance/asarSelectedCompanionCli.test.ts @@ -6,6 +6,7 @@ import { symlink, writeFile, } from "node:fs/promises"; +import { createHash } from "node:crypto"; import { join } from "node:path"; import { createPackageWithOptions, extractFile } from "@electron/asar"; import { expect, onTestFinished } from "vitest"; @@ -28,6 +29,7 @@ cliTest.skipIf(process.platform === "win32")( const bytes = Buffer.from("selected native companion"); await mkdir(source); await writeFile(join(source, "native.node"), bytes); + await writeFile(join(source, "packed.txt"), "packed bytes"); await createPackageWithOptions(source, selected, { unpack: "*.node" }); await rename(selected, canonical); await symlink(canonical, selected); @@ -54,11 +56,15 @@ cliTest.skipIf(process.platform === "win32")( const inventory = artifactInventoryResultSchema.parse( inventoryEvidence.normalized_result, ); + const occurrence = inventory.occurrences.find( + ({ logical_path }) => logical_path === "native.node", + ); + expect(occurrence?.hash_status).toBe("verified"); expect( - inventory.occurrences.find( - ({ logical_path }) => logical_path === "native.node", - )?.hash_status, - ).toBe("verified"); + inventory.nodes.find( + ({ artifact_id }) => artifact_id === occurrence?.artifact_id, + )?.sha256, + ).toBe(createHash("sha256").update(bytes).digest("hex")); const extracted = await cli.run({ arguments: ["extract-artifact", selected, "--json"], @@ -74,5 +80,8 @@ cliTest.skipIf(process.platform === "win32")( expect( (await readFile(join(result.output_root, "native.node"))).equals(bytes), ).toBe(true); + expect(await readFile(join(result.output_root, "packed.txt"), "utf8")).toBe( + "packed bytes", + ); }, ); diff --git a/tests/acceptance/bunStandaloneCli.test.ts b/tests/acceptance/bunStandaloneCli.test.ts index 78d0f8a4fc..763b815043 100644 --- a/tests/acceptance/bunStandaloneCli.test.ts +++ b/tests/acceptance/bunStandaloneCli.test.ts @@ -16,7 +16,7 @@ import { } from "../../src/domain/artifactGraph.js"; cliTest( - "inventories and extracts Bun source without executing its carrier", + "inventories and extracts Bun source from its carrier", async ({ cli }) => { const root = await createTestTempDirectory("rea-bun-cli-"); const source = join(root, "app"); diff --git a/tests/acceptance/godotPck.test.ts b/tests/acceptance/godotPck.test.ts index 24a58416fd..d16d4b86b2 100644 --- a/tests/acceptance/godotPck.test.ts +++ b/tests/acceptance/godotPck.test.ts @@ -387,7 +387,7 @@ cliTest( ); cliTest( - "rejects repeated-range expansion before hashing members or creating extraction output", + "rejects repeated-range expansion without creating extraction output", async ({ cli }) => { const root = await createTestTempDirectory("rea-pck-expansion-"); const source = join(root, "aliased.pck"); diff --git a/tests/acceptance/sourcePathFailuresCli.test.ts b/tests/acceptance/sourcePathFailuresCli.test.ts index 1ae2d7c45c..6d894c6205 100644 --- a/tests/acceptance/sourcePathFailuresCli.test.ts +++ b/tests/acceptance/sourcePathFailuresCli.test.ts @@ -6,7 +6,7 @@ import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; cliTest.skipIf(process.platform === "win32")( - "reports unresolvable source and scenario selections without permission advice", + "reports unresolvable source and scenario selections with path-specific guidance", async ({ cli }) => { const root = await createTestTempDirectory("rea-source-path-failures-"); await symlink("loop-b", join(root, "loop-a")); diff --git a/tests/boundary/cli/ampRegistrationPreservation.test.ts b/tests/boundary/cli/ampRegistrationPreservation.test.ts index 97d0842a10..507b8af71f 100644 --- a/tests/boundary/cli/ampRegistrationPreservation.test.ts +++ b/tests/boundary/cli/ampRegistrationPreservation.test.ts @@ -58,6 +58,7 @@ cliTest( expect(await readFile(registrationBackupPath, "utf8")).toBe(original); expect(refreshed.stderr).toContain(registrationBackupPath); expect(after.theme).toBe("dark"); + expect(after["amp.mcpServers"].other).toEqual({ command: "other" }); // The equals form --agent=amp is parsed as a spaced form before Incur sees // it, so the Amp registration lands and keeps the environment map. @@ -69,6 +70,7 @@ cliTest( command: expect.any(String), env: { KEEP_ME: "1" }, }); + expect(afterEquals["amp.mcpServers"].other).toEqual({ command: "other" }); expect(await readFile(registrationBackupPath, "utf8")).toBe(original); const recoveryBackupPath = equals.stderr.match( /Amp MCP registration backup retained at (.+)/, diff --git a/tests/boundary/cli/bundleComparisonCli.test.ts b/tests/boundary/cli/bundleComparisonCli.test.ts index 809ae9b8aa..fcf164b79b 100644 --- a/tests/boundary/cli/bundleComparisonCli.test.ts +++ b/tests/boundary/cli/bundleComparisonCli.test.ts @@ -91,7 +91,11 @@ cliTest( const display = await compare(displayPath); expect(display.exitCode).toBe(0); const displayResult = bundleComparisonResultSchema.parse(display.json); - expect(displayResult).toMatchObject({ status: "unchanged", changes: [] }); + expect(displayResult).toMatchObject({ + status: "unchanged", + summary: { records_unchanged: 1, unresolved: 0 }, + changes: [], + }); expect(displayResult.left_bundle_sha256).not.toBe( displayResult.right_bundle_sha256, ); diff --git a/tests/boundary/cli/cliEvidenceCallerPath.test.ts b/tests/boundary/cli/cliEvidenceCallerPath.test.ts index 5c2754d63c..e2c1d08ad3 100644 --- a/tests/boundary/cli/cliEvidenceCallerPath.test.ts +++ b/tests/boundary/cli/cliEvidenceCallerPath.test.ts @@ -55,7 +55,12 @@ it("binds authenticated partial Evidence but preserves raw partial observations" sha256: "a".repeat(64), bytes: 1, }, - { kind: "file-snapshot-hash", path: "/tmp/selected", bytes: 1 }, + { + kind: "file-snapshot-hash", + path: "/tmp/selected", + sha256: "a".repeat(64), + bytes: 1, + }, ], ] as const) { const result = await finishCliEvidenceCommand( @@ -74,5 +79,7 @@ it("binds authenticated partial Evidence but preserves raw partial observations" expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected cancellation"); expect(result.error.details?.partial_observation).toMatchObject(expected); + if ("kind" in expected) + expect(result.error.details?.partial_observation).toEqual(expected); } }); diff --git a/tests/boundary/cli/cliInputSchemaRequired.test.ts b/tests/boundary/cli/cliInputSchemaRequired.test.ts index 1a65c77a51..37d3a0d407 100644 --- a/tests/boundary/cli/cliInputSchemaRequired.test.ts +++ b/tests/boundary/cli/cliInputSchemaRequired.test.ts @@ -85,7 +85,11 @@ const registerParserAdvertisementInventoryTests = (): void => { it("require exactly the inputs whose omission the parser rejects", async () => { const cli = createCli({}); const mismatches: string[] = []; - for (const command of createCliInventory(cli).primary) { + const primary = createCliInventory(cli).primary; + expect(primary).toEqual( + expect.arrayContaining(["inspect-pcap-client", "inspect-pcap-capture"]), + ); + for (const command of primary) { const advertised = await advertisedSchema(cli, command); for (const group of INPUT_GROUPS) { const parser = parserSchema(cli, command, group); diff --git a/tests/boundary/cli/cutterVerifierPlatform.test.ts b/tests/boundary/cli/cutterVerifierPlatform.test.ts new file mode 100644 index 0000000000..1bef958449 --- /dev/null +++ b/tests/boundary/cli/cutterVerifierPlatform.test.ts @@ -0,0 +1,81 @@ +import { execFile } from "node:child_process"; +import { readFile, readdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; +import { parse } from "yaml"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const execute = promisify(execFile); + +it("enforces workflow platform labels before contacting Cutter", async () => { + const workflow = parse( + await readFile(".github/workflows/real-cutter.yml", "utf8"), + ); + const labels = workflow.jobs["cutter-upstream"].strategy.matrix.include.map( + (entry: { platform: string }) => entry.platform, + ); + expect(labels).toEqual(["linux", "macOS", "Windows"]); + const root = await createTestTempDirectory("rea-cutter-platform-"); + const preload = join(root, "preload.mjs"); + await writeFile( + preload, + ` +import childProcess from 'node:child_process'; +import { syncBuiltinESMExports } from 'node:module'; +Object.defineProperty(process, 'platform', { value: process.env.REA_PROBE_PLATFORM }); +childProcess.execFile = () => { throw new Error('fixture discovery reached'); }; +syncBuiltinESMExports(); +`, + ); + const check = async (selected: string | undefined, actual: string) => { + const env: NodeJS.ProcessEnv = { + ...process.env, + TMPDIR: root, + TMP: root, + TEMP: root, + REA_PROBE_PLATFORM: actual, + }; + if (selected === undefined) delete env.EXPECTED_PLATFORM; + else env.EXPECTED_PLATFORM = selected; + const result: unknown = await execute( + process.execPath, + ["--import", preload, "scripts/verify-real-cutter.mjs"], + { env, timeout: 10_000 }, + ).then( + () => { + throw new Error("Fixture discovery should stop the verifier"); + }, + (error: unknown) => error, + ); + if ( + !(result instanceof Error) || + !("stderr" in result) || + typeof result.stderr !== "string" + ) + throw new Error("Missing verifier diagnostics"); + return result.stderr; + }; + for (const [selected, actual] of [ + ["linux", "linux"], + ["macOS", "darwin"], + ["Windows", "win32"], + ] as const) + expect(await check(selected, actual)).toContain( + "fixture discovery reached", + ); + expect(await check("linux", "darwin")).toContain( + "Cutter runner did not match its declared platform", + ); + expect(await check("unknown", "linux")).toContain( + "Unrecognized Cutter runner platform", + ); + expect(await check(undefined, "linux")).toContain( + "fixture discovery reached", + ); + expect( + (await readdir(root)).filter((name) => + name.startsWith("rea-cutter-smoke-"), + ), + ).toEqual([]); +}); diff --git a/tests/boundary/cli/evidenceBundleCommands.test.ts b/tests/boundary/cli/evidenceBundleCommands.test.ts index d21424da9b..778f867692 100644 --- a/tests/boundary/cli/evidenceBundleCommands.test.ts +++ b/tests/boundary/cli/evidenceBundleCommands.test.ts @@ -475,7 +475,7 @@ cliTest( ); cliTest( - "diagnoses an obsolete explicit-null Evidence commitment without claiming the file was modified", + "diagnoses an obsolete explicit-null Evidence commitment", async ({ cli }) => { const root = await createTestTempDirectory("rea-legacy-coverage-identity-"); const path = join(root, "legacy.json"); diff --git a/tests/boundary/cli/pcapCli.test.ts b/tests/boundary/cli/pcapCli.test.ts index aa4986bd3b..e042f4985b 100644 --- a/tests/boundary/cli/pcapCli.test.ts +++ b/tests/boundary/cli/pcapCli.test.ts @@ -2,15 +2,6 @@ import { expect, it } from "vitest"; import { expectConfigurationRefusal, serveCli } from "./cliServeHarness.js"; -it.each(["inspect-pcap-client", "inspect-pcap-capture"])( - "%s advertises a schema through the public CLI", - async (command) => { - const result = await serveCli([command, "--schema", "--json"]); - expect(result.exitCode).toBe(0); - expect(JSON.parse(result.stdout)).toBeTruthy(); - }, -); - it.each([ ["inspect-pcap-client"], ["inspect-pcap-capture", "/nonexistent/capture.pcap"], diff --git a/tests/boundary/cli/reverseEngineeringCommands.test.ts b/tests/boundary/cli/reverseEngineeringCommands.test.ts index ee9a7d5eab..77a7ceef67 100644 --- a/tests/boundary/cli/reverseEngineeringCommands.test.ts +++ b/tests/boundary/cli/reverseEngineeringCommands.test.ts @@ -373,8 +373,9 @@ it("does not project cancellation cleanup as incomplete after a successful retry }); it("returns cancellation with the completed observation when the caller interrupts service close", async () => { + const evidence = observation("complete"); const service = { - inspectWithObjdump: vi.fn(async () => ok(observation("complete"))), + inspectWithObjdump: vi.fn(async () => ok(evidence)), close: vi.fn(async () => { process.emit("SIGINT"); }), @@ -382,7 +383,10 @@ it("returns cancellation with the completed observation when the caller interrup const result = await runObjdump(service); - expect(result).toMatchObject({ code: "cancelled" }); + expect(result).toMatchObject({ + code: "cancelled", + details: { partial_observation: evidence }, + }); expect(process.exitCode).toBe(130); }); diff --git a/tests/boundary/evm/evmoleInterfaceProvider.test.ts b/tests/boundary/evm/evmoleInterfaceProvider.test.ts index 1f79921ba6..9edd10309e 100644 --- a/tests/boundary/evm/evmoleInterfaceProvider.test.ts +++ b/tests/boundary/evm/evmoleInterfaceProvider.test.ts @@ -40,6 +40,7 @@ it.runIf(unsupportedHost)( encoding: "hex", }); if (result.ok) throw new Error("Expected a host refusal"); + expect(result.error._tag).toBe("AnalysisCapabilityUnavailableError"); const projected = projectAnalysisError(result.error); expect(projected).toMatchObject({ code: "capability_unavailable" }); expect(projected.message).toContain("Linux x64"); @@ -715,21 +716,6 @@ it.runIf(!unsupportedHost)( }, ); -it.runIf(unsupportedHost)( - "reports unverified host coverage before acquiring a provider", - async () => { - expect( - await new EvmoleInterfaceProvider({}).inspect({ - path: "/selected.hex", - encoding: "hex", - }), - ).toMatchObject({ - ok: false, - error: { _tag: "AnalysisCapabilityUnavailableError" }, - }); - }, -); - it.runIf(!unsupportedHost)( "preserves actual snapshot-write cancellation and root cleanup", async () => { diff --git a/tests/boundary/filesystem/asarSelectedCompanion.test.ts b/tests/boundary/filesystem/asarSelectedCompanion.test.ts index 9fbcc0f3f2..4b500abdba 100644 --- a/tests/boundary/filesystem/asarSelectedCompanion.test.ts +++ b/tests/boundary/filesystem/asarSelectedCompanion.test.ts @@ -38,40 +38,6 @@ const fixture = async () => { return { root, selected, canonical }; }; -it.skipIf(process.platform === "win32")( - "inventories and extracts the same selected ASAR companion as Electron tooling", - async () => { - const { root, selected } = await fixture(); - expect(extractFile(selected, "native.node").equals(bytes)).toBe(true); - const inventory = await inventoryArtifact(selected); - const occurrence = inventory.occurrences.find( - ({ logical_path }) => logical_path === "native.node", - ); - expect(occurrence?.hash_status).toBe("verified"); - expect( - inventory.nodes.find( - ({ artifact_id }) => artifact_id === occurrence?.artifact_id, - )?.sha256, - ).toBe(createHash("sha256").update(bytes).digest("hex")); - const result = await withArtifactResourceScope((resourceScope) => - extractArtifact({ - inputPath: selected, - inputFormat: "asar", - outputRoot: join(root, "output"), - environment: {}, - integrityPolicy: "fail", - resourceScope, - }), - ); - expect( - (await readFile(join(result.output_root, "native.node"))).equals(bytes), - ).toBe(true); - expect(await readFile(join(result.output_root, "packed.txt"), "utf8")).toBe( - "packed bytes", - ); - }, -); - it.skipIf(process.platform === "win32")( "does not substitute the canonical archive companion for an absent selected companion", async () => { diff --git a/tests/boundary/filesystem/bufferDeepEqualityLint.test.ts b/tests/boundary/filesystem/bufferDeepEqualityLint.test.ts index 38c433b070..4637774b26 100644 --- a/tests/boundary/filesystem/bufferDeepEqualityLint.test.ts +++ b/tests/boundary/filesystem/bufferDeepEqualityLint.test.ts @@ -63,14 +63,6 @@ new RuleTester({ expect(bytes).toEqual(value);`, ], invalid: [ - { - // Exact pre-fix ELF test shape: readFile without an encoding returns Buffer. - code: `import { readFile } from "node:fs/promises"; - import { expect } from "vitest"; - const before = await readFile(path); - expect(await readFile(path)).toEqual(before);`, - errors: [{ messageId: "nativeBytes" }], - }, { code: `import { readFileSync as read } from "node:fs"; import { expect } from "vitest"; diff --git a/tests/boundary/filesystem/ciCacheRetention.test.ts b/tests/boundary/filesystem/ciCacheRetention.test.ts index d455b83500..571d05bd22 100644 --- a/tests/boundary/filesystem/ciCacheRetention.test.ts +++ b/tests/boundary/filesystem/ciCacheRetention.test.ts @@ -269,13 +269,20 @@ describe("retention orchestration", () => { it("deletes nothing when a later list is incomplete", async () => { const deleted: number[] = []; - const page = Array.from({ length: 100 }, (_, index) => - cache(index + 1, `${prefix}${String(index)}`, "2026-10-20T01:00:00Z"), - ); + const requests: string[] = []; + const page = [ + cache(1, `${prefix}old`, "2026-10-20T01:00:00Z"), + cache(2, `${prefix}mid`, "2026-10-20T02:00:00Z"), + cache(3, `${prefix}new`, "2026-10-20T03:00:00Z"), + ]; await expect( retainCaches({ repository, request: async (_method, endpoint) => { + requests.push( + new URL(endpoint, "https://example.test").searchParams.get("key") ?? + "", + ); if (endpoint.includes("npm-download-")) throw new Error("list failed after the first family"); return { actions_caches: page }; @@ -283,10 +290,39 @@ describe("retention orchestration", () => { deleteCache: async (id) => { deleted.push(id); }, - families: [{ prefix, keepKey: `${prefix}0` }], + families: [{ prefix, keepKey: `${prefix}new` }], dedupeNpmDownloads: true, }), - ).rejects.toThrow(/list failed|exceeded/u); + ).rejects.toThrow("list failed after the first family"); + expect(requests).toEqual([prefix, "npm-download-"]); + expect(deleted).toEqual([]); + }); +}); + +describe("cache listing and retention", () => { + const repository = "morluto/rea"; + const prefix = "rea-static-v1-Linux-X64-"; + + it("deletes nothing when the cache listing reaches the page cap", async () => { + const deleted: number[] = []; + let requests = 0; + const page = Array.from({ length: 100 }, (_, index) => + cache(index + 1, `${prefix}${String(index)}`, "2026-10-20T01:00:00Z"), + ); + await expect( + retainCaches({ + repository, + request: async () => { + requests += 1; + return { actions_caches: page }; + }, + deleteCache: async (id) => { + deleted.push(id); + }, + families: [{ prefix, keepKey: `${prefix}0` }], + }), + ).rejects.toThrow(/exceeded.*pages/u); + expect(requests).toBe(10); expect(deleted).toEqual([]); }); diff --git a/tests/boundary/filesystem/ciFeedback.test.ts b/tests/boundary/filesystem/ciFeedback.test.ts index d05c7c93d4..48530fd75f 100644 --- a/tests/boundary/filesystem/ciFeedback.test.ts +++ b/tests/boundary/filesystem/ciFeedback.test.ts @@ -12,7 +12,6 @@ import { parseStaticTaskContext, recordStaticTaskContext, } from "../../../scripts/ci/static-task-context.mjs"; -import { createGhApiRequest } from "../../../scripts/ci/github-api.mjs"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const execute = promisify(execFile); @@ -339,17 +338,6 @@ it("records the actual static task graph with explicit source identity and no am expect(() => parseStaticTaskContext(invalid)).toThrow(); }); -it("rejects mutation methods at the shared GitHub transport before invoking the CLI", async () => { - const request = createGhApiRequest({ env: { PATH: "" } }); - await expect( - request("POST", "repos/owner/repo/actions/runs/1/rerun", { - requests: 0, - startedAt: Date.now(), - limitMs: 1000, - }), - ).rejects.toThrow("read-only GET"); -}); - it("collects basic CI telemetry before optional dependency installation can fail", async () => { const { jobs } = z .object({ diff --git a/tests/boundary/filesystem/ciJobTimingsBatch.test.ts b/tests/boundary/filesystem/ciJobTimingsBatch.test.ts index e0dcecc824..c3ea01bb55 100644 --- a/tests/boundary/filesystem/ciJobTimingsBatch.test.ts +++ b/tests/boundary/filesystem/ciJobTimingsBatch.test.ts @@ -91,15 +91,20 @@ it("collects the latest attempt for recent completed CI and Release runs into a headBranch: null, }, ]); - expect(manifest.runs).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - status: "collected", - rawPath: expect.any(String), - reportPath: expect.any(String), - }), - ]), - ); + for (const { id, runAttempt, status, rawPath, reportPath } of manifest.runs) { + const directory = join( + fixture.outputDirectory, + "runs", + `${id}-${runAttempt}`, + ); + expect({ status, rawPath, reportPath }).toEqual({ + status: "collected", + rawPath: join(directory, "jobs.json"), + reportPath: join(directory, "report.json"), + }); + await expect(readFile(rawPath, "utf8")).resolves.toBeTruthy(); + await expect(readFile(reportPath, "utf8")).resolves.toBeTruthy(); + } expect(calls).toContainEqual([ "api", "--paginate", diff --git a/tests/boundary/filesystem/ciPrHealth.test.ts b/tests/boundary/filesystem/ciPrHealth.test.ts index c9d7a968b4..ca1ace52aa 100644 --- a/tests/boundary/filesystem/ciPrHealth.test.ts +++ b/tests/boundary/filesystem/ciPrHealth.test.ts @@ -11,7 +11,6 @@ import { matchRunToPullRequest, writePrCiHealthReport, } from "../../../scripts/ci/pr-ci-health.mjs"; -import { createGhApiRequest } from "../../../scripts/ci/github-api.mjs"; import type { PrCiPullRequest, PrCiWorkflowRun, @@ -453,23 +452,44 @@ it("public CLI uses only GET and produces a report from fixture GitHub responses it("rejects non-GET methods before launching the GitHub client", async () => { const fixture = await createFixture(); - const request = createGhApiRequest({ - env: { - ...process.env, - PATH: `${fixture.fakeBin}${delimiter}${process.env.PATH ?? ""}`, - CI_GH_CALLS: join(fixture.directory, "should-not-exist"), - }, - }); - await expect( - request("POST", "repos/owner/rea/pulls", { - requests: 0, - startedAt: observedAt, - limitMs: 1_000, - }), - ).rejects.toThrow("read-only GET"); - await expect( - readFile(join(fixture.directory, "should-not-exist")), - ).rejects.toMatchObject({ code: "ENOENT" }); + const marker = join(fixture.directory, "gh-invoked"); + const preload = join(fixture.directory, "watch-gh.cjs"); + await writeFile( + preload, + `const fs = require("node:fs"); +const util = require("node:util"); +const child = require("node:child_process"); +child.execFile = () => { throw new Error("Unexpected callback invocation"); }; +child.execFile[util.promisify.custom] = async () => { + fs.appendFileSync(process.env.CI_GH_CALLS, "called\\n"); + return { stdout: "{}", stderr: "" }; +}; +require("node:module").syncBuiltinESMExports(); +`, + ); + const transport = new URL( + "../../../scripts/ci/github-api.mjs", + import.meta.url, + ).href; + await executeWorkflowFixture( + process.execPath, + [ + "--require", + preload, + "--input-type=module", + "-e", + ` + import assert from "node:assert/strict"; + import { createGhApiRequest } from ${JSON.stringify(transport)}; + const request = createGhApiRequest(); + const budget = { requests: 0, startedAt: Date.now(), limitMs: 10_000 }; + await assert.rejects(request("POST", "repos/owner/rea/pulls", budget), /read-only GET/); + await request("GET", "repos/owner/rea/pulls", budget); + `, + ], + { cwd: fixture.directory, env: { ...process.env, CI_GH_CALLS: marker } }, + ); + expect(await readFile(marker, "utf8")).toBe("called\n"); }); function validPull(overrides: Partial = {}): PrCiPullRequest { diff --git a/tests/boundary/filesystem/ciTestHistory.test.ts b/tests/boundary/filesystem/ciTestHistory.test.ts index 0d2093f7cb..9cf82548cb 100644 --- a/tests/boundary/filesystem/ciTestHistory.test.ts +++ b/tests/boundary/filesystem/ciTestHistory.test.ts @@ -132,23 +132,23 @@ it("selects the latest trusted full run that actually published timing history", expect(output).toBe("run-id=11\nartifact-name=test-timings-full\n"); expect(calls).toContain("name=test-timings-full"); expect(calls).toContain("per_page=100"); + expect(calls).not.toContain("actions/runs/13"); expect(calls.match(/actions\/runs\//gu)).toHaveLength(3); expect(calls).not.toContain("actions/workflows"); }); -it("rejects expired, foreign, old, and pull-request candidates", async () => { +it("rejects expired, old, and pull-request candidates", async () => { const { result, output } = await invoke( [ artifact(15, { expired: true }), - artifact(14, { - workflow_run: { ...artifact(14).workflow_run, head_repository_id: 2 }, - }), artifact(13, { created_at: new Date(now - 31 * 24 * 60 * 60 * 1000).toISOString(), }), artifact(12), ], { + 15: run(15), + 13: run(13), 12: run(12, { event: "pull_request" }), }, { tier: "full" }, @@ -197,22 +197,25 @@ it("bounds run lookups to five artifacts even when inventory has more", async () expect(calls.match(/actions\/runs\//gu)).toHaveLength(5); }); -it("returns empty history without querying for an invalid repository identity", async () => { - const { result, output, calls } = await invoke( - [], - {}, - { - repository: "owner/repo?per_page=100", - repositoryId: "not-an-id", - }, - ); - expect(JSON.parse(result.stdout)).toEqual({ - run_id: "", - artifact_name: "", - }); - expect(output).toBe("run-id=\nartifact-name=\n"); - expect(calls).toBe(""); -}); +it.each([ + { repository: "owner/repo?per_page=100", repositoryId: "1" }, + { repository: "example/rea", repositoryId: "not-an-id" }, +])( + "returns empty history without querying for an invalid repository identity: %j", + async (identity) => { + const { result, output, calls } = await invoke( + [], + {}, + { ...identity, tier: "full" }, + ); + expect(JSON.parse(result.stdout)).toEqual({ + run_id: "", + artifact_name: "", + }); + expect(output).toBe("run-id=\nartifact-name=\n"); + expect(calls).toBe(""); + }, +); it("selects fast history without reading the full-tier artifact", async () => { const { result, calls } = await invoke( diff --git a/tests/boundary/filesystem/ghidraBridgeSources.test.ts b/tests/boundary/filesystem/ghidraBridgeSources.test.ts index dc19ac0411..4ef89f268b 100644 --- a/tests/boundary/filesystem/ghidraBridgeSources.test.ts +++ b/tests/boundary/filesystem/ghidraBridgeSources.test.ts @@ -46,6 +46,7 @@ const installation = async ( javac, [ "#!/bin/sh", + 'printf "invoked\\n" >> "$REA_JAVAC_MARKER"', 'if [ "$1" = "-version" ]; then echo "javac 21.0.12"; exit 0; fi', ...(compilerStatus === 0 ? ["exit 0"] @@ -60,8 +61,10 @@ const installation = async ( await mkdir(scratchParent); return { scratchParent, + javacMarker: join(root, "javac-invocations"), environment: { ...process.env, + REA_JAVAC_MARKER: join(root, "javac-invocations"), GHIDRA_INSTALL_DIR: install, JAVA_HOME: javaHome, TMPDIR: scratchParent, @@ -193,6 +196,9 @@ it.skipIf(process.platform === "win32").each([ ), }); expect(await readdir(fixture.scratchParent)).toEqual([]); + await expect(readFile(fixture.javacMarker)).rejects.toMatchObject({ + code: "ENOENT", + }); }, ); diff --git a/tests/boundary/filesystem/ghidraInstallationProperties.test.ts b/tests/boundary/filesystem/ghidraInstallationProperties.test.ts index 97cf2a61da..5f8b147de0 100644 --- a/tests/boundary/filesystem/ghidraInstallationProperties.test.ts +++ b/tests/boundary/filesystem/ghidraInstallationProperties.test.ts @@ -277,6 +277,7 @@ it.each(["read", "capacity", "completed"] as const)( }, }, }); + expect(failure.cause).toBe(reason); } else if (outcome === "capacity") { expect(projectAnalysisError(failure).code).toBe("resource_constraint"); } else { diff --git a/tests/boundary/filesystem/ghidraLanguageCatalogRecovery.test.ts b/tests/boundary/filesystem/ghidraLanguageCatalogRecovery.test.ts index 632832d42d..31adbada80 100644 --- a/tests/boundary/filesystem/ghidraLanguageCatalogRecovery.test.ts +++ b/tests/boundary/filesystem/ghidraLanguageCatalogRecovery.test.ts @@ -218,6 +218,7 @@ it.each(["released", "still failing"] as const)( cause: reason, cleanupIncomplete: false, }); + expect(cancelled.error.cause).toBe(reason); expect(first.error.cleanupIncomplete).toBe(false); expect(registry.hasPendingOwnedResources()).toBe(false); } else { @@ -252,7 +253,7 @@ it("keeps the caller's reason and the catalog owner when cancellation lands duri cleanupIncomplete: true, cleanupResources: [definition], }); - expect(JSON.stringify(resolved.error.cause)).toContain(reason.caller); + expect(resolved.error.cause).toBe(reason); expect(provider.hasPendingOwnedResources()).toBe(true); fault.deny = false; fault.onClose = undefined; diff --git a/tests/boundary/filesystem/javascript-applications/javascriptPackageRendererProvenance.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptPackageRendererProvenance.test.ts index 07bb01a5e1..d35954b05c 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptPackageRendererProvenance.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptPackageRendererProvenance.test.ts @@ -39,13 +39,6 @@ it.each(["renderer", "browser", "module"] as const)( }), ); const metadata = graph.nodes.find(({ kind }) => kind === "package"); - expect(metadata?.observations).toContainEqual( - expect.objectContaining({ - properties: expect.objectContaining({ - renderer_declarations: { [field]: "entry.js" }, - }), - }), - ); expect(metadata?.observations[0]?.properties).not.toHaveProperty( "renderer", ); diff --git a/tests/boundary/filesystem/pythonPyc.test.ts b/tests/boundary/filesystem/pythonPyc.test.ts index 736db15458..25226f9f35 100644 --- a/tests/boundary/filesystem/pythonPyc.test.ts +++ b/tests/boundary/filesystem/pythonPyc.test.ts @@ -48,9 +48,6 @@ describe("Python pyc stable filesystem boundary", () => { const tooSmall = await service.inspect({ path, max_file_bytes: 16 }); if (tooSmall.ok) throw new Error("Expected resource budget failure"); expect(tooSmall.error._tag).toBe("AnalysisResourceConstraintError"); - const tooFewObjects = await service.inspect({ path, max_objects: 1 }); - if (tooFewObjects.ok) throw new Error("Expected object budget failure"); - expect(tooFewObjects.error._tag).toBe("AnalysisResourceConstraintError"); for (const [field, selected] of [ ["max_objects", 1], ["max_depth", 1], @@ -58,6 +55,8 @@ describe("Python pyc stable filesystem boundary", () => { ] as const) { const refused = await service.inspect({ path, [field]: selected }); if (refused.ok) throw new Error(`Expected ${field} refusal`); + if (field === "max_objects") + expect(refused.error._tag).toBe("AnalysisResourceConstraintError"); const projected = projectAnalysisError(refused.error); expect(projected).toMatchObject({ code: "resource_constraint", diff --git a/tests/boundary/filesystem/releaseNotes.test.ts b/tests/boundary/filesystem/releaseNotes.test.ts index 95ea310305..38a601a19d 100644 --- a/tests/boundary/filesystem/releaseNotes.test.ts +++ b/tests/boundary/filesystem/releaseNotes.test.ts @@ -528,15 +528,10 @@ it("accepts Release Please lockfile refreshes without allowing product changes", join(f.directory, "package-lock.json"), '{"lockfileVersion":3}', ); - await writeFile( - join(f.directory, "package-lock.json"), - '{"lockfileVersion":3}', - ); await git(f.directory, [ "add", "CHANGELOG.md", "package-lock.json", - "package-lock.json", ]); await git(f.directory, ["commit", "-m", "chore: release lockfiles"]); await f.invoke(["check", "--manifest", f.manifest, "--source", "HEAD"]); diff --git a/tests/boundary/go/goBinaryFinalization.test.ts b/tests/boundary/go/goBinaryFinalization.test.ts index 7522082264..8db9563eba 100644 --- a/tests/boundary/go/goBinaryFinalization.test.ts +++ b/tests/boundary/go/goBinaryFinalization.test.ts @@ -65,6 +65,7 @@ it.each(["control", "close-failure", "close-cancel", "close-both"] as const)( _tag: "AnalysisCancelledError", cause: reason, }); + expect(primary.cause).toBe(reason); } close.mockRestore(); await service.close(); diff --git a/tests/boundary/go/goBinaryImage.test.ts b/tests/boundary/go/goBinaryImage.test.ts index 362abad5a0..07805f1c4a 100644 --- a/tests/boundary/go/goBinaryImage.test.ts +++ b/tests/boundary/go/goBinaryImage.test.ts @@ -119,6 +119,7 @@ it.each([ it("continues past an empty file-backed ELF data load", () => { const fixture = createGoSplitElfFixture({ sectionless: true }); const expected = readGoBinaryImage(fixture.bytes).build_info; + expect(expected).not.toBeNull(); fixture.bytes.writeBigUInt64LE(0n, 128 + 32); expect(readGoBinaryImage(fixture.bytes).build_info).toEqual(expected); }); @@ -135,6 +136,7 @@ it.each([4, 5, 7])( it("prefers the named ELF section to an earlier writable-load marker", () => { const fixture = createGoSplitElfFixture(); const expected = readGoBinaryImage(fixture.bytes).build_info; + expect(expected).not.toBeNull(); fixture.bytes.copy( fixture.bytes, 512, diff --git a/tests/boundary/go/goBinaryP32PointerWidth.test.ts b/tests/boundary/go/goBinaryP32PointerWidth.test.ts deleted file mode 100644 index 0602d8549c..0000000000 --- a/tests/boundary/go/goBinaryP32PointerWidth.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { expect, it } from "vitest"; -import { readGoBinaryImage } from "../../../src/go/GoBinaryImage.js"; -import { - createGoBinaryFixture, - GO_MODULE_TEXT, -} from "../../fixtures/go/image.js"; - -it("decodes legacy ELF64 Go strings at the record's four-byte pointer width", () => { - const fixture = createGoBinaryFixture({ - format: "elf", - bits: 64, - encoding: "pointer", - pointerSize: 4, - goVersion: "go1.13.15", - }); - - expect(readGoBinaryImage(fixture.bytes)).toMatchObject({ - format: "elf", - architecture: "amd64", - bits: 64, - build_info: { - encoding: "pointer", - go_version: "go1.13.15", - module_text: GO_MODULE_TEXT, - version_location: { offset: fixture.versionOffset, bytes: 9 }, - module_location: { - offset: fixture.moduleOffset, - bytes: fixture.moduleBytes.length, - }, - }, - }); -}); diff --git a/tests/boundary/gpu/codeObjects.test.ts b/tests/boundary/gpu/codeObjects.test.ts index a508d009ff..73e693d59a 100644 --- a/tests/boundary/gpu/codeObjects.test.ts +++ b/tests/boundary/gpu/codeObjects.test.ts @@ -499,14 +499,17 @@ it.each([ const inventory = await inspect(bytes, { analysis: "inventory" }); if (!inventory.ok) throw inventory.error; const row = inventory.value.objects.find((value) => value.kind === "amdgpu")!; - expect(row.target).not.toBeNull(); - const selected = await inspect(bytes, { targets: [row.target!] }); + const expectedTarget = targetId.endsWith("opaque") + ? "amdgcn-amd-amdhsa--gfx900" + : "gfx900"; + expect(row.target).toBe(expectedTarget); + const selected = await inspect(bytes, { targets: [expectedTarget] }); if (!selected.ok) throw selected.error; expect(selected.value.kernels).toHaveLength(1); expect( selected.value.objects.find((value) => value.kind === "amdgpu"), ).toMatchObject({ - target: row.target, + target: expectedTarget, payload_target: "amdgcn-amd-amdhsa--gfx900", target_consistency: targetId.endsWith("opaque") ? "single-source" diff --git a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts index 81c9e97bde..2f6010e787 100644 --- a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts +++ b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts @@ -1173,7 +1173,9 @@ it.each([1, 0xff])( (padding) => { const bytes = createIl2cppBinaryFixture(); bytes.fill(padding, 0x604, 0x608); - expect(link(bytes)).toEqual(link(createIl2cppBinaryFixture())); + const linked = link(bytes); + expect(linked.method_addresses).toEqual(expectedAddresses); + expect(linked).toEqual(link(createIl2cppBinaryFixture())); bytes.writeUInt32LE(3, 0x600); expect(failure(bytes).reason).toBe("code-registration-not-found"); }, @@ -1834,12 +1836,6 @@ it("keeps a capacity refusal while naming a generic row a refusal", () => { expect((failure as Il2cppBinaryFormatFailure).kind).toBe("capacity"); }); -it("ignores CodeRegistration count alignment padding", () => { - const bytes = createIl2cppBinaryFixture(); - bytes.fill(0xff, 0x604, 0x608); - expect(link(bytes).method_addresses).toEqual(expectedAddresses); -}); - it.each([0, 8, 16])( "finds registration with %s file-backed trailing zero bytes", (tail) => { diff --git a/tests/boundary/mcp/cutterCancellationMcp.test.ts b/tests/boundary/mcp/cutterCancellationMcp.test.ts index 4a2a5ca589..f8b520e511 100644 --- a/tests/boundary/mcp/cutterCancellationMcp.test.ts +++ b/tests/boundary/mcp/cutterCancellationMcp.test.ts @@ -114,7 +114,7 @@ it.skipIf(process.platform === "win32")( }, ); -it("propagates MCP cancellation into an active Cutter command and reports uncertain completion", async () => { +it("propagates MCP cancellation into an active Cutter command", async () => { let markDispatched: (() => void) | undefined; let observedAbort = false; const dispatched = new Promise((resolve) => { diff --git a/tests/boundary/mcp/enhancedTools.swiftTypes.test.ts b/tests/boundary/mcp/enhancedTools.swiftTypes.test.ts index 82fc8d4945..9d6c0f2a95 100644 --- a/tests/boundary/mcp/enhancedTools.swiftTypes.test.ts +++ b/tests/boundary/mcp/enhancedTools.swiftTypes.test.ts @@ -147,15 +147,24 @@ describe("Swift classification through MCP", () => { it("returns cancellation during the symbol join", async () => { const controller = new AbortController(); + const reason = { stage: "symbol-join" }; const tools = new EnhancedTools({ execute: async (operation) => { - if (operation === "list_names") controller.abort(); + if (operation === "list_names") controller.abort(reason); return ok([{ address: "0x1", value: "$s4main4PairVMa" }]); }, }); - expect( - await tools.execute("analyze_swift_types", {}, controller.signal), - ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + const result = await tools.execute( + "analyze_swift_types", + {}, + controller.signal, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError", operation: "list_names" }, + }); + if (result.ok) throw new Error("Expected cancellation"); + expect(result.error.cause).toBe(reason); }); }); diff --git a/tests/boundary/mcp/flutterMcp.test.ts b/tests/boundary/mcp/flutterMcp.test.ts index d76c022367..dda9ac505c 100644 --- a/tests/boundary/mcp/flutterMcp.test.ts +++ b/tests/boundary/mcp/flutterMcp.test.ts @@ -13,6 +13,7 @@ import { flutterResultSchemas, } from "../../../src/domain/flutter/flutterBuildAnalysis.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js"; import { aotFixtureImage } from "../../../src/flutter/FlutterBuild.fixture.js"; import { createFlutterApk } from "../../fixtures/flutterApk.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; @@ -318,6 +319,14 @@ it("advertises an exact valid Flutter schema and records inline evidence through .map((part) => part.text ?? "") .join(""); const inspectedEvidence = parseEvidence(JSON.parse(inspectedText)); + const bundle = await client.callTool({ + name: "get_evidence_bundle", + arguments: {}, + }); + expect(bundle.isError).not.toBe(true); + const records = parseMcpEvidenceBundle(bundle).records; + expect(records).toContainEqual(evidence); + expect(records).toContainEqual(inspectedEvidence); expect(inspectedEvidence.normalized_result).toMatchObject({ abi: "arm64-v8a", libapp: { diff --git a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts index 82edcee870..77b3413ed5 100644 --- a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts +++ b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts @@ -20,6 +20,7 @@ import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contract import { createJavaScriptRuntimeObservationEvidence } from "../../../src/application/javascript/JavaScriptRuntimeObservationEvidence.js"; import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../../../src/inspector/providerIdentity.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js"; import { observed } from "../../fixtures/analysisExecution.js"; import { startFakeV8Inspector } from "../../fixtures/inspector/fakeV8Inspector.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -271,6 +272,14 @@ function registerRetainedRuntimeEvidence( }, }); const evidenceId = evidenceIdFrom(observedRuntime.structuredContent); + const bundle = await client.callTool({ + name: "get_evidence_bundle", + arguments: {}, + }); + expect(bundle.isError).not.toBe(true); + expect(parseMcpEvidenceBundle(bundle).records).toContainEqual( + observedRuntime.structuredContent, + ); expect(observedRuntime.structuredContent).toMatchObject({ evidence_id: evidenceId, operation: "observe_javascript_runtime", diff --git a/tests/boundary/mcp/managedMcp.test.ts b/tests/boundary/mcp/managedMcp.test.ts index 4e45cf91fb..ba370ca045 100644 --- a/tests/boundary/mcp/managedMcp.test.ts +++ b/tests/boundary/mcp/managedMcp.test.ts @@ -344,6 +344,16 @@ const verifyManagedCatalogAndNativeWorkflow = async ( await outputAssertion.assert("verify_managed_native_boundaries", verified); expect(verified).toMatchObject({ evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u), + operation: "verify_managed_native_boundaries", + provider: { id: "rea-dotnet-workflows" }, + confidence: "inferred", + authority: "analyst-inference", + evidence_links: [ + MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries.evidence_id, + ...MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations.map( + ({ evidence_id }) => evidence_id, + ), + ], normalized_result: { summary: { verified: 1 }, algorithm: { token_to_address_mapping: "not-inferred" }, diff --git a/tests/boundary/mcp/modelArtifactPartialCancellation.test.ts b/tests/boundary/mcp/modelArtifactPartialReadFailure.test.ts similarity index 95% rename from tests/boundary/mcp/modelArtifactPartialCancellation.test.ts rename to tests/boundary/mcp/modelArtifactPartialReadFailure.test.ts index 58df84f360..857c8fa0fa 100644 --- a/tests/boundary/mcp/modelArtifactPartialCancellation.test.ts +++ b/tests/boundary/mcp/modelArtifactPartialReadFailure.test.ts @@ -52,7 +52,7 @@ mcpTest.for([ { retained: false, fullHash: true }, { retained: true, fullHash: true }, ])( - "projects partial model Evidence through an MCP tool error ($retained, fullHash=$fullHash)", + "projects partial model Evidence after a denied read ($retained, fullHash=$fullHash)", async ({ retained, fullHash }, { mcp }) => { const root = await createTestTempDirectory("rea-model-mcp-partial-"); const path = join(root, fullHash ? "model.safetensors" : "model.gguf"); @@ -133,6 +133,14 @@ mcpTest.for([ }); if (!observedArrayChunk) throw new Error(JSON.stringify(response)); const projected = parseMcpToolError(response); + expect(projected.error).toMatchObject({ + code: "access_denied", + details: { + operation: "inspect_model_artifact", + path, + system_code: "EACCES", + }, + }); const details = projected.error.details; if (details === undefined) throw new Error("MCP error omitted its partial observation"); diff --git a/tests/boundary/mcp/pcapMcp.test.ts b/tests/boundary/mcp/pcapMcp.test.ts index 95154eb14e..8678cac2df 100644 --- a/tests/boundary/mcp/pcapMcp.test.ts +++ b/tests/boundary/mcp/pcapMcp.test.ts @@ -8,6 +8,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js"; import { ok } from "../../../src/domain/result.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js"; const identity = { id: "pcap", name: "Packet capture analysis", version: null }; @@ -116,6 +117,12 @@ it("advertises exact valid pcap schemas and records inline evidence through MCP" .map((part) => part.text ?? "") .join(""); const evidence = parseEvidence(JSON.parse(text)); + const bundle = await client.callTool({ + name: "get_evidence_bundle", + arguments: {}, + }); + expect(bundle.isError).not.toBe(true); + expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(evidence); expect(evidence.normalized_result).toMatchObject({ packet_count: 50, capture_start: "2026-10-11T14:13:40.608505Z", diff --git a/tests/boundary/mcp/promptMcp.test.ts b/tests/boundary/mcp/promptMcp.test.ts index 7b732a78d0..c57e0f7d35 100644 --- a/tests/boundary/mcp/promptMcp.test.ts +++ b/tests/boundary/mcp/promptMcp.test.ts @@ -1,5 +1,5 @@ import { ok as resultOk, err, ok } from "../../../src/domain/result.js"; -import { readFile, rm, writeFile } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import { basename, join } from "node:path"; import { @@ -45,17 +45,11 @@ import { observed } from "../../fixtures/analysisExecution.js"; import { serveStreamableHttp } from "../../../src/server/http/serveStreamableHttp.js"; const resources: Array<{ close(): Promise }> = []; -const temporaryDirectories: string[] = []; afterEach(async () => { await Promise.allSettled( resources.splice(0).map((resource) => resource.close()), ); - await Promise.all( - temporaryDirectories - .splice(0) - .map((directory) => rm(directory, { recursive: true, force: true })), - ); }); describe("guided prompts over MCP", () => { @@ -364,7 +358,6 @@ describe("guided prompt family scope", () => { it("preserves caller-selected paths and verbatim crash signals", async () => { const directory = await createTestTempDirectory("rea-prompt-exact-context-"); - temporaryDirectories.push(directory); const targetPath = join(directory, "target with trailing space "); await writeFile(targetPath, "owned prompt target", "utf8"); await expect(readFile(targetPath.trim(), "utf8")).rejects.toThrow(); @@ -493,7 +486,6 @@ describe("guided prompt completion registry", () => { it("guided prompt completion avoids cold startup and cancels warm lookup on close", async () => { const directory = await createTestTempDirectory("rea-prompt-warm-only-"); - temporaryDirectories.push(directory); const target = join(directory, "target.hop"); await writeFile(target, "fixture target"); const fixture = await createWarmCompletionFixture(target); @@ -696,7 +688,6 @@ describe("guided prompt completion lifecycle", () => { it("refreshes wire completions across open, switch, concurrency, and close", async () => { const directory = await createTestTempDirectory("rea-prompt-mcp-"); - temporaryDirectories.push(directory); const first = join(directory, "first.hop"); const second = join(directory, "second.hop"); await Promise.all([writeFile(first, "first"), writeFile(second, "second")]); diff --git a/tests/boundary/mcp/toolResult.test.ts b/tests/boundary/mcp/toolResult.test.ts index 1ee63a03aa..95bbe231b0 100644 --- a/tests/boundary/mcp/toolResult.test.ts +++ b/tests/boundary/mcp/toolResult.test.ts @@ -372,7 +372,7 @@ describe("tool result projection", () => { .details, ).not.toHaveProperty("launch_rejection"); }); - it("projects bounded private-display coordinates without raw stderr", () => { + it("projects private-display diagnostic fields and stderr byte count", () => { const result = delivery.toCallToolResult( err( new HopperProcessError(80, { @@ -405,6 +405,7 @@ describe("tool result projection", () => { mount_read_only: true, wsl: true, strategy: "user-mount-namespace", + xvfb_stderr_bytes: 512, }, }, }, diff --git a/tests/boundary/mcp/universalSliceMcp.test.ts b/tests/boundary/mcp/universalSliceMcp.test.ts index 9fab37a6ec..1009f3c1ad 100644 --- a/tests/boundary/mcp/universalSliceMcp.test.ts +++ b/tests/boundary/mcp/universalSliceMcp.test.ts @@ -355,6 +355,7 @@ sliceTest( ...thinReport.auditable_metadata, file_offset: (thinReport.auditable_metadata?.file_offset ?? 0) + offset, }); + expect(thinReport.rustc_commit?.hashes.length).toBeGreaterThan(0); expect(report.rustc_commit?.hashes).toEqual( thinReport.rustc_commit?.hashes.map((entry) => ({ ...entry, diff --git a/tests/boundary/model/modelArtifact.test.ts b/tests/boundary/model/modelArtifact.test.ts index be6aef9dfc..f181bb6a62 100644 --- a/tests/boundary/model/modelArtifact.test.ts +++ b/tests/boundary/model/modelArtifact.test.ts @@ -1233,7 +1233,7 @@ it("reports a truncated safetensors header whose length resembles a pickle marke }); }); -it("bounds expansion before allocation and distinguishes unsupported pickle, version and paths", async () => { +it("bounds expansion before allocation and distinguishes unsupported pickle and paths", async () => { const fixture = ggufFixture(); fixture.bytes.writeBigUInt64LE(0xffffffffffffffffn, 8); const { provider, path, root } = await providerAt(fixture.bytes); @@ -1245,9 +1245,6 @@ it("bounds expansion before allocation and distinguishes unsupported pickle, ver if (pickle.ok) throw new Error("Pickle admitted"); expect(pickle.error._tag).toBe("AnalysisUnsupportedTargetError"); await writeFile(path, ggufFixture({ version: 999 }).bytes); - const version = await provider.inspect({ path, hash: "header" }); - if (version.ok) throw new Error("Unknown version admitted"); - expect(version.error._tag).toBe("AnalysisUnsupportedTargetError"); const alias = join(root, "link"); await symlink(path, alias); const linked = await provider.inspect({ path: alias, hash: "header" }); diff --git a/tests/boundary/process/adbVerifierCleanup.test.ts b/tests/boundary/process/adbVerifierCleanup.test.ts index 0a8db02b3b..bfe2e4fe2c 100644 --- a/tests/boundary/process/adbVerifierCleanup.test.ts +++ b/tests/boundary/process/adbVerifierCleanup.test.ts @@ -21,6 +21,7 @@ afterEach(async () => { const runVerifier = async ( settings: { + args?: string[]; closeFails?: boolean; thrown?: "undefined" | "null"; removal?: "fails" | "leaves_probe"; @@ -63,7 +64,8 @@ const runVerifier = async ( await writeFile( join(root, "dist", "composition", "adb.js"), `import { writeFile } from "node:fs/promises"; -export const createAdbDeviceAnalysisProvider = () => ({ +import { writeFileSync } from "node:fs"; +export const createAdbDeviceAnalysisProvider = () => (writeFileSync(process.env.REA_ADB_CREATE_MARKER, "created"), { async close() { await writeFile(process.env.REA_ADB_CLOSE_MARKER, "closed"); if (process.env.REA_ADB_CLOSE_FAIL === "1") { @@ -112,16 +114,18 @@ export class AdbDeviceAnalysisService { }\n`, ); const marker = join(root, "provider-closed"); + const createdMarker = join(root, "provider-created"); const adbLog = join(root, "adb.log"); const run = spawnSync( process.execPath, - [join(root, "scripts", "verify-adb.mjs")], + [join(root, "scripts", "verify-adb.mjs"), ...(settings.args ?? [])], { encoding: "utf8", timeout: 10_000, env: { ...process.env, REA_ADB_CLOSE_MARKER: marker, + REA_ADB_CREATE_MARKER: createdMarker, REA_ADB_CLOSE_FAIL: settings.closeFails ? "1" : "0", REA_FAKE_ADB: adb, REA_FAKE_ADB_LOG: adbLog, @@ -135,11 +139,12 @@ export class AdbDeviceAnalysisService { }, ); const closed = await readFile(marker, "utf8").catch(() => null); + const created = await readFile(createdMarker, "utf8").catch(() => null); const adbCalls = await readFile(adbLog, "utf8").then( (text) => text.trim().split("\n"), () => [], ); - return { ...run, closed, adbCalls, deviceFiles: await readdir(device) }; + return { ...run, created, closed, adbCalls, deviceFiles: await readdir(device) }; }; // The fake adb client is a POSIX shell script. @@ -251,3 +256,18 @@ it("reports both an unexpected verifier throw and provider cleanup failure", asy expect(result.stderr).toContain("fixture/private-root"); expect(result.closed).toBe("closed"); }); + + +it.each([["--install-apk", "/tmp/unrelated.apk"], ["--install-apk"]])( + "rejects %s before contacting the device or acquiring provider resources", + async (...args) => { + const result = await runVerifier({ args }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("APK identity and device cleanup ownership cannot be established safely"); + expect(result.created).toBeNull(); + expect(result.closed).toBeNull(); + expect(result.adbCalls).toEqual([]); + expect(result.deviceFiles).toEqual([]); + }, +); diff --git a/tests/boundary/process/applicationWorkflowVerifier.test.ts b/tests/boundary/process/applicationWorkflowVerifier.test.ts index f420e817ca..2e10cd563d 100644 --- a/tests/boundary/process/applicationWorkflowVerifier.test.ts +++ b/tests/boundary/process/applicationWorkflowVerifier.test.ts @@ -1,5 +1,5 @@ import { execFile } from "node:child_process"; -import { copyFile, mkdir, rm } from "node:fs/promises"; +import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; @@ -107,3 +107,65 @@ describe("local application workflow verifier", () => { }); }, 20_000); }); + +it.each([false, true])( + "joins sibling analysis cleanup after a first failure (cleanup fails: %s)", + async (cleanupFails) => { + const root = await createTestTempDirectory( + "rea-application-verifier-cleanup-", + ); + const left = join(root, "left"); + const right = join(root, "right"); + const marker = join(root, "cleanup-completed"); + const preload = join(root, "instrument.mjs"); + await Promise.all([mkdir(left), mkdir(right)]); + const fake = ` +import { writeFile } from 'node:fs/promises'; +export async function analyzeJavaScriptApplicationWithOwnedScope({input_path}) { + if (input_path === ${JSON.stringify(left)}) throw new Error('authored first failure'); + await new Promise(resolve => setTimeout(resolve, 200)); + await writeFile(${JSON.stringify(marker)}, 'closed'); + if (${JSON.stringify(cleanupFails)}) throw new Error('authored late cleanup failure'); + return {}; +} +`; + await writeFile( + preload, + ` +import { registerHooks } from 'node:module'; +registerHooks({ resolve(specifier, context, next) { + if (specifier === '../../lib/javascript-application-analysis.mjs' && context.parentURL?.endsWith('/scripts/verify/javascript/application-workflows.mjs')) + return { shortCircuit: true, url: 'data:text/javascript,' + encodeURIComponent(${JSON.stringify(fake)}) }; + return next(specifier, context); +}}); +`, + ); + const result: unknown = await execute( + process.execPath, + [ + "--import", + preload, + "scripts/verify/javascript/application-workflows.mjs", + "--left", + left, + "--right", + right, + ], + { timeout: 10_000 }, + ).then( + () => { + throw new Error("Expected analysis failure"); + }, + (error: unknown) => error, + ); + expect(await readFile(marker, "utf8")).toBe("closed"); + expect(result).toMatchObject({ + stderr: expect.stringContaining("authored first failure"), + }); + if (cleanupFails) + expect(result).toMatchObject({ + stderr: expect.stringContaining("authored late cleanup failure"), + }); + }, + 20_000, +); diff --git a/tests/boundary/process/bunVerifierOwnerInitialization.test.ts b/tests/boundary/process/bunVerifierOwnerInitialization.test.ts new file mode 100644 index 0000000000..68c711ebac --- /dev/null +++ b/tests/boundary/process/bunVerifierOwnerInitialization.test.ts @@ -0,0 +1,80 @@ +import { spawnSync } from "node:child_process"; +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, it } from "vitest"; + +it.each([ + ["initialization failure", "init", true], + ["initialization and root cleanup failures", "init_close", false], + ["command failure", "run", true], + ["command cleanup failure", "close", false], +] as const)( + "retains the verifier root correctly after %s", + async (_name, failure, rootClosed) => { + const root = await mkdtemp(join(tmpdir(), "rea-bun-owner-test-")); + try { + await mkdir(join(root, "scripts", "lib"), { recursive: true }); + await mkdir(join(root, "dist", "process"), { recursive: true }); + await mkdir(join(root, "dist", "domain"), { recursive: true }); + await writeFile( + join(root, "scripts", "verify-bun-standalone.mjs"), + await readFile( + new URL( + "../../../scripts/verify-bun-standalone.mjs", + import.meta.url, + ), + ), + ); + await writeFile( + join(root, "scripts", "lib", "verifier-cleanup.mjs"), + await readFile( + new URL("../../../scripts/lib/verifier-cleanup.mjs", import.meta.url), + ), + ); + await writeFile( + join(root, "scripts", "lib", "verify-commands.mjs"), + `export async function createVerifierCommands() { + if (process.env.FAULT.startsWith("init")) throw new Error("owner load refused"); + return { async exec() { throw new Error("command refused"); }, async close() { + if (process.env.FAULT === "close") throw new Error("owner close refused"); + } }; + }`, + ); + await writeFile( + join(root, "dist", "process", "PrivateRuntimeRoot.js"), + `import { writeFile } from "node:fs/promises"; + export class PrivateRuntimeRoot { static async create() { return { + path: process.env.ROOT, async close() { if (process.env.FAULT === "init_close") throw new Error("root close refused"); await writeFile(process.env.CLOSED, "closed"); } + }; } }`, + ); + await writeFile( + join(root, "dist", "domain", "evidence.js"), + `export function parseEvidence() { throw new Error("unexpected"); }`, + ); + const marker = join(root, "closed"); + const result = spawnSync( + process.execPath, + [join(root, "scripts", "verify-bun-standalone.mjs")], + { + encoding: "utf8", + timeout: 10_000, + env: { ...process.env, ROOT: root, CLOSED: marker, FAULT: failure }, + }, + ); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + failure.startsWith("init") ? "owner load refused" : "command refused", + ); + if (failure === "close") + expect(result.stderr).toContain("owner close refused"); + if (failure === "init_close") + expect(result.stderr).toContain("root close refused"); + expect(await readFile(marker, "utf8").catch(() => null)).toBe( + rootClosed ? "closed" : null, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); diff --git a/tests/boundary/process/claudeVerifierSetupTargets.test.ts b/tests/boundary/process/claudeVerifierSetupTargets.test.ts new file mode 100644 index 0000000000..995770f6e7 --- /dev/null +++ b/tests/boundary/process/claudeVerifierSetupTargets.test.ts @@ -0,0 +1,94 @@ +import { spawnSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +it.each(([ + ["claude", "relative path", false], + ["claude", "sibling", false], + ["claude", "parent traversal", false], + ["claude", "owned child", true], + ["opencode", "relative path", false], + ["opencode", "sibling", false], + ["opencode", "parent traversal", false], + ["opencode", "owned child", true], +] as const).filter(([client]) => process.platform !== "win32" || client !== "opencode"))( + "%s checks a %s setup-plan target before writing", + async (client, kind, allowed) => { + const state = await mkdtemp(join(tmpdir(), "rea-claude-plan-")); + try { + const commands = join(state, "commands.mjs"); + const fixture = join(state, "fixture.mjs"); + const hook = join(state, "hook.mjs"); + await writeFile( + commands, + `import { writeFile } from 'node:fs/promises'; +import { isAbsolute, join, relative } from 'node:path'; +export async function createVerifierCommands(){return { + async exec(command,args,options={}){ + if(command==='git')return {stdout:'',stderr:''}; + if(args[0]==='--version')return {stdout:'fake client',stderr:''}; + if(args.includes('setup') && args.includes('--dry-run')){ + const client=process.env.CLIENT; const profile=client==='claude'?options.env.CLAUDE_CONFIG_DIR:options.env.OPENCODE_CONFIG_DIR; const skill=client==='claude'?join(profile,'skills','reverse-engineer-anything'):join(options.env.USERPROFILE,'.agents','skills','reverse-engineer-anything'); + const candidate=process.env.KIND==='relative path' ? relative(process.cwd(),join(skill,'AGENTS.md')) : process.env.KIND==='sibling' ? skill+'-sibling/AGENTS.md' : process.env.KIND==='parent traversal' ? skill+'/../outside/AGENTS.md' : skill+'/AGENTS.md'; + if (process.env.KIND==='relative path' && (candidate==='' || isAbsolute(candidate))) throw Error('Fixture expected a relative target'); + return {stdout:JSON.stringify({status:'planned',plannedActions:[{target:join(profile,client==='claude'?'.claude.json':'opencode.jsonc')},{target:skill},{target:candidate}]}),stderr:''}; + } + await writeFile(process.env.STATE+'/setup-advanced','yes'); throw Error('intentional stop before setup mutation'); + }, async close(){} };}`, + ); + await writeFile( + fixture, + `export async function createAnthropicModelFixture(){return {baseUrl:'http://127.0.0.1:1',async close(){}};} export const createOpenAiModelFixture=createAnthropicModelFixture;`, + ); + await writeFile( + hook, + `import {registerHooks} from 'node:module';import{pathToFileURL}from'node:url'; +registerHooks({resolve(specifier,context,next){if(context.parentURL?.endsWith('/scripts/verify-'+process.env.CLIENT+'-client.mjs')){let f;if(specifier==='./lib/verify-commands.mjs')f='commands.mjs';if(specifier==='./verify/clients/anthropic-model-fixture.mjs'||specifier==='./verify/clients/model-fixture.mjs')f='fixture.mjs';if(f)return{url:pathToFileURL(process.env.STATE+'/'+f).href,shortCircuit:true};}return next(specifier,context);}});`, + ); + const result = spawnSync( + process.execPath, + [ + "--import", + hook, + fileURLToPath( + new URL( + `../../../scripts/verify-${client}-client.mjs`, + import.meta.url, + ), + ), + ], + { + encoding: "utf8", + cwd: state, + timeout: 10_000, + env: { + ...process.env, + STATE: state, + KIND: kind, + CLIENT: client, + TMPDIR: state, + TMP: state, + TEMP: state, + }, + }, + ); + expect(result.status).not.toBe(0); + const advanced = await readFile( + join(state, "setup-advanced"), + "utf8", + ).then( + () => true, + () => false, + ); + expect(advanced).toBe(allowed); + expect(result.stderr).toContain( + allowed ? "intentional stop before setup mutation" : "AssertionError", + ); + } finally { + await rm(state, { recursive: true, force: true }); + } + }, +); diff --git a/tests/boundary/process/clientVerifierSetupTargets.test.ts b/tests/boundary/process/clientVerifierSetupTargets.test.ts new file mode 100644 index 0000000000..cbb0f16077 --- /dev/null +++ b/tests/boundary/process/clientVerifierSetupTargets.test.ts @@ -0,0 +1,99 @@ +import { spawnSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +it.each(([ + ["gemini", "relative path", false], + ["gemini", "profile traversal", false], + ["gemini", "profile root alias", false], + ["gemini", "skill root alias", false], + ["gemini", "skill traversal", false], + ["gemini", "owned child", true], + ["copilot", "relative path", false], + ["copilot", "sibling", false], + ["copilot", "skill traversal", false], + ["copilot", "owned child", true], + ["qwen", "relative path", false], + ["qwen", "profile traversal", false], + ["qwen", "profile root alias", false], + ["qwen", "skill root alias", false], + ["qwen", "skill traversal", false], + ["qwen", "profile child", true], + ["qwen", "skill child", true], +] as const).filter(([client]) => process.platform !== "win32" || client !== "copilot"))( + "%s checks %s setup-plan targets before writing", + async (client, kind, allowed) => { + const state = await mkdtemp(join(tmpdir(), "rea-client-plan-")); + try { + const commands = join(state, "commands.mjs"); + const fixture = join(state, "fixture.mjs"); + const hook = join(state, "hook.mjs"); + await writeFile( + commands, + `import {writeFile} from 'node:fs/promises'; import {join} from 'node:path'; +export async function createVerifierCommands(){return { + async exec(command,args,options={}){ + if(command==='git')return {stdout:'',stderr:''}; + if(args.includes('--version'))return {stdout:'fake client',stderr:''}; + if(args.includes('setup') && args.includes('--dry-run')){ + const client=process.env.CLIENT; const kind=process.env.KIND; + const profile=client==='gemini'?options.env.GEMINI_CLI_HOME:client==='copilot'?options.env.COPILOT_HOME:options.env.QWEN_HOME; + const skills=join(client==='gemini'?profile:options.env.HOME,'.agents','skills'); + const skill=join(skills,'reverse-engineer-anything'); + const config=join(profile,client==='gemini'?'.gemini/settings.json':client==='copilot'?'mcp-config.json':'settings.json'); + const candidate=kind==='relative path'?'relative/AGENTS.md':kind==='profile root alias'?profile+'/child/..':kind==='skill root alias'?skills+'/child/..':kind==='profile traversal'?profile+'/../outside/AGENTS.md':kind==='skill traversal'?(client==='copilot'?skill:skills)+'/../outside/AGENTS.md':kind==='sibling'?skill+'-sibling/AGENTS.md':kind==='profile child'?config:join(skill,'SKILL.md'); + const selectedSkill=(kind==='skill traversal'||kind==='skill root alias')&&client==='gemini'?candidate:join(skill,'SKILL.md'); + const actions=client==='qwen'?[{target:candidate}]:[{target:config},{target:client==='copilot'?skill:selectedSkill},{target:candidate}]; + return {stdout:JSON.stringify({status:'planned',plannedActions:actions}),stderr:''}; + } + await writeFile(process.env.STATE+'/setup-advanced','yes'); throw Error('intentional stop before setup mutation'); + }, async close(){} };}`, + ); + await writeFile( + fixture, + `export async function createOpenAiModelFixture(){return {baseUrl:'http://127.0.0.1:1',async close(){}};}`, + ); + await writeFile( + hook, + `import {registerHooks} from 'node:module';import{pathToFileURL}from'node:url'; +registerHooks({resolve(specifier,context,next){if(context.parentURL?.endsWith('/scripts/verify-'+process.env.CLIENT+'-client.mjs')){let f;if(specifier==='./lib/verify-commands.mjs')f='commands.mjs';if(specifier==='./verify/clients/model-fixture.mjs')f='fixture.mjs';if(f)return{url:pathToFileURL(process.env.STATE+'/'+f).href,shortCircuit:true};}return next(specifier,context);}});`, + ); + const script = fileURLToPath( + new URL( + `../../../scripts/verify-${client}-client.mjs`, + import.meta.url, + ), + ); + const result = spawnSync(process.execPath, ["--import", hook, script], { + encoding: "utf8", + timeout: 10_000, + env: { + ...process.env, + STATE: state, + CLIENT: client, + KIND: kind, + TMPDIR: state, + TMP: state, + TEMP: state, + }, + }); + expect(result.status).not.toBe(0); + const advanced = await readFile( + join(state, "setup-advanced"), + "utf8", + ).then( + () => true, + () => false, + ); + expect(advanced).toBe(allowed); + expect(result.stderr).toContain( + allowed ? "intentional stop before setup mutation" : "AssertionError", + ); + } finally { + await rm(state, { recursive: true, force: true }); + } + }, +); diff --git a/tests/boundary/process/commandcodeVerifierSkillBody.test.ts b/tests/boundary/process/commandcodeVerifierSkillBody.test.ts new file mode 100644 index 0000000000..3df9272e11 --- /dev/null +++ b/tests/boundary/process/commandcodeVerifierSkillBody.test.ts @@ -0,0 +1,138 @@ +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +const posix = it.skipIf(process.platform === "win32"); +posix.each([ + ["empty", false], + ["empty body", false], + ["wrong role", false], + ["wrong id", false], + ["missing body", false], + ["complete", true], +] as const)( + "requires CommandCode skill proof for %s", + async (mode, allowed) => { + const state = await mkdtemp(join(tmpdir(), "rea-commandcode-skill-")); + try { + const account = join(state, "account"); + await mkdir(account); + await writeFile( + join(account, ".rea-client-verification"), + "Disposable REA client verification account\n", + ); + const commands = String.raw` +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { TOOL_CONTRACTS } from REPLACE_CONTRACTS; + +let setup = 0; +export async function createVerifierCommands() { + return { + async exec(command, args, options = {}) { + if (command === "git") return { stdout: "", stderr: "" }; + if (args.includes("--version")) return { stdout: "fake", stderr: "" }; + const account = options.env.USERPROFILE; + const profile = join(account, ".commandcode"); + const config = join(profile, "mcp.json"); + const skill = join(account, ".agents", "skills", "reverse-engineer-anything"); + if (args.includes("setup") && args.includes("--dry-run")) { + return { stdout: JSON.stringify({ status: "planned", plannedActions: [{ target: config }, { target: skill }] }), stderr: "" }; + } + if (args.includes("setup")) { + if (setup++ === 0) { + await writeFile(config + ".rea.backup", await readFile(config)); + await mkdir(skill, { recursive: true }); + const frontmatter = "---\nname: reverse-engineer-anything\n---\n"; + const body = 'Complete "authored" instructions\nKeep a \\ literal\n'; + await writeFile(join(skill, "SKILL.md"), process.env.MODE === "empty" ? "" : frontmatter + (process.env.MODE === "empty body" ? "" : body)); + return { stdout: JSON.stringify({ status: "ready" }), stderr: "" }; + } + return { stdout: JSON.stringify({ appliedActions: [] }), stderr: "" }; + } + if (args.includes("doctor")) return { stdout: JSON.stringify({ healthy: true }), stderr: "" }; + const tools = [ + { name: "search_tools" }, + { name: "activate_skill", parameters: { properties: { name: { enum: ["reverse-engineer-anything"] } } } }, + ]; + const system = { role: "system", content: TOOL_CONTRACTS.map((t) => "mcp__rea__" + t.name).join(" ") }; + const first = await globalThis.fakeRequest({ body: { messages: [system] }, tools }); + const body = 'Complete "authored" instructions\nKeep a \\ literal'; + const response = { + role: process.env.MODE === "wrong role" ? "user" : "tool", + tool_call_id: process.env.MODE === "wrong id" ? "wrong" : first.tool_calls[0].id, + content: process.env.MODE === "missing body" ? "partial" : body, + }; + const second = await globalThis.fakeRequest({ body: { messages: [system, response] }, tools }); + await writeFile(process.env.STATE + "/observed.json", JSON.stringify({ first, second })); + throw Error("intentional stop before client output"); + }, + async close() {}, + }; +} +`.replace( + "REPLACE_CONTRACTS", + JSON.stringify( + new URL("../../../dist/contracts/toolContracts.js", import.meta.url).href, + ), + ); + await writeFile(join(state, "commands.mjs"), commands); + await writeFile( + join(state, "fixture.mjs"), + "export async function createOpenAiModelFixture({onRequest}){globalThis.fakeRequest=onRequest;return {baseUrl:'http://127.0.0.1:1',async close(){}};}\n", + ); + await writeFile( + join(state, "hook.mjs"), + "import{registerHooks}from'node:module';import{pathToFileURL}from'node:url';registerHooks({resolve(specifier,context,next){if(context.parentURL?.endsWith('/scripts/verify-commandcode-client.mjs')){let f;if(specifier==='node:os')f='os.mjs';if(specifier==='./lib/verify-commands.mjs')f='commands.mjs';if(specifier==='./verify/clients/model-fixture.mjs')f='fixture.mjs';if(f)return{url:pathToFileURL(process.env.STATE+'/'+f).href,shortCircuit:true};}return next(specifier,context);}});\n", + ); + await writeFile( + join(state, "os.mjs"), + "export * from 'node:os';export function homedir(){return process.env.REA_VERIFY_COMMANDCODE_ACCOUNT_HOME;}\n", + ); + const script = fileURLToPath( + new URL( + "../../../scripts/verify-commandcode-client.mjs", + import.meta.url, + ), + ); + const result = spawnSync( + process.execPath, + ["--import", join(state, "hook.mjs"), script], + { + encoding: "utf8", + timeout: 10_000, + env: { + ...process.env, + STATE: state, + MODE: mode, + REA_VERIFY_COMMANDCODE_ACCOUNT_HOME: account, + }, + }, + ); + expect(result.status).not.toBe(0); + const observed = await readFile( + join(state, "observed.json"), + "utf8", + ).then(JSON.parse, () => null); + expect(observed !== null).toBe(allowed); + if (allowed) + expect(observed.second.tool_calls[0].function.name).toBe( + "search_tools", + ); + expect(result.stderr).toContain( + allowed + ? "intentional stop before client output" + : mode === "empty" + ? "Installed skill must have authored frontmatter" + : mode === "empty body" + ? "Installed skill body must not be empty" + : "The model must receive the complete activated skill body", + ); + } finally { + await rm(state, { recursive: true, force: true }); + } + }, +); diff --git a/tests/boundary/process/deepseekVerifierSkillBody.test.ts b/tests/boundary/process/deepseekVerifierSkillBody.test.ts new file mode 100644 index 0000000000..f20d1c0e25 --- /dev/null +++ b/tests/boundary/process/deepseekVerifierSkillBody.test.ts @@ -0,0 +1,82 @@ +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +const posix = it.skipIf(process.platform === "win32"); +posix.each([ + ["empty", false], + ["outside wrapper", false], + ["closed wrapper", false], + ["closed early", false], + ["inside resources", false], + ["split messages", false], + ["complete", true], + ["complete escaped", true], + ["complete CRLF", true], + ["complete tags", true], +] as const)( + "requires actual skill body when model receives %s content", + async (body, allowed) => { + const state = await mkdtemp(join(tmpdir(), "rea-deepseek-skill-")); + try { + const commands = + "import {mkdir,readFile,writeFile} from 'node:fs/promises'; import {join} from 'node:path'; import {TOOL_CONTRACTS} from REPLACE_CONTRACTS;\nlet setup=0;\nexport async function createVerifierCommands(){return {async exec(command,args,options={}){\n if(command==='git')return {stdout:'',stderr:''}; if(args.includes('--version'))return {stdout:'fake',stderr:''};\n const profile=options.env.DSH_HOME;const skill=join(options.env.HOME,'.agents','skills','reverse-engineer-anything');const config=join(profile,'cordis.patch.yml');\n if(args.includes('setup')&&args.includes('--dry-run'))return {stdout:JSON.stringify({status:'planned',plannedActions:[{target:config},{target:skill}]}),stderr:''};\n if(args.includes('setup')){if(setup++===0){await writeFile(config+'.rea.backup',await readFile(config));await mkdir(skill,{recursive:true});await writeFile(join(skill,'SKILL.md'),('---\\nname: reverse-engineer-anything\\n---\\n'+(process.env.BODY==='complete tags'?'Literal and \\n':'')+'Essential \"authored\" instructions\\nKeep a \\\\ literal\\n').replaceAll('\\n',process.env.BODY==='complete CRLF'?'\\r\\n':'\\n'));return {stdout:JSON.stringify({status:'ready'}),stderr:''};}return {stdout:JSON.stringify({appliedActions:[]}),stderr:''};}\n if(args.includes('doctor'))return {stdout:JSON.stringify({healthy:true}),stderr:''};\n const tools=[...TOOL_CONTRACTS.map(t=>({name:'mcp__rea__'+t.name,parameters:{type:'object',properties:{v:{type:'string',pattern:'^x$'}}}})),{name:'skill'}];\n const first=await globalThis.fakeRequest({body:{messages:[]},tools,requestIndex:1});\n const body=((process.env.BODY==='complete tags'?'Literal and \\n':'')+'Essential \"authored\" instructions\\nKeep a \\\\ literal').replaceAll('\\n',process.env.BODY==='complete CRLF'?'\\r\\n':'\\n');\n const escapedSkill=skill.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>'); const wrap='\\n\\n'+escapedSkill+(process.env.BODY==='closed early'?'':'')+(process.env.BODY==='inside resources'?body:'')+'\\n\\n\\n';\n const payload=wrap+(process.env.BODY==='closed wrapper'?'':'')+((process.env.BODY.startsWith('complete')||process.env.BODY==='closed wrapper'||process.env.BODY==='closed early')?'\\n'+body+'\\n\\n':'')+''+(process.env.BODY==='outside wrapper'?body:'');\n const messages=[{role:'tool',content:payload},...(process.env.BODY==='split messages'?[{role:'tool',content:body}]:[])];\n const second=await globalThis.fakeRequest({body:{messages},tools,requestIndex:2});\n await writeFile(process.env.STATE+'/observed.json',JSON.stringify({first,second,payload}));throw Error('intentional stop after empty-skill request');\n },async close(){}};}\n".replace( + "REPLACE_CONTRACTS", + JSON.stringify( + new URL("../../../dist/contracts/toolContracts.js", import.meta.url).href, + ), + ); + await writeFile(join(state, "commands.mjs"), commands); + await writeFile( + join(state, "fixture.mjs"), + "export async function createOpenAiModelFixture({onRequest}){globalThis.fakeRequest=onRequest;return {baseUrl:'http://127.0.0.1:1',async close(){}};}\n", + ); + await writeFile( + join(state, "hook.mjs"), + "import{registerHooks}from'node:module';import{pathToFileURL}from'node:url';registerHooks({resolve(specifier,context,next){if(context.parentURL?.endsWith('/scripts/verify-deepseek-client.mjs')){let f;if(specifier==='./lib/verify-commands.mjs')f='commands.mjs';if(specifier==='./verify/clients/model-fixture.mjs')f='fixture.mjs';if(f)return{url:pathToFileURL(process.env.STATE+'/'+f).href,shortCircuit:true};}return next(specifier,context);}});\n", + ); + const script = fileURLToPath( + new URL("../../../scripts/verify-deepseek-client.mjs", import.meta.url), + ); + const temporary = + body === "complete escaped" ? join(state, "dir &") : state; + if (temporary !== state) await mkdir(temporary); + const result = spawnSync( + process.execPath, + ["--import", join(state, "hook.mjs"), script], + { + encoding: "utf8", + timeout: 10_000, + env: { + ...process.env, + STATE: state, + BODY: body, + TMPDIR: temporary, + TMP: temporary, + TEMP: temporary, + }, + }, + ); + expect(result.status).not.toBe(0); + const observed = await readFile( + join(state, "observed.json"), + "utf8", + ).then(JSON.parse, () => null); + expect(observed !== null).toBe(allowed); + if (allowed) + expect(observed.second.tool_calls[0].function.name).toBe( + "mcp__rea__analyze_javascript_application", + ); + expect(result.stderr).toContain( + allowed + ? "intentional stop after empty-skill request" + : "Harness must load the installed skill before analysis", + ); + } finally { + await rm(state, { recursive: true, force: true }); + } + }, +); diff --git a/tests/boundary/process/flutterVerifierDefaultAbi.test.ts b/tests/boundary/process/flutterVerifierDefaultAbi.test.ts new file mode 100644 index 0000000000..bf8a1394c8 --- /dev/null +++ b/tests/boundary/process/flutterVerifierDefaultAbi.test.ts @@ -0,0 +1,28 @@ +import { execFile } from "node:child_process"; +import { resolve } from "node:path"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; + +const execute = promisify(execFile); + +it("requires the default Flutter AOT result to belong to the identified ABI set", async () => { + const invoke = (kind: string) => + execute( + process.execPath, + [ + "--import", + resolve("tests/fixtures/flutterVerifierPreload.mjs"), + "scripts/verify-flutter.mjs", + "--apk", + "/tmp/fixture.apk", + ], + { env: { ...process.env, REA_FLUTTER_PROBE: kind }, timeout: 10_000 }, + ); + await expect(invoke("wrong")).rejects.toMatchObject({ + stderr: expect.stringContaining( + "Default AOT selection must be one of the identified ABIs", + ), + }); + const result = await invoke("correct"); + expect(result.stdout).toContain("all asserted Flutter CLI/MCP checks passed"); +}); diff --git a/tests/boundary/process/fridaVerifier.test.ts b/tests/boundary/process/fridaVerifier.test.ts new file mode 100644 index 0000000000..e55fb24641 --- /dev/null +++ b/tests/boundary/process/fridaVerifier.test.ts @@ -0,0 +1,70 @@ +import { execFile } from "node:child_process"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const execute = promisify(execFile); + +it("loads the Frida verifier help without starting instrumentation", async () => { + const result = await execute(process.execPath, [ + "scripts/verify-frida.mjs", + "--help", + ]); + expect(result.stdout).toContain( + "Without flags, verifies a local Node.js process", + ); +}); + +it("requires marker identity to match the selected target", async () => { + const root = await createTestTempDirectory("rea-frida-verifier-"); + const preload = join(root, "provider.mjs"); + await writeFile( + preload, + ` +import { registerHooks } from 'node:module'; +const service = \` +export class FridaInstrumentationService { + async listProcesses() { return { ok: true, value: { processes: [{ pid: 41, name: 'selected' }] } }; } + async instrument() { + const pid = process.env.REA_PROBE_KIND === 'missing' ? undefined : process.env.REA_PROBE_KIND === 'correct' ? 41 : 42; + return { ok: true, value: { cleanupError: null, evidence: { normalized_result: { pid, messages: [{ type: 'send', payload: { rea_frida_verification: 'ok', pid } }] } } } }; + } +}\`; +const manager = \`export class FridaInstrumentationManager { async closeAll() {} }\`; +registerHooks({ resolve(specifier, context, next) { + if (context.parentURL?.endsWith('/scripts/verify-frida.mjs')) { + if (specifier.endsWith('/FridaInstrumentationService.js')) return { shortCircuit: true, url: 'data:text/javascript,' + encodeURIComponent(service) }; + if (specifier.endsWith('/FridaInstrumentationManager.js')) return { shortCircuit: true, url: 'data:text/javascript,' + encodeURIComponent(manager) }; + } + return next(specifier, context); +}}); +`, + ); + const invoke = (kind: string, remote: boolean) => + execute( + process.execPath, + [ + "--import", + preload, + "scripts/verify-frida.mjs", + ...(remote ? ["--address", "127.0.0.1:27042", "--pid", "41"] : []), + ], + { env: { ...process.env, REA_PROBE_KIND: kind }, timeout: 10_000 }, + ); + for (const [kind, remote, message] of [ + ["missing", false, "Frida returned no valid target PID"], + ["wrong", true, "Frida observed a different target PID"], + ] as const) { + await expect(invoke(kind, remote)).rejects.toMatchObject({ + stderr: expect.stringContaining(message), + }); + } + const result = await invoke("correct", true); + expect(JSON.parse(result.stdout)).toMatchObject({ + ok: true, + target_pid: 41, + script_message_observed: true, + }); +}); diff --git a/tests/boundary/process/geminiVerifierSkillBody.test.ts b/tests/boundary/process/geminiVerifierSkillBody.test.ts new file mode 100644 index 0000000000..46d1d53d9c --- /dev/null +++ b/tests/boundary/process/geminiVerifierSkillBody.test.ts @@ -0,0 +1,76 @@ +import { spawnSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +it.each([ + ["empty", false], + ["outside wrapper", false], + ["inside resources", false], + ["closed early", false], + ["split messages", false], + ["complete", true], + ["complete CRLF", true], + ["complete tags", true], +] as const)( + "requires actual Gemini skill body in %s response", + async (body, allowed) => { + const state = await mkdtemp(join(tmpdir(), "rea-gemini-skill-")); + try { + const commands = + "import {mkdir,readFile,writeFile} from 'node:fs/promises';import {join} from 'node:path';import {TOOL_CONTRACTS} from REPLACE_CONTRACTS;\nlet setup=0;export async function createVerifierCommands(){return {async exec(command,args,options={}){\nif(command==='git')return {stdout:'',stderr:''};if(args.includes('--version'))return {stdout:'fake',stderr:''};\nconst profile=options.env.GEMINI_CLI_HOME;const config=join(profile,'.gemini','settings.json');const skill=join(profile,'.agents','skills','reverse-engineer-anything');\nif(args.includes('setup')&&args.includes('--dry-run'))return {stdout:JSON.stringify({status:'planned',plannedActions:[{target:config},{target:join(skill,'SKILL.md')}]}),stderr:''};\nif(args.includes('setup')){if(setup++===0){await writeFile(config+'.rea.backup',await readFile(config));await mkdir(skill,{recursive:true});await writeFile(join(skill,'SKILL.md'),('---\\nname: reverse-engineer-anything\\n---\\n'+(process.env.BODY==='complete tags'?'literal and \\n':'')+'Essential \"instructions\"\\nKeep a \\\\ literal\\n').replaceAll('\\n',process.env.BODY==='complete CRLF'?'\\r\\n':'\\n'));return {stdout:JSON.stringify({status:'ready'}),stderr:''};}return {stdout:JSON.stringify({appliedActions:[]}),stderr:''};}\nif(args.includes('doctor'))return {stdout:JSON.stringify({healthy:true}),stderr:''};\nconst tools=[...TOOL_CONTRACTS.map(t=>({name:'rea__'+t.name,parametersJsonSchema:{type:'object'}})),{name:'activate_skill'}];\nconst first=await globalThis.fakeRequest({body:{contents:[]},tools});\nconst body=((process.env.BODY==='complete tags'?'literal and \\n':'')+'Essential \"instructions\"\\nKeep a \\\\ literal').replaceAll('\\n',process.env.BODY==='complete CRLF'?'\\r\\n':'\\n');\nconst valid=(process.env.BODY.startsWith('complete')||process.env.BODY==='closed early');\nconst instructions=valid?' \\n '+body+'\\n \\n\\n':'';\nconst output='\\n'+instructions+' \\n '+skill+(process.env.BODY==='closed early'?'':'')+(process.env.BODY==='inside resources'?body:'')+'\\n \\n'+(process.env.BODY==='outside wrapper'?body:'');\nconst responses=[{functionResponse:{name:'activate_skill',response:{output}}},...(process.env.BODY==='split messages'?[{functionResponse:{name:'activate_skill',response:{output:body}}}]:[])];\nconst second=await globalThis.fakeRequest({body:{contents:[{parts:responses}]},tools});const payload=output;\nawait writeFile(process.env.STATE+'/observed.json',JSON.stringify({first,second,payload}));throw Error('intentional stop before client mutation');},async close(){}};}\n".replace( + "REPLACE_CONTRACTS", + JSON.stringify( + new URL("../../../dist/contracts/toolContracts.js", import.meta.url).href, + ), + ); + await writeFile(join(state, "commands.mjs"), commands); + await writeFile( + join(state, "fixture.mjs"), + "export async function createGeminiModelFixture({onRequest}){globalThis.fakeRequest=onRequest;return {baseUrl:'http://127.0.0.1:1',async close(){}};}\n", + ); + await writeFile( + join(state, "hook.mjs"), + "import{registerHooks}from'node:module';import{pathToFileURL}from'node:url';registerHooks({resolve(specifier,context,next){if(context.parentURL?.endsWith('/scripts/verify-gemini-client.mjs')){let f;if(specifier==='./lib/verify-commands.mjs')f='commands.mjs';if(specifier==='./verify/clients/gemini-model-fixture.mjs')f='fixture.mjs';if(f)return{url:pathToFileURL(process.env.STATE+'/'+f).href,shortCircuit:true};}return next(specifier,context);}});\n", + ); + const script = fileURLToPath( + new URL("../../../scripts/verify-gemini-client.mjs", import.meta.url), + ); + const result = spawnSync( + process.execPath, + ["--import", join(state, "hook.mjs"), script], + { + encoding: "utf8", + timeout: 10_000, + env: { + ...process.env, + STATE: state, + BODY: body, + TMPDIR: state, + TMP: state, + TEMP: state, + }, + }, + ); + expect(result.status).not.toBe(0); + const observed = await readFile( + join(state, "observed.json"), + "utf8", + ).then(JSON.parse, () => null); + expect(observed !== null).toBe(allowed); + if (allowed) + expect(observed.second[0].functionCall.name).toBe( + "rea__analyze_javascript_application", + ); + expect(result.stderr).toContain( + allowed + ? "intentional stop before client mutation" + : "Gemini must load the installed personal REA skill", + ); + } finally { + await rm(state, { recursive: true, force: true }); + } + }, +); diff --git a/tests/boundary/process/ghidraJavaLifecycle.test.ts b/tests/boundary/process/ghidraJavaLifecycle.test.ts index ccf8c53233..d2527d8433 100644 --- a/tests/boundary/process/ghidraJavaLifecycle.test.ts +++ b/tests/boundary/process/ghidraJavaLifecycle.test.ts @@ -176,6 +176,9 @@ posix.each([true, false])( code: "version_unresolved", cancellation: { reason }, }); + if (result.error._tag !== "GhidraJavaProbeFailure") + throw new Error("Expected the invalid Java report"); + expect(result.error.cancellation?.reason).toBe(reason); expect(Object.keys(result.error)).not.toContain("cancellation"); expect(() => JSON.stringify(result.error)).not.toThrow(); } diff --git a/tests/boundary/process/ghidraVerifierAssertions.test.ts b/tests/boundary/process/ghidraVerifierAssertions.test.ts index bd6554bf7e..6f8aebe956 100644 --- a/tests/boundary/process/ghidraVerifierAssertions.test.ts +++ b/tests/boundary/process/ghidraVerifierAssertions.test.ts @@ -66,3 +66,48 @@ processTest( expect(result.exitCode, result.stderr).toBe(0); }, ); + +processTest( + "Ghidra cleanup rejects unreadable paths and accepts only confirmed absence", + async ({ processes }) => { + const assertionsUrl = new URL( + "../../../scripts/verify-real-ghidra-assertions.mjs", + import.meta.url, + ).href; + const source = ` + import assert from "node:assert/strict"; + import { createRequire, syncBuiltinESMExports } from "node:module"; + const fs = createRequire(import.meta.url)("node:fs/promises"); + const { assertCleanup } = await import(${JSON.stringify(assertionsUrl)}); + const root = await fs.mkdtemp((await import("node:os")).tmpdir() + "/rea-ghidra-cleanup-test-"); + try { + const dangling = root + "/dangling"; + if (process.platform !== "win32") { + await fs.symlink(root + "/missing-target", dangling); + await assert.rejects(assertCleanup({ runtime_root: dangling }), /Ghidra cleanup left runtime_root/); + } + } finally { await fs.rm(root, { recursive: true, force: true }); } + let result = "exists"; + fs.lstat = async () => { + if (result === "exists") return; + const error = new Error(result); + error.code = result; + throw error; + }; + syncBuiltinESMExports(); + const coordinates = { runtime_root: "/owned-runtime" }; + await assert.rejects(assertCleanup(coordinates), /Ghidra cleanup left runtime_root/); + result = "ENOENT"; + await assertCleanup(coordinates); + result = "EACCES"; + await assert.rejects(assertCleanup(coordinates), { code: "EACCES" }); + `; + const result = await processes.run( + process.execPath, + ["--input-type=module", "--eval", source], + { timeoutMs: 10_000 }, + ); + expect(result.signal, result.stderr).toBeNull(); + expect(result.exitCode, result.stderr).toBe(0); + }, +); diff --git a/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts b/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts new file mode 100644 index 0000000000..1fe3fb7320 --- /dev/null +++ b/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts @@ -0,0 +1,150 @@ +import { execFile } from "node:child_process"; +import { realpath, truncate, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { + applicationFixture, + electronRuntimeEvidence, + SOURCE, +} from "../../fixtures/javascriptRuntimeReconciliation.js"; + +const execute = promisify(execFile); + +it.each(["once", "always", "physical"] as const)( + "retries the exact Evidence descriptor after close denial: %s", + async (denial) => { + const root = await createTestTempDirectory("rea-verifier-evidence-close-"); + const evidence = join(root, "evidence.json"); + const preload = join(root, "instrument.mjs"); + const application = await applicationFixture(); + await writeFile( + evidence, + JSON.stringify(electronRuntimeEvidence(application, SOURCE)), + ); + const selected = await realpath(evidence); + await writeFile( + preload, + ` +import fs from 'node:fs'; +import { syncBuiltinESMExports } from 'node:module'; +const open = fs.promises.open; +let attempts = 0; +fs.promises.open = async function(path, ...args) { + const handle = await open.call(this, path, ...args); + if (String(path) === ${JSON.stringify(selected)}) { + const close = handle.close.bind(handle); + if (${JSON.stringify(denial)} === 'physical') + handle.read = async () => { throw new Error('authored Evidence read failure'); }; + handle.close = async () => { + attempts++; + if (${JSON.stringify(denial)} === 'always' || attempts === 1) + throw new Error('authored Evidence close denial'); + return close(); + }; + } + return handle; +}; +syncBuiltinESMExports(); +process.on('exit', () => fs.writeSync(2, 'Evidence close attempts: ' + attempts + '\\n')); +`, + ); + const invocation = execute( + process.execPath, + [ + "--import", + preload, + "scripts/verify/javascript/runtime-observation.mjs", + "--application", + application, + "--runtime-evidence", + evidence, + ], + { timeout: 20_000 }, + ); + if (denial === "once") { + const result = await invocation; + expect(JSON.parse(result.stdout)).toMatchObject({ verified: true }); + expect(result.stderr).toContain("Evidence close attempts: 2"); + expect(result.stderr).not.toContain("authored Evidence close denial"); + return; + } + const result: unknown = await invocation.then( + () => { + throw new Error("Expected Evidence failure"); + }, + (error: unknown) => error, + ); + expect(result).toMatchObject({ + stderr: expect.stringContaining("Evidence close attempts: 2"), + }); + if ( + !(result instanceof Error) || + !("stderr" in result) || + typeof result.stderr !== "string" + ) + throw new Error("Missing verifier diagnostics"); + if (denial === "always") { + expect(result.stderr).toContain("authored Evidence close denial"); + expect(result.stderr).toContain( + "AggregateError: Verifier cleanup failed", + ); + expect(result.stderr).toContain("cleanupLeases"); + } else { + expect(result.stderr).toContain("authored Evidence read failure"); + expect(result.stderr).not.toContain("authored Evidence close denial"); + expect(result.stderr).not.toContain( + "AggregateError: Verifier cleanup failed", + ); + } + }, + 30_000, +); + +it("refuses oversized Evidence before a whole-file read", async () => { + const root = await createTestTempDirectory("rea-verifier-evidence-limit-"); + const evidence = join(root, "evidence.json"); + const preload = join(root, "instrument.mjs"); + await writeFile(evidence, ""); + await truncate(evidence, 64 * 1024 * 1024 + 1); + const selected = await realpath(evidence); + await writeFile( + preload, + ` +import fs from 'node:fs'; +import { syncBuiltinESMExports } from 'node:module'; +const readFile = fs.promises.readFile; +fs.promises.readFile = function(path, ...args) { + if (String(path) === ${JSON.stringify(selected)}) throw new Error('oversized whole-file read'); + return readFile.call(this, path, ...args); +}; +syncBuiltinESMExports(); +`, + ); + const application = await applicationFixture(); + const result = await execute( + process.execPath, + [ + "--import", + preload, + "scripts/verify/javascript/runtime-observation.mjs", + "--application", + application, + "--runtime-evidence", + evidence, + ], + { timeout: 20_000 }, + ).then( + () => { + throw new Error("Expected oversized Evidence rejection"); + }, + (error: unknown) => error, + ); + expect(result).toMatchObject({ + stderr: expect.stringContaining( + `Runtime Evidence exceeds 64 MiB: ${evidence}`, + ), + }); +}, 30_000); diff --git a/tests/boundary/process/jebVerifier.test.ts b/tests/boundary/process/jebVerifier.test.ts new file mode 100644 index 0000000000..b13038c95a --- /dev/null +++ b/tests/boundary/process/jebVerifier.test.ts @@ -0,0 +1,65 @@ +import { execFile } from "node:child_process"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const execute = promisify(execFile); + +it("reports a decompile marker only when a nonempty requested marker was observed", async () => { + const root = await createTestTempDirectory("rea-jeb-verifier-"); + const preload = join(root, "provider.mjs"); + await writeFile( + preload, + ` +import { registerHooks } from 'node:module'; +const service = \` +export class JebAnalysisService { + async execute(name) { + if (name === 'inspect_jeb_client') return { ok: true, value: { normalized_result: { engine: { version: 'fixture', gui_client: false, endpoint: 'fixture' } } } }; + if (name === 'open_jeb_project') return { ok: false, error: { message: 'not available' } }; + if (name === 'list_jeb_units') return { ok: true, value: { normalized_result: { units: [{}] } } }; + if (name === 'decompile_jeb_item') return { ok: true, value: { normalized_result: { text: process.env.REA_JEB_PROBE_TEXT ?? '' } } }; + throw new Error('unexpected operation'); + } +}\`; +const provider = \`export const createJebAnalysisProvider = () => ({ close: async () => {} });\`; +registerHooks({ resolve(specifier, context, next) { + if (context.parentURL?.endsWith('/scripts/verify-jeb.mjs')) { + if (specifier.endsWith('/JebAnalysisService.js')) return { shortCircuit: true, url: 'data:text/javascript,' + encodeURIComponent(service) }; + if (specifier.endsWith('/composition/jeb.js')) return { shortCircuit: true, url: 'data:text/javascript,' + encodeURIComponent(provider) }; + } + return next(specifier, context); +}}); +`, + ); + const invoke = (text: string, marker?: string) => + execute( + process.execPath, + [ + "--import", + preload, + "scripts/verify-jeb.mjs", + "--item-address", + "Lx;->x()V", + ...(marker === undefined ? [] : ["--marker", marker]), + ], + { env: { ...process.env, REA_JEB_PROBE_TEXT: text }, timeout: 10_000 }, + ); + await expect(invoke("", undefined)).rejects.toMatchObject({ + stderr: expect.stringContaining("Decompiled pseudo-code was empty"), + }); + await expect(invoke("void x() {}", "missing")).rejects.toMatchObject({ + stderr: expect.stringContaining("did not contain the fixture marker"), + }); + await expect(invoke("void x() {}", "")).rejects.toMatchObject({ + stderr: expect.stringContaining("--marker must not be empty"), + }); + expect( + JSON.parse((await invoke("void x() {}", undefined)).stdout), + ).toMatchObject({ ok: true, decompiled_marker_observed: false }); + expect(JSON.parse((await invoke("void x() {}", "x()")).stdout)).toMatchObject( + { ok: true, decompiled_marker_observed: true }, + ); +}); diff --git a/tests/boundary/process/qwenVerifierSkillBody.test.ts b/tests/boundary/process/qwenVerifierSkillBody.test.ts new file mode 100644 index 0000000000..e0d2ab6b99 --- /dev/null +++ b/tests/boundary/process/qwenVerifierSkillBody.test.ts @@ -0,0 +1,58 @@ +import { spawnSync } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +it.each([ + ["no skill tool", false], + ["user mention", false], + ["wrong id", false], + ["missing body", false], + ["split", false], + ["complete", true], + ["CRLF", true], +] as const)("binds Qwen skill evidence for %s", async (mode, loaded) => { + const state = await mkdtemp(join(tmpdir(), "rea-qwen-skill-")); + try { + await writeFile( + join(state, "commands.mjs"), + "import {mkdir,readFile,writeFile} from 'node:fs/promises';import {join} from 'node:path';\nexport async function createVerifierCommands(){return {async exec(command,args,options={}){\n if(args.includes('--version'))return {stdout:'fake',stderr:''};\n const profile=options.env.QWEN_HOME;const config=join(profile,'settings.json');\n if(args.includes('setup')&&args.includes('--dry-run'))return {stdout:JSON.stringify({plannedActions:[{target:config}]}),stderr:''};\n if(args.includes('setup')){\n const original=await readFile(config,'utf8');await writeFile(config+'.rea.backup',original);\n const value=JSON.parse(original);value.mcpServers.rea={command:'/fake/rea',args:['mcp']};await writeFile(config,JSON.stringify(value));\n const skill=join(options.env.HOME,'.agents','skills','reverse-engineer-anything');await mkdir(skill,{recursive:true});\n const source='---\\nname: reverse-engineer-anything\\n---\\nQwen \"authored\" body\\nKeep a \\\\ literal\\n';\n await writeFile(join(skill,'SKILL.md'),process.env.MODE==='CRLF'?source.replaceAll('\\n','\\r\\n'):source);\n return {stdout:'{}',stderr:''};\n }\n if(args.includes('doctor'))return {stdout:'{}',stderr:''};\n const skill=join(options.env.HOME,'.agents','skills','reverse-engineer-anything');\n const content='Base directory for this skill: '+skill+'\\nImportant: ALWAYS resolve absolute paths from this base directory when working with skills.\\n\\nQwen \"authored\" body\\nKeep a \\\\ literal\\n';\n const tools=[...(process.env.MODE==='no skill tool'?[]:[{name:'skill'}]),{name:'mcp__rea__analyze_javascript_application'}];\n const first=await globalThis.fakeRequest({body:{messages:[]},tools,requestIndex:1});const callId=first.tool_calls[0].id;\n let messages=(process.env.MODE==='user mention'||process.env.MODE==='no skill tool')?[{role:'user',content:'Unrelated '+skill}]:[{role:'tool',tool_call_id:process.env.MODE==='wrong id'?'other':callId,content:process.env.MODE==='missing body'?'Base directory for this skill: '+skill:content}];\n if(process.env.MODE==='split')messages=[{role:'tool',tool_call_id:callId,content:'Base directory for this skill: '+skill},{role:'tool',tool_call_id:'other',content}];\n const second=await globalThis.fakeRequest({body:{messages},tools,requestIndex:2});await writeFile(process.env.STATE+'/observed.json',JSON.stringify({first,second,messages}));throw Error('intentional stop before client output');\n},async close(){}};}\n", + ); + await writeFile( + join(state, "fixture.mjs"), + "export async function createOpenAiModelFixture({onRequest}){globalThis.fakeRequest=onRequest;return {baseUrl:'http://127.0.0.1:1',async close(){}};}\n", + ); + await writeFile( + join(state, "hook.mjs"), + "import {registerHooks}from'node:module';import{pathToFileURL}from'node:url';registerHooks({resolve(specifier,context,next){if(context.parentURL?.endsWith('/scripts/verify-qwen-client.mjs')){let f;if(specifier==='./lib/verify-commands.mjs')f='commands.mjs';if(specifier==='./verify/clients/model-fixture.mjs')f='fixture.mjs';if(f)return{url:pathToFileURL(process.env.STATE+'/'+f).href,shortCircuit:true};}return next(specifier,context);}});\n", + ); + const script = fileURLToPath( + new URL("../../../scripts/verify-qwen-client.mjs", import.meta.url), + ); + const result = spawnSync( + process.execPath, + ["--import", join(state, "hook.mjs"), script], + { + encoding: "utf8", + timeout: 10_000, + env: { + ...process.env, + STATE: state, + MODE: mode, + TMPDIR: state, + TMP: state, + TEMP: state, + }, + }, + ); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("intentional stop before client output"); + const receipt = JSON.parse(result.stdout.trim().split("\n")[0] ?? ""); + expect(receipt.selectedSkillLoaded).toBe(loaded); + expect(receipt.passed).toBe(false); + } finally { + await rm(state, { recursive: true, force: true }); + } +}); diff --git a/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts b/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts index b167e484e5..c5080464f8 100644 --- a/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts +++ b/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts @@ -159,28 +159,6 @@ it.skipIf(process.platform === "win32")( }, ); -it.skipIf(process.platform === "win32")( - "rejects a non-TTY path before spawning Rizin", - async () => { - let spawned = false; - const manager = createManager( - { REA_RIZIN_COMMAND: "injected-rizin" }, - { - spawnProcess: async () => { - spawned = true; - throw new Error("must not spawn with an invalid TTY"); - }, - }, - ); - const result = await manager.start({ - path: "/tmp/not-a-tty-target", - inferior_tty: "/dev/null", - }); - expect(result.ok).toBe(false); - expect(spawned).toBe(false); - }, -); - it.skipIf(process.platform === "win32")( "closeAll drains concurrent startups and blocks later admission", async () => { diff --git a/tests/boundary/providers/auxiliaryDeclarations.test.ts b/tests/boundary/providers/auxiliaryDeclarations.test.ts index 15e42fc32a..c28137f5af 100644 --- a/tests/boundary/providers/auxiliaryDeclarations.test.ts +++ b/tests/boundary/providers/auxiliaryDeclarations.test.ts @@ -7,7 +7,13 @@ import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver it("keeps declaration discovery and client creation lazy and independently owned", async () => { const target = await parseBinaryTarget(process.execPath); if (!target.ok) throw target.error; - for (const declaration of auxiliaryAnalysisProviderDeclarations({})) { + const declarations = auxiliaryAnalysisProviderDeclarations({}); + expect(declarations.map(({ identity }) => identity.id).sort()).toEqual([ + "native-macos", + "rea-artifact-graph", + "rea-dotnet-static", + ]); + for (const declaration of declarations) { let loads = 0; const provider = new LazyAnalysisProvider({ ...declaration, diff --git a/tests/boundary/providers/cutterBridgeProducer.test.ts b/tests/boundary/providers/cutterBridgeProducer.test.ts index 721ffc05a1..c81cf904b3 100644 --- a/tests/boundary/providers/cutterBridgeProducer.test.ts +++ b/tests/boundary/providers/cutterBridgeProducer.test.ts @@ -239,10 +239,10 @@ it("retains failed receipt acknowledgements and retries before another command", bridge.client.execute({ sessionId: bridge.session.session_id, expectedGeneration: bridge.session.document_generation, - command: "iI", + command: "fixture-receipt-count", json: false, }), - ).resolves.toMatchObject({ executionState: "complete" }); + ).resolves.toMatchObject({ executionState: "complete", output: "0" }); } finally { await bridge.close(); } @@ -263,10 +263,10 @@ it("retains a receipt when an ACK reply omits its acknowledgement field", async bridge.client.execute({ sessionId: bridge.session.session_id, expectedGeneration: bridge.session.document_generation, - command: "iI", + command: "fixture-receipt-count", json: false, }), - ).resolves.toMatchObject({ executionState: "complete" }); + ).resolves.toMatchObject({ executionState: "complete", output: "0" }); } finally { await bridge.close(); } diff --git a/tests/boundary/providers/jeb/failureEnvelopes.test.ts b/tests/boundary/providers/jeb/failureEnvelopes.test.ts index ca64c0e9d9..00342bc52c 100644 --- a/tests/boundary/providers/jeb/failureEnvelopes.test.ts +++ b/tests/boundary/providers/jeb/failureEnvelopes.test.ts @@ -170,7 +170,6 @@ it.each( expect(producer.calls).toEqual([ { name: "open_project", arguments: { file_path: input.path } }, ]); - expect(projected.error.code).not.toBe("unsupported_target"); } finally { try { await client.close(); diff --git a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts index c480f52a77..ffea9aa070 100644 --- a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts +++ b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts @@ -1,4 +1,4 @@ -import { expect, it as test } from "vitest"; +import { expect, it as test, vi } from "vitest"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaResourceScope.js"; @@ -147,3 +147,98 @@ it("says why Kuna cannot answer instead of a generic unavailable message", async expect(await client.close()).toEqual({ ok: true, value: null }); } }); + +it("preserves a caller reason when a target command is already cancelled", async () => { + const { kuna, target, cacheRoot } = await session( + "rea-kuna-command-preabort-", + ); + const installed = await inspectKunaInstallation({ + REA_KUNA_COMMAND: kuna, + REA_KUNA_CACHE_DIR: cacheRoot, + }); + if (!installed.ok) throw new Error(installed.error.reason); + const controller = new AbortController(); + const reason = { caller: "before target command admission" }; + controller.abort(reason); + const callsBefore = await fakeKunaCalls(kuna); + const releaseRun = vi.fn(); + const commands = new KunaTargetCommands(target, { + signal: controller.signal, + }); + try { + const result = await commands.run({ + installation: installed.value, + operation: "list_procedures", + arguments: (input) => ["functions", input, "--json"], + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, + outputBytes: 100, + signal: controller.signal, + releaseRun, + }); + if (result.ok) throw new Error("Expected pre-aborted command to cancel"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); + expect(releaseRun).toHaveBeenCalledOnce(); + expect(await fakeKunaCalls(kuna)).toEqual(callsBefore); + } finally { + expect(await commands.close()).toEqual({ ok: true, value: null }); + } +}); + +it("preserves the lifetime reason when workspace preparation is already cancelled", async () => { + const { kuna, target } = await session("rea-kuna-prepare-preabort-"); + const controller = new AbortController(); + const reason = { caller: "before workspace preparation" }; + controller.abort(reason); + const callsBefore = await fakeKunaCalls(kuna); + const commands = new KunaTargetCommands(target, { + signal: controller.signal, + }); + try { + const result = await commands.prepare(); + if (result.ok) throw new Error("Expected cancelled preparation"); + expect(result.error).toMatchObject({ + _tag: "AnalysisCancelledError", + operation: "kuna:workspace", + }); + expect(result.error.cause).toBe(reason); + expect(await fakeKunaCalls(kuna)).toEqual(callsBefore); + } finally { + expect(await commands.close()).toEqual({ ok: true, value: null }); + } +}); + +it("does not invent a caller cause when closed without cancellation", async () => { + const { kuna, target, cacheRoot } = await session("rea-kuna-command-closed-"); + const installed = await inspectKunaInstallation({ + REA_KUNA_COMMAND: kuna, + REA_KUNA_CACHE_DIR: cacheRoot, + }); + if (!installed.ok) throw new Error(installed.error.reason); + const controller = new AbortController(); + const callsBefore = await fakeKunaCalls(kuna); + const releaseRun = vi.fn(); + const commands = new KunaTargetCommands(target, { + signal: controller.signal, + }); + expect(await commands.close()).toEqual({ ok: true, value: null }); + const preparation = await commands.prepare(); + if (preparation.ok) throw new Error("Expected closed preparation to cancel"); + expect(preparation.error._tag).toBe("AnalysisCancelledError"); + expect(preparation.error.cause).toBeUndefined(); + const result = await commands.run({ + installation: installed.value, + operation: "list_procedures", + arguments: (input) => ["functions", input, "--json"], + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, + outputBytes: 100, + signal: controller.signal, + releaseRun, + }); + if (result.ok) throw new Error("Expected closed command to cancel"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBeUndefined(); + expect(releaseRun).toHaveBeenCalledOnce(); + expect(await fakeKunaCalls(kuna)).toEqual(callsBefore); + expect(await commands.close()).toEqual({ ok: true, value: null }); +}); diff --git a/tests/boundary/providers/kuna/kunaProjectCache.test.ts b/tests/boundary/providers/kuna/kunaProjectCache.test.ts index 3b949c8024..311a97ea8d 100644 --- a/tests/boundary/providers/kuna/kunaProjectCache.test.ts +++ b/tests/boundary/providers/kuna/kunaProjectCache.test.ts @@ -705,6 +705,28 @@ it("shares one Kuna run between concurrent requests for one export", async () => expect(await callsOf(kuna, "decompile-project")).toHaveLength(1); }); +it("preserves a pre-aborted caller reason without launching an export", async () => { + const { kuna, installation, cache, target } = await fixture( + "rea-kuna-preabort-reason-", + ); + const input = await target("program"); + const controller = new AbortController(); + const reason = { caller: "before admission" }; + controller.abort(reason); + const callsBefore = await fakeKunaCalls(kuna); + const result = await cache.build({ + installation, + ...input, + signal: controller.signal, + }); + if (result.ok) + throw new Error("Expected pre-aborted cache request to cancel"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); + expect(await fakeKunaCalls(kuna)).toEqual(callsBefore); + expect(await cache.close()).toEqual({ ok: true, value: null }); +}); + it("stops the run only when every waiting request is cancelled", async () => { const { kuna, installation, cacheRoot, cache, target } = await fixture( "rea-kuna-cancel-", @@ -727,23 +749,31 @@ it("stops the run only when every waiting request is cancelled", async () => { await expect.poll(() => callsOf(kuna, "decompile-project")).toHaveLength(1); const pid = (await callsOf(kuna, "decompile-project"))[0]?.pid ?? 0; - firstCaller.abort(); - expect(await first).toMatchObject({ + const firstReason = { caller: "first waiter" }; + firstCaller.abort(firstReason); + const firstResult = await first; + expect(firstResult).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" }, }); + if (firstResult.ok) throw new Error("Expected first waiter cancellation"); // The other request still owns the run. expect(processGone(pid)).toBe(false); - secondCaller.abort(); - expect(await second).toMatchObject({ + const secondReason = { caller: "last waiter" }; + secondCaller.abort(secondReason); + const secondResult = await second; + expect(secondResult).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" }, }); + if (secondResult.ok) throw new Error("Expected last waiter cancellation"); expect(await cache.close()).toEqual({ ok: true, value: null }); expect(processGone(pid)).toBe(true); expect(await entries(cacheRoot, "staging")).toEqual([]); expect(await entries(cacheRoot, "projects")).toEqual([]); + expect(firstResult.error.cause).toBe(firstReason); + expect(secondResult.error.cause).toBe(secondReason); }); it("applies one deadline to the inventory and the export together", async () => { diff --git a/tests/boundary/providers/managed/managedHashCancellation.test.ts b/tests/boundary/providers/managed/managedHashCancellation.test.ts index d917a6e6a2..2681f7a48d 100644 --- a/tests/boundary/providers/managed/managedHashCancellation.test.ts +++ b/tests/boundary/providers/managed/managedHashCancellation.test.ts @@ -94,6 +94,7 @@ it.each([ code: "cleanup_incomplete", details: { diagnostics: { primary_error: { code: "cancelled" } } }, }); + expect(cancelled.error.cleanupPrimary?.cause).toBe(reason); expect(cancelledHandle?.fd).toBeGreaterThanOrEqual(0); const readsBeforeRetry = readCount; const blocked = await client.execute("inspect_managed_members", {}); diff --git a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts index 51154ab80d..d703a0b58f 100644 --- a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts +++ b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts @@ -36,8 +36,12 @@ it("selects namespace-bearing nested declarations from parsed type facts and com expect(selected.result.methods).toHaveLength(2); expect(selected.result.fields).toHaveLength(2); const comparison = compareManagedMembers(selected, selected); + expect(comparison.methods).toHaveLength(2); + expect(comparison.fields).toHaveLength(2); expect( - comparison.methods.every(({ match }) => match.status === "matched"), + [...comparison.methods, ...comparison.fields].every( + ({ match }) => match.status === "matched", + ), ).toBe(true); expect(comparison.summary).toMatchObject({ added: 0, removed: 0 }); expect( @@ -2569,62 +2573,43 @@ describe("managed member comparison of ownerless members", () => { } }); - it("keeps a resolved copy out of the declared round when an ownerless competitor is on the left, as main does", () => { - const base = inspect(buildManagedPeFixture(), "/tmp/ownerless-left.dll"); - const [method] = base.result.methods; - if (method === undefined) throw new Error("Expected a fixture method"); - const ownerless = { - ...method, - declaring_type: null, - declaring_type_token: null, - }; - // The left holds the resolved Foo and an ownerless Foo with the same CIL - // and signature; the right holds only the resolved Foo. The ownerless - // Foo's owner may be the resolved one, so the three stay ambiguous. - const withOwnerless = { - ...base, - result: { ...base.result, methods: [method, ownerless], fields: [] }, - }; - const resolvedOnly = { - ...base, - result: { ...base.result, methods: [method], fields: [] }, - }; + it.each(["left", "right"] as const)( + "keeps a resolved copy out of the declared round when an ownerless competitor is on the %s", + (side) => { + const base = inspect( + buildManagedPeFixture(), + `/tmp/ownerless-${side}.dll`, + ); + const [method] = base.result.methods; + if (method === undefined) throw new Error("Expected a fixture method"); + const ownerless = { + ...method, + declaring_type: null, + declaring_type_token: null, + }; + // The ownerless Foo may belong to the same type as the resolved Foo, + // so both comparison directions must leave the three members ambiguous. + const withOwnerless = { + ...base, + result: { ...base.result, methods: [method, ownerless], fields: [] }, + }; + const resolvedOnly = { + ...base, + result: { ...base.result, methods: [method], fields: [] }, + }; - const result = compareManagedMembers(withOwnerless, resolvedOnly); + const result = + side === "left" + ? compareManagedMembers(withOwnerless, resolvedOnly) + : compareManagedMembers(resolvedOnly, withOwnerless); - expect(result.matching.ambiguous).toBe(1); - expect( - result.methods.filter(({ match }) => match.status === "matched"), - ).toEqual([]); - expect(result.summary).toMatchObject({ added: 0, removed: 0 }); - }); - - it("keeps a resolved copy out of the declared round when an ownerless competitor is on the right, as main does", () => { - const base = inspect(buildManagedPeFixture(), "/tmp/ownerless-right.dll"); - const [method] = base.result.methods; - if (method === undefined) throw new Error("Expected a fixture method"); - const ownerless = { - ...method, - declaring_type: null, - declaring_type_token: null, - }; - const withOwnerless = { - ...base, - result: { ...base.result, methods: [method, ownerless], fields: [] }, - }; - const resolvedOnly = { - ...base, - result: { ...base.result, methods: [method], fields: [] }, - }; - - const result = compareManagedMembers(resolvedOnly, withOwnerless); - - expect(result.matching.ambiguous).toBe(1); - expect( - result.methods.filter(({ match }) => match.status === "matched"), - ).toEqual([]); - expect(result.summary).toMatchObject({ added: 0, removed: 0 }); - }); + expect(result.matching.ambiguous).toBe(1); + expect( + result.methods.filter(({ match }) => match.status === "matched"), + ).toEqual([]); + expect(result.summary).toMatchObject({ added: 0, removed: 0 }); + }, + ); it("keeps an ownerless method ambiguous beside a resolved method of the same shape", () => { const base = inspect(buildManagedPeFixture(), "/tmp/ownerless-shape.dll"); diff --git a/tests/boundary/providers/managed/managedStaticProvider.test.ts b/tests/boundary/providers/managed/managedStaticProvider.test.ts index 6e0dd5b9b0..81eecd7970 100644 --- a/tests/boundary/providers/managed/managedStaticProvider.test.ts +++ b/tests/boundary/providers/managed/managedStaticProvider.test.ts @@ -27,11 +27,13 @@ import * as boundedFileBytes from "../../../../src/process/BoundedFileBytes.js"; import { primaryAnalysisError } from "../../../../src/domain/analysisErrorBase.js"; it.each( - [false, true].flatMap((warm) => - [false, true].flatMap((changed) => - [false, true].map((failedClose) => ({ warm, changed, failedClose })), - ), - ), + [false, true] + .flatMap((warm) => + [false, true].flatMap((changed) => + [false, true].map((failedClose) => ({ warm, changed, failedClose })), + ), + ) + .filter(({ changed, failedClose }) => changed || failedClose), )( "preserves selected managed identity through finalization (cached=$warm, changed=$changed, failedClose=$failedClose)", async ({ warm, changed, failedClose }) => { @@ -291,8 +293,9 @@ describe("managed static completed snapshot cancellation", () => { kind: "file-snapshot-hash", path, bytes: bytes.length, - sha256: parsed.value.sha256, + sha256: createHash("sha256").update(bytes).digest("hex"), }; + expect(parsed.value.sha256).toBe(observation.sha256); expect(result.error.partialObservation).toEqual(observation); expect(projectAnalysisError(result.error)).toMatchObject({ code: "cancelled", @@ -302,6 +305,9 @@ describe("managed static completed snapshot cancellation", () => { expect(owners.every((owner) => owner.handle.fd === -1)).toBe(true); if (retryClose) expect(owners[0]).toBe(owners[1]); expect(await client.close()).toEqual({ ok: true, value: null }); + expect(result.error.partialObservation).toEqual(observation); + expect(result.error.cleanupIncomplete).toBe(false); + expect(owners.every((owner) => owner.handle.fd === -1)).toBe(true); } finally { close.mockRestore(); await client.close(); @@ -529,15 +535,11 @@ describe("managed static provider path boundary", () => { field_accesses: [expect.objectContaining({ field_name: "counter" })], }); const memberResult = asManagedMemberResult(members.value); - expect(members.value.locations).toEqual( - expect.arrayContaining( - memberResult.methods.flatMap((method) => - method.body.file_offset === null - ? [] - : [{ kind: "file-offset", offset: method.body.file_offset }], - ), - ), - ); + expect(memberResult.methods[0]?.body.file_offset).toBe(0x0a00); + expect(members.value.locations).toContainEqual({ + kind: "file-offset", + offset: 0x0a00, + }); const boundaries = await client.execute( "inspect_managed_native_boundaries", diff --git a/tests/composition/idaProvider.test.ts b/tests/composition/idaProvider.test.ts index 2c6c08f934..eda322f510 100644 --- a/tests/composition/idaProvider.test.ts +++ b/tests/composition/idaProvider.test.ts @@ -236,6 +236,8 @@ describe("IDA provider selection cancellation", () => { ok: false, error: { _tag: "AnalysisCancelledError", cause: cancellationReason }, }); + if (replacement.ok) throw new Error("Expected cancellation"); + expect(replacement.error.cause).toBe(cancellationReason); expect(session.activeTarget()?.sha256).toBe(first.target.sha256); expect( (await session.execute("procedure_pseudo_code", { procedure: "main" })) @@ -272,6 +274,8 @@ describe("IDA provider selection cancellation", () => { ok: false, error: { _tag: "AnalysisCancelledError", cause: reason }, }); + if (profile.ok) throw new Error("Expected cancellation"); + expect(profile.error.cause).toBe(reason); expect(runCalls).toBe(0); }); }); diff --git a/tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts b/tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts index 0e936b0624..200361bec9 100644 --- a/tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts +++ b/tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts @@ -19,6 +19,17 @@ describe("real Windows Ghidra workflow trust boundary", () => { repository_dispatch: { types: ["real-ghidra-windows"] }, }); expect(workflow.permissions).toEqual({ contents: "read" }); + for (const name of ["native-artifact", "verify"]) { + const job = workflow.jobs[name]; + expect(job.if).toBe("github.ref == 'refs/heads/main'"); + const checkouts = job.steps.filter( + (step: { uses?: string }) => step.uses?.startsWith("actions/checkout@"), + ); + expect(checkouts).toHaveLength(1); + expect(checkouts[0].uses).toMatch(/^actions\/checkout@[0-9a-f]{40}$/); + expect(checkouts[0].with?.ref).toBeUndefined(); + expect(checkouts[0].with?.repository).toBeUndefined(); + } expect(workflow).toMatchObject({ jobs: { verify: { diff --git a/tests/conformance/providers/managed/managedNativeVerification.test.ts b/tests/conformance/providers/managed/managedNativeVerification.test.ts index 60835973c1..b4e25f1b25 100644 --- a/tests/conformance/providers/managed/managedNativeVerification.test.ts +++ b/tests/conformance/providers/managed/managedNativeVerification.test.ts @@ -913,21 +913,6 @@ it("retains every observed native candidate", () => { expect(result.pinvoke_imports[0]?.candidates).toHaveLength(2); }); -it("wraps verification in derived workflow Evidence", () => { - const evidence = executeManagedNativeBoundaries(exampleInput()); - - if (!evidence.ok) throw evidence.error; - expect(evidence.value.evidence).toMatchObject({ - operation: "verify_managed_native_boundaries", - provider: { id: "rea-dotnet-workflows" }, - confidence: "inferred", - authority: "analyst-inference", - normalized_result: { - summary: { verified: 1 }, - }, - }); - expect(evidence.value.evidence.evidence_links).toHaveLength(2); -}); describe("managed/native verification result algebra", () => { it("rejects states without their required native match", () => { diff --git a/tests/evaluation/evidenceJsonPreadmissionRule.test.ts b/tests/evaluation/evidenceJsonPreadmissionRule.test.ts index fc435b3713..850a927070 100644 --- a/tests/evaluation/evidenceJsonPreadmissionRule.test.ts +++ b/tests/evaluation/evidenceJsonPreadmissionRule.test.ts @@ -216,14 +216,6 @@ new RuleTester({ createEvidence(target, provider, { rawResult: jsonValueSchema.parse(raw) });`, "rawResult", ), - invalid( - "aliased imports retain their owner", - ` - import { createEvidence as record } from "../domain/evidence.js"; - import { jsonValueSchema as json } from "../domain/jsonValue.js"; - record(target, provider, { result: json.parse(input) });`, - "result", - ), invalid( "namespace members retain their owner", ` diff --git a/tests/evaluation/model-evals/agentEvaluationReport.test.ts b/tests/evaluation/model-evals/agentEvaluationReport.test.ts index 8d2f8375cd..9c4566c058 100644 --- a/tests/evaluation/model-evals/agentEvaluationReport.test.ts +++ b/tests/evaluation/model-evals/agentEvaluationReport.test.ts @@ -131,12 +131,6 @@ describe("scoped factual report", () => { epistemicCuePresent: false, }), ).toBe(false); - expect( - agentEvaluationPassed({ - ...routingSuccess, - factualCorrectness: "failed", - }), - ).toBe(false); }); it("reports assessed failures separately from unassessed scenarios", () => { diff --git a/tests/evaluation/testDiscovery.test.ts b/tests/evaluation/testDiscovery.test.ts index d4ef905089..70996f2236 100644 --- a/tests/evaluation/testDiscovery.test.ts +++ b/tests/evaluation/testDiscovery.test.ts @@ -15,7 +15,23 @@ describe("test discovery migration guard", () => { [{ file: resolve(root, first), projectName: "domain" }], root, ), - ).toEqual({ missing: [], unexpected: [], duplicates: [], discovered: 1 }); + ).toEqual({ + missing: [], + unexpected: [], + duplicates: [], + wrongProjects: [], + discovered: 1, + }); + }); + + it("reports a discovered test in the wrong project", () => { + expect( + compareTestDiscovery( + [first], + [{ file: first, projectName: "cli-boundary" }], + root, + ).wrongProjects, + ).toEqual([{ file: first, expected: "domain", actual: "cli-boundary" }]); }); it("reports a new provider test omitted by the current project globs", () => { diff --git a/tests/fixtures/android/jniElf.ts b/tests/fixtures/android/jniElf.ts index b6cdaef0f9..f76686da5b 100644 --- a/tests/fixtures/android/jniElf.ts +++ b/tests/fixtures/android/jniElf.ts @@ -328,5 +328,5 @@ export const jniElfFixture = ( }, writer, ); - return { bytes, base, word, functions, slots, relocationSize }; + return { bytes, base, word }; }; diff --git a/tests/fixtures/binaryDiagnostics/appleIps.ts b/tests/fixtures/binaryDiagnostics/appleIps.ts index 4bb557fee7..61bb9eb4ac 100644 --- a/tests/fixtures/binaryDiagnostics/appleIps.ts +++ b/tests/fixtures/binaryDiagnostics/appleIps.ts @@ -1,4 +1,4 @@ -export interface AppleIpsFixtureOptions { +interface AppleIpsFixtureOptions { readonly bugType?: string; readonly procName?: string; readonly uuid?: string; diff --git a/tests/fixtures/binaryDiagnostics/nativeExports.ts b/tests/fixtures/binaryDiagnostics/nativeExports.ts index ea6a1a644b..c055b48cf8 100644 --- a/tests/fixtures/binaryDiagnostics/nativeExports.ts +++ b/tests/fixtures/binaryDiagnostics/nativeExports.ts @@ -1,5 +1,5 @@ /** One PE export-address-table slot: code RVA, forwarder text, or unused. */ -export type PeFixtureFunction = +type PeFixtureFunction = | { readonly rva: number } | { readonly forwarder: string } | null; @@ -132,7 +132,7 @@ export const peRepeatedExportNamesImage = ( }; /** One `.dynsym` entry; `version` is its raw `.gnu.version` half-word. */ -export interface ElfFixtureSymbol { +interface ElfFixtureSymbol { readonly name: string; readonly binding?: number; readonly type?: number; @@ -145,7 +145,7 @@ export interface ElfFixtureSymbol { readonly nameOffset?: number; } -export interface ElfFixtureVersionRequirement { +interface ElfFixtureVersionRequirement { readonly index: number; readonly name: string; } diff --git a/tests/fixtures/binaryDiagnostics/peImports.ts b/tests/fixtures/binaryDiagnostics/peImports.ts index bdbdce47b2..5c775b72d4 100644 --- a/tests/fixtures/binaryDiagnostics/peImports.ts +++ b/tests/fixtures/binaryDiagnostics/peImports.ts @@ -8,11 +8,11 @@ const SPLIT_GAP_BYTES = 0x200; export const peImportFixtureOffset = (rva: number): number => rva - SECTION_RVA + SECTION_OFFSET; -export type PeImportFixtureFunction = +type PeImportFixtureFunction = | { readonly name: string; readonly hint?: number } | { readonly ordinal: number }; -export interface PeImportFixtureDll { +interface PeImportFixtureDll { readonly name: string; readonly functions: readonly PeImportFixtureFunction[]; } diff --git a/tests/fixtures/cutter_bridge_protocol_test.py b/tests/fixtures/cutter_bridge_protocol_test.py index fd605f7cdd..957731cf18 100644 --- a/tests/fixtures/cutter_bridge_protocol_test.py +++ b/tests/fixtures/cutter_bridge_protocol_test.py @@ -143,6 +143,7 @@ class ServeDispatcher: elif request.get("kind") == "ack": if self.malformed_next_ack: self.malformed_next_ack = False + self.hide_receipts_after_ack_failure = True reply.update({"ok": True, "session_id": request["session_id"]}) request["_event"].set() return @@ -198,7 +199,11 @@ class ServeDispatcher: else: reply.update({ "ok": True, - "output": 'round-trip: 雪 ☃️ quote=" slash=\\\\ line=\\n tab=\\t', + "output": ( + str(len(self.completed_commands)) + if request.get("command") == "fixture-receipt-count" + else 'round-trip: 雪 ☃️ quote=" slash=\\\\ line=\\n tab=\\t' + ), "current_file": self.current_file, "document_generation": self.generation, "cutter_version": self.cutter_version, @@ -601,14 +606,17 @@ class ProtocolProducerTest(unittest.TestCase): dispatcher.completed_commands = {} dispatcher._receipt_lock = threading.Lock() dispatcher._refresh_document_identity = lambda: None - bridge.cutter.cmd = lambda command: f"output:{command}" + commands = [] + bridge.cutter.cmd = lambda command: commands.append(command) or f"output:{command}" - def handle(value): + def handle(value, cancelled=False): request = { **value, "_event": threading.Event(), "_cancelled": threading.Event(), } + if cancelled: + request["_cancelled"].set() reply = {} bridge._RequestDispatcher._handle(dispatcher, request, reply) self.assertTrue(request["_event"].is_set()) @@ -616,6 +624,15 @@ class ProtocolProducerTest(unittest.TestCase): request_a = "4b18ce4d-bf31-456e-ae31-e8c547b2f9db" request_b = "8ca93d11-107d-41ed-8b40-31dd53cd26a6" + request_cancelled = "9c8074cf-4906-4cc1-ac92-3fca3d305f8f" + cancelled = handle({ + "kind": "command", + "request_id": request_cancelled, + "expected_generation": 4, + "command": "cancelled", + }, cancelled=True) + self.assertEqual(cancelled["error"], "request-cancelled-before-execution") + self.assertEqual(cancelled["execution_state"], "not_started") for request_id, command in ((request_a, "A"), (request_b, "B")): result = handle({ "kind": "command", @@ -624,7 +641,14 @@ class ProtocolProducerTest(unittest.TestCase): "command": command, }) self.assertTrue(result["ok"]) + self.assertEqual(commands, ["A", "B"]) + status = handle({"kind": "status"}) + self.assertEqual( + {receipt["request_id"]: receipt["execution_state"] for receipt in status["completed_commands"]}, + {request_cancelled: "not_started", request_a: "complete", request_b: "complete"}, + ) + self.assertTrue(handle({"kind": "ack", "request_id": request_cancelled})["acknowledged"]) status = handle({"kind": "status"}) self.assertEqual( {receipt["request_id"] for receipt in status["completed_commands"]}, @@ -757,23 +781,12 @@ class ProtocolProducerTest(unittest.TestCase): class RequestFrameLimitTest(unittest.TestCase): def test_payload_limit_excludes_the_line_terminator(self): - class BufferedSocket: - def __init__(self, data): - self.data = data - - def settimeout(self, _timeout): - pass - - def recv(self, count): - part, self.data = self.data[:count], self.data[count:] - return part - payload = b"{}" + b" " * (bridge.MAX_FRAME_BYTES - 2) self.assertEqual( - bridge._BridgeServer._receive_frame(BufferedSocket(payload + b"\n")), {} + bridge._BridgeServer._receive_frame(_BufferedSocket(payload + b"\n")), {} ) with self.assertRaisesRegex(ValueError, "frame-limit-exceeded"): - bridge._BridgeServer._receive_frame(BufferedSocket(payload + b" " + b"\n")) + bridge._BridgeServer._receive_frame(_BufferedSocket(payload + b" " + b"\n")) def test_delimiter_in_a_later_chunk_is_accepted(self): class ShortReads: diff --git a/tests/fixtures/electron/fuseImage.ts b/tests/fixtures/electron/fuseImage.ts index d618a375f7..6de380a43a 100644 --- a/tests/fixtures/electron/fuseImage.ts +++ b/tests/fixtures/electron/fuseImage.ts @@ -1,20 +1,13 @@ -import { - ELECTRON_FUSE_SENTINEL, - ELECTRON_FUSE_STATE_BYTES, -} from "../../../src/domain/electron/electronFuses.js"; - -export const FUSE_SENTINEL = Buffer.from(ELECTRON_FUSE_SENTINEL, "ascii"); +// Electron's documented on-disk fuse wire, independent of the reader constants: +// https://github.com/electron/electron/blob/main/docs/tutorial/fuses.md#the-hard-way +export const FUSE_SENTINEL = Buffer.from( + "dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX", + "ascii", +); /** Obsidian's observed version-1 states: disabled, enabled, then six more with the last enabled. */ export const OBSIDIAN_FUSE_BYTES = [ - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.enabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.disabled, - ELECTRON_FUSE_STATE_BYTES.enabled, + 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, ] as const; export const fuseWire = (version: number, fuses: readonly number[]): Buffer => @@ -56,7 +49,7 @@ export const universalFuseImage = (): { Buffer.concat([FUSE_SENTINEL, fuseWire(1, OBSIDIAN_FUSE_BYTES)]), ); const rightFuses = [...OBSIDIAN_FUSE_BYTES]; - rightFuses[3] = ELECTRON_FUSE_STATE_BYTES.enabled; + rightFuses[3] = 0x31; const right = thinMacho( 0x0100000c, Buffer.concat([FUSE_SENTINEL, fuseWire(1, rightFuses)]), diff --git a/tests/fixtures/environmentStateDerivedValues.ts b/tests/fixtures/environmentStateDerivedValues.ts index 76a2f2f006..ca7f419d58 100644 --- a/tests/fixtures/environmentStateDerivedValues.ts +++ b/tests/fixtures/environmentStateDerivedValues.ts @@ -167,7 +167,7 @@ const slugOf = (title: string): string => .replace(/^-|-$/gu, ""); /** A folder named by the uid its creator chose, with an address from that uid and its title, and a time-based numeric id. */ -export const folder = ( +const folder = ( uid: string, title: string, id: number, diff --git a/tests/fixtures/environmentStateItemWrites.ts b/tests/fixtures/environmentStateItemWrites.ts index 203cb03a30..381093e66e 100644 --- a/tests/fixtures/environmentStateItemWrites.ts +++ b/tests/fixtures/environmentStateItemWrites.ts @@ -55,7 +55,7 @@ const TAGS = [ ]; /** An item as the application answers it: its parent location and tags spelled out from the ids a call sent. */ -export const inventoryItem = ( +const inventoryItem = ( id: string, name: string, parent: Entity | null, diff --git a/tests/fixtures/environmentStateWrites.ts b/tests/fixtures/environmentStateWrites.ts index 23e4455c4f..12f8d092d1 100644 --- a/tests/fixtures/environmentStateWrites.ts +++ b/tests/fixtures/environmentStateWrites.ts @@ -283,7 +283,7 @@ export const UNCHAPTERED_ROWS: readonly TimedCaptureRow[] = renamingRows( ); /** A book whose sent handle the application stores as its code and embeds in its link. */ -export const handledBook = (entity: Book, handle: string): Book => ({ +const handledBook = (entity: Book, handle: string): Book => ({ ...entity, code: handle, link: `https://books.example.test/b/${handle}`, @@ -348,7 +348,7 @@ const shelved = (entry: Book, shelf: string): Book => ({ ...entry, shelf }); const SHELVED_FIRST = shelved(FIRST_BOOK, "a"); const SHELVED_SECOND = shelved(SECOND_BOOK, "b"); /** The book the create at exchange 7 of QUERY_ROWS made, on shelf a. */ -export const SHELVED_THIRD = shelved(book(3, "Third Book", bookClock(7)), "a"); +const SHELVED_THIRD = shelved(book(3, "Third Book", bookClock(7)), "a"); /** * One list tool recorded whole, filtered by shelf, searched, sorted and with a flag that changes nothing; item reads; @@ -373,7 +373,7 @@ export const QUERY_ROWS: readonly TimedCaptureRow[] = [ ]; /** A clock spelled with a `+00:00` offset where the books application's item reads spell `Z`. */ -export const offsetClock = (clock: JsonValue | undefined): string => +const offsetClock = (clock: JsonValue | undefined): string => String(clock).replace(/Z$/u, "+00:00"); /** A book as a list that spells its clocks with an offset shows it. */ diff --git a/tests/fixtures/fakeCdpBrowserEvents.ts b/tests/fixtures/fakeCdpBrowserEvents.ts index 7133d6ad32..5ea79d28e5 100644 --- a/tests/fixtures/fakeCdpBrowserEvents.ts +++ b/tests/fixtures/fakeCdpBrowserEvents.ts @@ -67,7 +67,7 @@ const emitMainWebMcpTools = ( description: "Search orders; authorization=Bearer tool-secret", frameId: "frame-main", backendNodeId: 42, - inputSchema: options.webMcpInputSchema ?? { + inputSchema: { type: "object", properties: inputSchemaProperties, required: ["orderId"], diff --git a/tests/fixtures/fakeCdpBrowserTypes.ts b/tests/fixtures/fakeCdpBrowserTypes.ts index 3ece0256db..1ef4e07d1a 100644 --- a/tests/fixtures/fakeCdpBrowserTypes.ts +++ b/tests/fixtures/fakeCdpBrowserTypes.ts @@ -116,7 +116,6 @@ export interface FakeOptions { readonly webMcpSameUrlRegistrations?: "retain" | "remove-second"; readonly webMcpFrameCount?: number; readonly webMcpSchemaPropertyCount?: number; - readonly webMcpInputSchema?: JsonValue; readonly webMcpChildLeavesScope?: boolean; readonly webMcpChildTransientBlank?: boolean; readonly webMcpChildRecoversAfterTransient?: boolean; diff --git a/tests/fixtures/flutterVerifierPreload.mjs b/tests/fixtures/flutterVerifierPreload.mjs new file mode 100644 index 0000000000..863c2e0fb4 --- /dev/null +++ b/tests/fixtures/flutterVerifierPreload.mjs @@ -0,0 +1,34 @@ +import { registerHooks } from "node:module"; +const model = ` +const identity={sha256:'a'.repeat(64),bytes:1}; +export function result(operation,input) { + const path=input.path;const target={path,...identity}; + if(operation==='identify_flutter_build') return {target,flutter_detected:true,coverage:'complete',abis:[{abi:'x86_64',libapp:{present:true,elf_header_valid:true,snapshot_hash_sources:2,snapshot_hash:'hash',sha256:'b'.repeat(64),bytes:100},libflutter:{present:true,build_id:'12345678',build_id_candidates:['12345678']}}]}; + const abi=input.abi ?? (process.env.REA_FLUTTER_PROBE === 'wrong' ? 'ghost' : 'x86_64'); + return {target,abi,coverage:'complete',libapp:{coverage:'complete',sha256:'b'.repeat(64),bytes:100,snapshot_hash_sources:2,snapshot_hash:'hash',sections:['_kDartVmSnapshotData','_kDartVmSnapshotInstructions','_kDartIsolateSnapshotData','_kDartIsolateSnapshotInstructions'].map(name=>({name,offset:0,size:64,magic_valid:true,kind:3,header_length:48}))},string_pool:{coverage:'complete',package_uri_count_at_least:1,dart_uri_count_at_least:1}}; +} +export function envelope(op,input){const resultValue=result(op,input);return {normalized_result:resultValue,subject:{local_path:input.path,digest:{sha256:identity.sha256}},limitations:[]};} +`; +const modelUrl = "data:text/javascript," + encodeURIComponent(model); +const mocks = { + "node:child_process": `import {promisify} from 'node:util';import {envelope} from ${JSON.stringify(modelUrl)};export const execFile=Object.assign(()=>{}, {[promisify.custom]:async (_cmd,args)=>{const op=args[1].replaceAll('-','_');const index=args.indexOf('--abi');const input={path:args[2],...(index<0?{}:{abi:args[index+1]})};return {stdout:JSON.stringify(envelope(op,input)),stderr:''};}});`, + "@modelcontextprotocol/client": `import {envelope} from ${JSON.stringify(modelUrl)};export class Client {async connect(){} async close(){} async callTool({name,arguments:input}){return {structuredContent:envelope(name,input)}}}`, + "@modelcontextprotocol/client/stdio": `export class StdioClientTransport {async close(){}}`, + "../dist/contracts/toolContracts.js": `export const toolContract=()=>({outputSchema:{parse:value=>value}});`, + "../dist/artifacts/ArtifactResourceScope.js": `export class ArtifactResourceScope {async close(){}}`, + "./lib/mcp-verifier-results.mjs": `export const requireMcpOutput=response=>response.structuredContent;`, + "./lib/verifier-file-resources.mjs": `export const hashVerifierFile=async()=>({sha256:'a'.repeat(64),bytes:1});`, +}; +registerHooks({ + resolve(specifier, context, next) { + if ( + context.parentURL?.endsWith("/scripts/verify-flutter.mjs") && + Object.hasOwn(mocks, specifier) + ) + return { + shortCircuit: true, + url: "data:text/javascript," + encodeURIComponent(mocks[specifier]), + }; + return next(specifier, context); + }, +}); diff --git a/tests/fixtures/il2cpp/metadata.ts b/tests/fixtures/il2cpp/metadata.ts index 45bc4a8f7a..cb2e490924 100644 --- a/tests/fixtures/il2cpp/metadata.ts +++ b/tests/fixtures/il2cpp/metadata.ts @@ -7,7 +7,7 @@ type SectionName = IL2cppMetadataSectionName; type Width = 1 | 2 | 4; /** Synthetic version 39 metadata small enough to read in a hex dump. */ -export interface Il2cppMetadataFixtureOptions { +interface Il2cppMetadataFixtureOptions { /** Serialized TypeIndex width. */ readonly typeIndexWidth?: Width; /** Omit interfaceOffsets so the TypeIndex width comes from type records. */ @@ -46,7 +46,7 @@ export interface Il2cppMetadataFixtureOptions { * System.Größe, Game.Player.Slot.Add and System.Größe.ToString; * the fixture names each container's parameters T0, T1 and so on. */ -export const FIXTURE_GENERIC_CONTAINERS = [ +const FIXTURE_GENERIC_CONTAINERS = [ [3, 1, 0, 0], [2, 2, 1, 1], [4, 1, 1, 3], @@ -134,7 +134,7 @@ export const FIXTURE_STRING_LITERALS = [ * Per-image method tokens. Row ids restart in each image and do not follow * metadata order, so linking must use the token rather than the position. */ -export const FIXTURE_METHOD_TOKENS = [ +const FIXTURE_METHOD_TOKENS = [ 0x06000002, 0x06000001, 0x06000003, 0x06000001, 0x06000002, ] as const; diff --git a/tests/fixtures/mcpAdvertisedSchemaValidity.ts b/tests/fixtures/mcpAdvertisedSchemaValidity.ts index 4549cc0b98..30f76b9631 100644 --- a/tests/fixtures/mcpAdvertisedSchemaValidity.ts +++ b/tests/fixtures/mcpAdvertisedSchemaValidity.ts @@ -6,7 +6,7 @@ import { } from "./mcpClientSchemaConstraints.js"; /** Advertised schema pair as a client receives it from `tools/list`. */ -export interface AdvertisedToolSchemas { +interface AdvertisedToolSchemas { readonly name: string; readonly inputSchema: Record; readonly outputSchema?: Record | undefined; diff --git a/tests/fixtures/sqlite/database.ts b/tests/fixtures/sqlite/database.ts index 445ba6b458..07729df8c7 100644 --- a/tests/fixtures/sqlite/database.ts +++ b/tests/fixtures/sqlite/database.ts @@ -12,7 +12,7 @@ export const SQLITE_NATIVE_LIMITS_AVAILABLE = (() => { })(); /** Exact selected SQLite fixture with committed rows and optional live WAL. */ -export interface SqliteDatabaseFixture { +interface SqliteDatabaseFixture { readonly path: string; readonly walPath: string; readonly quotedTable: string; diff --git a/tests/fixtures/uimage.ts b/tests/fixtures/uimage.ts index 5c4f8ec8d0..64cfd5e462 100644 --- a/tests/fixtures/uimage.ts +++ b/tests/fixtures/uimage.ts @@ -1,10 +1,9 @@ import { crc32 } from "node:zlib"; -import { - UIMAGE_HEADER_SIZE, - UIMAGE_MAGIC, - UIMAGE_NAME_LENGTH, -} from "../../src/domain/uimageFormat.js"; +// U-Boot legacy image header layout, independent of the REA reader. +const UIMAGE_HEADER_SIZE = 64; +const UIMAGE_MAGIC = 0x27051956; +const UIMAGE_NAME_LENGTH = 32; export interface UImageFixtureOptions { readonly arch?: number; diff --git a/tests/fixtures/unityfs.ts b/tests/fixtures/unityfs.ts index 27b78770e1..28c65d511a 100644 --- a/tests/fixtures/unityfs.ts +++ b/tests/fixtures/unityfs.ts @@ -8,7 +8,7 @@ export interface UnityFsMember { readonly size?: number; } -export interface UnityFsOptions { +interface UnityFsOptions { readonly formatVersion?: number; readonly playerVersion?: string; readonly engineVersion?: string; @@ -29,7 +29,7 @@ const cstring = (value: string): Buffer => const align16 = (value: number): number => (value + 15) & ~15; /** One final LZ4 literal sequence; separate sequences require intervening matches. */ -export const lz4LiteralBlock = (data: Buffer): Buffer => { +const lz4LiteralBlock = (data: Buffer): Buffer => { const head = [Math.min(data.length, 15) << 4]; if (data.length >= 15) { let extra = data.length - 15; diff --git a/tests/process-global/ghidraLaunchRollback.test.ts b/tests/process-global/ghidraLaunchRollback.test.ts index c38993e100..e5dd4eb663 100644 --- a/tests/process-global/ghidraLaunchRollback.test.ts +++ b/tests/process-global/ghidraLaunchRollback.test.ts @@ -226,9 +226,10 @@ it.skipIf(process.platform === "win32")( expect(result.error.cause).toBeInstanceOf(AggregateError); if (!(result.error.cause instanceof AggregateError)) throw new Error("Expected composite failure"); - expect(result.error.cause.errors[0]).toMatchObject({ - cause: fault.failure, - }); + const primary: unknown = result.error.cause.errors[0]; + expect(primary).toBeInstanceOf(GhidraLaunchError); + if (!(primary instanceof GhidraLaunchError)) throw primary; + expect(primary.cause).toBe(fault.failure); expect(result.error.cause.errors[1]).toBeUndefined(); expect(result.error.partialLaunch).toBeDefined(); denyCleanup = false; diff --git a/tests/process-global/shortFileReads.test.ts b/tests/process-global/shortFileReads.test.ts index e392cd8e2d..957c1bd46c 100644 --- a/tests/process-global/shortFileReads.test.ts +++ b/tests/process-global/shortFileReads.test.ts @@ -388,10 +388,11 @@ it("preserves cancellation during continued short reads and releases the descrip const path = join(root, "Electron"); await writeFile(path, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes); const controller = new AbortController(); + const reason = new Error("cancel continued read"); fault.path = path; fault.maximum = 2; fault.onRead = () => { - if (fault.reads === 2) controller.abort(new Error("cancel continued read")); + if (fault.reads === 2) controller.abort(reason); }; const provider = new ElectronFuseProvider(); try { @@ -402,6 +403,7 @@ it("preserves cancellation during continued short reads and releases the descrip expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected cancellation"); expect(result.error).toBeInstanceOf(AnalysisCancelledError); + expect(result.error.cause).toBe(reason); expect(fault.reads).toBe(2); expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); } finally {