From 1201c25542bc1e6233265fb220b334f81886550c Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Tue, 27 Oct 2026 21:24:30 +0800 Subject: [PATCH] fix(ci): align static checks and regression fixtures with current contracts (#7349) * fix(ci): retire unused internal analysis declarations * test: use an admitted app bundle for provider routing (cherry picked from commit 00aa17f12a1f9db0205fe82104d14e93bec63bdd) * test: keep bundle routing coverage outside oversized group (cherry picked from commit 8658353b0d0bce41c9f2501e048622b98775b641) * fix(ci): align regression fixtures with current admission contracts * test: bind extraction close fixtures to their ZIP digest (cherry picked from commit 626d0250dbfe07fc55fe17686b0bc437c510b32b) * test(cli): match typed analysis outcomes in function detail fixture * test(ghidra): preserve empty Java property separator framing * test(electron): retain unknown IPC ownership for native module gaps * fix(electron): share source-scoped call path identity * test(captures): assert safe source identity on SAZ refusal * test(apple): match root occurrence kind to inventory manifest * fix(examples): align directory root occurrence identity * test(electron): align equivalent fixture assertions with upstream * test(process): assert retained capture root identity * test(windows): declare the PE fixture optional header * test(ci): refresh owned registration and verifier fixtures * test(setup): use owned registrations for refresh fixtures * test(process): control unavailable ownership in CLI cleanup fixture * test(process): guard controlled compiler output arguments * test(setup): align equivalent entrypoint guards with upstream * test(process): release the cleanup target through an owned gate * test(ci): align semantic identities and owned fixture facts * test(pcap): retain the upstream DNS wire fixture branch * test(pcap): use the canonical wire ownership fixture * test(pcap): assert exact DNS wire query labels * test(ci): preserve admission and ownership fixture prerequisites * test(ci): align authenticated fixture and cleanup boundaries * test(windows): align native artifact fixtures with upstream * test(android): request complete evidence for inline MCP contracts * test: avoid pinning diagnostic wording * test: verify cleanup diagnostics at their source * test: strengthen evidence and cleanup checks * test: avoid pinning registration guidance --------- Co-authored-by: Plastic-time Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- docs/testing.md | 5 + scripts/ci/test-report.mjs | 9 + .../environment/environmentBuild.test.ts | 48 +++--- .../ElectronBoundaryGraphCallPaths.ts | 4 +- .../javascript/ElectronCallPathIdentity.ts | 8 + ...vaScriptSemanticGraphElectronProjection.ts | 4 +- .../PyInstallerArtifactReader.test.ts | 90 ++++++---- .../mobileApplicationGraphExamples.ts | 5 +- .../apkSignature/apkSigningBlock.test.ts | 3 +- src/domain/apple/appleApplication.test.ts | 9 +- .../javascriptStaticAnalysisHelpers.ts | 6 - ...ptStaticAnalysisInvocationReadSite.test.ts | 3 + src/domain/native/objcSwiftMetadata.ts | 3 - src/ida/IdaDoctor.test.ts | 56 ++++--- .../applications/nativePlistNesting.test.ts | 3 +- .../clientConfigurationDiagnosticsCli.test.ts | 4 +- .../setup/hermesAliasedTableSetup.test.ts | 2 +- .../setup/launcherAliasesCli.test.ts | 3 +- .../setup/tomlCommentPreservationCli.test.ts | 8 +- .../tomlConfigurationIntegerRangeCli.test.ts | 4 +- tests/acceptance/webRuntimeCli.test.ts | 50 +++++- tests/boundary/browser/sazWebCapture.test.ts | 32 +++- .../cli/analyzeFunctionDetail.test.ts | 9 +- tests/boundary/cli/electronCli.test.ts | 14 +- .../boundary/cli/environmentExportCli.test.ts | 10 +- tests/boundary/cli/ghidraJavaDoctor.test.ts | 15 +- .../cli/managedMemberComparisonCli.test.ts | 21 ++- tests/boundary/filesystem/ciPrHealth.test.ts | 2 +- .../boundary/filesystem/ciTestReports.test.ts | 50 ++++++ .../filesystem/clientConfigurationBom.test.ts | 6 +- .../clientConfigurationEnvironment.test.ts | 14 +- .../clientConfigurationTransactions.test.ts | 8 +- .../filesystem/clientLifecycle.test.ts | 54 ++++-- .../clientRegistrationRefresh.test.ts | 2 +- .../clientRegistrationStatus.test.ts | 27 +-- .../filesystem/ghidraJavaProbe.test.ts | 7 +- .../kimiClientConfiguration.test.ts | 16 +- .../safeOutputTreeCloseFailures.test.ts | 5 +- .../zcodeClientConfiguration.test.ts | 4 +- .../mcp/artifactInspectionCoverageMcp.test.ts | 5 +- tests/boundary/mcp/electronMcp.test.ts | 12 +- .../mcp/enhancedTools.limitations.test.ts | 56 ++++++- tests/boundary/mcp/fridaMcp.test.ts | 20 ++- .../mcp/packetCaptureAvailability.test.ts | 46 ++---- tests/boundary/mcp/packetCaptureMcp.test.ts | 5 +- tests/boundary/mcp/serverLifecycleMcp.test.ts | 5 +- tests/boundary/mcp/signalRecordingMcp.test.ts | 5 +- tests/boundary/mcp/wasmArtifactMcp.test.ts | 46 ++++-- .../model/modelArtifactViewIdentity.test.ts | 23 ++- .../pcap/pcapProviderCrossPlatform.test.ts | 11 ++ .../process/androidAnalysisMcp.test.ts | 4 + tests/boundary/process/ownedCommand.test.ts | 29 +++- ...processCaptureCancellationAdapters.test.ts | 38 ++++- .../process/processCaptureLifecycle.test.ts | 15 +- .../processCleanupRetryOwnership.test.ts | 4 +- tests/boundary/process/processCli.test.ts | 155 +++++++++++++----- .../providers/native/nativeProvider.test.ts | 2 +- .../setup/setupJebEndpointSelection.test.ts | 2 +- .../analysis-sessions/providerBinding.test.ts | 66 +++++--- tests/fixtures/mcpEvidenceBundle.ts | 12 ++ tests/fixtures/pcap/tshark-stub.cmd | 2 +- .../browserScenarioCleanup.test.ts | 42 ++++- .../ledgerMutationFailures.test.ts | 16 +- tests/process-global/updateOutput.test.ts | 7 +- 64 files changed, 889 insertions(+), 362 deletions(-) create mode 100644 src/application/javascript/ElectronCallPathIdentity.ts create mode 100644 tests/fixtures/mcpEvidenceBundle.ts diff --git a/docs/testing.md b/docs/testing.md index bc88379914..5e76ff0dba 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1630,6 +1630,11 @@ results after a failure. Available failed-shard reports remain diagnostic only: failed, cancelled, skipped, or missing selected shards cannot pass the gate or publish successful timing history. This deliberately avoids relying on an import graph for generated files, filesystem inputs, subprocesses, or bridges. +The portable report rejects an entirely skipped module inventory and lists +fully skipped modules in its summary. A mixed passed/skipped inventory is +allowed: Vitest's skip state does not establish whether a skipped module was +required on that runner. Review the listed skips against platform and provider +prerequisites; the inventory alone does not prove every required case ran. Scheduled qualification is admitted hourly at minute 23. A forced full run at 03:23 UTC refreshes runner and dependency drift every day. Other hourly runs diff --git a/scripts/ci/test-report.mjs b/scripts/ci/test-report.mjs index 7f85c6ec5b..4dff45ea13 100644 --- a/scripts/ci/test-report.mjs +++ b/scripts/ci/test-report.mjs @@ -92,6 +92,8 @@ if (process.env.CI_TEST_SELECTION) { } if (passed && verifyInventory) { const actual = files.map(({ name }) => name); + if (actual.length === 0) + throw new Error("No portable test modules were reported"); const expectedSet = new Set(expected); const actualSet = new Set(actual); const missing = expected.filter((path) => !actualSet.has(path)); @@ -100,7 +102,10 @@ if (passed && verifyInventory) { throw new Error( `Incomplete portable test inventory: ${JSON.stringify({ missing, unexpected, duplicateFiles: actual.length - actualSet.size })}`, ); + if (files.every(({ state }) => state === "skipped")) + throw new Error("Every portable test module was skipped"); } +const skippedFiles = files.filter(({ state }) => state === "skipped"); const summary = [ groupsCompleted ? "## Merged test diagnostics" @@ -120,6 +125,10 @@ const summary = [ ...(process.env.SOURCE_REQUIRED === "true" ? [`Source result: ${process.env.SOURCE_RESULT ?? "unknown"}.`] : []), + `Skipped test modules: ${skippedFiles.length}. Their skip reasons and required coverage are not established by this report.`, + ...(skippedFiles.length ? [""] : []), + ...skippedFiles.map(({ name }) => `- ${name.replaceAll("|", "\\|")}`), + "", passed ? "Successful timing history is available." : "This report is diagnostic only and is not a successful timing baseline.", diff --git a/src/application/environment/environmentBuild.test.ts b/src/application/environment/environmentBuild.test.ts index f62f5f5302..1bc6c6588f 100644 --- a/src/application/environment/environmentBuild.test.ts +++ b/src/application/environment/environmentBuild.test.ts @@ -416,29 +416,31 @@ describe("buildEnvironment cancellation and input checks", () => { }); }); - it("refuses an unreadable package tarball on a dry run as a build does", async () => { - if (process.getuid?.() === 0) return; - const root = await createTestTempDirectory("rea-environment-build-tgz-"); - const tarball = join(root, "rea-agents.tgz"); - await writeFile(tarball, "packed"); - await chmod(tarball, 0o000); - try { - const planned = await buildEnvironment( - await buildInput(root, undefined, { - dryRun: true, - packageTarball: tarball, - }), - ); - const built = await buildEnvironment( - await buildInput(root, undefined, { packageTarball: tarball }), - ); - if (planned.ok || built.ok) throw new Error("Expected both refusals"); - expect(projectAnalysisError(planned.error).code).toBe("access_denied"); - expect(projectAnalysisError(built.error).code).toBe("access_denied"); - } finally { - await chmod(tarball, 0o600); - } - }); + it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( + "refuses an unreadable package tarball on a dry run as a build does", + async () => { + const root = await createTestTempDirectory("rea-environment-build-tgz-"); + const tarball = join(root, "rea-agents.tgz"); + await writeFile(tarball, "packed"); + await chmod(tarball, 0o000); + try { + const planned = await buildEnvironment( + await buildInput(root, undefined, { + dryRun: true, + packageTarball: tarball, + }), + ); + const built = await buildEnvironment( + await buildInput(root, undefined, { packageTarball: tarball }), + ); + if (planned.ok || built.ok) throw new Error("Expected both refusals"); + expect(projectAnalysisError(planned.error).code).toBe("access_denied"); + expect(projectAnalysisError(built.error).code).toBe("access_denied"); + } finally { + await chmod(tarball, 0o600); + } + }, + ); it("refuses a loopback capture whose file name is too long, and quotes the next command", async () => { const root = await createTestTempDirectory("rea-environment-build-long-"); diff --git a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts index efae2e4f00..0d792cce69 100644 --- a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts +++ b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts @@ -2,7 +2,7 @@ import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSour import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js"; import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; -import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; +import { electronCallPathId } from "./ElectronCallPathIdentity.js"; import type { ElectronIpcContext, ElectronIpcRecord, @@ -36,7 +36,7 @@ export const addElectronCallPaths = ( if (javascript === null) continue; const coverage = javascriptAnalysisCoverage(javascript); for (const path of javascript.electron.call_paths ?? []) { - const pathId = sha256Text(JSON.stringify([file.path, path])); + const pathId = electronCallPathId(file.path, path); const nodes = path.steps.map((step, index) => context.accumulator.addNode({ kind: "javascript-expression", diff --git a/src/application/javascript/ElectronCallPathIdentity.ts b/src/application/javascript/ElectronCallPathIdentity.ts new file mode 100644 index 0000000000..6c70ea3995 --- /dev/null +++ b/src/application/javascript/ElectronCallPathIdentity.ts @@ -0,0 +1,8 @@ +import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js"; +import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; + +/** Keep wrapper-path identity source-scoped across both graph projections. */ +export const electronCallPathId = ( + filePath: string, + path: ElectronCallPath, +): string => sha256Text(JSON.stringify([filePath, path])); diff --git a/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts b/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts index c10e8f6327..4491c4781e 100644 --- a/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphElectronProjection.ts @@ -1,6 +1,6 @@ import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; -import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; +import { electronCallPathId } from "./ElectronCallPathIdentity.js"; import { addSemanticGraphRelation, retainSemanticGraphNode, @@ -16,7 +16,7 @@ export function* projectElectronSemanticCallPaths( ): Generator { for (const path of paths) { yield; - const pathId = sha256Text(JSON.stringify(path)); + const pathId = electronCallPathId(file.path, path); let previous: ReturnType = null; let previousKind: ElectronCallPath["steps"][number]["kind"] | undefined; for (const [index, step] of path.steps.entries()) { diff --git a/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts b/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts index 352b54c72d..a92b2e2a52 100644 --- a/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts +++ b/src/artifacts/pyinstaller/PyInstallerArtifactReader.test.ts @@ -970,7 +970,7 @@ it("inventories and extracts a Windows member name under its canonical path", as ).toBe(0); }); -it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ data", async () => { +const posixPathFixture = async () => { const backslashName = String.raw`a\b.txt`; const slashName = "a/b.txt"; const backslashPayload = Buffer.from("literal-backslash"); @@ -1010,6 +1010,18 @@ it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ }), ); const snapshot = await scanCanonicalArtifactInventory(path); + return { + path, + snapshot, + backslashName, + slashName, + backslashPayload, + slashPayload, + }; +}; + +it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ data", async () => { + const { snapshot, backslashName, slashName } = await posixPathFixture(); const backslashEntry = snapshot.occurrences.find( (occurrence) => occurrence.logical_path === backslashName, ); @@ -1034,39 +1046,53 @@ it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ }, }); expect(snapshot.coverage.status).toBe("complete"); - if (process.platform === "win32") return; - - const directory = await createTestTempDirectory("rea-pyinstaller-posix-out-"); - const output = join(directory, "extracted"); - await materializeArtifactInventory( - { - inputPath: path, - inputFormat: "elf", - outputRoot: output, - environment: process.env, - integrityPolicy: "fail", - }, - path, - snapshot, - ); - expect( - Buffer.compare( - await readFile(join(output, backslashName)), - backslashPayload, - ), - ).toBe(0); - expect( - Buffer.compare(await readFile(join(output, "a", "b.txt")), slashPayload), - ).toBe(0); - for (const [name, body] of [ - [backslashName, backslashPayload], - [slashName, slashPayload], - ] as const) - expect((await readFile(join(output, "PYZ.pyz", name))).equals(body)).toBe( - true, - ); }); +it.skipIf(process.platform === "win32")( + "extracts distinct POSIX backslash and slash paths in CArchive files and PYZ data", + async () => { + const { + path, + snapshot, + backslashName, + slashName, + backslashPayload, + slashPayload, + } = await posixPathFixture(); + const directory = await createTestTempDirectory( + "rea-pyinstaller-posix-out-", + ); + const output = join(directory, "extracted"); + await materializeArtifactInventory( + { + inputPath: path, + inputFormat: "elf", + outputRoot: output, + environment: process.env, + integrityPolicy: "fail", + }, + path, + snapshot, + ); + expect( + Buffer.compare( + await readFile(join(output, backslashName)), + backslashPayload, + ), + ).toBe(0); + expect( + Buffer.compare(await readFile(join(output, "a", "b.txt")), slashPayload), + ).toBe(0); + for (const [name, body] of [ + [backslashName, backslashPayload], + [slashName, slashPayload], + ] as const) + expect((await readFile(join(output, "PYZ.pyz", name))).equals(body)).toBe( + true, + ); + }, +); + it("reads a PE signature beyond the carrier prefix before projecting paths", async () => { const path = await writeArchive( "distant-pe", diff --git a/src/contracts/mobileApplicationGraphExamples.ts b/src/contracts/mobileApplicationGraphExamples.ts index 92fb913224..fc9794b1de 100644 --- a/src/contracts/mobileApplicationGraphExamples.ts +++ b/src/contracts/mobileApplicationGraphExamples.ts @@ -17,10 +17,11 @@ const inventory = ( ) => { const sha = digit.repeat(64); const artifactId = artifactIdForContent(sha); + const rootEntryKind = format === "directory" ? "directory" : "file"; const occurrenceId = occurrenceIdForLocation({ rootArtifactId: artifactId, logicalPath: ".", - entryKind: "file", + entryKind: rootEntryKind, }); const nodes: ArtifactNode[] = [ { @@ -40,7 +41,7 @@ const inventory = ( artifact_id: artifactId, parent_occurrence_id: null, logical_path: ".", - entry_kind: "file", + entry_kind: rootEntryKind, artifact_kind: "resource", artifact_format: format, declared_size: 1, diff --git a/src/domain/apkSignature/apkSigningBlock.test.ts b/src/domain/apkSignature/apkSigningBlock.test.ts index 04d73ad804..8fef125656 100644 --- a/src/domain/apkSignature/apkSigningBlock.test.ts +++ b/src/domain/apkSignature/apkSigningBlock.test.ts @@ -102,7 +102,8 @@ describe("APK signing block locator", () => { if (!tail.ok) return; const block = locateApkSigningBlock(bytes, tail.value); expect(block.ok).toBe(true); - if (!block.ok || block.value === null) return expect(block).toBeTruthy(); + if (!block.ok || block.value === null) + throw new Error("Expected an APK signing block"); expect(block.value.pairs.map((p) => p.id)).toEqual([ APK_BLOCK_ID.signatureV2, APK_BLOCK_ID.signatureV3, diff --git a/src/domain/apple/appleApplication.test.ts b/src/domain/apple/appleApplication.test.ts index 8a9685a887..0fedfde02b 100644 --- a/src/domain/apple/appleApplication.test.ts +++ b/src/domain/apple/appleApplication.test.ts @@ -81,7 +81,14 @@ const inventoryEvidence = ( limitations: [], }); const nodes = new Map([[rootId, node(rootSha, rootFormat)]]); - const occurrences = [occurrence(".", "file", rootId, rootFormat)]; + const occurrences = [ + occurrence( + ".", + rootFormat === "directory" ? "directory" : "file", + rootId, + rootFormat, + ), + ]; for (const { path, kind = "file", diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index 5322a7ebeb..d732aa1921 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -311,12 +311,6 @@ export interface JavaScriptStaticSourceUrlPath { readonly directory: boolean; } -/** Convert an exact static file URL expression to a POSIX path. */ -export const staticFileUrlPath = ( - node: t.CallExpression | t.NewExpression, - pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery, -): string | undefined => staticFileUrlPathFact(node, pathIntrinsics)?.path; - const staticFileUrlPathFact = ( node: t.CallExpression | t.NewExpression, pathIntrinsics?: JavaScriptStaticPathIntrinsicQuery, diff --git a/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts b/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts index abbf4637b7..f9e1aaad18 100644 --- a/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisInvocationReadSite.test.ts @@ -47,6 +47,9 @@ it("retains complete coverage for an inline Electron factory export", () => { load(9); })();`); expect(result.parse_status).toBe("complete"); + expect(result.references).toContainEqual( + expect.objectContaining({ kind: "require", specifier: "electron" }), + ); expect(result.references.every(({ specifier }) => specifier !== null)).toBe( true, ); diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts index 4781bb3acb..02ebb2bb4b 100644 --- a/src/domain/native/objcSwiftMetadata.ts +++ b/src/domain/native/objcSwiftMetadata.ts @@ -376,9 +376,6 @@ export const objcClassImportBindingCoverageSchema = examined: z.number().int().nonnegative(), decoded: z.number().int().nonnegative(), }); -export type ObjcClassImportBindingCoverage = z.infer< - typeof objcClassImportBindingCoverageSchema ->; /** Discovery retains unknown or inapplicable import identity separately from scan coverage. */ export const objcClassDiscoveryCoverageSchema = nativeMetadataCoverageSchema diff --git a/src/ida/IdaDoctor.test.ts b/src/ida/IdaDoctor.test.ts index aaffe3bf2c..d3c23635d3 100644 --- a/src/ida/IdaDoctor.test.ts +++ b/src/ida/IdaDoctor.test.ts @@ -76,33 +76,35 @@ it.skipIf(process.platform === "win32")( }, ); -it("marks an unusable system temporary parent unavailable when workspaceRoot is omitted", async () => { - if (process.platform === "win32") return; - const root = await mkdtemp(join(tmpdir(), "rea-ida-default-parent-")); - roots.push(root); - const path = join(root, "registration.json"); - const unusableDefault = join(root, "not-a-directory"); - await writeFile(unusableDefault, "file"); - await writeFile( - path, - JSON.stringify({ command: "python", mode: "headless" }), - ); - const previous = process.env.TMPDIR; - process.env.TMPDIR = unusableDefault; - try { - const inspection = await inspectIdaRegistration(path, resourceScope); - expect(inspection.available).toBe(false); - expect( - inspection.checks.find(({ name }) => name === "private_workspace"), - ).toMatchObject({ - ok: false, - code: "invalid_configuration", - }); - } finally { - if (previous === undefined) delete process.env.TMPDIR; - else process.env.TMPDIR = previous; - } -}); +it.skipIf(process.platform === "win32")( + "marks an unusable system temporary parent unavailable when workspaceRoot is omitted", + async () => { + const root = await mkdtemp(join(tmpdir(), "rea-ida-default-parent-")); + roots.push(root); + const path = join(root, "registration.json"); + const unusableDefault = join(root, "not-a-directory"); + await writeFile(unusableDefault, "file"); + await writeFile( + path, + JSON.stringify({ command: "python", mode: "headless" }), + ); + const previous = process.env.TMPDIR; + process.env.TMPDIR = unusableDefault; + try { + const inspection = await inspectIdaRegistration(path, resourceScope); + expect(inspection.available).toBe(false); + expect( + inspection.checks.find(({ name }) => name === "private_workspace"), + ).toMatchObject({ + ok: false, + code: "invalid_configuration", + }); + } finally { + if (previous === undefined) delete process.env.TMPDIR; + else process.env.TMPDIR = previous; + } + }, +); it("keeps IDA configuration for setup without copying transport credentials or claiming runtime verification", async () => { const root = await mkdtemp(join(tmpdir(), "rea-ida-doctor-")); roots.push(root); diff --git a/tests/acceptance/applications/nativePlistNesting.test.ts b/tests/acceptance/applications/nativePlistNesting.test.ts index faa2624865..7df7423ec7 100644 --- a/tests/acceptance/applications/nativePlistNesting.test.ts +++ b/tests/acceptance/applications/nativePlistNesting.test.ts @@ -101,8 +101,7 @@ describe.skipIf(process.platform !== "darwin")( reported_limits: { maximum_nesting_depth: 998 }, }, remediation: { - action: - "The 998-level native plist nesting allowance (no runtime override) is a fixed allowance. Worker memory limits, inherited limits, and additional host memory do not raise it. Select another complete input within the allowance when that preserves the needed observation. Repeating the unchanged request fails the same way.", + action: expect.stringMatching(/\S/u), }, }; expect(output.json).toMatchObject(expectedFailure); diff --git a/tests/acceptance/setup/clientConfigurationDiagnosticsCli.test.ts b/tests/acceptance/setup/clientConfigurationDiagnosticsCli.test.ts index 3aa9a21432..a3ed9425f0 100644 --- a/tests/acceptance/setup/clientConfigurationDiagnosticsCli.test.ts +++ b/tests/acceptance/setup/clientConfigurationDiagnosticsCli.test.ts @@ -139,7 +139,9 @@ for (const scenario of cases) appliedActions: [], }); if ("syntax" in scenario) { - expect(setup.json).toMatchObject({ remediation: expect.any(String) }); + expect(setup.json).toMatchObject({ + remediation: expect.stringMatching(/\S/u), + }); if (scenario.syntax === "JSON") expect(setup.stdout).toContain("Invalid JSON/JSONC"); } else { diff --git a/tests/acceptance/setup/hermesAliasedTableSetup.test.ts b/tests/acceptance/setup/hermesAliasedTableSetup.test.ts index 3e7b49626e..33e09c7ea8 100644 --- a/tests/acceptance/setup/hermesAliasedTableSetup.test.ts +++ b/tests/acceptance/setup/hermesAliasedTableSetup.test.ts @@ -16,7 +16,7 @@ const configuration = ( "# Personal settings\nmodel: custom\nmask: 0x1F\n" + "server_defaults: &servers\n" + (existing - ? " rea: &server\n command: rea-old\n args: [ 'mcp' ]\n env:\n FLAG: 'on'\n other: *server\n" + ? " rea: &server\n command: npx\n args: [ '-y', 'rea-agents@6.1.0', 'mcp' ]\n env:\n FLAG: 'on'\n other: *server\n" : " other: &server\n command: other-server\n args: [ 'on', '0600' ]\n"); return { defaults, diff --git a/tests/acceptance/setup/launcherAliasesCli.test.ts b/tests/acceptance/setup/launcherAliasesCli.test.ts index 502a2e88de..8270a3b622 100644 --- a/tests/acceptance/setup/launcherAliasesCli.test.ts +++ b/tests/acceptance/setup/launcherAliasesCli.test.ts @@ -244,7 +244,8 @@ cliTest.skipIf(process.platform === "win32").for([ client: "cursor", state: "unknown", command, - remediation: expect.stringContaining("symlink targets"), + config_path: config, + remediation: expect.stringMatching(/\S/u), }), ]), }, diff --git a/tests/acceptance/setup/tomlCommentPreservationCli.test.ts b/tests/acceptance/setup/tomlCommentPreservationCli.test.ts index 7e03fd7588..379393cdaa 100644 --- a/tests/acceptance/setup/tomlCommentPreservationCli.test.ts +++ b/tests/acceptance/setup/tomlCommentPreservationCli.test.ts @@ -32,7 +32,8 @@ const scenarios = [ "# about rea", "[mcp_servers.rea]", "# inside the rea table", - 'command = "old"', + 'command = "npx"', + 'args = ["-y", "rea-agents@6.1.0", "mcp"]', "", "# between rea and its environment", "[mcp_servers.rea.env]", @@ -63,7 +64,8 @@ const scenarios = [ original: [ "# about rea", "[mcp_servers.rea]", - 'command = "old"', + 'command = "npx"', + 'args = ["-y", "rea-agents@6.1.0", "mcp"]', "# trailing note without a line break", ].join("\n"), comments: ["# about rea", "# trailing note without a line break"], @@ -121,7 +123,7 @@ for (const scenario of scenarios) configured.push(text); for (const comment of scenario.comments) expect(text, path).toContain(comment); - expect(text, path).not.toContain('command = "old"'); + expect(text, path).not.toContain("rea-agents@6.1.0"); expect(unrelatedValues(text), path).toEqual( unrelatedValues(scenario.original), ); diff --git a/tests/acceptance/setup/tomlConfigurationIntegerRangeCli.test.ts b/tests/acceptance/setup/tomlConfigurationIntegerRangeCli.test.ts index eb558517a3..a9a6c5ed01 100644 --- a/tests/acceptance/setup/tomlConfigurationIntegerRangeCli.test.ts +++ b/tests/acceptance/setup/tomlConfigurationIntegerRangeCli.test.ts @@ -79,7 +79,7 @@ cliTest( const original = "# retained comment\n" + 'mcp_servers = { other = { command = "other", budget = 9007199254740993 }, ' + - 'rea = { command = "previous", tool_timeout_sec = 9223372036854775807 } }\n'; + 'rea = { command = "npx", args = ["-y", "rea-agents@6.1.0", "mcp"], tool_timeout_sec = 9223372036854775807 } }\n'; await mkdir(dirname(config), { recursive: true }); await writeFile(config, original); const applied = await cli.run({ @@ -92,7 +92,7 @@ cliTest( expect(configured.startsWith("# retained comment\n")).toBe(true); expect(configured).toContain("budget = 9007199254740993"); expect(configured).toContain("tool_timeout_sec = 9223372036854775807"); - expect(configured).not.toContain('"previous"'); + expect(configured).not.toContain("rea-agents@6.1.0"); }, ); diff --git a/tests/acceptance/webRuntimeCli.test.ts b/tests/acceptance/webRuntimeCli.test.ts index 25bd201449..c28d9b4513 100644 --- a/tests/acceptance/webRuntimeCli.test.ts +++ b/tests/acceptance/webRuntimeCli.test.ts @@ -5,6 +5,7 @@ import type { Socket } from "node:net"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; import { parseEvidence } from "../../src/domain/evidence.js"; import { webEventListenersSchema } from "../../src/domain/webEventListeners.js"; import { startRuntimeBrowser } from "../fixtures/webRuntime.js"; @@ -18,6 +19,45 @@ const environment = () => ({ HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper", }); +const executionProgress = z.object({ + rea_progress: z.object({ + phase: z.literal("browser_execution"), + completed: z.literal(1), + }), +}); + +/** Buffer complete stderr lines so a progress event cannot borrow fields from another line. */ +const executionReadyReader = () => { + let pending = ""; + return (chunk: string): boolean => { + const lines = (pending + chunk).split("\n"); + pending = lines.pop() ?? ""; + return lines.some((line) => { + try { + return executionProgress.safeParse(JSON.parse(line)).success; + } catch { + // Other stderr lines are not necessarily JSON progress events. + return false; + } + }); + }; +}; + +it("arms only for a complete matching browser execution progress event", () => { + const ready = executionReadyReader(); + expect( + ready('{"rea_progress":{"phase":"browser_execution","completed":0}}\n'), + ).toBe(false); + expect(ready('{"rea_progress":{"phase":"other","completed":1}}\n')).toBe( + false, + ); + expect( + ready('{"rea_progress":{"phase":"browser_execution","completed":10}}\n'), + ).toBe(false); + expect(ready('other stderr\n{"rea_progress":{"phase":"browser_')).toBe(false); + expect(ready('execution","completed":1}}\n')).toBe(true); +}); + it("advertises the canonical observation duration bounds through the compiled CLI", async () => { const { stdout } = await promisify(execFile)( process.execPath, @@ -86,17 +126,15 @@ it("translates SIGINT after real CLI arming into awaited instrumentation cleanup let stderr = ""; let stdout = ""; let armed = false; + const executionReady = executionReadyReader(); const timer = setTimeout(() => child.kill("SIGKILL"), 20_000); + child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => { stdout += chunk; }); - child.stderr.on("data", (chunk) => { + child.stderr.on("data", (chunk: string) => { stderr += chunk; - if ( - !armed && - stderr.includes('"completed":1') && - stderr.includes("browser_execution") - ) { + if (!armed && executionReady(chunk)) { armed = true; child.kill("SIGINT"); } diff --git a/tests/boundary/browser/sazWebCapture.test.ts b/tests/boundary/browser/sazWebCapture.test.ts index 0cdd28ae37..622a65da04 100644 --- a/tests/boundary/browser/sazWebCapture.test.ts +++ b/tests/boundary/browser/sazWebCapture.test.ts @@ -239,6 +239,7 @@ it("checks trailer credentials before hidden metadata blocks analysis and withho ], ]); const path = await fixture(bytes); + const sha256 = createHash("sha256").update(bytes).digest("hex"); const { call } = await connectLocalToolsMcp(); const base = { capture_path: path, @@ -267,16 +268,35 @@ it("checks trailer credentials before hidden metadata blocks analysis and withho ...base, record_ordinals: ordinals, }); - const error = parseMcpToolError(response).error; + const failure = parseMcpToolError(response); + const error = failure.error; expect(error.code).toBe("capability_unavailable"); expect(JSON.stringify(response)).not.toContain("content_base64"); expect(error.details?.partial_observation).toEqual({ kind: "file-snapshot-hash", path, - sha256: createHash("sha256").update(bytes).digest("hex"), + sha256, bytes: bytes.length, }); + expect(failure).not.toHaveProperty("normalized_result"); } + const excludedResponse = await call("inspect_web_network_capture", { + ...base, + record_ordinals: [0, 1], + sensitive_values: ["X-Debug", path, sha256], + }); + const excludedFailure = parseMcpToolError(excludedResponse); + const excludedError = excludedFailure.error; + expect(excludedError.code).toBe("capability_unavailable"); + expect(excludedError.details?.partial_observation).toEqual({ + kind: "file-snapshot-hash", + path: null, + sha256: null, + bytes: bytes.length, + }); + expect(excludedFailure).not.toHaveProperty("normalized_result"); + for (const excluded of [path, sha256, "content_base64", "fixture-only"]) + expect(JSON.stringify(excludedResponse)).not.toContain(excluded); const opaqueReply = await call("inspect_web_network_capture", { capture_path: path, format: "saz", @@ -889,13 +909,17 @@ it.each([ }, }); expect(error).not.toHaveProperty("normalized_result"); - expect(error.details.partial_observation).toEqual({ + const sourceObservation = { kind: "file-snapshot-hash", path, sha256: createHash("sha256").update(bytes).digest("hex"), bytes: bytes.length, - }); + }; + expect(error.details.partial_observation).toEqual(sourceObservation); expect(mcpFailure).not.toHaveProperty("normalized_result"); + expect(mcpFailure.error.details?.partial_observation).toEqual( + sourceObservation, + ); expect(Buffer.compare(await readFile(path), bytes)).toBe(0); } expect(JSON.stringify(result)).not.toContain("malformed-private-input"); diff --git a/tests/boundary/cli/analyzeFunctionDetail.test.ts b/tests/boundary/cli/analyzeFunctionDetail.test.ts index 38b5c67a93..ecc1390e0c 100644 --- a/tests/boundary/cli/analyzeFunctionDetail.test.ts +++ b/tests/boundary/cli/analyzeFunctionDetail.test.ts @@ -11,6 +11,7 @@ import { CLI_COMMANDS } from "../../../src/cliCommandNames.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js"; import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js"; +import { ok } from "../../../src/domain/result.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; // A complete analyze_function Evidence as runDirectAnalysis would return it: @@ -28,14 +29,14 @@ const cannedCompleteEvidence = () => ); const harness = () => { - const runDirectAnalysis = vi.fn( - async () => cannedCompleteEvidence() as unknown, - ); + const runDirectAnalysis = vi.fn< + Parameters[2] + >(async () => ok(cannedCompleteEvidence())); const cli = Cli.create("rea-fn-detail-test", { version: "1", sync: false }); registerCoreBinaryCommands( cli as unknown as CliInstance, pino({ enabled: false }), - runDirectAnalysis as never, + runDirectAnalysis, {}, ); const command = Cli.toCommands.get(cli)?.get(CLI_COMMANDS.function); diff --git a/tests/boundary/cli/electronCli.test.ts b/tests/boundary/cli/electronCli.test.ts index 4dcd71a1fa..d5e2e274a5 100644 --- a/tests/boundary/cli/electronCli.test.ts +++ b/tests/boundary/cli/electronCli.test.ts @@ -270,7 +270,11 @@ it( summary: { browser_windows: 3, context_bridge_apis: 2, - ipc: { paired_renderer_transmissions: 4 }, + ipc: { + paired_renderer_transmissions: 0, + ambiguous_renderer_transmissions: 7, + unpaired_literal_renderer_transmissions: 0, + }, }, }, }); @@ -326,7 +330,13 @@ describe("Electron CLI artifact behavior", () => { normalized_result: { input_path: root, format: "directory", - summary: { ipc: { paired_renderer_transmissions: 4 } }, + summary: { + ipc: { + paired_renderer_transmissions: 0, + ambiguous_renderer_transmissions: 7, + unpaired_literal_renderer_transmissions: 0, + }, + }, }, }); }, diff --git a/tests/boundary/cli/environmentExportCli.test.ts b/tests/boundary/cli/environmentExportCli.test.ts index c540142b65..9d1bb5405c 100644 --- a/tests/boundary/cli/environmentExportCli.test.ts +++ b/tests/boundary/cli/environmentExportCli.test.ts @@ -50,9 +50,15 @@ cliTest( }); expect(result.exitCode).toBe(1); expect(result.json).toMatchObject({ - details: { operation: "environment-export" }, + code: "cleanup_incomplete", + details: { + operation: "environment-export", + cleanup: "incomplete", + resources: [files.manifestPath], + diagnostics: { input_path: files.manifestPath, input_role: "manifest" }, + cleanup_reason: "controlled persistent close failure", + }, }); - expect(JSON.stringify(result.json)).toContain("cleanup"); await expect(readFile(join(directory, "README.md"))).rejects.toMatchObject({ code: "ENOENT", }); diff --git a/tests/boundary/cli/ghidraJavaDoctor.test.ts b/tests/boundary/cli/ghidraJavaDoctor.test.ts index cac908f4d8..c08d0f8092 100644 --- a/tests/boundary/cli/ghidraJavaDoctor.test.ts +++ b/tests/boundary/cli/ghidraJavaDoctor.test.ts @@ -1,5 +1,5 @@ import { chmod, mkdir, readFile, symlink, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { delimiter, isAbsolute, join } from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { expect } from "vitest"; import { elf, thinMach } from "../../../src/domain/binaryTarget.fixture.js"; @@ -9,6 +9,7 @@ import { observeOwnedProcessGroups, systemProcessOwnershipHost, } from "../../../src/process/ProcessOwnershipObservation.js"; +import { resolveExecutable } from "../../../src/process/capture/ProcessScenarioRuntimeValidation.js"; cliTest.skipIf(!["darwin", "linux"].includes(process.platform)).for([ ["missing", "missing_dependency", "runtime_missing"], @@ -237,6 +238,15 @@ cliTest.skipIf(!["darwin", "linux"].includes(process.platform))( const root = await createTestTempDirectory("rea-ghidra-cwd-doctor-"); const install = join(root, "installation"); const marker = join(root, "launcher-execution"); + const bin = join(root, "bin"); + await mkdir(bin); + const hostPath = process.env.PATH?.split(delimiter) + .filter((directory) => isAbsolute(directory)) + .join(delimiter); + await symlink( + await resolveExecutable("ps", process.cwd(), hostPath), + join(bin, "ps"), + ); await mkdir(join(install, "Ghidra"), { recursive: true }); await mkdir(join(install, "support"), { recursive: true }); await writeFile( @@ -253,7 +263,8 @@ cliTest.skipIf(!["darwin", "linux"].includes(process.platform))( arguments: ["doctor", "--provider", "ghidra", "--json"], cwd: root, environment: { - PATH: "", + // Keep CWD discovery first while admitting only the ownership prerequisite. + PATH: ["", bin].join(delimiter), JAVA_HOME: join(root, "missing-jdk"), REA_LOG_LEVEL: "silent", }, diff --git a/tests/boundary/cli/managedMemberComparisonCli.test.ts b/tests/boundary/cli/managedMemberComparisonCli.test.ts index 2e2d25ce23..b52c3dff4e 100644 --- a/tests/boundary/cli/managedMemberComparisonCli.test.ts +++ b/tests/boundary/cli/managedMemberComparisonCli.test.ts @@ -256,9 +256,24 @@ it.each([true, false])( partial_observation: { evidence_id: evidence.evidence_id }, }, }); - expect(JSON.stringify(result)).toContain("comparison close denied"); - if (!success) - expect(JSON.stringify(result)).toContain("primary comparison failure"); + expect(result).toMatchObject({ + details: { + diagnostics: success + ? { reason: "comparison close denied" } + : { + primary_error: { + code: "unreadable_output", + details: { reason: "primary comparison failure" }, + }, + cleanup_error: { + code: "cleanup_incomplete", + details: { + diagnostics: { reason: "comparison close denied" }, + }, + }, + }, + }, + }); expect(close).toHaveBeenCalledOnce(); } finally { run.mockRestore(); diff --git a/tests/boundary/filesystem/ciPrHealth.test.ts b/tests/boundary/filesystem/ciPrHealth.test.ts index 756fb47489..c9d7a968b4 100644 --- a/tests/boundary/filesystem/ciPrHealth.test.ts +++ b/tests/boundary/filesystem/ciPrHealth.test.ts @@ -409,7 +409,7 @@ it("public CLI uses only GET and produces a report from fixture GitHub responses await writeFile( ghResponse, JSON.stringify([ - [rawPull(51, { mergeable: true })], + [rawPull(51, { mergeable: true, created_at: new Date().toISOString() })], { total_count: 0, workflow_runs: [] }, ]), ); diff --git a/tests/boundary/filesystem/ciTestReports.test.ts b/tests/boundary/filesystem/ciTestReports.test.ts index 8d20c80142..971efc4371 100644 --- a/tests/boundary/filesystem/ciTestReports.test.ts +++ b/tests/boundary/filesystem/ciTestReports.test.ts @@ -326,6 +326,56 @@ it("rejects successful shard reports with missing or duplicate portable files", await expect( readFile(join(root, ".cache/ci-test-history/test-results.json")), ).rejects.toMatchObject({ code: "ENOENT" }); + + const empty = await createTestTempDirectory("rea-ci-report-empty-"); + await workflowGit(empty, ["init"]); + await writeFile( + join(empty, "test-timings.json"), + JSON.stringify({ version: 1, success: true, files: [] }), + ); + await expect( + executeTimingReport(empty, { CI_VERIFY_TEST_INVENTORY: "true" }), + ).rejects.toMatchObject({ + stderr: expect.stringContaining("No portable test modules were reported"), + }); +}); + +it("reports skipped modules without treating an entirely skipped suite as successful", async () => { + const root = await createTestTempDirectory("rea-ci-report-skips-"); + await workflowGit(root, ["init"]); + await mkdir(join(root, "tests")); + for (const name of ["required", "platform"]) + await writeFile(join(root, `tests/${name}.test.ts`), "// fixture\n"); + await workflowGit(root, ["add", "tests"]); + const run = async (requiredState: string) => { + await writeFile( + join(root, "test-timings.json"), + JSON.stringify({ + version: 1, + success: true, + files: [ + { + name: "tests/required.test.ts", + durationMs: 1, + state: requiredState, + }, + { name: "tests/platform.test.ts", durationMs: 1, state: "skipped" }, + ], + }), + ); + return executeTimingReport(root, { CI_VERIFY_TEST_INVENTORY: "true" }); + }; + + const mixed = await run("passed"); + expect(mixed.summary).toContain("Skipped test modules: 1"); + expect(mixed.summary).toContain("- tests/platform.test.ts"); + expect(mixed.summary).toContain("required coverage are not established"); + await expect(run("skipped")).rejects.toMatchObject({ + stderr: expect.stringContaining("Every portable test module was skipped"), + }); + await expect( + readFile(join(root, ".cache/ci-test-history/test-results.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("runs the existing onboarding and Python producer guards from their selected CI owners", async () => { diff --git a/tests/boundary/filesystem/clientConfigurationBom.test.ts b/tests/boundary/filesystem/clientConfigurationBom.test.ts index 554577e60f..638edf7144 100644 --- a/tests/boundary/filesystem/clientConfigurationBom.test.ts +++ b/tests/boundary/filesystem/clientConfigurationBom.test.ts @@ -42,7 +42,9 @@ describe.each(["vscode", "copilot_cli", "opencode", "commandcode"])( ).replaceAll("\n", "\r\n")}\r\n`; await mkdir(dirname(client.configPath), { recursive: true }); await writeFile(client.configPath, original); - const currentCommandPath = join(home, "rea"); + const currentCommandPath = CURRENT_ENTRYPOINT_PATH; + if (currentCommandPath === undefined) + throw new Error("test runner entrypoint is unavailable"); const command = [process.execPath, currentCommandPath, "mcp"]; expect(await inspectClientConfiguration(client, {}, command)).toEqual({ status: "update", @@ -200,7 +202,7 @@ it.each(["", "\n \n", "\uFEFF"])( throw new Error("missing Antigravity JSON config"); await mkdir(dirname(client.configPath), { recursive: true }); await writeFile(client.configPath, original); - const command = [join(home, "rea"), "mcp"]; + const command = ["rea", "mcp"]; expect(await inspectClientConfiguration(client, {}, command)).toEqual({ status: "update", diff --git a/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts b/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts index 8c0a103205..5b61d8786d 100644 --- a/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts +++ b/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts @@ -26,6 +26,8 @@ import { npxRegistrationCommand } from "../../../src/application/ClientRegistrat import { ok } from "../../../src/domain/result.js"; import { MCP_STARTUP_POLICY } from "../../../src/mcpStartupPolicy.js"; +const staleOwnedCommand = ["npx", "-y", "rea-agents@6.6.0", "mcp"] as const; + it("awaits the owner-parsed provider request budget without changing caller strings", async () => { const parsed = parseClientConfiguration( JSON.stringify({ @@ -211,8 +213,8 @@ it.skipIf(process.platform !== "win32")( JSON.stringify({ mcpServers: { rea: { - command: "old-rea", - args: ["mcp"], + command: staleOwnedCommand[0], + args: staleOwnedCommand.slice(1), env: { rea_mcp_input_schema_profile: "full" }, }, }, @@ -378,7 +380,7 @@ it.each([ { ["__proto__"]: { caller: "retain root value" }, [serversKey]: { - rea: clientRegistrationEntry(format, ["rea-old", "mcp"], custom), + rea: clientRegistrationEntry(format, staleOwnedCommand, custom), other: clientRegistrationEntry(format, ["other"], { OTHER: "keep" }), ["__proto__"]: clientRegistrationEntry(format, ["peer"], {}), }, @@ -651,7 +653,7 @@ it("rejects a non-string caller environment key before editing configuration", a const root = await createTestTempDirectory("rea-invalid-own-environment-"); const configPath = join(root, "config.json"); const source = - '{"mcpServers":{"rea":{"command":"old-rea","args":["mcp"],"env":{"__proto__":123}}}}'; + '{"mcpServers":{"rea":{"command":"rea","args":["mcp"],"env":{"__proto__":123}}}}'; await writeFile(configPath, source); const details = clientRegistrationEnvironmentDetails( parseClientConfiguration(source, "json"), @@ -666,7 +668,7 @@ it("rejects a non-string caller environment key before editing configuration", a {}, ["rea", "mcp"], ), - ).toMatchObject({ status: "failed" }); + ).toEqual({ status: "failed", reason: "readback" }); expect(await readFile(configPath, "utf8")).toBe(source); await expect(access(`${configPath}.rea.backup`)).rejects.toMatchObject({ code: "ENOENT", @@ -712,7 +714,7 @@ it("retains legacy OpenCode environment when migrating into a native server tabl configPath, JSON.stringify({ mcp: { - rea: clientRegistrationEntry("opencode", ["old", "mcp"], custom), + rea: clientRegistrationEntry("opencode", staleOwnedCommand, custom), servers: { other: clientRegistrationEntry("opencode_v2", ["other"], {}), }, diff --git a/tests/boundary/filesystem/clientConfigurationTransactions.test.ts b/tests/boundary/filesystem/clientConfigurationTransactions.test.ts index 4e9679596b..78f4411a73 100644 --- a/tests/boundary/filesystem/clientConfigurationTransactions.test.ts +++ b/tests/boundary/filesystem/clientConfigurationTransactions.test.ts @@ -342,7 +342,7 @@ describe("TOML client configuration comparison", () => { const value = "-inf"; const root = await createTestTempDirectory("rea-client-toml-"); const configPath = join(root, "config.toml"); - const original = `unrelated = ${value}\n[mcp_servers.rea]\ncommand = "old"\nstartup_timeout_sec = ${value}\n`; + const original = `unrelated = ${value}\n[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@6.1.0", "mcp"]\nstartup_timeout_sec = ${value}\n`; await writeFile(configPath, original); const client = { name: "codex", format: "toml", configPath } as const; expect( @@ -397,17 +397,17 @@ describe("Codex configuration text preservation", () => { }, { name: "quoted tables and nested environment", - original: `${unrelatedToml}[mcp_servers."rea"]\ncommand = "old"\n[mcp_servers.rea.env]\nOLD = "value"\n${unrelatedServer}`, + original: `${unrelatedToml}[mcp_servers."rea"]\ncommand = "npx"\nargs = ["-y", "rea-agents@6.1.0", "mcp"]\n[mcp_servers.rea.env]\nOLD = "value"\n${unrelatedServer}`, preserved: [unrelatedToml, unrelatedServer], }, { name: "dotted registration keys", - original: `${unrelatedToml}mcp_servers.rea.command = "old"\n${unrelatedServer}`, + original: `${unrelatedToml}mcp_servers.rea.command = "npx"\nmcp_servers.rea.args = ["-y", "rea-agents@6.1.0", "mcp"]\n${unrelatedServer}`, preserved: [unrelatedToml, unrelatedServer], }, { name: "inline server table", - original: `${unrelatedToml}mcp_servers = { rea = { command = "old" }, other = { command = 'other' } } # Shared table.\n`, + original: `${unrelatedToml}mcp_servers = { rea = { command = "npx", args = ["-y", "rea-agents@6.1.0", "mcp"] }, other = { command = 'other' } } # Shared table.\n`, preserved: [ unrelatedToml, "other = { command = 'other' }", diff --git a/tests/boundary/filesystem/clientLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts index 7028284a40..7bb9cc22d5 100644 --- a/tests/boundary/filesystem/clientLifecycle.test.ts +++ b/tests/boundary/filesystem/clientLifecycle.test.ts @@ -175,7 +175,7 @@ describe("Codex MCP tool deadlines", () => { await mkdir(dirname(configPath), { recursive: true }); await writeFile( configPath, - '[mcp_servers.rea]\ncommand = "old-rea"\nargs = ["mcp"]\nstartup_timeout_sec = 5\ntool_timeout_sec = 10\n', + '[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@6.1.0", "mcp"]\nstartup_timeout_sec = 5\ntool_timeout_sec = 10\n', ); expect( await configureClientConfiguration( @@ -250,8 +250,8 @@ describe("Grok Build server table edits", () => { "", "# about rea", "[mcp_servers.rea]", - 'command = "old"', - 'args = ["mcp"]', + 'command = "npx"', + 'args = ["-y", "rea-agents@6.1.0", "mcp"]', "", "# rea environment", "[mcp_servers.rea.env]", @@ -302,7 +302,7 @@ describe("Grok Build server table edits", () => { expect(configured).toContain("[mcp_servers.rea]"); expect(configured).toContain("startup_timeout_sec = 30"); expect(configured).toContain('OLD = "1"'); - expect(configured).not.toContain('command = "old"'); + expect(configured).not.toContain("rea-agents@6.1.0"); expect(parseToml(configured)).toMatchObject({ mcp_servers: { blender: { command: "blender", args: ["--mcp"] }, @@ -469,8 +469,8 @@ describe("Grok Build text that resembles a server table", () => { "'''", "", "[mcp_servers.rea]", - 'command = "old"', - 'args = ["mcp"]', + 'command = "npx"', + 'args = ["-y", "rea-agents@6.1.0", "mcp"]', "", "# Production server: preserve this configuration", '[mcp_servers."rea.env"]', @@ -501,7 +501,7 @@ describe("Grok Build text that resembles a server table", () => { expect(configured).toContain('[mcp_servers."rea.env"]'); expect(configured).toContain('[mcp_servers."other[prod]"]'); expect(configured).toContain('[[marketplace."sources[prod]"]]'); - expect(configured).not.toContain('command = "old"'); + expect(configured).not.toContain("rea-agents@6.1.0"); expect(parseToml(configured)).toMatchObject({ banner: "welcome\n[mcp_servers.rea]\nkeep this text\n[privacy]\n", mcp_servers: { @@ -545,8 +545,8 @@ describe("Grok Build text that resembles a server table", () => { configPath, [ "[ mcp_servers . rea ]", - 'command = "old"', - 'args = ["mcp"]', + 'command = "npx"', + 'args = ["-y", "rea-agents@6.1.0", "mcp"]', "", '[mcp_servers."\\u0072ea".env]', 'OLD = "1"', @@ -708,6 +708,30 @@ describe("Grok Build inline and dotted assignments", () => { "", ].join("\n"); await writeFile(configPath, inline); + expect( + await configureClientConfiguration( + { name: "grok_build", configPath, format: "grok" }, + undefined, + ["rea", "mcp"], + ), + ).toMatchObject({ status: "failed", reason: "registration_ownership" }); + expect(await readFile(configPath, "utf8")).toBe(inline); + await expect(readFile(`${configPath}.rea.backup`)).rejects.toMatchObject({ + code: "ENOENT", + }); + await writeFile( + configPath, + [ + "[mcp_servers]", + 'rea = { command = "npx", args = ["-y", "rea-agents@6.1.0", "mcp"] }', + 'other = { command = "other" }', + "", + "# next table", + "[privacy]", + "share = false", + "", + ].join("\n"), + ); expect( await configureClientConfiguration( { name: "grok_build", configPath, format: "grok" }, @@ -716,7 +740,7 @@ describe("Grok Build inline and dotted assignments", () => { ), ).toMatchObject({ status: "configured" }); const configuredInline = await readFile(configPath, "utf8"); - expect(configuredInline).not.toContain("keep? no, this is the old value"); + expect(configuredInline).not.toContain("rea-agents@6.1.0"); expect(configuredInline).toContain('other = { command = "other" }'); expect(configuredInline).toContain("# next table"); expect(configuredInline).toContain("[privacy]"); @@ -769,8 +793,8 @@ describe("Grok Build inline and dotted assignments", () => { configPath, [ "[mcp_servers]", - 'rea.command = "old"', - 'rea.args = ["old"]', + 'rea.command = "npx"', + 'rea.args = ["-y", "rea-agents@6.1.0", "mcp"]', 'other.command = "other"', "", ].join("\n"), @@ -784,7 +808,7 @@ describe("Grok Build inline and dotted assignments", () => { ).toMatchObject({ status: "configured" }); const configuredDotted = await readFile(configPath, "utf8"); expect(configuredDotted).toContain('other.command = "other"'); - expect(configuredDotted).not.toContain('rea.command = "old"'); + expect(configuredDotted).not.toContain("rea-agents@6.1.0"); expect(parseToml(configuredDotted)).toMatchObject({ mcp_servers: { other: { command: "other" }, @@ -801,7 +825,7 @@ describe("Grok Build closed inline tables", () => { await mkdir(dirname(configPath), { recursive: true }); const original = [ "# preamble", - 'mcp_servers = { other = { command = "other" }, "\\x72ea" = { command = "old", args = ["old"] } }', + 'mcp_servers = { other = { command = "other" }, "\\x72ea" = { command = "npx", args = ["-y", "rea-agents@6.1.0", "mcp"] } }', "", "# next table", "[privacy]", @@ -950,7 +974,7 @@ describe("Grok Build scalar and nested values", () => { await mkdir(dirname(configPath), { recursive: true }); await writeFile( configPath, - 'mcp_servers = { rea.command = "old-rea", rea.args = ["mcp"], rea.startup_timeout_sec = 10 }\n', + 'mcp_servers = { rea.command = "npx", rea.args = ["-y", "rea-agents@6.1.0", "mcp"], rea.startup_timeout_sec = 10 }\n', ); expect( await configureClientConfiguration( diff --git a/tests/boundary/filesystem/clientRegistrationRefresh.test.ts b/tests/boundary/filesystem/clientRegistrationRefresh.test.ts index ec1fe47c1e..ddee962804 100644 --- a/tests/boundary/filesystem/clientRegistrationRefresh.test.ts +++ b/tests/boundary/filesystem/clientRegistrationRefresh.test.ts @@ -10,7 +10,7 @@ it.each([90, 120])( async (timeout) => { const directory = await createTestTempDirectory("rea-grok-refresh-"); const configPath = join(directory, "config.toml"); - const source = `[mcp_servers.rea]\ncommand = "old-rea"\nargs = ["mcp"]\nstartup_timeout_sec = ${timeout}\ncustom_policy = "caller"\n\n[mcp_servers.rea.env]\nPATH = "/caller/bin:/usr/bin"\nCUSTOM = "preserved"\n`; + const source = `[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@6.1.0", "mcp"]\nstartup_timeout_sec = ${timeout}\ncustom_policy = "caller"\n\n[mcp_servers.rea.env]\nPATH = "/caller/bin:/usr/bin"\nCUSTOM = "preserved"\n`; await writeFile(configPath, source); const client = { name: "grok_build", configPath, format: "grok" } as const; expect( diff --git a/tests/boundary/filesystem/clientRegistrationStatus.test.ts b/tests/boundary/filesystem/clientRegistrationStatus.test.ts index a46a1d3981..117e8ec33f 100644 --- a/tests/boundary/filesystem/clientRegistrationStatus.test.ts +++ b/tests/boundary/filesystem/clientRegistrationStatus.test.ts @@ -16,6 +16,7 @@ import { existingMaintenanceScope } from "../../../src/application/UpdateMainten import { ConfigurationError } from "../../../src/domain/configurationErrors.js"; import { err } from "../../../src/domain/result.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; +import { CURRENT_ENTRYPOINT_PATH } from "../../../src/process/CurrentEntrypointPath.js"; beforeEach(clearClientLocationEnvironment); afterEach(() => { @@ -87,19 +88,19 @@ describe("client registration status", () => { client: "cursor", state: "stale", command: [foreignEntry, "mcp"], - remediation: expect.any(String), + remediation: expect.stringMatching(/\S/u), }), expect.objectContaining({ client: "gemini_cli", state: "invalid", command: [], - remediation: expect.any(String), + remediation: expect.stringMatching(/\S/u), }), expect.objectContaining({ client: "claude_code", state: "missing", command: [], - remediation: expect.any(String), + remediation: expect.stringMatching(/\S/u), }), ]), ); @@ -236,12 +237,11 @@ describe("client registration launcher identity", () => { const home = await createTestTempDirectory("rea-registration-unknown-"); await mkdir(join(home, ".cursor")); const command = ["/opt/rea/bin/rea", "mcp"]; - await writeFile( - join(home, ".cursor/mcp.json"), - JSON.stringify({ - mcpServers: { rea: { command: command[0], args: [command[1]] } }, - }), - ); + const config = join(home, ".cursor/mcp.json"); + const original = JSON.stringify({ + mcpServers: { rea: { command: command[0], args: [command[1]] } }, + }); + await writeFile(config, original); const [status] = await readClientRegistrationStatuses(home, ""); @@ -249,7 +249,8 @@ describe("client registration launcher identity", () => { client: "cursor", command, state: "unknown", - remediation: expect.stringContaining("registration was retained"), + config_path: config, + remediation: expect.stringMatching(/\S/u), }); expect(status).not.toHaveProperty("commandIdentity"); expect(await readClientRegistrationInspections(home, "")).toEqual([ @@ -259,6 +260,7 @@ describe("client registration launcher identity", () => { commandIdentity: "unknown", }), ]); + expect(await readFile(config, "utf8")).toBe(original); }); it("does not use the REA process cwd to resolve a client's relative launcher path", async () => { @@ -498,7 +500,8 @@ describe("Ghidra multi-function deadline registration advice", () => { async ({ provider, timeout, hasBatchScope }) => { const home = await createTestTempDirectory("rea-codex-decompile-scope-"); await mkdir(join(home, ".codex")); - const entry = resolve("scripts/rea.mjs"); + const entry = CURRENT_ENTRYPOINT_PATH; + if (entry === undefined) throw new Error("missing current entrypoint"); const configPath = join(home, ".codex/config.toml"); // Two 600-second Ghidra decompiles plus startup/headroom would need an // estimated 1650 seconds. A caller's shorter or longer choice stays theirs. @@ -758,7 +761,7 @@ it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( config_path: path, state: "invalid", command: [], - remediation: expect.any(String), + remediation: expect.stringMatching(/\S/u), }); expect( (await existingMaintenanceScope(home, "/current/rea", process.env)) diff --git a/tests/boundary/filesystem/ghidraJavaProbe.test.ts b/tests/boundary/filesystem/ghidraJavaProbe.test.ts index 47b3004355..1411035708 100644 --- a/tests/boundary/filesystem/ghidraJavaProbe.test.ts +++ b/tests/boundary/filesystem/ghidraJavaProbe.test.ts @@ -145,9 +145,14 @@ it.each([ { label: "omitted", property: "", shown: "bitness unknown" }, { label: "empty", - property: "sun.arch.data.model =\n", + property: "sun.arch.data.model = \n", shown: 'bitness unknown (sun.arch.data.model = "")', }, + { + label: "nonstandard framing", + property: "sun.arch.data.model =\n", + shown: "bitness unknown", + }, { label: "malformed", property: "sun.arch.data.model = 64junk\n", diff --git a/tests/boundary/filesystem/kimiClientConfiguration.test.ts b/tests/boundary/filesystem/kimiClientConfiguration.test.ts index 462b336877..22493bcbfc 100644 --- a/tests/boundary/filesystem/kimiClientConfiguration.test.ts +++ b/tests/boundary/filesystem/kimiClientConfiguration.test.ts @@ -13,12 +13,10 @@ import { import { systemUninstallHost } from "../../../src/application/Uninstall.js"; import { MCP_STARTUP_POLICY } from "../../../src/mcpStartupPolicy.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { - clearClientLocationEnvironment, - NPX_REGISTRATION_COMMAND, -} from "../../fixtures/clientEnvironment.js"; +import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js"; +import { npxRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js"; -const command = NPX_REGISTRATION_COMMAND; +const command = npxRegistrationCommand(); beforeEach(clearClientLocationEnvironment); afterEach(() => vi.unstubAllEnvs()); @@ -94,7 +92,7 @@ describe("Kimi Code registration lifecycle", () => { client.configPath, JSON.stringify({ mcpServers: { - rea: { command: "old-rea", args: ["mcp"], enabledTools }, + rea: { command: "rea", args: ["mcp"], enabledTools }, }, }), ); @@ -117,7 +115,7 @@ describe("Kimi Code registration lifecycle", () => { const original = JSON.stringify({ mcpServers: { rea: { - ...expectedRegistration(["old-rea", "mcp"]), + ...expectedRegistration(["rea", "mcp"]), startupTimeoutMs: 90_000, toolTimeoutMs: 300_000, }, @@ -243,7 +241,7 @@ describe("Kimi Code input-schema profile selection", () => { JSON.stringify({ mcpServers: { rea: { - command: "old-rea", + command: "rea", args: ["mcp"], env: { REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, }, @@ -297,7 +295,7 @@ describe("Kimi Code input-schema profile selection", () => { JSON.stringify({ mcpServers: { rea: { - command: "old-rea", + command: "rea", args: ["mcp"], env: { REA_MCP_INPUT_SCHEMA_PROFILE: storedProfile }, }, diff --git a/tests/boundary/filesystem/safeOutputTreeCloseFailures.test.ts b/tests/boundary/filesystem/safeOutputTreeCloseFailures.test.ts index d835b1e460..562db6232a 100644 --- a/tests/boundary/filesystem/safeOutputTreeCloseFailures.test.ts +++ b/tests/boundary/filesystem/safeOutputTreeCloseFailures.test.ts @@ -184,11 +184,12 @@ it.skipIf(process.platform === "win32").each([false, true])( const output = join(root, "published"); const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("payload.txt", new TextReader("committed bytes")); - await writeFile(source, await writer.close()); + const bytes = await writer.close(); + await writeFile(source, bytes); const client = new ArtifactProvider(process.env).createClient({ path: source, sourcePath: source, - sha256: "0".repeat(64), + sha256: createHash("sha256").update(bytes).digest("hex"), kind: "archive", format: "zip", }); diff --git a/tests/boundary/filesystem/zcodeClientConfiguration.test.ts b/tests/boundary/filesystem/zcodeClientConfiguration.test.ts index 39e2c82c91..cda9a8ae3b 100644 --- a/tests/boundary/filesystem/zcodeClientConfiguration.test.ts +++ b/tests/boundary/filesystem/zcodeClientConfiguration.test.ts @@ -177,7 +177,7 @@ describe("ZCode .agents fallback preservation", () => { mcpServers: { other: { command: "other-server", env: { TOKEN: "x" } }, rea: { - command: "old-rea", + command: "rea", args: ["mcp"], env: { REA_MCP_INPUT_SCHEMA_PROFILE: "compact", @@ -365,7 +365,7 @@ describe("ZCode fallback admission", () => { const { home, client } = await zcodeClient(); await mkdir(join(home, ".agents"), { recursive: true }); const original = JSON.stringify({ - mcpServers: { rea: { command: "old-rea", env } }, + mcpServers: { rea: { command: "rea", args: ["mcp"], env } }, }); await writeFile(fallbackPath(home), original); expect( diff --git a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts index f2ab203a66..80d95685db 100644 --- a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts +++ b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts @@ -6,6 +6,7 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; +import { machoImage } from "../../../src/artifacts/apple/MachoImage.fixture.js"; import { createArtifactBinarySession } from "../../../src/composition/binary.js"; import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; @@ -22,9 +23,9 @@ const writeBundle = async (withFifo: boolean): Promise => { await mkdir(bundle); await writeFile( join(bundle, "Info.plist"), - "CFBundleExecutablemain.js", + "CFBundleExecutablemain", ); - await writeFile(join(bundle, "main.js"), "export const value = 1;\n"); + await writeFile(join(bundle, "main"), machoImage({})); if (withFifo) execFileSync("mkfifo", [join(bundle, "omitted.fifo")]); return bundle; }; diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts index 15dd86f751..7ab719aa3b 100644 --- a/tests/boundary/mcp/electronMcp.test.ts +++ b/tests/boundary/mcp/electronMcp.test.ts @@ -660,7 +660,11 @@ it("exposes the target-free static JavaScript application workflow", async () => summary: z.object({ browser_windows: z.number(), context_bridge_apis: z.number(), - ipc: z.object({ paired_renderer_transmissions: z.number() }), + ipc: z.object({ + paired_renderer_transmissions: z.number(), + ambiguous_renderer_transmissions: z.number(), + unpaired_literal_renderer_transmissions: z.number(), + }), }), graph: z.object({ graph_id: z.string(), @@ -682,7 +686,11 @@ it("exposes the target-free static JavaScript application workflow", async () => summary: { browser_windows: 3, context_bridge_apis: 2, - ipc: { paired_renderer_transmissions: 4 }, + ipc: { + paired_renderer_transmissions: 0, + ambiguous_renderer_transmissions: 7, + unpaired_literal_renderer_transmissions: 0, + }, }, }); expect(projected.normalized_result.graph.nodes.length).toBeGreaterThan(0); diff --git a/tests/boundary/mcp/enhancedTools.limitations.test.ts b/tests/boundary/mcp/enhancedTools.limitations.test.ts index b361c80964..4ecc330d68 100644 --- a/tests/boundary/mcp/enhancedTools.limitations.test.ts +++ b/tests/boundary/mcp/enhancedTools.limitations.test.ts @@ -4,6 +4,7 @@ import { err, ok } from "../../../src/domain/result.js"; import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { connect, closeEnhancedToolResources } from "./enhancedToolsHarness.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; afterEach(closeEnhancedToolResources); @@ -50,11 +51,12 @@ it("retains provider coverage caveats in a composed MCP overview without repeate expect(calls.length).toBe(new Set(calls).size); }); -it("counts all own inventory dictionary entries in the composed overview", async () => { - const procedures = JSON.parse( - '{"__proto__":"observed procedure","0x1000":"ordinary procedure"}', - ); - const strings = JSON.parse('{"__proto__":"observed string"}'); +it("counts canonical inventory rows in the composed overview without altering labels", async () => { + const procedures = [ + { address: "0x1000", value: "__proto__" }, + { address: "0x2000", value: "ordinary procedure" }, + ]; + const strings = [{ address: "0x3000", value: "__proto__" }]; const before = [JSON.stringify(procedures), JSON.stringify(strings)]; const client = await connect({ execute: async (operation) => { @@ -70,7 +72,7 @@ it("counts all own inventory dictionary entries in the composed overview", async operation === "current_document" ? "fixture" : operation === "list_procedures" - ? { procedures } + ? procedures : operation === "list_strings" ? strings : []; @@ -91,8 +93,46 @@ it("counts all own inventory dictionary entries in the composed overview", async const evidence = parseEvidence(response.structuredContent); expect(evidence.normalized_result).toMatchObject({ document: "fixture", - procedure_count: Object.keys(procedures).length, - string_count: Object.keys(strings).length, + procedure_count: procedures.length, + string_count: strings.length, }); expect([JSON.stringify(procedures), JSON.stringify(strings)]).toEqual(before); }); + +it.each(["list_procedures", "list_strings"])( + "refuses legacy %s dictionaries instead of declaring a complete overview", + async (invalidOperation) => { + const client = await connect({ + execute: async (operation) => { + if (operation === "read_binary_inventory_summary") + return err( + new AnalysisCapabilityUnavailableError( + "fixture", + operation, + "No native overview", + ), + ); + return ok( + createAnalysisExecution( + operation === "current_document" + ? "fixture" + : operation === invalidOperation + ? { "0x1000": "legacy" } + : [], + { id: "fixture", name: "Fixture", version: "1" }, + ), + ); + }, + }); + const response = await client.callTool({ + name: "binary_overview", + arguments: {}, + }); + expect(parseMcpToolError(response).error).toMatchObject({ + code: "unreadable_output", + details: { + operation: invalidOperation, + }, + }); + }, +); diff --git a/tests/boundary/mcp/fridaMcp.test.ts b/tests/boundary/mcp/fridaMcp.test.ts index 6eb34fdc3d..100a30f62f 100644 --- a/tests/boundary/mcp/fridaMcp.test.ts +++ b/tests/boundary/mcp/fridaMcp.test.ts @@ -13,6 +13,7 @@ import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/a import { fridaSessionStatusSchema } from "../../../src/domain/fridaSessionStatus.js"; import { evidenceBundleSummarySchema } from "../../../src/domain/evidenceRecordSummary.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; +import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js"; const createFridaTestProvider = ( overrides: Partial = {}, @@ -241,9 +242,7 @@ it("delivers Frida status summaries with complete retained Evidence for both tru arguments: {}, }); expect(bundle.isError).not.toBe(true); - expect(JSON.stringify(bundle.structuredContent)).toContain( - "caller-selected-nonsecret", - ); + expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(parent); expect(session.evidenceById(parent.evidence_id)).toEqual(parent); // Inline/file projection validates the owning result and exact source selection. const malformed = createEvidence(undefined, parent.provider ?? undefined, { @@ -651,8 +650,17 @@ it("retains cancellation Evidence after the MCP client stops waiting", async () name: "get_evidence_bundle", arguments: {}, }); - expect(JSON.stringify(bundle.structuredContent)).toContain("cancel-observed"); - expect(JSON.stringify(bundle.structuredContent)).toContain( - "instrument_with_frida", + expect(bundle.isError).not.toBe(true); + expect(parseMcpEvidenceBundle(bundle).records).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + operation: "instrument_with_frida", + normalized_result: expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ payload: "cancel-observed" }), + ]), + }), + }), + ]), ); }); diff --git a/tests/boundary/mcp/packetCaptureAvailability.test.ts b/tests/boundary/mcp/packetCaptureAvailability.test.ts index c8d9ea7a45..490dd72ef6 100644 --- a/tests/boundary/mcp/packetCaptureAvailability.test.ts +++ b/tests/boundary/mcp/packetCaptureAvailability.test.ts @@ -1,7 +1,6 @@ import { chmod, mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; import { z } from "zod"; @@ -9,9 +8,9 @@ import { expandToolAvailability, sessionProvider, } from "../../../src/contracts/toolOutputSchemaPrimitives.js"; -import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; // The server enables default packet tools only on Linux, where they can run. const linux = process.platform === "linux"; @@ -23,33 +22,24 @@ const packetCaptureAvailability = async ( const session = createTestBinarySession(() => { throw new Error("binary analysis must not start during status discovery"); }); - const server = createServer( - { kind: "session", session }, - { environment: { ...environment, REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" } }, - ); - const client = new Client({ - name: "packet-capture-availability", - version: "1", + const { client } = await createApplicationMcpHarness({ + session, + serverOptions: { + environment: { ...environment, REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" }, + }, + clientIdentity: { name: "packet-capture-availability", version: "1" }, }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - await server.connect(serverTransport); - await client.connect(clientTransport); - try { - const response = await client.callTool({ - name: "binary_session", - arguments: {}, - }); - if (response.isError === true) throw new Error("binary_session failed"); - const parsed = z - .object({ result: sessionProvider }) - .parse(response.structuredContent).result; - return expandToolAvailability(parsed.tool_availability).find( - (tool) => tool.name === name, - ); - } finally { - await Promise.allSettled([client.close(), server.close(), session.close()]); - } + const response = await client.callTool({ + name: "binary_session", + arguments: {}, + }); + if (response.isError === true) throw new Error("binary_session failed"); + const parsed = z + .object({ result: sessionProvider }) + .parse(response.structuredContent).result; + return expandToolAvailability(parsed.tool_availability).find( + (tool) => tool.name === name, + ); }; const executableTshark = async (prefix: string): Promise => { diff --git a/tests/boundary/mcp/packetCaptureMcp.test.ts b/tests/boundary/mcp/packetCaptureMcp.test.ts index a5acf19251..9a80bf254c 100644 --- a/tests/boundary/mcp/packetCaptureMcp.test.ts +++ b/tests/boundary/mcp/packetCaptureMcp.test.ts @@ -7,6 +7,7 @@ import { expect, it, onTestFinished } from "vitest"; import { Ajv2020 } from "ajv/dist/2020.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js"; it.each(["inspect_packet_capture", "inspect_packet_fields"])( "advertises valid target-free schemas and missing-tool diagnostics: %s", @@ -122,9 +123,7 @@ it.each(["cancelled", "cleanup"])( arguments: {}, }); expect(bundle.isError).not.toBe(true); - expect(JSON.stringify(bundle.structuredContent)).toContain( - evidence.evidence_id, - ); + expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(evidence); expect(harness.session.evidenceById(evidence.evidence_id)).toEqual( evidence, ); diff --git a/tests/boundary/mcp/serverLifecycleMcp.test.ts b/tests/boundary/mcp/serverLifecycleMcp.test.ts index 2970890cc9..600c1bd328 100644 --- a/tests/boundary/mcp/serverLifecycleMcp.test.ts +++ b/tests/boundary/mcp/serverLifecycleMcp.test.ts @@ -1,4 +1,5 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import type { CallToolResult } from "@modelcontextprotocol/server"; import { afterEach, expect, it } from "vitest"; @@ -208,9 +209,7 @@ it.runIf(SQLITE_NATIVE_LIMITS_AVAILABLE)( arguments: {}, }); expect(bundle.isError).not.toBe(true); - expect(JSON.stringify(bundle.structuredContent)).toContain( - error.details.partial_observation.evidence_id, - ); + expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(retained); } finally { removalAllowed = true; await client.close(); diff --git a/tests/boundary/mcp/signalRecordingMcp.test.ts b/tests/boundary/mcp/signalRecordingMcp.test.ts index edd74a2bf9..0ef153f355 100644 --- a/tests/boundary/mcp/signalRecordingMcp.test.ts +++ b/tests/boundary/mcp/signalRecordingMcp.test.ts @@ -7,6 +7,7 @@ import { expect, it, onTestFinished } from "vitest"; import { Ajv2020 } from "ajv/dist/2020.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js"; it.each(["decode_signal_recording", "inspect_signal_decoders"])( "advertises valid target-free schemas and missing-tool diagnostics: %s", @@ -117,9 +118,7 @@ it.each(["cancelled", "cleanup"])( arguments: {}, }); expect(bundle.isError).not.toBe(true); - expect(JSON.stringify(bundle.structuredContent)).toContain( - evidence.evidence_id, - ); + expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(evidence); expect(harness.session.evidenceById(evidence.evidence_id)).toEqual( evidence, ); diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index 0de931dbd2..eb9d5fa467 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -619,34 +619,35 @@ it("rejects inconsistent decoded-WAT digests in portable Evidence without a prov await harness.client.ping(); }); -it("rejects absent or inconsistent WASM source identity before deriving portable views", async () => { +it("checks WASM semantic identity while preserving rebound display paths", async () => { const value = wasmArtifactFixture(); const harness = await createApplicationMcpHarness({ serverOptions: { environment: {} }, }); onTestFinished(harness.close); + const views = [ + { kind: "summary" }, + { kind: "wasm", facet: "exports", offset: 0, limit: 1 }, + ] as const; for (const target of [ undefined, - { - path: `${value.artifact.path}.other`, - sha256: value.artifact.sha256, - format: "file" as const, - }, { path: value.artifact.path, sha256: "f".repeat(64), format: "file" as const, }, + { + path: value.artifact.path, + sha256: value.artifact.sha256, + format: "elf" as const, + }, ]) { const source = createEvidence( target, { id: "historical-wabt", name: "Historical WABT", version: "1" }, { operation: "inspect_wasm_artifact", parameters: {}, result: value }, ); - for (const view of [ - { kind: "summary" }, - { kind: "wasm", facet: "exports", offset: 0, limit: 1 }, - ]) { + for (const view of views) { const reply = await harness.client.callTool({ name: "inspect_analysis_view", arguments: { source: { kind: "inline", evidence: source }, view }, @@ -661,9 +662,32 @@ it("rejects absent or inconsistent WASM source identity before deriving portable }); } } + const rebound = createEvidence( + { + path: `${value.artifact.path}.other`, + sha256: value.artifact.sha256, + format: "file", + }, + { id: "historical-wabt", name: "Historical WABT", version: "1" }, + { operation: "inspect_wasm_artifact", parameters: {}, result: value }, + ); + for (const view of views) { + const reply = await harness.client.callTool({ + name: "inspect_analysis_view", + arguments: { source: { kind: "inline", evidence: rebound }, view }, + }); + expect(reply.isError, JSON.stringify(reply.structuredContent)).not.toBe( + true, + ); + const projected = parseEvidence(reply.structuredContent); + expect(projected.subject).toEqual(rebound.subject); + expect(projected.evidence_links).toEqual([rebound.evidence_id]); + expect(projected.normalized_result).toMatchObject({ + artifact: { path: value.artifact.path, sha256: value.artifact.sha256 }, + }); + } await harness.client.ping(); }); - it("rejects malformed paths, refuses WASM summary delivery before provider work when retention is unavailable, and keeps fixed-server complete delivery", async () => { const value = wasmArtifactFixture(); const inspect = vi.fn(() => diff --git a/tests/boundary/model/modelArtifactViewIdentity.test.ts b/tests/boundary/model/modelArtifactViewIdentity.test.ts index 34e8988a1f..0c4d4070a5 100644 --- a/tests/boundary/model/modelArtifactViewIdentity.test.ts +++ b/tests/boundary/model/modelArtifactViewIdentity.test.ts @@ -9,7 +9,7 @@ import { freezeJsonSnapshot } from "../../../src/domain/immutableJson.js"; import { ggufFixture } from "../../fixtures/model/model.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -it("checks each model parent subject independently of cached declarations", async () => { +it("checks each model parent digest independently of cached declarations and display paths", async () => { const root = await createTestTempDirectory("rea-model-view-identity-"); const path = join(root, "selected.gguf"); await writeFile(path, ggufFixture().bytes); @@ -38,9 +38,11 @@ it("checks each model parent subject independently of cached declarations", asyn ); const digest = original.subject.digest.sha256; const valid = parent("valid", path, digest); + const aliasPath = join(root, "other.gguf"); + const rebound = parent("rebound-path", aliasPath, digest); const conflicting = [ - parent("wrong-path", join(root, "other.gguf"), digest), parent("wrong-digest", path, "00".repeat(32)), + parent("wrong-digest-rebound", aliasPath, "00".repeat(32)), ]; const project = (record: typeof valid) => inspectAnalysisView( @@ -53,7 +55,7 @@ it("checks each model parent subject independently of cached declarations", asyn }, (id) => (id === record.evidence_id ? record : undefined), ); - for (const record of conflicting) + for (const record of [...conflicting, rebound]) expect(record.normalized_result).toBe(report); for (const phase of ["cold", "warm"]) { for (const record of conflicting) { @@ -65,12 +67,15 @@ it("checks each model parent subject independently of cached declarations", asyn }, }); } - const projected = await project(valid); - if (!projected.ok) throw projected.error; - expect(projected.value.subject).toEqual(original.subject); - expect(projected.value.normalized_result).toMatchObject({ - artifact: { path, sha256: digest }, - }); + for (const record of [valid, rebound]) { + const projected = await project(record); + if (!projected.ok) throw projected.error; + expect(projected.value.subject).toEqual(record.subject); + expect(projected.value.evidence_links).toEqual([record.evidence_id]); + expect(projected.value.normalized_result).toMatchObject({ + artifact: { path, sha256: digest }, + }); + } } } finally { await service.close(); diff --git a/tests/boundary/pcap/pcapProviderCrossPlatform.test.ts b/tests/boundary/pcap/pcapProviderCrossPlatform.test.ts index 7bc56e900d..0c0882c30f 100644 --- a/tests/boundary/pcap/pcapProviderCrossPlatform.test.ts +++ b/tests/boundary/pcap/pcapProviderCrossPlatform.test.ts @@ -426,6 +426,17 @@ it.runIf(hasNativeBound)( expect((result.protocol_distribution as unknown[]).length).toBeGreaterThan( 0, ); + expect(result.dns_queries).toEqual([ + { + query: "api.example.com", + query_labels_hex: ["617069", "6578616d706c65", "636f6d"], + response_addresses: ["93.184.216.34"], + response_address_count: 1, + }, + ]); + expect(result.unattributed_dns_addresses).toEqual([]); + expect(result.unattributed_dns_address_count).toBe(0); + expect(result.unattributed_dns_packet_count).toBe(0); expect(result.coverage).toBe("complete"); }, ); diff --git a/tests/boundary/process/androidAnalysisMcp.test.ts b/tests/boundary/process/androidAnalysisMcp.test.ts index 8e2efb5c90..5394176179 100644 --- a/tests/boundary/process/androidAnalysisMcp.test.ts +++ b/tests/boundary/process/androidAnalysisMcp.test.ts @@ -154,6 +154,10 @@ it("publishes and executes all APK contracts with inline Evidence and no active }); const input = { path: apk, + // Inline Evidence opts out of any contract's summary delivery default. + ...(Object.hasOwn(contract.inputSchema.shape, "detail") + ? { detail: "complete" } + : {}), ...(contract.name === "search_android_classes" ? { query: "Target" } : {}), diff --git a/tests/boundary/process/ownedCommand.test.ts b/tests/boundary/process/ownedCommand.test.ts index f71204233b..ed61c9a6a2 100644 --- a/tests/boundary/process/ownedCommand.test.ts +++ b/tests/boundary/process/ownedCommand.test.ts @@ -554,14 +554,27 @@ it.skipIf(process.platform === "win32")( expect(remainsLive).toBe(false); expect(() => process.kill(unrelated.process.pid!, 0)).not.toThrow(); } finally { - if (descendantOwner !== undefined) - expect( - ( - await (descendantOwner.cleanup?.() ?? - cleanupOwnedProcessGroup(descendantOwner.ownership)) - ).cleaned, - ).toBe(true); - await cleanupOwnedProcessGroup(unrelated.ownership); + const descendant = descendantOwner; + const results = await Promise.allSettled([ + ...(descendant === undefined + ? [] + : [ + Promise.resolve().then( + () => + descendant.cleanup?.() ?? + cleanupOwnedProcessGroup(descendant.ownership), + ), + ]), + Promise.resolve().then(() => + cleanupOwnedProcessGroup(unrelated.ownership), + ), + ]); + const failures = results.flatMap((result) => { + if (result.status === "rejected") return [result.reason]; + return result.value.cleaned ? [] : [new Error(result.value.reason)]; + }); + if (failures.length > 0) + throw new AggregateError(failures, "Fixture process cleanup failed"); } }, ); diff --git a/tests/boundary/process/processCaptureCancellationAdapters.test.ts b/tests/boundary/process/processCaptureCancellationAdapters.test.ts index 4fd088d9c8..8cca53e8af 100644 --- a/tests/boundary/process/processCaptureCancellationAdapters.test.ts +++ b/tests/boundary/process/processCaptureCancellationAdapters.test.ts @@ -67,11 +67,26 @@ const waitForExit = async (pid: number): Promise => { const cleanupFixture = async (marker: Marker | undefined): Promise => { if (marker === undefined) return; // Teardown uses the fixture's capture run token, never a bare PID kill. - await cleanupOwnedProcessGroup({ + const result = await cleanupOwnedProcessGroup({ runId: marker.runId, leaderPid: marker.rootPid, processGroupId: marker.rootPid, }); + if (!result.cleaned) + throw new Error(`Fixture cleanup failed: ${result.reason}`); +}; + +const finishCleanup = async (steps: readonly (() => Promise)[]) => { + const failures: unknown[] = []; + for (const step of steps) { + try { + await step(); + } catch (cause: unknown) { + failures.push(cause); + } + } + if (failures.length > 0) + throw new AggregateError(failures, "Fixture cleanup failed"); }; itWithCaptureCapability.each(["SIGINT", "SIGTERM"] as const)( @@ -110,9 +125,14 @@ itWithCaptureCapability.each(["SIGINT", "SIGTERM"] as const)( }); let marker: Marker | undefined; onTestFinished(async () => { - await cleanupFixture(marker); - if (cli.exitCode === null && cli.signalCode === null) cli.kill("SIGKILL"); - await closed; + await finishCleanup([ + () => cleanupFixture(marker), + async () => { + if (cli.exitCode === null && cli.signalCode === null) + cli.kill("SIGKILL"); + await closed; + }, + ]); }); marker = await readMarker(root); expect(await isRunning(marker.rootPid)).toBe(true); @@ -256,10 +276,12 @@ itWithCaptureCapability( }); let marker: Marker | undefined; onTestFinished(async () => { - await cleanupFixture(marker); - await client.close(); - await server.close(); - await session.close(); + await finishCleanup([ + () => cleanupFixture(marker), + () => client.close(), + () => server.close(), + () => session.close(), + ]); }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/process/processCaptureLifecycle.test.ts b/tests/boundary/process/processCaptureLifecycle.test.ts index ad1cf560a2..598e14c9f4 100644 --- a/tests/boundary/process/processCaptureLifecycle.test.ts +++ b/tests/boundary/process/processCaptureLifecycle.test.ts @@ -353,7 +353,14 @@ itWithCaptureCapability( "retains collected observations when completion and cleanup both fail", async () => { let snapshotCalls = 0; - const captureSnapshot: typeof snapshotRoots = async () => { + const snapshotObserverPaths: string[][] = []; + const captureSnapshot: typeof snapshotRoots = async ( + _scenario, + _signal, + _before, + resources, + ) => { + snapshotObserverPaths.push([...(resources?.observerPaths ?? [])]); snapshotCalls += 1; if (snapshotCalls === 1) return { @@ -396,12 +403,16 @@ itWithCaptureCapability( expect(result.error.executionFailure).toBe( "fixture final snapshot failure", ); + const temporaryRoot = snapshotObserverPaths[0]?.[0]; + if (temporaryRoot === undefined) + throw new Error("expected the owned temporary root in snapshot inputs"); + expect(snapshotObserverPaths).toEqual([[temporaryRoot], [temporaryRoot]]); const projected = projectAnalysisError(result.error); expect(projected).toMatchObject({ code: "cleanup_incomplete", details: { cleanup: "incomplete", - resources: ["owned_process_group", "temporary_root"], + resources: ["owned_process_group", temporaryRoot], }, }); expect(projected).not.toHaveProperty("stack"); diff --git a/tests/boundary/process/processCleanupRetryOwnership.test.ts b/tests/boundary/process/processCleanupRetryOwnership.test.ts index 2369c29eed..d87df69072 100644 --- a/tests/boundary/process/processCleanupRetryOwnership.test.ts +++ b/tests/boundary/process/processCleanupRetryOwnership.test.ts @@ -168,7 +168,7 @@ it("keeps mixed root errors and retry aggregates bound to only unresolved roots" expect(second.cleanupIncomplete).toBe(true); expect(bothUnresolved.cleanupIncomplete).toBe(true); expect(retryFailure.cleanupIncomplete).toBe(true); - expect(retryFailure.cleanupResources).toEqual(["temporary_root"]); + expect(retryFailure.cleanupResources).toEqual([roots[1]]); denied.clear(); await scope.retryCleanup(); @@ -288,7 +288,7 @@ it("blocks scope admission on owner-bound retry failure and resumes after releas if (!(retryFailure instanceof ProcessCaptureError)) throw new Error("Expected a typed owner-bound retry failure"); expect(retryFailure.cleanupIncomplete).toBe(true); - expect(retryFailure.cleanupResources).toEqual(["temporary_root"]); + expect(retryFailure.cleanupResources).toEqual([root]); expect(origin.cleanupIncomplete).toBe(true); denyRemoval = false; diff --git a/tests/boundary/process/processCli.test.ts b/tests/boundary/process/processCli.test.ts index b2e068699c..b7765effe6 100644 --- a/tests/boundary/process/processCli.test.ts +++ b/tests/boundary/process/processCli.test.ts @@ -26,6 +26,50 @@ import { const execFileAsync = promisify(execFile); const CLI_INTEGRATION_TIMEOUT_MS = 60_000; +const cleanupTargetSource = ` +const fs = process.getBuiltinModule("node:fs"); +const [marker, release] = process.argv.slice(2); +if (!marker || !release) throw new Error("Missing cleanup target paths"); +fs.writeFileSync(marker, String(process.pid)); +setTimeout(() => process.exit(2), ${CLI_INTEGRATION_TIMEOUT_MS}).unref(); +const waiting = setInterval(() => { + if (!fs.existsSync(release)) return; + clearInterval(waiting); +}, 10); +`; + +const writeUnavailableOwnershipCompiler = async (root: string) => { + // Controlled protocol facts force the refusal independently of native + // platform-binary visibility. No token ever authorizes a signal. + const helper = `#!${process.execPath} +const arguments_ = process.argv.slice(2); +const identities = arguments_[0] === "--identities"; +const pids = (identities ? arguments_.slice(1) : arguments_).map(Number); +console.log(JSON.stringify({ results: pids.map(pid => identities ? { + pid, state: "readable", identity: "controlled-start-" + pid, + lifecycle: "non-zombie", + relation: { state: "observed", parent_pid: 1, process_group_id: pid }, +} : { + pid, state: "unavailable", reason: "platform_binary_environment_withheld", +}) })); +`; + const compiler = join(root, "controlled-swiftc"); + await writeFile( + compiler, + `#!${process.execPath} +const fs = process.getBuiltinModule("node:fs"); +const outputIndex = process.argv.indexOf("-o"); +const output = process.argv[outputIndex + 1]; +if (outputIndex < 0 || !output || output.startsWith("-")) + throw new Error("Missing controlled helper output"); +fs.writeFileSync(output, ${JSON.stringify(helper)}); +fs.chmodSync(output, 0o755); +`, + { mode: 0o755 }, + ); + return compiler; +}; + describe("documented process CLI workflow", () => { cliTest( "feeds the documented JSON capture into the comparison CLI", @@ -79,59 +123,90 @@ describe("documented process CLI workflow", () => { describe("process CLI cleanup errors", () => { cliTest.skipIf(process.platform !== "darwin")( - "reports the retained capture root when platform-binary finalization cannot establish ownership", + "reports the retained capture root with a controlled unavailable ownership reader", async ({ cli }) => { const root = await createTestTempDirectory("rea-process-cli-cleanup-"); + const compiler = await writeUnavailableOwnershipCompiler(root); const scenario = join(root, "scenario.json"); const marker = join(root, "target.pid"); + const release = join(root, "release-target"); + const target = join(root, "cleanup-target.cjs"); + await writeFile(target, cleanupTargetSource); await writeFile( scenario, JSON.stringify({ - executable: "/bin/sh", - arguments: [ - "-c", - 'printf "%s" "$$" > "$1"; exec /bin/sleep 3', - "capture-cleanup-fixture", - marker, - ], + executable: process.execPath, + arguments: [target, marker, release], timeout_ms: 500, idle_timeout_ms: 500, finalization_ms: 100, }), ); - const result = await cli.run({ - arguments: ["capture-process", scenario, "--json"], - environment: { TMPDIR: root }, - }); - const targetPid = Number(await readFile(marker, "utf8")); - expect(Number.isSafeInteger(targetPid) && targetPid > 0).toBe(true); - // The fail-closed command deliberately leaves the platform binary alive. - // Observe its bounded natural exit before the temporary fixture is removed. - await expect - .poll(() => observeProcessStartIdentity(targetPid), { - timeout: 7_000, - interval: 25, - }) - .toBeUndefined(); - expect(result.exitCode, result.stdout || result.stderr).toBe(1); - const retained = (await readdir(root)).filter((name) => - /^rea-process-(?!token-)/u.test(name), - ); - expect(retained).toHaveLength(1); - const retainedRoot = retained[0]; - if (retainedRoot === undefined) throw new Error("Missing retained root"); - expect(result.json).toMatchObject({ - code: "cleanup_incomplete", - message: expect.stringContaining( - "platform_binary_environment_withheld", - ), - details: { - resources: ["owned_process_group", join(root, retainedRoot)], - cleanup_report: { - temporary_root: { state: "unverified" }, + try { + // A retained PTY can keep its CLI process alive. Read the completed + // compiled command result before releasing the child and joining exit. + if (cli.sharedCliRun === undefined) + throw new Error("Expected the compiled CLI command session"); + const result = await cli.sharedCliRun({ + arguments: ["capture-process", scenario, "--json"], + environment: { + TMPDIR: root, + REA_DARWIN_SWIFTC_PATH: compiler, + SDKROOT: root, + REA_DARWIN_MODULE_CACHE_PATH: join(root, "modules"), }, - }, - }); + }); + const targetPid = Number(await readFile(marker, "utf8")); + expect(Number.isSafeInteger(targetPid) && targetPid > 0).toBe(true); + expect(process.kill(targetPid, 0), "the target remains alive").toBe( + true, + ); + expect(result.exitCode, result.stdout || result.stderr).toBe(1); + const retained = (await readdir(root)).filter((name) => + /^rea-process-(?!token-)/u.test(name), + ); + expect(retained).toHaveLength(1); + const retainedRoot = retained[0]; + if (retainedRoot === undefined) + throw new Error("Missing retained root"); + expect(result.json).toMatchObject({ + code: "cleanup_incomplete", + message: expect.stringContaining( + "platform_binary_environment_withheld", + ), + details: { + resources: ["owned_process_group", join(root, retainedRoot)], + cleanup_report: { + temporary_root: { state: "unverified" }, + }, + }, + }); + } finally { + // Release even when the CLI or an assertion fails. A target starting + // after this point also sees the release and exits without a signal. + await writeFile(release, "release"); + const targetPid = await readFile(marker, "utf8").then( + (value) => Number(value), + (cause: unknown) => { + if ( + cause instanceof Error && + "code" in cause && + cause.code === "ENOENT" + ) + return undefined; + throw cause; + }, + ); + if (targetPid !== undefined) { + expect(Number.isSafeInteger(targetPid) && targetPid > 0).toBe(true); + await expect + .poll(() => observeProcessStartIdentity(targetPid), { + timeout: 7_000, + interval: 25, + }) + .toBeUndefined(); + } + } }, CLI_INTEGRATION_TIMEOUT_MS, ); diff --git a/tests/boundary/providers/native/nativeProvider.test.ts b/tests/boundary/providers/native/nativeProvider.test.ts index a058b50b7f..88f2a69f24 100644 --- a/tests/boundary/providers/native/nativeProvider.test.ts +++ b/tests/boundary/providers/native/nativeProvider.test.ts @@ -1449,7 +1449,7 @@ describe("native plist defaults for iOS-style bundles", () => { new FixtureRunner(), "darwin", ).createClient({ - ...machoTarget(executable, app), + ...(await nativeMachoTargetForFile(executable, app)), bundleInfoPlist: join(app, "Info.plist"), }); diff --git a/tests/boundary/setup/setupJebEndpointSelection.test.ts b/tests/boundary/setup/setupJebEndpointSelection.test.ts index 1353d35817..1c7b014b93 100644 --- a/tests/boundary/setup/setupJebEndpointSelection.test.ts +++ b/tests/boundary/setup/setupJebEndpointSelection.test.ts @@ -16,7 +16,7 @@ it("validates selected and existing JEB endpoints without losing caller selectio JSON.stringify({ mcpServers: { rea: { - command: "rea-agents", + command: "rea", args: ["mcp"], env: { REA_JEB_MCP_URL: endpoint }, }, diff --git a/tests/composition/analysis-sessions/providerBinding.test.ts b/tests/composition/analysis-sessions/providerBinding.test.ts index f38174497a..0c18ac9762 100644 --- a/tests/composition/analysis-sessions/providerBinding.test.ts +++ b/tests/composition/analysis-sessions/providerBinding.test.ts @@ -1,7 +1,7 @@ -import { writeFile } from "node:fs/promises"; +import { mkdir, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, onTestFinished } from "vitest"; import { z } from "zod"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -21,6 +21,7 @@ import { composeBinarySession } from "../../../src/application/binary/BinarySess import { SessionProviderRouter } from "../../../src/application/binary/SessionProviderRouter.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js"; +import { thinMach } from "../../../src/domain/binaryTarget.fixture.js"; import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js"; import { err, ok } from "../../../src/domain/result.js"; @@ -127,28 +128,6 @@ describe("target-bound provider routing", () => { expect(beta.clients[0]?.closed).toBe(true); }); - it("preserves bundle-root identity for artifact inventory operations", async () => { - const target = await databaseTarget(); - const analysis = deepProvider("alpha"); - const artifact = auxiliaryProvider("artifact", "inventory_artifact"); - const session = selectableSession([analysis.provider], artifact); - - expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true); - const result = await session.execute("inventory_artifact", {}); - if (!result.ok) throw result.error; - - expect(result).toMatchObject({ - ok: true, - value: { - subject: { - path: target, - sha256: "f".repeat(64), - format: "directory", - }, - }, - }); - }); - it("preserves the active client when a provider switch is cancelled while draining", async () => { const target = await databaseTarget(); const alpha = deepProvider("alpha"); @@ -191,6 +170,45 @@ describe("target-bound provider routing", () => { }); }); +it("preserves bundle-root identity for artifact inventory operations", async () => { + const directory = await createTestTempDirectory("rea-provider-bundle-"); + const target = join(directory, "Fixture.app"); + const executable = join(target, "Fixture"); + const infoPlist = join(target, "Info.plist"); + await mkdir(target); + await writeFile( + infoPlist, + "CFBundleExecutableFixture", + ); + await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c)); + const analysis = deepProvider("alpha"); + const artifact = auxiliaryProvider("artifact", "inventory_artifact"); + const session = selectableSession([analysis.provider], artifact); + onTestFinished(async () => { + await session.close(); + }); + + expect((await session.open(target, { providerId: "alpha" })).ok).toBe(true); + expect(session.activeTarget()).toMatchObject({ + path: await realpath(executable), + sourcePath: await realpath(target), + bundleInfoPlist: await realpath(infoPlist), + }); + const result = await session.execute("inventory_artifact", {}); + if (!result.ok) throw result.error; + + expect(result).toMatchObject({ + ok: true, + value: { + subject: { + path: target, + sha256: "f".repeat(64), + format: "directory", + }, + }, + }); +}); + describe("selected provider capability provenance", () => { it("labels selected-provider capabilities with the resolved identity", async () => { const target = await databaseTarget(); diff --git a/tests/fixtures/mcpEvidenceBundle.ts b/tests/fixtures/mcpEvidenceBundle.ts new file mode 100644 index 0000000000..dd57b78b54 --- /dev/null +++ b/tests/fixtures/mcpEvidenceBundle.ts @@ -0,0 +1,12 @@ +import type { CallToolResult } from "@modelcontextprotocol/server"; + +import { toolContract } from "../../src/contracts/toolContracts.js"; +import { parseEvidenceBundle } from "../../src/domain/evidenceBundle.js"; + +/** Parse the full bundle through the advertised result and its owning contract. */ +export const parseMcpEvidenceBundle = (response: CallToolResult) => + parseEvidenceBundle( + toolContract("get_evidence_bundle").outputSchema.parse( + response.structuredContent, + ).result, + ); diff --git a/tests/fixtures/pcap/tshark-stub.cmd b/tests/fixtures/pcap/tshark-stub.cmd index edf74e44c8..c269e27e3f 100644 --- a/tests/fixtures/pcap/tshark-stub.cmd +++ b/tests/fixtures/pcap/tshark-stub.cmd @@ -15,7 +15,7 @@ if not "%~5"=="json" exit /b 2 for %%A in (%*) do if "%%~A"=="-x" goto rawDns rem Emit the same JSON as the POSIX stub -echo [{"_source":{"layers":{"frame.number":["1"],"frame.protocols":["eth:ethertype:ip:udp:dns"],"ip.src":["192.168.1.100"],"ip.dst":["8.8.8.8"],"dns.qry.name":["api.example.com"],"dns.flags.response":["1"],"dns.a":["93.184.216.34"]}}},{"_source":{"layers":{"frame.number":["2"],"frame.protocols":["eth:ethertype:ip:tcp:tls"],"ip.src":["192.168.1.100"],"ip.dst":["93.184.216.34"],"tls.handshake.extensions_server_name":["api.example.com"]}}},{"_source":{"layers":{"frame.number":["3"],"frame.protocols":["eth:ethertype:ip:tcp:http"],"ip.src":["192.168.1.100"],"ip.dst":["93.184.216.34"],"http.request.full_uri":["http://api.example.com/v1/data"]}}}] +echo [{"_source":{"layers":{"frame.number":["1"],"frame.protocols":["eth:ethertype:ip:udp:dns"],"ip.src":["192.168.1.100"],"ip.dst":["8.8.8.8"],"dns.qry.name":["api.example.com"],"dns.flags.response":["1"],"dns.a":["93.184.216.34"],"dns.qry.type":["1"],"dns.qry.class":["0x0001"],"dns.count.queries":["1"],"dns.count.answers":["1"],"dns.count.auth_rr":["0"],"dns.count.add_rr":["0"],"dns.resp.name":["api.example.com"],"dns.resp.type":["1"],"dns.resp.class":["0x0001"]}}},{"_source":{"layers":{"frame.number":["2"],"frame.protocols":["eth:ethertype:ip:tcp:tls"],"ip.src":["192.168.1.100"],"ip.dst":["93.184.216.34"],"tls.handshake.extensions_server_name":["api.example.com"]}}},{"_source":{"layers":{"frame.number":["3"],"frame.protocols":["eth:ethertype:ip:tcp:http"],"ip.src":["192.168.1.100"],"ip.dst":["93.184.216.34"],"http.request.full_uri":["http://api.example.com/v1/data"]}}}] exit /b 0 :rawDns diff --git a/tests/process-global/browserScenarioCleanup.test.ts b/tests/process-global/browserScenarioCleanup.test.ts index ff7a2af357..87cbbf920d 100644 --- a/tests/process-global/browserScenarioCleanup.test.ts +++ b/tests/process-global/browserScenarioCleanup.test.ts @@ -367,10 +367,30 @@ it("preserves the initial partial observation when the provider cleanup retry al try { const result = await serveScenario(); expect(result.output).toMatchObject({ code: "cleanup_incomplete" }); - expect(JSON.stringify(result.output)).toContain("injected close failure 1"); - expect(JSON.stringify(result.output)).not.toContain( - "injected close failure 2", - ); + expect(result.output).toMatchObject({ + details: { + diagnostics: { + primary_error: { + details: { + diagnostics: { + primary_error: { + details: { cleanup_reason: "injected close failure 1" }, + }, + }, + }, + }, + cleanup_error: { + details: { diagnostics: { reason: "injected close failure 2" } }, + }, + }, + partial_observation: { + capture: { + browser: { cleanup: "incomplete" }, + steps: [{ step_id: "scenario_start" }, { step_id: "wait" }], + }, + }, + }, + }); expect(close).toHaveBeenCalledTimes(1); expect(session.closeBrowserCalls).toBe(2); expect(session.profileRemovalCalls).toBe(0); @@ -483,12 +503,18 @@ it("preserves a thrown capture error when provider shutdown also rejects", async try { const result = await serveScenario(); expect(result.output).toMatchObject({ - code: "provider_timeout", + code: "cleanup_incomplete", category: "timeout", + details: { + execution_failure: "provider_timeout", + diagnostics: { + primary_error: { code: "provider_timeout", category: "timeout" }, + cleanup_error: { + details: { diagnostics: { reason: "secondary shutdown failure" } }, + }, + }, + }, }); - expect(JSON.stringify(result.output)).not.toContain( - "secondary shutdown failure", - ); expect(close).toHaveBeenCalledTimes(1); } finally { await provider.close(); diff --git a/tests/process-global/ledgerMutationFailures.test.ts b/tests/process-global/ledgerMutationFailures.test.ts index 15d01321b1..d7c612221d 100644 --- a/tests/process-global/ledgerMutationFailures.test.ts +++ b/tests/process-global/ledgerMutationFailures.test.ts @@ -287,20 +287,30 @@ it("authenticates all Evidence before committing an imported unknown history", a if (!created.ok) throw created.error; const bundle = source.exportEvidenceBundle(); const expectedIds = bundle.records.map(({ evidence_id }) => evidence_id); - const original = evidenceDomain.immutableEvidence; + const original = evidenceBundleDomain.safeParseImmutableEvidenceBundle; let incomingVisibleDuringAuthentication = false; const authenticate = vi - .spyOn(evidenceDomain, "immutableEvidence") + .spyOn(evidenceBundleDomain, "safeParseImmutableEvidenceBundle") .mockImplementation((input) => { const visible = session .exportEvidenceBundle() .records.some(({ evidence_id }) => expectedIds.includes(evidence_id)); if (visible) incomingVisibleDuringAuthentication = true; - return original(input); + const admitted = original(input); + if ( + session + .exportEvidenceBundle() + .records.some(({ evidence_id }) => + expectedIds.includes(evidence_id), + ) + ) + incomingVisibleDuringAuthentication = true; + return admitted; }); onTestFinished(() => authenticate.mockRestore()); const imported = session.importEvidenceBundle(bundle); + expect(authenticate).toHaveBeenCalledTimes(1); authenticate.mockRestore(); expect(imported.ok).toBe(true); expect(incomingVisibleDuringAuthentication).toBe(false); diff --git a/tests/process-global/updateOutput.test.ts b/tests/process-global/updateOutput.test.ts index 699e9ea5d9..b4e911a8ef 100644 --- a/tests/process-global/updateOutput.test.ts +++ b/tests/process-global/updateOutput.test.ts @@ -188,9 +188,12 @@ it("stops and joins metadata discovery when the update host closes", async () => const probing = host.installation(); await ready; expect(child?.exitCode).toBeNull(); - expect(await host.close?.()).toBeUndefined(); - await probing; + expect(host.close).toBeTypeOf("function"); + if (host.close === undefined) throw new Error("Missing update host close"); + expect(await host.close()).toBeUndefined(); + // Close must join the probe itself, not rely on the later probe deadline. expect(child?.signalCode).not.toBeNull(); + await probing; }); it("bounds a stalled metadata probe and keeps the runner usable", async () => {