diff --git a/docs/testing.md b/docs/testing.md index 1893eec94..803aaf544 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -25,10 +25,10 @@ registries but do not cross production filesystem, process, socket, or browser b Boundary tests cross one production boundary. Only acceptance tests assemble the complete runtime or invoke the compiled product surface. -Focused immutable builders shared by a domain test family live beside their -production owner as `src/domain/*.fixture.ts`. They are typechecked with the -suite and excluded from package builds; broader runtime and provider fixtures -remain under `tests/fixtures/**`. +Focused immutable builders and recording ports shared by one test family live +beside their production owner as `src/**/*.fixture.ts`. They are typechecked +with the suite and excluded from package builds; broader runtime and provider +fixtures remain under `tests/fixtures/**`. `tests/process-global/**` is reserved for cases with a demonstrated dependency on process-global state. Those tests run without file parallelism. Reusable, @@ -70,8 +70,10 @@ Local full-suite Vitest runs are intentionally capped at one worker and one project at a time. Boundary fixtures own real subprocesses, and this local cap keeps aggregate memory predictable; CI retains the existing two-worker budget. The pure domain/contracts and recording-port service projects share one worker -module context because their tests own no mutable runtime resources; adapter, -composition, and boundary projects retain per-file isolation. +module context because their tests own no mutable runtime resources. MCP +boundary files also share the immutable server module graph while creating and +closing independent in-memory sessions. Adapter, composition, acceptance, +process-global, and other boundary projects retain per-file isolation. `npm test`, `npm run docs:check`, and `npm run docs:generate` share repository-local locks and fail fast when the same class of command is already running. The `npm test` build is inside that lock. `check:pr` runs its test task diff --git a/knip.json b/knip.json index ff7c4f8ed..c06e4e361 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,6 @@ { "$schema": "./node_modules/knip/schema.json", - "entry": ["src/main.ts", "src/cli.ts"], + "entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"], "project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"], "ignoreDependencies": ["electron"], "ignoreBinaries": ["ps"], diff --git a/tests/composition/analysis-sessions/analysisProviderRegistry.test.ts b/src/application/AnalysisProviderRegistry.test.ts similarity index 97% rename from tests/composition/analysis-sessions/analysisProviderRegistry.test.ts rename to src/application/AnalysisProviderRegistry.test.ts index 31d2518f7..ee06e2e2e 100644 --- a/tests/composition/analysis-sessions/analysisProviderRegistry.test.ts +++ b/src/application/AnalysisProviderRegistry.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { AnalysisProviderRegistry, type AnalysisProviderBinding, -} from "../../../src/application/AnalysisProviderRegistry.js"; +} from "./AnalysisProviderRegistry.js"; import { createAnalysisExecution, type AnalysisClient, @@ -12,15 +12,15 @@ import { type ProviderAvailability, type ProviderIdentity, type ProviderTargetSupport, -} from "../../../src/application/AnalysisProvider.js"; -import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; -import type { BinaryTarget } from "../../../src/domain/binaryTarget.js"; +} from "./AnalysisProvider.js"; +import { createAnalysisProfile } from "../domain/analysisProfile.js"; +import type { BinaryTarget } from "../domain/binaryTarget.js"; import { ProviderAdapterError, ProviderSelectionError, projectAnalysisError, -} from "../../../src/domain/errors.js"; -import { err, ok } from "../../../src/domain/result.js"; +} from "../domain/errors.js"; +import { err, ok } from "../domain/result.js"; const DATABASE_TARGET: BinaryTarget = { path: "/tmp/fixture.hop", diff --git a/src/application/Doctor.fixture.ts b/src/application/Doctor.fixture.ts new file mode 100644 index 000000000..2c4725dfa --- /dev/null +++ b/src/application/Doctor.fixture.ts @@ -0,0 +1,32 @@ +import { CATALOG_IDENTITY } from "../catalogIdentity.js"; +import { PRODUCT_IDENTITY } from "../identity.js"; +import type { DoctorHost } from "./Doctor.js"; + +/** Healthy recording host for focused doctor and projection tests. */ +export const createDoctorHostFixture = ( + overrides: Partial = {}, +): DoctorHost => ({ + platform: "darwin", + architecture: "x64", + nodeVersion: "24.18.0", + macosVersion: () => Promise.resolve("14.0"), + linuxDistribution: () => Promise.resolve(undefined), + validTarget: (path) => Promise.resolve(path.includes("Hopper")), + executable: (path) => Promise.resolve(path.includes("Hopper")), + supportedLinuxHopper: () => Promise.resolve(true), + linuxDemoRuntimeCheck: () => + Promise.resolve({ + name: "hopper-demo-runtime", + ok: true, + classification: "healthy", + }), + brewHopperPath: () => Promise.resolve(undefined), + manualHopperPaths: () => Promise.resolve([]), + installedSkillIdentity: () => + Promise.resolve({ + version: PRODUCT_IDENTITY.skillVersion, + toolCount: CATALOG_IDENTITY.counts.mcp_tools, + catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, + }), + ...overrides, +}); diff --git a/tests/boundary/cli/doctorRuntimeExecutables.test.ts b/src/application/Doctor.runtimeExecutables.test.ts similarity index 64% rename from tests/boundary/cli/doctorRuntimeExecutables.test.ts rename to src/application/Doctor.runtimeExecutables.test.ts index f36f05596..01ba18dd6 100644 --- a/tests/boundary/cli/doctorRuntimeExecutables.test.ts +++ b/src/application/Doctor.runtimeExecutables.test.ts @@ -1,34 +1,7 @@ import { describe, expect, it } from "vitest"; -import { runDoctor, type DoctorHost } from "../../../src/application/Doctor.js"; -import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js"; -import { PRODUCT_IDENTITY } from "../../../src/identity.js"; - -const host = (overrides: Partial = {}): DoctorHost => ({ - platform: "darwin", - architecture: "x64", - nodeVersion: "24.18.0", - macosVersion: () => Promise.resolve("12.0"), - linuxDistribution: () => Promise.resolve(undefined), - validTarget: (path) => Promise.resolve(path.includes("Hopper")), - executable: (path) => Promise.resolve(path.includes("Hopper")), - supportedLinuxHopper: () => Promise.resolve(true), - linuxDemoRuntimeCheck: () => - Promise.resolve({ - name: "hopper-demo-runtime", - ok: true, - classification: "healthy", - }), - brewHopperPath: () => Promise.resolve(undefined), - manualHopperPaths: () => Promise.resolve([]), - installedSkillIdentity: () => - Promise.resolve({ - version: PRODUCT_IDENTITY.skillVersion, - toolCount: CATALOG_IDENTITY.counts.mcp_tools, - catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, - }), - ...overrides, -}); +import { createDoctorHostFixture as host } from "./Doctor.fixture.js"; +import { runDoctor } from "./Doctor.js"; describe("doctor runtime executable diagnostics", () => { it("reports a broken shadowed Node candidate without hiding the healthy launcher", async () => { diff --git a/tests/boundary/cli/doctor.test.ts b/src/application/Doctor.test.ts similarity index 91% rename from tests/boundary/cli/doctor.test.ts rename to src/application/Doctor.test.ts index 2345bbd12..74f2545d9 100644 --- a/tests/boundary/cli/doctor.test.ts +++ b/src/application/Doctor.test.ts @@ -1,34 +1,7 @@ import { describe, expect, it } from "vitest"; -import type { ClientRegistrationStatus } from "../../../src/application/ClientRegistrationStatus.js"; -import { runDoctor, type DoctorHost } from "../../../src/application/Doctor.js"; -import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js"; -import { PRODUCT_IDENTITY } from "../../../src/identity.js"; - -const host = (overrides: Partial = {}): DoctorHost => ({ - platform: "darwin", - architecture: "x64", - nodeVersion: "24.18.0", - macosVersion: () => Promise.resolve("12.0"), - linuxDistribution: () => Promise.resolve(undefined), - validTarget: (path) => Promise.resolve(path.includes("Hopper")), - executable: (path) => Promise.resolve(path.includes("Hopper")), - supportedLinuxHopper: () => Promise.resolve(true), - linuxDemoRuntimeCheck: () => - Promise.resolve({ - name: "hopper-demo-runtime", - ok: true, - classification: "healthy", - }), - brewHopperPath: () => Promise.resolve(undefined), - manualHopperPaths: () => Promise.resolve([]), - installedSkillIdentity: () => - Promise.resolve({ - version: PRODUCT_IDENTITY.skillVersion, - toolCount: CATALOG_IDENTITY.counts.mcp_tools, - catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, - }), - ...overrides, -}); +import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js"; +import { createDoctorHostFixture as host } from "./Doctor.fixture.js"; +import { runDoctor } from "./Doctor.js"; describe("doctor", () => { it("returns exact recovery for every failed diagnostic", async () => { diff --git a/tests/boundary/providers/hopper/linuxHopper.test.ts b/src/application/LinuxHopper.test.ts similarity index 99% rename from tests/boundary/providers/hopper/linuxHopper.test.ts rename to src/application/LinuxHopper.test.ts index 65a92016f..f3dbb1e50 100644 --- a/tests/boundary/providers/hopper/linuxHopper.test.ts +++ b/src/application/LinuxHopper.test.ts @@ -12,7 +12,7 @@ import { type LinuxHopperInstallHost, type LinuxHopperLauncherStatus, type LinuxPackageFamily, -} from "../../../../src/application/LinuxHopper.js"; +} from "./LinuxHopper.js"; class RecordingLinuxHost implements LinuxHopperInstallHost { distributionValue: LinuxDistribution | undefined = { diff --git a/tests/boundary/providers/hopper/macHopper.test.ts b/src/application/MacHopper.test.ts similarity index 98% rename from tests/boundary/providers/hopper/macHopper.test.ts rename to src/application/MacHopper.test.ts index 921e5de4d..951560a27 100644 --- a/tests/boundary/providers/hopper/macHopper.test.ts +++ b/src/application/MacHopper.test.ts @@ -6,7 +6,7 @@ import { installMacHopper, macHopperInstallDisclosure, type MacHopperInstallHost, -} from "../../../../src/application/MacHopper.js"; +} from "./MacHopper.js"; class FakeMacHopperHost implements MacHopperInstallHost { readonly archive = new TextEncoder().encode("hopper-dmg"); diff --git a/tests/boundary/providers/managed/managedReconstruction.test.ts b/src/application/ManagedReconstructionService.test.ts similarity index 91% rename from tests/boundary/providers/managed/managedReconstruction.test.ts rename to src/application/ManagedReconstructionService.test.ts index 63cc3e29f..0706ef7c6 100644 --- a/tests/boundary/providers/managed/managedReconstruction.test.ts +++ b/src/application/ManagedReconstructionService.test.ts @@ -2,14 +2,14 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; -import { importManagedReconstructionEvidence } from "../../../../src/application/ManagedReconstructionService.js"; -import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../../../../src/contracts/managedWorkflowExamples.js"; +import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../contracts/managedWorkflowExamples.js"; import { importManagedReconstruction, managedReconstructionImportInputSchema, -} from "../../../../src/domain/managedReconstruction.js"; -import { managedMemberInspectionSchema } from "../../../../src/domain/managedArtifact.js"; -import { createEvidence } from "../../../../src/domain/evidence.js"; +} from "../domain/managedReconstruction.js"; +import { managedMemberInspectionSchema } from "../domain/managedArtifact.js"; +import { createEvidence } from "../domain/evidence.js"; +import { importManagedReconstructionEvidence } from "./ManagedReconstructionService.js"; const exampleInput = () => managedReconstructionImportInputSchema.parse( diff --git a/tests/boundary/providers/native/nativeApiInspection.test.ts b/src/application/NativeApiInspection.test.ts similarity index 88% rename from tests/boundary/providers/native/nativeApiInspection.test.ts rename to src/application/NativeApiInspection.test.ts index 3e5862e2b..d6e1ef4c4 100644 --- a/tests/boundary/providers/native/nativeApiInspection.test.ts +++ b/src/application/NativeApiInspection.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { projectNativeApiInspection } from "../../../../src/application/NativeApiInspection.js"; -import { functionDossierSchema } from "../../../../src/domain/hopperValues.js"; -import { nativeApiBoundarySchema } from "../../../../src/domain/nativeApiBoundary.js"; -import { ghidraFunctionDossier } from "../../../fixtures/ghidraFunction.js"; +import { functionDossierSchema } from "../domain/hopperValues.js"; +import { ghidraFunctionDossier } from "../domain/hopperValues.fixture.js"; +import { nativeApiBoundarySchema } from "../domain/nativeApiBoundary.js"; +import { projectNativeApiInspection } from "./NativeApiInspection.js"; describe("native API inspection", () => { it("projects structured boundary evidence through inspectable substeps", () => { diff --git a/src/application/ProcessCaptureError.test.ts b/src/application/ProcessCaptureError.test.ts new file mode 100644 index 000000000..0156484c1 --- /dev/null +++ b/src/application/ProcessCaptureError.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; + +import { projectAnalysisError } from "../domain/errors.js"; +import { + ProcessCaptureError, + processCaptureCancelled, +} from "./ProcessCaptureError.js"; + +describe("process capture error projection", () => { + it("reports incomplete cleanup resources without exposing its cause", () => { + const projected = projectAnalysisError( + new ProcessCaptureError("terminal cleanup failed", { + cause: new Error("secret-token"), + reason: "cleanup_incomplete", + cleanupResources: ["process_group"], + }), + ); + + expect(projected).toMatchObject({ + code: "cleanup_incomplete", + details: { cleanup: "incomplete", resources: ["process_group"] }, + }); + expect(JSON.stringify(projected)).not.toContain("secret-token"); + }); + + it("projects caller cancellation as completed cleanup", () => { + expect(projectAnalysisError(processCaptureCancelled())).toMatchObject({ + code: "cancelled", + category: "cancelled", + details: { operation: "process_capture", cleanup: "complete" }, + }); + }); +}); diff --git a/src/application/ProcessCaptureJournal.test.ts b/src/application/ProcessCaptureJournal.test.ts new file mode 100644 index 000000000..eca7af52d --- /dev/null +++ b/src/application/ProcessCaptureJournal.test.ts @@ -0,0 +1,35 @@ +import { expect, it } from "vitest"; + +import type { ProcessCaptureEventJournalEntry } from "../domain/processCapture.js"; +import { createProcessCaptureJournal } from "./ProcessCaptureJournal.js"; + +it("publishes journal entries in capture order and supports nested recording", () => { + const journal = createProcessCaptureJournal(); + const observed: ProcessCaptureEventJournalEntry[] = []; + const unsubscribe = journal.subscribe((entry) => observed.push(entry)); + journal.recordEvent("lifecycle", 0); + const settlementEntry = journal.record("lifecycle", 1); + unsubscribe(); + journal.recordEvent("frames", 0); + + expect(journal.entries).toEqual([ + { capture_order: 0, collection: "lifecycle", index: 0 }, + { capture_order: 1, collection: "lifecycle", index: 1 }, + { capture_order: 2, collection: "frames", index: 0 }, + ]); + expect(observed).toEqual(journal.entries.slice(0, 2)); + expect(settlementEntry).toEqual(journal.entries[1]); + + const nestedJournal = createProcessCaptureJournal(); + const notifications: string[] = []; + nestedJournal.subscribe((entry) => { + notifications.push(`a:${String(entry.capture_order)}`); + if (entry.capture_order === 0) nestedJournal.record("frames", 1); + }); + nestedJournal.subscribe((entry) => + notifications.push(`b:${String(entry.capture_order)}`), + ); + nestedJournal.record("frames", 0); + + expect(notifications).toEqual(["a:0", "b:0", "a:1", "b:1"]); +}); diff --git a/tests/boundary/process/processCaptureObservation.test.ts b/src/application/ProcessCaptureObservation.test.ts similarity index 93% rename from tests/boundary/process/processCaptureObservation.test.ts rename to src/application/ProcessCaptureObservation.test.ts index 0f272ffd0..3a3ea98ab 100644 --- a/tests/boundary/process/processCaptureObservation.test.ts +++ b/src/application/ProcessCaptureObservation.test.ts @@ -1,26 +1,23 @@ import { expect, it } from "vitest"; -import { snapshotRoots } from "../../../src/application/FilesystemSnapshot.js"; +import { snapshotRoots } from "./FilesystemSnapshot.js"; import { buildCaptureResult, prepareProcessCapture, type ProcessPreparationHost, -} from "../../../src/application/ProcessCaptureLifecycle.js"; -import { ProcessCheckpoints } from "../../../src/application/ProcessCheckpoints.js"; -import { normalizeProcessSamples } from "../../../src/application/ProcessNormalization.js"; -import { - isInitializedPtyRoot, - readLinuxChildren, -} from "../../../src/application/ProcessSampling.js"; -import { TerminalRenderer } from "../../../src/application/TerminalRenderer.js"; +} from "./ProcessCaptureLifecycle.js"; +import { ProcessCheckpoints } from "./ProcessCheckpoints.js"; +import { normalizeProcessSamples } from "./ProcessNormalization.js"; +import { isInitializedPtyRoot, readLinuxChildren } from "./ProcessSampling.js"; +import { TerminalRenderer } from "./TerminalRenderer.js"; import { authorizeProcessScenario, compareProcessCaptures, parseProcessCapture, parseProcessScenario, type ProcessCapture, -} from "../../../src/domain/processCapture.js"; -import { emptyProcessCapture as emptyCapture } from "../../fixtures/processCapture.js"; +} from "../domain/processCapture.js"; +import { emptyProcessCapture as emptyCapture } from "../domain/processCapture.fixture.js"; const base = { approved: true as const, diff --git a/src/application/ProcessPairedExperiment.test.ts b/src/application/ProcessPairedExperiment.test.ts index 7ae235b21..d82bcb825 100644 --- a/src/application/ProcessPairedExperiment.test.ts +++ b/src/application/ProcessPairedExperiment.test.ts @@ -4,7 +4,7 @@ import { runPairedProcessExperiment, type ProcessCapturePort, } from "./ProcessPairedExperiment.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js"; import { parseProcessCapture, type EnabledProcessExecutionPolicy, diff --git a/src/application/ReconstructionObligationLedger.authority.test.ts b/src/application/ReconstructionObligationLedger.authority.test.ts index 3e51feefb..64376927e 100644 --- a/src/application/ReconstructionObligationLedger.authority.test.ts +++ b/src/application/ReconstructionObligationLedger.authority.test.ts @@ -5,7 +5,7 @@ import { buildReconstructionObligationLedgerEvidenceValidated, resolveReconstructionObligationLedgerRequest, } from "./ReconstructionObligationLedgerService.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js"; import { createEvidence, type Evidence } from "../domain/evidence.js"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; diff --git a/src/application/ReconstructionObligationLedger.closure.test.ts b/src/application/ReconstructionObligationLedger.closure.test.ts index bcd6aed1f..c9d5f0d6d 100644 --- a/src/application/ReconstructionObligationLedger.closure.test.ts +++ b/src/application/ReconstructionObligationLedger.closure.test.ts @@ -5,7 +5,7 @@ import { buildReconstructionObligationLedgerEvidenceValidated, resolveReconstructionObligationLedgerRequest, } from "./ReconstructionObligationLedgerService.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js"; import { createEvidence, type Evidence } from "../domain/evidence.js"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; diff --git a/tests/acceptance/setup/setupConfiguration.test.ts b/src/application/Setup.configuration.test.ts similarity index 98% rename from tests/acceptance/setup/setupConfiguration.test.ts rename to src/application/Setup.configuration.test.ts index 4840df75e..965e25330 100644 --- a/tests/acceptance/setup/setupConfiguration.test.ts +++ b/src/application/Setup.configuration.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { runSetup } from "../../../src/application/Setup.js"; -import { FakeSetupHost, options } from "./setupTestSupport.js"; +import { FakeSetupHost, options } from "./Setup.fixture.js"; +import { runSetup } from "./Setup.js"; describe("setup workflow", () => { it("omits an aligned managed skill from an otherwise empty plan", async () => { diff --git a/tests/acceptance/setup/setupTestSupport.ts b/src/application/Setup.fixture.ts similarity index 94% rename from tests/acceptance/setup/setupTestSupport.ts rename to src/application/Setup.fixture.ts index f69513d48..da314d0cc 100644 --- a/tests/acceptance/setup/setupTestSupport.ts +++ b/src/application/Setup.fixture.ts @@ -6,14 +6,11 @@ import { type SetupOptions, type SetupHopperInstallResult, type SetupProviderEnvironment, -} from "../../../src/application/Setup.js"; -import type { - DoctorCheck, - DoctorReport, - DoctorScope, -} from "../../../src/application/Doctor.js"; -import type { LinuxDistribution } from "../../../src/application/LinuxHopper.js"; +} from "./Setup.js"; +import type { DoctorCheck, DoctorReport, DoctorScope } from "./Doctor.js"; +import type { LinuxDistribution } from "./LinuxHopper.js"; +/** Recording setup host for service-level planning and recovery tests. */ export class FakeSetupHost implements SetupHost { readonly platform: NodeJS.Platform; nodeVersion = "25.1.0"; @@ -160,6 +157,7 @@ export class FakeSetupHost implements SetupHost { }; } +/** Build setup options for approved or planning-only service tests. */ export const options = ( approved: boolean, installHopper = false, diff --git a/tests/acceptance/setup/setupPlanning.test.ts b/src/application/Setup.planning.test.ts similarity index 98% rename from tests/acceptance/setup/setupPlanning.test.ts rename to src/application/Setup.planning.test.ts index d410f16e5..593e16d40 100644 --- a/tests/acceptance/setup/setupPlanning.test.ts +++ b/src/application/Setup.planning.test.ts @@ -4,8 +4,8 @@ import { registrationPermissionEnvironment, runSetup, type SetupProgressEvent, -} from "../../../src/application/Setup.js"; -import { FakeSetupHost, options } from "./setupTestSupport.js"; +} from "./Setup.js"; +import { FakeSetupHost, options } from "./Setup.fixture.js"; describe("setup workflow", () => { it("propagates only the explicit non-secret investigation root policy", () => { diff --git a/tests/acceptance/setup/setupRecovery.test.ts b/src/application/Setup.recovery.test.ts similarity index 97% rename from tests/acceptance/setup/setupRecovery.test.ts rename to src/application/Setup.recovery.test.ts index a89bf2fc9..1cdf6b659 100644 --- a/tests/acceptance/setup/setupRecovery.test.ts +++ b/src/application/Setup.recovery.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { runSetup } from "../../../src/application/Setup.js"; -import { FakeSetupHost, options } from "./setupTestSupport.js"; +import { FakeSetupHost, options } from "./Setup.fixture.js"; +import { runSetup } from "./Setup.js"; describe("setup workflow", () => { it("records every detected client outcome after an earlier failure", async () => { diff --git a/tests/acceptance/setup/setupInstallFailure.test.ts b/src/application/SetupInstallFailure.test.ts similarity index 95% rename from tests/acceptance/setup/setupInstallFailure.test.ts rename to src/application/SetupInstallFailure.test.ts index 9745c88e8..5dfeaec94 100644 --- a/tests/acceptance/setup/setupInstallFailure.test.ts +++ b/src/application/SetupInstallFailure.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { setupInstallFailure, type HopperInstallFailureReason, -} from "../../../src/application/SetupInstallFailure.js"; +} from "./SetupInstallFailure.js"; describe("setup installer failure projection", () => { it.each([ diff --git a/src/application/StatusProjection.test.ts b/src/application/StatusProjection.test.ts index 693cbd777..6c69eeeb8 100644 --- a/src/application/StatusProjection.test.ts +++ b/src/application/StatusProjection.test.ts @@ -1,35 +1,9 @@ import { describe, expect, it } from "vitest"; import { runCapabilityStatus, runProviderStatus } from "./DirectAnalysis.js"; +import { createDoctorHostFixture } from "./Doctor.fixture.js"; import { projectDoctorReport } from "./DoctorProjection.js"; -import { runDoctor, type DoctorHost } from "./Doctor.js"; -import { CATALOG_IDENTITY } from "../catalogIdentity.js"; -import { PRODUCT_IDENTITY } from "../identity.js"; - -const doctorHost = (): DoctorHost => ({ - platform: "darwin", - architecture: "x64", - nodeVersion: "24.18.0", - macosVersion: () => Promise.resolve("14.0"), - linuxDistribution: () => Promise.resolve(undefined), - validTarget: () => Promise.resolve(true), - executable: (path) => Promise.resolve(path.includes("Hopper")), - supportedLinuxHopper: () => Promise.resolve(true), - linuxDemoRuntimeCheck: () => - Promise.resolve({ - name: "hopper-demo-runtime", - ok: true, - classification: "healthy", - }), - brewHopperPath: () => Promise.resolve(undefined), - manualHopperPaths: () => Promise.resolve([]), - installedSkillIdentity: () => - Promise.resolve({ - version: PRODUCT_IDENTITY.skillVersion, - toolCount: CATALOG_IDENTITY.counts.mcp_tools, - catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, - }), -}); +import { runDoctor } from "./Doctor.js"; describe("purpose-specific CLI projections", () => { it("keeps provider and capability summaries concise with explicit full detail", async () => { @@ -59,7 +33,10 @@ describe("purpose-specific CLI projections", () => { }); it("omits the catalog tool array only from the default doctor projection", async () => { - const report = await runDoctor(undefined, doctorHost()); + const report = await runDoctor( + undefined, + createDoctorHostFixture({ validTarget: () => Promise.resolve(true) }), + ); const summary = projectDoctorReport(report, "summary"); const full = projectDoctorReport(report, "full"); diff --git a/tests/acceptance/setup/upgrade.test.ts b/src/application/Upgrade.test.ts similarity index 99% rename from tests/acceptance/setup/upgrade.test.ts rename to src/application/Upgrade.test.ts index 1aaea5369..009fd0db5 100644 --- a/tests/acceptance/setup/upgrade.test.ts +++ b/src/application/Upgrade.test.ts @@ -6,7 +6,7 @@ import { type UpgradeHost, type UpgradeOutput, type NpmInstallationHost, -} from "../../../src/application/Upgrade.js"; +} from "./Upgrade.js"; class FakeUpgradeHost implements UpgradeHost { latest: string | undefined = "0.6.0"; diff --git a/tests/boundary/browser/cdpCaptureCompleteness.test.ts b/src/browser/CdpCaptureCompleteness.test.ts similarity index 95% rename from tests/boundary/browser/cdpCaptureCompleteness.test.ts rename to src/browser/CdpCaptureCompleteness.test.ts index 72d551d9e..87ff08eb2 100644 --- a/tests/boundary/browser/cdpCaptureCompleteness.test.ts +++ b/src/browser/CdpCaptureCompleteness.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { CdpCaptureCompleteness } from "../../../src/browser/CdpCaptureCompleteness.js"; +import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; describe("CdpCaptureCompleteness", () => { it("reports attach-window limitations without claiming a complete history", () => { diff --git a/tests/boundary/browser/cdpCaptureDocuments.test.ts b/src/browser/CdpCaptureDocuments.test.ts similarity index 96% rename from tests/boundary/browser/cdpCaptureDocuments.test.ts rename to src/browser/CdpCaptureDocuments.test.ts index fdbedfaea..2d3a8f870 100644 --- a/tests/boundary/browser/cdpCaptureDocuments.test.ts +++ b/src/browser/CdpCaptureDocuments.test.ts @@ -5,8 +5,8 @@ import { captureDom, captureFrames, captureResources, -} from "../../../src/browser/CdpCaptureDocuments.js"; -import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js"; +} from "./CdpCaptureDocuments.js"; +import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; const origin = "https://app.example.test"; diff --git a/tests/boundary/browser/playwrightBrowserScenarioProvider.test.ts b/src/browser/PlaywrightBrowserScenarioProvider.test.ts similarity index 95% rename from tests/boundary/browser/playwrightBrowserScenarioProvider.test.ts rename to src/browser/PlaywrightBrowserScenarioProvider.test.ts index e8abcdb73..06a076f53 100644 --- a/tests/boundary/browser/playwrightBrowserScenarioProvider.test.ts +++ b/src/browser/PlaywrightBrowserScenarioProvider.test.ts @@ -1,19 +1,19 @@ import { describe, expect, it } from "vitest"; -import type { BrowserScenarioSessionPort } from "../../../src/browser/BrowserScenarioSessionPort.js"; -import { PlaywrightBrowserScenarioProvider } from "../../../src/browser/PlaywrightBrowserScenarioProvider.js"; -import { BrowserScenarioCaptureBudget } from "../../../src/browser/PlaywrightScenarioArtifacts.js"; -import { sanitizeBrowserUrl } from "../../../src/domain/browserObservation.js"; +import type { BrowserScenarioSessionPort } from "./BrowserScenarioSessionPort.js"; +import { PlaywrightBrowserScenarioProvider } from "./PlaywrightBrowserScenarioProvider.js"; +import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js"; +import { sanitizeBrowserUrl } from "../domain/browserObservation.js"; import { browserScenarioSchema, type BrowserScenario, type BrowserScenarioAction, -} from "../../../src/domain/browserScenario.js"; +} from "../domain/browserScenario.js"; import { browserStepArtifactsSchema, type BrowserScenarioEvent, type BrowserStepArtifacts, -} from "../../../src/domain/browserScenarioCapture.js"; +} from "../domain/browserScenarioCapture.js"; const scenario = ( options: { diff --git a/tests/boundary/browser/electronActiveActions.test.ts b/src/browser/PlaywrightElectronActiveActions.test.ts similarity index 95% rename from tests/boundary/browser/electronActiveActions.test.ts rename to src/browser/PlaywrightElectronActiveActions.test.ts index 2a0fb9374..b3e598ab2 100644 --- a/tests/boundary/browser/electronActiveActions.test.ts +++ b/src/browser/PlaywrightElectronActiveActions.test.ts @@ -3,9 +3,9 @@ import { expect, it } from "vitest"; import { electronActiveObservationInputSchema, electronActiveObservationResultSchema, -} from "../../../src/domain/electronActiveObservation.js"; -import { runElectronActions } from "../../../src/browser/PlaywrightElectronActiveActions.js"; -import { createElectronActiveObservationFixtureResult } from "../../fixtures/electronActiveObservationResult.js"; +} from "../domain/electronActiveObservation.js"; +import { createElectronActiveObservationFixtureResult } from "../domain/electronActiveObservation.fixture.js"; +import { runElectronActions } from "./PlaywrightElectronActiveActions.js"; it("parses bounded window, renderer, and deep-link actions for agents", () => { const input = electronActiveObservationInputSchema.parse({ diff --git a/tests/boundary/browser/playwrightExecutionBoundary.test.ts b/src/browser/PlaywrightExecutionBoundary.test.ts similarity index 87% rename from tests/boundary/browser/playwrightExecutionBoundary.test.ts rename to src/browser/PlaywrightExecutionBoundary.test.ts index 9825992e5..f9cfe9c5d 100644 --- a/tests/boundary/browser/playwrightExecutionBoundary.test.ts +++ b/src/browser/PlaywrightExecutionBoundary.test.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { withPlaywrightExecutionBoundary } from "../../../src/browser/PlaywrightExecutionBoundary.js"; +import { withPlaywrightExecutionBoundary } from "./PlaywrightExecutionBoundary.js"; import { AnalysisCancelledError, AnalysisTimeoutError, -} from "../../../src/domain/errors.js"; +} from "../domain/errors.js"; describe("withPlaywrightExecutionBoundary", () => { afterEach(() => vi.useRealTimers()); diff --git a/tests/boundary/browser/playwrightScenarioEvents.test.ts b/src/browser/PlaywrightScenarioEvents.test.ts similarity index 88% rename from tests/boundary/browser/playwrightScenarioEvents.test.ts rename to src/browser/PlaywrightScenarioEvents.test.ts index c5f86cfcb..4c2e3f5d2 100644 --- a/tests/boundary/browser/playwrightScenarioEvents.test.ts +++ b/src/browser/PlaywrightScenarioEvents.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; import type { Page } from "playwright-core"; -import { BrowserScenarioSecrets } from "../../../src/browser/BrowserScenarioSecrets.js"; -import { BrowserScenarioCaptureBudget } from "../../../src/browser/PlaywrightScenarioArtifacts.js"; -import { PlaywrightScenarioEvents } from "../../../src/browser/PlaywrightScenarioEvents.js"; -import { browserScenarioSchema } from "../../../src/domain/browserScenario.js"; +import { browserScenarioSchema } from "../domain/browserScenario.js"; +import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js"; +import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js"; +import { PlaywrightScenarioEvents } from "./PlaywrightScenarioEvents.js"; describe("PlaywrightScenarioEvents", () => { it("bounds oversized page errors before validating captured events", () => { diff --git a/tests/boundary/cli/cliLogging.test.ts b/src/cliLogging.test.ts similarity index 52% rename from tests/boundary/cli/cliLogging.test.ts rename to src/cliLogging.test.ts index 3a9bc6f6c..f941e3388 100644 --- a/tests/boundary/cli/cliLogging.test.ts +++ b/src/cliLogging.test.ts @@ -1,18 +1,8 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; -import { - isCliOperationFailure, - logCliCommand, -} from "../../../src/cliLogging.js"; -import { silentLogger } from "../../../src/logger.js"; +import { isCliOperationFailure } from "./cliLogging.js"; -const originalExitCode = process.exitCode; - -afterEach(() => { - process.exitCode = originalExitCode; -}); - -describe("CLI operation status", () => { +describe("CLI operation status classification", () => { it.each([ ["typed error", { error: "Analysis failed", category: "timeout" }], ["unhealthy diagnostic", { healthy: false, checks: [] }], @@ -34,20 +24,4 @@ describe("CLI operation status", () => { ])("keeps %s successful", (_label, value) => { expect(isCliOperationFailure(value)).toBe(false); }); - - it("sets a nonzero process status without replacing structured output", async () => { - const output = { - error: "Analysis failed", - category: "integrity_mismatch", - message: "Artifact integrity check failed.", - details: { logical_path: "main.js" }, - }; - - await expect( - logCliCommand(silentLogger, "inventory-artifact", () => - Promise.resolve(output), - ), - ).resolves.toBe(output); - expect(process.exitCode).toBe(1); - }); }); diff --git a/tests/boundary/filesystem/configIntegration.test.ts b/src/config.integration.test.ts similarity index 98% rename from tests/boundary/filesystem/configIntegration.test.ts rename to src/config.integration.test.ts index e3b62e132..6380c2d15 100644 --- a/tests/boundary/filesystem/configIntegration.test.ts +++ b/src/config.integration.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { parseConfig } from "../../../src/config.js"; +import { parseConfig } from "./config.js"; describe("configuration parsing integration", () => { it("returns defaults when no environment variables are set", () => { diff --git a/src/contracts/processCaptureExample.ts b/src/contracts/processCaptureExample.ts index ed72a5cfc..5aed2939c 100644 --- a/src/contracts/processCaptureExample.ts +++ b/src/contracts/processCaptureExample.ts @@ -1,72 +1,2 @@ -import type { JsonValue } from "../domain/jsonValue.js"; -import { digestProcessCommitment } from "../domain/processCapture.js"; - -const normalization = { - paths: true, - pids: true, - ports: true, - time_bucket_ms: 10, - patterns: [], -}; -const scenario = { executable_sha256: "0".repeat(64) }; -const comparisonContract = {}; -const shimPlan: JsonValue[] = []; -const replayPlan = {}; - -/** Minimal valid process capture used only in public contract examples. */ -export const EMPTY_PROCESS_CAPTURE_EXAMPLE = { - schema_version: 4, - manifest: { - rea_version: "1.1.0", - provider_version: "3", - platform: "fixture", - architecture: "fixture", - pty_backend: "node-pty", - started_at: "2026-01-01T00:00:00.000Z", - completed_at: "2026-01-01T00:00:00.001Z", - scenario, - comparison_contract: comparisonContract, - shim_plan: shimPlan, - replay_plan: replayPlan, - full_scenario_sha256: digestProcessCommitment(scenario), - comparison_contract_sha256: digestProcessCommitment(comparisonContract), - executable_sha256: "0".repeat(64), - normalization_sha256: digestProcessCommitment(normalization), - shim_plan_sha256: digestProcessCommitment(shimPlan), - replay_plan_sha256: digestProcessCommitment(replayPlan), - }, - normalization, - frames: [], - rendered_frames: [], - interaction_events: [], - exit: { code: 0, signal: null, reason: "exited" }, - settlement: { - state: "quiesced", - elapsed_ms: 50, - cleanup_outcome: "not_required", - }, - process_samples: [], - filesystem_checkpoints: [ - { name: "before", at_ms: 0, files: [], effects: [], truncated: false }, - { - name: "after_settlement", - at_ms: 50, - files: [], - effects: [], - truncated: false, - }, - ], - shim_events: [], - protocol_events: [], - replay_transitions: [], - files_before: [], - files_after: [], - filesystem_effects: [], - truncated: false, - limitations: [], - residual_unknowns: [], - cleanup: { - owned_process_group: "verified", - temporary_root: "removed", - }, -} satisfies JsonValue; +/** Minimal valid process capture retained as a public contract example. */ +export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCaptureExample.js"; diff --git a/src/domain/analysisErrorProjection.test.ts b/src/domain/analysisErrorProjection.test.ts new file mode 100644 index 000000000..08ecd58ef --- /dev/null +++ b/src/domain/analysisErrorProjection.test.ts @@ -0,0 +1,219 @@ +import { describe, expect, it } from "vitest"; + +import { + AnalysisCapabilityUnavailableError, + AnalysisInputError, + ArtifactOperationError, + BinaryTargetError, + HopperProcessError, + PermissionRequiredError, + ProviderAdapterError, + ReplayPlanStaleError, + UnknownRegistryError, + projectAnalysisError, +} from "./errors.js"; + +describe("analysis error projection: provider failures", () => { + it("preserves detached, actionable provider diagnostics", () => { + const diagnostics = { + runtime_root: "/tmp/rea-ghidra-fixture", + profile_digest: "a".repeat(64), + exit_code: 1, + }; + const error = new ProviderAdapterError("ghidra", "health", { + diagnostics, + }); + + expect(error.diagnostics).not.toBe(diagnostics); + expect(projectAnalysisError(error)).toMatchObject({ + details: { + provider_id: "ghidra", + operation: "health", + diagnostics, + }, + }); + }); + + it("maps the Linux startup compatibility exit codes without host internals", () => { + const expected = [ + [70, "private_display_unavailable"], + [71, "x11_authorization_failed"], + [72, "unsupported_hopper_build"], + [73, "invalid_launch_command"], + [74, "process_ownership_mismatch"], + [75, "hopper_exited_during_startup"], + [76, "unsupported_demo_dialog"], + [77, "unexpected_display_geometry"], + [78, "x11_input_failed"], + [79, "runtime_dependency_unavailable"], + [80, "x11_socket_directory_unusable"], + ] as const; + + for (const [exitCode, code] of expected) { + const projected = projectAnalysisError(new HopperProcessError(exitCode)); + expect(projected).toMatchObject({ + code: "provider_unavailable", + details: { failure_code: code, exit_code: exitCode }, + }); + expect(projected.message.length).toBeGreaterThan(20); + expect(JSON.stringify(projected)).not.toContain("/proc/"); + } + }); +}); + +describe("analysis error projection: caller contract", () => { + it("maps representative failures without exposing causes", () => { + const secretCause = new Error("secret-token"); + const projected = [ + projectAnalysisError( + new AnalysisInputError("overview", { cause: secretCause }), + ), + projectAnalysisError( + new AnalysisCapabilityUnavailableError("fixture", "overview", "absent"), + ), + projectAnalysisError( + new BinaryTargetError("/local/targets/app", "invalid", { + cause: secretCause, + }), + ), + projectAnalysisError( + new ReplayPlanStaleError("a".repeat(64), "b".repeat(64)), + ), + ]; + + expect(projected.map(({ code }) => code)).toEqual([ + "invalid_request", + "capability_unavailable", + "target_unavailable", + "plan_stale", + ]); + expect(projected[1]).toMatchObject({ category: "unsupported_provider" }); + expect(projected[2]).toMatchObject({ + details: { path: "/local/targets/app" }, + }); + expect(JSON.stringify(projected)).not.toContain("secret-token"); + }); + + it("preserves exact artifact-integrity coordinates", () => { + expect( + projectAnalysisError( + new ArtifactOperationError("inventory_artifact", "integrity", { + logicalPath: "main.js", + declaredSha256: "a".repeat(64), + calculatedSha256: "b".repeat(64), + unpacked: true, + }), + ), + ).toMatchObject({ + category: "integrity_mismatch", + details: { + logical_path: "main.js", + declared_sha256: "a".repeat(64), + calculated_sha256: "b".repeat(64), + unpacked: true, + }, + }); + }); + + it.each([ + [ + "configure", + false, + false, + "Add the exact missing scope beneath the administrator ceiling, then retry.", + ], + [ + "elicit", + false, + true, + "Approve the exact missing scope, then retry the operation.", + ], + [ + "restart", + true, + false, + "Add the exact missing scope to the administrator configuration, then restart the registered MCP server or client.", + ], + ] as const)( + "projects %s permission remediation into stable protocol fields", + (remediation, restartRequired, elicitationSupported, action) => { + const projected = projectAnalysisError( + new PermissionRequiredError({ + requested: { + capability: "evidence_read", + roots: ["/workspace/evidence.json"], + executables: [], + environment_names: [], + network: "none", + mount: false, + operation_identity: "read:evidence", + }, + missing: { roots: ["/workspace/evidence.json"] }, + ceiling: null, + remediation, + }), + ); + + expect(projected.remediation).toEqual({ + action, + restart_required: restartRequired, + elicitation_supported: elicitationSupported, + }); + }, + ); + + it("gives missing residual unknowns lookup-specific remediation", () => { + expect( + projectAnalysisError(new UnknownRegistryError("not-found")), + ).toMatchObject({ + code: "execution_failure", + message: + "The requested residual unknown does not exist in this session. Check the unknown_id and try again.", + remediation: { + action: + "Check that the unknown_id belongs to this session, then retry.", + }, + details: { reason: "not-found" }, + }); + }); + + it("retains schema-authored input guidance", () => { + expect( + projectAnalysisError( + new AnalysisInputError("find_changed_behavior", undefined, [ + { + path: [], + reason: "invalid_value", + message: + "Supply either existing comparisons or one investigation_run", + }, + ]), + ), + ).toMatchObject({ + details: { + issues: [ + { + path: [], + reason: "invalid_value", + message: + "Supply either existing comparisons or one investigation_run", + }, + ], + }, + }); + }); + + it("distinguishes disabled integrity continuation from format support", () => { + expect( + projectAnalysisError( + new ArtifactOperationError("inventory_artifact", "policy"), + ), + ).toMatchObject({ + code: "artifact_operation_failed", + category: "unavailable", + message: expect.stringContaining( + "REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true", + ), + }); + }); +}); diff --git a/tests/boundary/providers/native/bytecodeProvider.test.ts b/src/domain/bytecodeProvider.test.ts similarity index 92% rename from tests/boundary/providers/native/bytecodeProvider.test.ts rename to src/domain/bytecodeProvider.test.ts index 4e1c2d23f..75616c19f 100644 --- a/tests/boundary/providers/native/bytecodeProvider.test.ts +++ b/src/domain/bytecodeProvider.test.ts @@ -7,8 +7,7 @@ import { detectArtifactFormat, detectBytecodeFamily, type BytecodeArtifact, - type BytecodeSymbol, -} from "../../../../src/domain/bytecodeProvider.js"; +} from "./bytecodeProvider.js"; describe("bytecode provider", () => { it("detects JVM family from extensions", () => { @@ -137,16 +136,4 @@ describe("bytecode provider", () => { const parsed = bytecodeAnalysisSchema.safeParse(result); expect(parsed.success).toBe(true); }); - - it("uses BytecodeSymbol type", () => { - const sym: BytecodeSymbol = { - name: "com.example.Test", - raw_location: "0x100", - kind: "class", - bytecode_version: 52, - provenance: "application", - is_public: true, - }; - expect(sym.provenance).toBe("application"); - }); }); diff --git a/src/domain/bytecodeProvider.ts b/src/domain/bytecodeProvider.ts index d14aa54d4..9f0131c3f 100644 --- a/src/domain/bytecodeProvider.ts +++ b/src/domain/bytecodeProvider.ts @@ -37,7 +37,6 @@ export const bytecodeSymbolSchema = z.strictObject({ /** Whether the symbol is accessible (public). */ is_public: z.boolean().default(false), }); -export type BytecodeSymbol = z.infer; /** A discovered bytecode artifact (class file, wheel, etc). */ export const bytecodeArtifactSchema = z.strictObject({ diff --git a/tests/boundary/browser/customProtocolCapture.test.ts b/src/domain/customProtocolCapture.test.ts similarity index 84% rename from tests/boundary/browser/customProtocolCapture.test.ts rename to src/domain/customProtocolCapture.test.ts index 6ed1be11c..ac51e6704 100644 --- a/tests/boundary/browser/customProtocolCapture.test.ts +++ b/src/domain/customProtocolCapture.test.ts @@ -10,12 +10,8 @@ import { looksLikeCredentialFrame, successfulAuthEvents, type AuthFlowEvent, - type FrameDirection, - type AuthStage, - type TokenLifecycleEvent, - type CustomProtocolCapture, type ProtocolFrame, -} from "../../../src/domain/customProtocolCapture.js"; +} from "./customProtocolCapture.js"; const sampleFrames: ProtocolFrame[] = [ { @@ -137,21 +133,4 @@ describe("custom protocol capture", () => { it("returns null for empty auth events", () => { expect(authFlowDuration([])).toBeNull(); }); - - it("uses all exported types", () => { - const dir: FrameDirection = "sent"; - expect(dir).toBe("sent"); - const stage: AuthStage = "init"; - expect(stage).toBe("init"); - const event: TokenLifecycleEvent = "issued"; - expect(event).toBe("issued"); - const capture: CustomProtocolCapture = { - transport: "tcp", - frames: sampleFrames, - auth_events: sampleAuthEvents, - has_truncated: false, - credentials_detected: false, - }; - expect(capture.transport).toBe("tcp"); - }); }); diff --git a/src/domain/customProtocolCapture.ts b/src/domain/customProtocolCapture.ts index 6e0a4aa11..4e84a320c 100644 --- a/src/domain/customProtocolCapture.ts +++ b/src/domain/customProtocolCapture.ts @@ -15,7 +15,6 @@ export type TransportType = z.infer; /** Direction of a protocol frame. */ export const frameDirectionSchema = z.enum(["sent", "received", "intercepted"]); -export type FrameDirection = z.infer; /** A captured protocol frame. */ export const protocolFrameSchema = z.strictObject({ @@ -57,7 +56,6 @@ export const authStageSchema = z.enum([ "failure", "success", ]); -export type AuthStage = z.infer; /** Token lifecycle event. */ export const tokenLifecycleEventSchema = z.enum([ @@ -67,7 +65,6 @@ export const tokenLifecycleEventSchema = z.enum([ "revoked", "renewed", ]); -export type TokenLifecycleEvent = z.infer; /** A captured authentication flow event. */ export const authFlowEventSchema = z.strictObject({ @@ -107,7 +104,6 @@ export const customProtocolCaptureSchema = z.strictObject({ /** Whether credentials were detected. */ credentials_detected: z.boolean().default(false), }); -export type CustomProtocolCapture = z.infer; /** Correlate a frame with process identity. */ export function correlateProcessIdentity( diff --git a/tests/fixtures/electronActiveObservationResult.ts b/src/domain/electronActiveObservation.fixture.ts similarity index 93% rename from tests/fixtures/electronActiveObservationResult.ts rename to src/domain/electronActiveObservation.fixture.ts index 8a6a230ce..e5f6d8a82 100644 --- a/tests/fixtures/electronActiveObservationResult.ts +++ b/src/domain/electronActiveObservation.fixture.ts @@ -1,4 +1,4 @@ -import { electronActiveObservationResultSchema } from "../../src/domain/electronActiveObservation.js"; +import { electronActiveObservationResultSchema } from "./electronActiveObservation.js"; /** Create a bounded active-capture result for MCP contract tests. */ export const createElectronActiveObservationFixtureResult = ( diff --git a/tests/boundary/process/eventProcessTree.test.ts b/src/domain/eventProcessTree.test.ts similarity index 90% rename from tests/boundary/process/eventProcessTree.test.ts rename to src/domain/eventProcessTree.test.ts index 3b48feb74..3ee43faf3 100644 --- a/tests/boundary/process/eventProcessTree.test.ts +++ b/src/domain/eventProcessTree.test.ts @@ -6,10 +6,7 @@ import { maxTreeDepth, reconstructProcessTree, type ProcessTreeEvent, - type ProcessEventType, - type ProcessNode, - type ProcessTreeReconstruction, -} from "../../../src/domain/eventProcessTree.js"; +} from "./eventProcessTree.js"; describe("event-backed process tree reconstruction", () => { it("reconstructs a simple process tree", () => { @@ -388,37 +385,4 @@ describe("event-backed process tree depth and types", () => { const result = reconstructProcessTree(events, 1); expect(maxTreeDepth(result.processes, 1)).toBe(2); }); - - it("uses exported types", () => { - const node: ProcessNode = { - pid: 1, - ppid: null, - process_name: "test", - executable: "/test", - arguments: [], - children: [], - spawn_time_ms: 0, - exit_time_ms: null, - exit_code: null, - is_re_exec: false, - is_reparented: false, - previous_ppid: null, - }; - expect(node.pid).toBe(1); - const recon: ProcessTreeReconstruction = { - root_pid: 1, - processes: [node], - events_consumed: 1, - events_unmatched: 0, - short_lived_descendants: 0, - has_re_exec: false, - has_reparenting: false, - }; - expect(recon.root_pid).toBe(1); - }); - - it("uses ProcessEventType", () => { - const t: ProcessEventType = "spawn"; - expect(t).toBe("spawn"); - }); }); diff --git a/src/domain/eventProcessTree.ts b/src/domain/eventProcessTree.ts index 5f91c15af..9fc1d9518 100644 --- a/src/domain/eventProcessTree.ts +++ b/src/domain/eventProcessTree.ts @@ -11,7 +11,6 @@ export const processEventTypeSchema = z.enum([ "thread_create", "thread_exit", ]); -export type ProcessEventType = z.infer; /** A single event-backed process-tree event. */ export const processTreeEventSchema = z.strictObject({ diff --git a/tests/fixtures/ghidraFunction.ts b/src/domain/hopperValues.fixture.ts similarity index 98% rename from tests/fixtures/ghidraFunction.ts rename to src/domain/hopperValues.fixture.ts index 73ed36753..9ad68d903 100644 --- a/tests/fixtures/ghidraFunction.ts +++ b/src/domain/hopperValues.fixture.ts @@ -1,4 +1,4 @@ -import type { JsonValue } from "../../src/domain/jsonValue.js"; +import type { JsonValue } from "./jsonValue.js"; export const ghidraFunctionClassification = () => ({ external: false, diff --git a/tests/boundary/providers/hopper/hopperValues.test.ts b/src/domain/hopperValues.test.ts similarity index 98% rename from tests/boundary/providers/hopper/hopperValues.test.ts rename to src/domain/hopperValues.test.ts index 8947c326b..6e99b56e7 100644 --- a/tests/boundary/providers/hopper/hopperValues.test.ts +++ b/src/domain/hopperValues.test.ts @@ -8,7 +8,7 @@ import { parseProcedures, parseRelatedAddresses, parseSegments, -} from "../../../../src/domain/hopperValues.js"; +} from "./hopperValues.js"; describe("Hopper boundary values", () => { it("parses Hopper's address-keyed name and string maps", () => { diff --git a/src/domain/managedMemberComparison.test.ts b/src/domain/managedMemberComparison.test.ts new file mode 100644 index 000000000..3c548db1b --- /dev/null +++ b/src/domain/managedMemberComparison.test.ts @@ -0,0 +1,286 @@ +import { createHash } from "node:crypto"; + +import { describe, expect, it } from "vitest"; + +import { + compareManagedMembers, + managedMemberComparisonResultSchema, +} from "./managedMemberComparison.js"; +import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js"; +import { + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_MEMBER_FIXTURE_LIMITS, +} from "../dotnet/ManagedPe.fixture.js"; + +const comparisonLimits = { + max_method_matches: 100, + max_field_matches: 100, + max_candidates: 10, +}; + +describe("managed member comparison", () => { + it("remaps renamed methods by exact CIL/signature without using names", () => { + const leftBytes = buildManagedPeFixture(); + const rightBytes = buildManagedPeFixture({ + mvid: Buffer.from("00112233445566778899aabbccddeefe", "hex"), + typeName: "A", + typeNamespace: "X", + methodName: "b", + fieldName: "c", + }); + const left = inspect(leftBytes, "/tmp/left.dll"); + const right = inspect(rightBytes, "/tmp/right.dll"); + const result = compareManagedMembers( + { evidenceId: left.evidenceId, result: left.result }, + { evidenceId: right.evidenceId, result: right.result }, + comparisonLimits, + ); + + expect(result.algorithm.name_matching).toBe("not-used"); + expect(result.left.mvid).toBe("00112233-4455-6677-8899-aabbccddeeff"); + expect(result.right.mvid).toBe("33221100-5544-7766-8899-aabbccddeefe"); + expect(result.matching.exact_il_signature).toBe(1); + expect(result.methods).toEqual([ + expect.objectContaining({ + status: "unchanged", + left: expect.objectContaining({ token: "0x06000001", name: "Main" }), + right: expect.objectContaining({ token: "0x06000001", name: "b" }), + match: expect.objectContaining({ + status: "matched", + basis: "exact-il-signature", + confidence: "exact", + }), + dimensions: [], + }), + ]); + expect(result.limitations).toContain( + "Names are reported as observations but are not used as a matching basis.", + ); + }); + + it("uses structural method shape when build-local token operands drift", () => { + const leftBytes = buildManagedPeFixture(); + const rightBytes = buildManagedPeFixture({ + ilBody: Buffer.from([ + 0x32, 0x02, 0x7b, 0x02, 0x00, 0x00, 0x04, 0x28, 0x02, 0x00, 0x00, 0x0a, + 0x2a, + ]), + }); + const left = inspect(leftBytes, "/tmp/left.dll"); + const right = inspect(rightBytes, "/tmp/right.dll"); + const result = compareManagedMembers( + { evidenceId: left.evidenceId, result: left.result }, + { evidenceId: right.evidenceId, result: right.result }, + comparisonLimits, + ); + + expect(result.matching.exact_il_signature).toBe(0); + expect(result.matching.structural_method_shape).toBe(1); + expect(result.methods[0]).toMatchObject({ + status: "changed", + match: { status: "matched", basis: "structural-method-shape" }, + dimensions: ["cil"], + }); + }); + + it("rejects member states that disagree with their observed sides or match", () => { + const left = inspect(buildManagedPeFixture(), "/tmp/left.dll"); + const right = inspect(buildManagedPeFixture(), "/tmp/right.dll"); + const result = compareManagedMembers( + { evidenceId: left.evidenceId, result: left.result }, + { evidenceId: right.evidenceId, result: right.result }, + comparisonLimits, + ); + const method = result.methods[0]; + expect(method).toBeDefined(); + if (method === undefined) return; + + expect( + managedMemberComparisonResultSchema.safeParse({ + ...result, + methods: [{ ...method, status: "unchanged", right: null }], + }).success, + ).toBe(false); + expect( + managedMemberComparisonResultSchema.safeParse({ + ...result, + methods: [ + { + ...method, + match: { + ...method.match, + basis: "none", + confidence: "unknown", + }, + }, + ], + }).success, + ).toBe(false); + }); + + it("does not match identical instruction-limited method prefixes", () => { + const limited = { + ...MANAGED_MEMBER_FIXTURE_LIMITS, + maxMethodInstructions: 1, + }; + const left = inspect( + buildManagedPeFixture(), + "/tmp/left-partial.dll", + limited, + ); + const right = inspect( + buildManagedPeFixture(), + "/tmp/right-partial.dll", + limited, + ); + const result = compareManagedMembers( + { evidenceId: left.evidenceId, result: left.result }, + { evidenceId: right.evidenceId, result: right.result }, + comparisonLimits, + ); + + expect(result.matching.exact_il_signature).toBe(0); + expect(result.matching.structural_method_shape).toBe(0); + expect(result.coverage).toMatchObject({ + status: "partial", + left_status: "partial", + right_status: "partial", + }); + }); +}); + +describe("managed member comparison uncertainty", () => { + it("keeps unmatched members unknown when the opposite page is incomplete", () => { + const left = inspect(buildManagedPeFixture(), "/tmp/left-paged.dll"); + const right = inspect(buildManagedPeFixture(), "/tmp/right-paged.dll"); + const leftPartial = { + ...left.result, + fields: { + ...left.result.fields, + items: [], + returned: 0, + dropped: 1, + complete: false, + }, + }; + const rightPartial = { + ...right.result, + methods: { + ...right.result.methods, + items: [], + returned: 0, + dropped: 1, + complete: false, + }, + }; + const result = compareManagedMembers( + { evidenceId: left.evidenceId, result: leftPartial }, + { evidenceId: right.evidenceId, result: rightPartial }, + comparisonLimits, + ); + + expect(result.summary).toMatchObject({ + added: 0, + removed: 0, + unknown: 2, + }); + expect(result.methods[0]).toMatchObject({ + status: "unknown", + left: { token: "0x06000001" }, + right: null, + limitations: [expect.stringContaining("unknown-within-unobserved-page")], + }); + expect(result.fields[0]).toMatchObject({ + status: "unknown", + left: null, + right: { token: "0x04000001" }, + limitations: [expect.stringContaining("unknown-within-unobserved-page")], + }); + expect(result.coverage).toEqual({ + status: "truncated", + left_status: "partial", + right_status: "partial", + omitted_methods: 1, + omitted_fields: 1, + omitted_candidates: 0, + }); + }); + + it("does not guess ambiguous field signature matches", () => { + const left = inspect(buildManagedPeFixture(), "/tmp/left.dll"); + const leftField = left.result.fields.items[0]; + expect(leftField).toBeDefined(); + if (leftField === undefined) return; + const duplicatedLeft = { + ...left.result, + fields: { + ...left.result.fields, + items: [ + leftField, + { + ...leftField, + token: "0x04000002", + name: "other", + }, + ], + total: 2, + returned: 2, + }, + }; + const right = inspect(buildManagedPeFixture(), "/tmp/right.dll"); + const rightField = right.result.fields.items[0]; + expect(rightField).toBeDefined(); + if (rightField === undefined) return; + const duplicatedRight = { + ...right.result, + fields: { + ...right.result.fields, + items: [ + rightField, + { + ...rightField, + token: "0x04000002", + name: "renamed", + }, + ], + total: 2, + returned: 2, + }, + }; + const result = compareManagedMembers( + { evidenceId: left.evidenceId, result: duplicatedLeft }, + { evidenceId: right.evidenceId, result: duplicatedRight }, + comparisonLimits, + ); + + expect(result.matching.ambiguous).toBe(1); + expect(result.fields).toEqual([ + expect.objectContaining({ + status: "unknown", + match: expect.objectContaining({ + status: "ambiguous", + basis: "field-signature", + candidate_left_tokens: ["0x04000001", "0x04000002"], + candidate_right_tokens: ["0x04000001", "0x04000002"], + }), + }), + ]); + }); +}); + +const inspect = ( + bytes: Buffer, + path: string, + limits: typeof MANAGED_MEMBER_FIXTURE_LIMITS = MANAGED_MEMBER_FIXTURE_LIMITS, +) => { + const target = managedPeFixtureTarget(bytes, path); + const result = inspectManagedMembersBytes(bytes, target, limits); + return { + result, + evidenceId: `ev_${hash(Buffer.from(path))}`, + }; +}; + +const hash = (bytes: Buffer): string => + createHash("sha256").update(bytes).digest("hex"); diff --git a/tests/acceptance/investigations/mobileApplicationInvestigation.test.ts b/src/domain/mobileApplicationInvestigation.test.ts similarity index 98% rename from tests/acceptance/investigations/mobileApplicationInvestigation.test.ts rename to src/domain/mobileApplicationInvestigation.test.ts index 6f6d984e9..d528793fc 100644 --- a/tests/acceptance/investigations/mobileApplicationInvestigation.test.ts +++ b/src/domain/mobileApplicationInvestigation.test.ts @@ -13,7 +13,7 @@ import { type SigningInfo, type NativeLibrary, type MobileResource, -} from "../../../src/domain/mobileApplicationInvestigation.js"; +} from "./mobileApplicationInvestigation.js"; describe("mobile application investigation", () => { it("detects mobile format from extension", () => { diff --git a/tests/boundary/providers/native/objcSwiftMetadata.test.ts b/src/domain/objcSwiftMetadata.test.ts similarity index 79% rename from tests/boundary/providers/native/objcSwiftMetadata.test.ts rename to src/domain/objcSwiftMetadata.test.ts index 4d68bb5f6..873007e43 100644 --- a/tests/boundary/providers/native/objcSwiftMetadata.test.ts +++ b/src/domain/objcSwiftMetadata.test.ts @@ -10,14 +10,10 @@ import { swiftDeclsByKind, type ObjcClass, type ObjcMethod, - type ObjcProperty, - type DbOperation, - type ObjcSwiftMetadata, type ObjcProtocol, - type ObjcPropertyAttribute, type SwiftDecl, type DbSaveResult, -} from "../../../../src/domain/objcSwiftMetadata.js"; +} from "./objcSwiftMetadata.js"; describe("ObjC/Swift metadata", () => { it("identifies getter selectors", () => { @@ -105,25 +101,4 @@ describe("ObjC/Swift metadata", () => { const result = objcSwiftMetadataSchema.safeParse(meta); expect(result.success).toBe(true); }); - - it("uses all exported types", () => { - const prop: ObjcProperty = { - name: "title", - type_encoding: 'T@"NSString"&,N,V_title', - attributes: [] as ObjcPropertyAttribute[], - is_readonly: false, - getter: "title", - setter: "setTitle:", - }; - expect(prop.name).toBe("title"); - const op: DbOperation = "save"; - expect(op).toBe("save"); - const meta: ObjcSwiftMetadata = { - objc_classes: [], - objc_protocols: [], - swift_decls: [], - db_save_result: null, - }; - expect(meta.objc_classes).toHaveLength(0); - }); }); diff --git a/tests/acceptance/analysis/packageAnalysis.test.ts b/src/domain/packageAnalysis.test.ts similarity index 93% rename from tests/acceptance/analysis/packageAnalysis.test.ts rename to src/domain/packageAnalysis.test.ts index 88d10830c..d33aa0a98 100644 --- a/tests/acceptance/analysis/packageAnalysis.test.ts +++ b/src/domain/packageAnalysis.test.ts @@ -10,8 +10,7 @@ import { resourcesByType, totalResourceSize, type PackageManifest, - type DigitalSignature, -} from "../../../src/domain/packageAnalysis.js"; +} from "./packageAnalysis.js"; describe("package analysis", () => { it("detects package format from extension", () => { @@ -99,9 +98,4 @@ describe("package analysis", () => { }; expect(hasValidSignature(unsigned)).toBe(false); }); - - it("uses DigitalSignature type", () => { - const sig: DigitalSignature = validManifest.signature; - expect(sig.is_signed).toBe(true); - }); }); diff --git a/src/domain/packageAnalysis.ts b/src/domain/packageAnalysis.ts index fe917cf1a..4eed30652 100644 --- a/src/domain/packageAnalysis.ts +++ b/src/domain/packageAnalysis.ts @@ -44,7 +44,6 @@ export const digitalSignatureSchema = z.strictObject({ /** Timestamp signer if present. */ timestamp_signer: z.string().nullable(), }); -export type DigitalSignature = z.infer; /** A Windows package manifest for static analysis. */ export const packageManifestSchema = z.strictObject({ diff --git a/tests/boundary/providers/managed/peInspection.test.ts b/src/domain/peInspection.test.ts similarity index 86% rename from tests/boundary/providers/managed/peInspection.test.ts rename to src/domain/peInspection.test.ts index 694d0a9ad..c129fee24 100644 --- a/tests/boundary/providers/managed/peInspection.test.ts +++ b/src/domain/peInspection.test.ts @@ -11,13 +11,7 @@ import { parseSectionCharacteristics, peManifestSchema, type PeManifest, - type PeSection, - type PeImport, - type PeExport, - type PeResource, - type PeDebugEntry, - type PeTlsCallback, -} from "../../../../src/domain/peInspection.js"; +} from "./peInspection.js"; const validManifest: PeManifest = { dos_magic: 0x5a4d, @@ -196,19 +190,4 @@ describe("PE/COFF static inspection", () => { it("counts executable sections", () => { expect(countExecutableSections(validManifest)).toBe(1); }); - - it("uses all exported types", () => { - const section: PeSection = validManifest.sections[0]!; - expect(section.name).toBe(".text"); - const imp: PeImport = validManifest.imports[0]!; - expect(imp.dll).toBe("KERNEL32.DLL"); - const exp: PeExport = validManifest.exports[0]!; - expect(exp.name).toBe("DllMain"); - const res: PeResource = validManifest.resources[0]!; - expect(res.type).toBe("RT_VERSION"); - const dbg: PeDebugEntry = validManifest.debug_entries[0]!; - expect(dbg.type).toBe("CODEVIEW"); - const tls: PeTlsCallback = validManifest.tls_callbacks[0]!; - expect(tls.index).toBe(0); - }); }); diff --git a/src/domain/peInspection.ts b/src/domain/peInspection.ts index cd7193aa6..39ac4deb7 100644 --- a/src/domain/peInspection.ts +++ b/src/domain/peInspection.ts @@ -25,7 +25,6 @@ export const peSectionSchema = z.strictObject({ raw_offset: z.number().int().nonnegative(), characteristics: sectionCharacteristicsSchema, }); -export type PeSection = z.infer; /** PE import entry. */ export const peImportSchema = z.strictObject({ @@ -34,7 +33,6 @@ export const peImportSchema = z.strictObject({ ordinal: z.number().int().nullable(), is_delay_import: z.boolean().default(false), }); -export type PeImport = z.infer; /** PE export entry. */ export const peExportSchema = z.strictObject({ @@ -43,7 +41,6 @@ export const peExportSchema = z.strictObject({ rva: z.number().int().nonnegative(), forwarder: z.string().nullable(), }); -export type PeExport = z.infer; /** PE/COFF resource entry. */ export const peResourceSchema = z.strictObject({ @@ -53,7 +50,6 @@ export const peResourceSchema = z.strictObject({ rva: z.number().int().nonnegative(), size: z.number().int().nonnegative(), }); -export type PeResource = z.infer; /** PE debug directory entry. */ export const peDebugEntrySchema = z.strictObject({ @@ -67,14 +63,12 @@ export const peDebugEntrySchema = z.strictObject({ /** PDB path if this is a CodeView entry. */ pdb_path: z.string().nullable(), }); -export type PeDebugEntry = z.infer; /** TLS callback entry. */ export const peTlsCallbackSchema = z.strictObject({ callback_rva: z.number().int().nonnegative(), index: z.number().int().nonnegative(), }); -export type PeTlsCallback = z.infer; /** Architecture/machine type. */ export const peMachineTypeSchema = z.enum([ diff --git a/tests/boundary/process/processCaptureComparison.test.ts b/src/domain/processCapture.comparison.test.ts similarity index 99% rename from tests/boundary/process/processCaptureComparison.test.ts rename to src/domain/processCapture.comparison.test.ts index d53e1e148..0cad15d7d 100644 --- a/tests/boundary/process/processCaptureComparison.test.ts +++ b/src/domain/processCapture.comparison.test.ts @@ -3,7 +3,7 @@ import { expect, it } from "vitest"; import { compareUnverifiedProcessCaptures as compareProcessCaptures, emptyProcessCapture as emptyCapture, -} from "../../fixtures/processCapture.js"; +} from "./processCapture.fixture.js"; it("classifies missing observations as unknown and one-sided evidence as added", () => { const base = { diff --git a/tests/fixtures/processCapture.ts b/src/domain/processCapture.fixture.ts similarity index 68% rename from tests/fixtures/processCapture.ts rename to src/domain/processCapture.fixture.ts index c03439976..eaca915ae 100644 --- a/tests/fixtures/processCapture.ts +++ b/src/domain/processCapture.fixture.ts @@ -1,17 +1,22 @@ -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../src/contracts/processCaptureExample.js"; import { compareProcessCaptures, parseProcessCapture, processCaptureSchema, type UnverifiedProcessCapture, -} from "../../src/domain/processCapture.js"; +} from "./processCapture.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js"; +export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js"; + +/** Parse a detached valid empty capture for tests that need trusted evidence. */ export const emptyProcessCapture = () => parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE); +/** Parse a mutable unverified capture for boundary-validation tests. */ export const emptyUnverifiedProcessCapture = (): UnverifiedProcessCapture => processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE); +/** Project validation failures without coupling tests to Zod internals. */ export const processCaptureIssues = ( capture: UnverifiedProcessCapture, ): readonly { readonly path: string; readonly message: string }[] => { @@ -24,6 +29,7 @@ export const processCaptureIssues = ( })); }; +/** Compare unverified fixture values through the production parser. */ export const compareUnverifiedProcessCaptures = ( left: UnverifiedProcessCapture, right: UnverifiedProcessCapture, diff --git a/tests/boundary/process/processCaptureTraceProjection.test.ts b/src/domain/processCapture.traceProjection.test.ts similarity index 96% rename from tests/boundary/process/processCaptureTraceProjection.test.ts rename to src/domain/processCapture.traceProjection.test.ts index 73717fd93..a2190ed72 100644 --- a/tests/boundary/process/processCaptureTraceProjection.test.ts +++ b/src/domain/processCapture.traceProjection.test.ts @@ -1,10 +1,10 @@ import { expect, it } from "vitest"; -import { digestProcessCommitment } from "../../../src/domain/processCapture.js"; +import { digestProcessCommitment } from "./processCapture.js"; import { compareUnverifiedProcessCaptures as compareProcessCaptures, emptyProcessCapture as emptyCapture, -} from "../../fixtures/processCapture.js"; +} from "./processCapture.fixture.js"; it("detects changes in normalized process sample metadata", () => { const capture = { diff --git a/tests/boundary/process/processCaptureValidation.test.ts b/src/domain/processCapture.validation.test.ts similarity index 83% rename from tests/boundary/process/processCaptureValidation.test.ts rename to src/domain/processCapture.validation.test.ts index 19391ac30..72a94f3a9 100644 --- a/tests/boundary/process/processCaptureValidation.test.ts +++ b/src/domain/processCapture.validation.test.ts @@ -1,19 +1,17 @@ import { expect, it } from "vitest"; -import { createProcessCaptureJournal } from "../../../src/application/ProcessCaptureJournal.js"; import { digestProcessCommitment, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessCapture, processCaptureSchema, - type ProcessCaptureEventJournalEntry, type UnverifiedProcessCapture, -} from "../../../src/domain/processCapture.js"; +} from "./processCapture.js"; import { compareUnverifiedProcessCaptures as compareProcessCaptures, emptyUnverifiedProcessCapture as emptyCapture, processCaptureIssues, -} from "../../fixtures/processCapture.js"; +} from "./processCapture.fixture.js"; it("never considers truncated captures equivalent", () => { const capture = { @@ -102,33 +100,6 @@ it("rejects settlement and cleanup combinations that cannot occur", () => { }); it("accepts old captures without a journal and validates complete journals", () => { - const journal = createProcessCaptureJournal(); - const observed: ProcessCaptureEventJournalEntry[] = []; - const unsubscribe = journal.subscribe((entry) => observed.push(entry)); - journal.recordEvent("lifecycle", 0); - const settlementEntry = journal.record("lifecycle", 1); - unsubscribe(); - journal.recordEvent("frames", 0); - expect(journal.entries).toEqual([ - { capture_order: 0, collection: "lifecycle", index: 0 }, - { capture_order: 1, collection: "lifecycle", index: 1 }, - { capture_order: 2, collection: "frames", index: 0 }, - ]); - expect(observed).toEqual(journal.entries.slice(0, 2)); - expect(settlementEntry).toEqual(journal.entries[1]); - - const nestedJournal = createProcessCaptureJournal(); - const notifications: string[] = []; - nestedJournal.subscribe((entry) => { - notifications.push(`a:${String(entry.capture_order)}`); - if (entry.capture_order === 0) nestedJournal.record("frames", 1); - }); - nestedJournal.subscribe((entry) => - notifications.push(`b:${String(entry.capture_order)}`), - ); - nestedJournal.record("frames", 0); - expect(notifications).toEqual(["a:0", "b:0", "a:1", "b:1"]); - const capture = emptyCapture(); const { event_journal: _eventJournal, ...oldCapture } = capture; expect(parseProcessCapture(oldCapture).event_journal).toEqual([]); diff --git a/src/domain/processCaptureExample.ts b/src/domain/processCaptureExample.ts new file mode 100644 index 000000000..70d0971b2 --- /dev/null +++ b/src/domain/processCaptureExample.ts @@ -0,0 +1,72 @@ +import type { JsonValue } from "./jsonValue.js"; +import { digestProcessCommitment } from "./processCapture.js"; + +const normalization = { + paths: true, + pids: true, + ports: true, + time_bucket_ms: 10, + patterns: [], +}; +const scenario = { executable_sha256: "0".repeat(64) }; +const comparisonContract = {}; +const shimPlan: JsonValue[] = []; +const replayPlan = {}; + +/** Minimal valid process capture retained as a public contract example. */ +export const EMPTY_PROCESS_CAPTURE_EXAMPLE = { + schema_version: 4, + manifest: { + rea_version: "1.1.0", + provider_version: "3", + platform: "fixture", + architecture: "fixture", + pty_backend: "node-pty", + started_at: "2026-01-01T00:00:00.000Z", + completed_at: "2026-01-01T00:00:00.001Z", + scenario, + comparison_contract: comparisonContract, + shim_plan: shimPlan, + replay_plan: replayPlan, + full_scenario_sha256: digestProcessCommitment(scenario), + comparison_contract_sha256: digestProcessCommitment(comparisonContract), + executable_sha256: "0".repeat(64), + normalization_sha256: digestProcessCommitment(normalization), + shim_plan_sha256: digestProcessCommitment(shimPlan), + replay_plan_sha256: digestProcessCommitment(replayPlan), + }, + normalization, + frames: [], + rendered_frames: [], + interaction_events: [], + exit: { code: 0, signal: null, reason: "exited" }, + settlement: { + state: "quiesced", + elapsed_ms: 50, + cleanup_outcome: "not_required", + }, + process_samples: [], + filesystem_checkpoints: [ + { name: "before", at_ms: 0, files: [], effects: [], truncated: false }, + { + name: "after_settlement", + at_ms: 50, + files: [], + effects: [], + truncated: false, + }, + ], + shim_events: [], + protocol_events: [], + replay_transitions: [], + files_before: [], + files_after: [], + filesystem_effects: [], + truncated: false, + limitations: [], + residual_unknowns: [], + cleanup: { + owned_process_group: "verified", + temporary_root: "removed", + }, +} satisfies JsonValue; diff --git a/tests/boundary/process/processObservation.test.ts b/src/domain/processObservation.test.ts similarity index 95% rename from tests/boundary/process/processObservation.test.ts rename to src/domain/processObservation.test.ts index f62f97da7..89b50a1bb 100644 --- a/tests/boundary/process/processObservation.test.ts +++ b/src/domain/processObservation.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js"; import { processCaptureSchema, type UnverifiedProcessCapture, -} from "../../../src/domain/processCapture.js"; +} from "./processCapture.js"; import { createProcessObservation, projectProcessObservation, -} from "../../../src/domain/processObservation.js"; +} from "./processObservation.js"; const emptyCapture = processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE); diff --git a/src/domain/processTraceAdapters.test.ts b/src/domain/processTraceAdapters.test.ts index 83f056631..c807e8699 100644 --- a/src/domain/processTraceAdapters.test.ts +++ b/src/domain/processTraceAdapters.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js"; import { compareProcessCaptures, parseProcessCapture, diff --git a/src/domain/processTraceComparison.test.ts b/src/domain/processTraceComparison.test.ts index 699343e14..cdcd95185 100644 --- a/src/domain/processTraceComparison.test.ts +++ b/src/domain/processTraceComparison.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js"; import { parseProcessCapture, type ProcessCapture } from "./processCapture.js"; import { compareProcessTraces, diff --git a/src/domain/processTraceComparisonPart2.test.ts b/src/domain/processTraceComparisonPart2.test.ts index b28d0cd47..13b37fe69 100644 --- a/src/domain/processTraceComparisonPart2.test.ts +++ b/src/domain/processTraceComparisonPart2.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js"; import { parseProcessCapture, type ProcessCapture } from "./processCapture.js"; import { compareProcessTraces, diff --git a/tests/boundary/browser/protocolCapture.test.ts b/src/domain/protocolCapture.test.ts similarity index 93% rename from tests/boundary/browser/protocolCapture.test.ts rename to src/domain/protocolCapture.test.ts index b40263acb..bf2baac1e 100644 --- a/tests/boundary/browser/protocolCapture.test.ts +++ b/src/domain/protocolCapture.test.ts @@ -9,8 +9,7 @@ import { looksLikeCredential, protocolCaptureSchema, type ProtocolMessage, - type ProtocolCapture, -} from "../../../src/domain/protocolCapture.js"; +} from "./protocolCapture.js"; describe("protocol capture", () => { it("classifies gRPC from content type", () => { @@ -179,19 +178,6 @@ describe("schema inference", () => { }); }); -describe("uses ProtocolCapture type", () => { - it("validates a well-formed capture typed value", () => { - const capture: ProtocolCapture = { - family: "grpc", - messages: [], - inferred_schema: [], - has_truncated: false, - credentials_detected: false, - }; - expect(capture.family).toBe("grpc"); - }); -}); - describe("protocol capture schema validation", () => { it("validates a well-formed capture", () => { const capture = { diff --git a/src/domain/protocolCapture.ts b/src/domain/protocolCapture.ts index 9012e62d5..671ab7d8e 100644 --- a/src/domain/protocolCapture.ts +++ b/src/domain/protocolCapture.ts @@ -77,7 +77,6 @@ export const protocolCaptureSchema = z.strictObject({ /** Whether credentials were detected and redacted. */ credentials_detected: z.boolean().default(false), }); -export type ProtocolCapture = z.infer; /** Classification of a byte sequence as a known protocol family. */ export function classifyProtocolFamily( diff --git a/src/domain/reconstructionVerification.test.ts b/src/domain/reconstructionVerification.test.ts index e7b59272e..b1ba1f125 100644 --- a/src/domain/reconstructionVerification.test.ts +++ b/src/domain/reconstructionVerification.test.ts @@ -7,7 +7,7 @@ import { verifyReconstruction, } from "./reconstructionVerification.js"; import type { JsonValue } from "./jsonValue.js"; -import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js"; +import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js"; import { createResidualUnknown } from "./residualUnknown.js"; diff --git a/tests/boundary/providers/replay/replayMachineRun.test.ts b/src/domain/replayMachineRun.test.ts similarity index 99% rename from tests/boundary/providers/replay/replayMachineRun.test.ts rename to src/domain/replayMachineRun.test.ts index 1bfc33957..513af6f12 100644 --- a/tests/boundary/providers/replay/replayMachineRun.test.ts +++ b/src/domain/replayMachineRun.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { replayMachineRunInputSchema, runReplayMachine, -} from "../../../../src/domain/replayMachineRun.js"; +} from "./replayMachineRun.js"; const loginRun = () => replayMachineRunInputSchema.parse({ diff --git a/tests/boundary/providers/replay/replayMachineRuntimeCaptures.test.ts b/src/domain/replayMachineRuntime.captures.test.ts similarity index 98% rename from tests/boundary/providers/replay/replayMachineRuntimeCaptures.test.ts rename to src/domain/replayMachineRuntime.captures.test.ts index 5e5e87978..9953b5e41 100644 --- a/tests/boundary/providers/replay/replayMachineRuntimeCaptures.test.ts +++ b/src/domain/replayMachineRuntime.captures.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; -import { replayMachineSchema } from "../../../../src/domain/replayMachine.js"; -import { ReplayMachineRuntime } from "../../../../src/domain/replayMachineRuntime.js"; +import { replayMachineSchema } from "./replayMachine.js"; +import { ReplayMachineRuntime } from "./replayMachineRuntime.js"; const machine = replayMachineSchema.parse({ initial_state: "login", diff --git a/tests/boundary/providers/replay/replayMachineRuntime.test.ts b/src/domain/replayMachineRuntime.test.ts similarity index 98% rename from tests/boundary/providers/replay/replayMachineRuntime.test.ts rename to src/domain/replayMachineRuntime.test.ts index 87a59349d..f305423aa 100644 --- a/tests/boundary/providers/replay/replayMachineRuntime.test.ts +++ b/src/domain/replayMachineRuntime.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest"; -import { replayMachineSchema } from "../../../../src/domain/replayMachine.js"; -import { ReplayMachineRuntime } from "../../../../src/domain/replayMachineRuntime.js"; +import { replayMachineSchema } from "./replayMachine.js"; +import { ReplayMachineRuntime } from "./replayMachineRuntime.js"; const machine = replayMachineSchema.parse({ initial_state: "login", diff --git a/src/dotnet/ManagedArtifactFailures.test.ts b/src/dotnet/ManagedArtifactFailures.test.ts new file mode 100644 index 000000000..c35e7901b --- /dev/null +++ b/src/dotnet/ManagedArtifactFailures.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; + +import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js"; +import { + buildManagedPeFixture, + buildNativePeFixture, + managedPeFixtureTarget, + MANAGED_ARTIFACT_FIXTURE_LIMITS, +} from "./ManagedPe.fixture.js"; + +describe("managed artifact failure classification", () => { + it("distinguishes native, malformed, unsupported, and bounded metadata", () => { + const nativeBytes = buildNativePeFixture(); + const native = inspectManagedArtifactBytes( + nativeBytes, + managedPeFixtureTarget(nativeBytes), + MANAGED_ARTIFACT_FIXTURE_LIMITS, + ); + expect(native).toMatchObject({ + classification: { status: "not-managed" }, + metadata: { status: "absent" }, + coverage: { state: "unavailable" }, + }); + + const malformedBytes = buildManagedPeFixture({ + corruptMetadataSignature: true, + }); + const malformed = inspectManagedArtifactBytes( + malformedBytes, + managedPeFixtureTarget(malformedBytes), + MANAGED_ARTIFACT_FIXTURE_LIMITS, + ); + expect(malformed.classification.status).toBe("malformed"); + expect(malformed.coverage.issues).toEqual([ + expect.objectContaining({ code: "invalid-metadata-root" }), + ]); + + const unsupportedTableBytes = buildManagedPeFixture({ + metadataValidMaskExtra: 1n << 50n, + }); + const unsupportedTable = inspectManagedArtifactBytes( + unsupportedTableBytes, + managedPeFixtureTarget(unsupportedTableBytes), + MANAGED_ARTIFACT_FIXTURE_LIMITS, + ); + expect(unsupportedTable.coverage.issues).toEqual([ + expect.objectContaining({ code: "invalid-tables" }), + ]); + + const limitedBytes = buildManagedPeFixture(); + const limited = inspectManagedArtifactBytes( + limitedBytes, + managedPeFixtureTarget(limitedBytes), + { ...MANAGED_ARTIFACT_FIXTURE_LIMITS, maxMetadataBytes: 256 }, + ); + expect(limited.metadata.status).toBe("partial"); + expect(limited.coverage.issues).toEqual([ + expect.objectContaining({ code: "limit-exceeded" }), + ]); + }); +}); diff --git a/tests/boundary/providers/managed/managedArtifactProvider.inventory.test.ts b/src/dotnet/ManagedArtifactInspector.test.ts similarity index 62% rename from tests/boundary/providers/managed/managedArtifactProvider.inventory.test.ts rename to src/dotnet/ManagedArtifactInspector.test.ts index 7b574d024..4d078cdcb 100644 --- a/tests/boundary/providers/managed/managedArtifactProvider.inventory.test.ts +++ b/src/dotnet/ManagedArtifactInspector.test.ts @@ -2,60 +2,26 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; -import { inspectManagedArtifactBytes } from "../../../../src/dotnet/ManagedArtifactInspector.js"; -import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { inspectManagedNativeBoundariesBytes } from "../../../../src/dotnet/ManagedNativeBoundaryInspector.js"; -import { buildManagedPeFixture } from "../../../fixtures/managedPe.js"; - -const limits = { - referenceOffset: 0, - referenceLimit: 100, - resourceOffset: 0, - resourceLimit: 100, - attributeOffset: 0, - attributeLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, -}; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; - -const nativeBoundaryLimits = { - moduleRefOffset: 0, - moduleRefLimit: 100, - importOffset: 0, - importLimit: 100, - implementationOffset: 0, - implementationLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, -}; +import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js"; +import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; +import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js"; +import { + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_ARTIFACT_FIXTURE_LIMITS, + MANAGED_MEMBER_FIXTURE_LIMITS, + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, +} from "./ManagedPe.fixture.js"; describe("managed artifact inventory", () => { it("inventories module, assembly, framework, references, and resources without loading CLR code", () => { const resource = Buffer.from("source-owned resource"); const bytes = buildManagedPeFixture({ resourceData: resource }); - const result = inspectManagedArtifactBytes(bytes, target(bytes), limits); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + MANAGED_ARTIFACT_FIXTURE_LIMITS, + ); expect(result.classification).toMatchObject({ status: "managed", @@ -100,10 +66,14 @@ describe("managed artifact inventory", () => { const bytes = buildManagedPeFixture({ references: ["System.Runtime", "UnityEngine.CoreModule"], }); - const result = inspectManagedArtifactBytes(bytes, target(bytes), { - ...limits, - referenceLimit: 1, - }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + { + ...MANAGED_ARTIFACT_FIXTURE_LIMITS, + referenceLimit: 1, + }, + ); expect(result.classification.runtime_family).toBe("unity-mono"); expect(result.references).toMatchObject({ @@ -119,16 +89,20 @@ describe("managed artifact inventory", () => { const bytes = buildManagedPeFixture({ mvid: Buffer.from("3aebc60edc4a544b1f458b4ed40b33b1", "hex"), }); - const result = inspectManagedArtifactBytes(bytes, target(bytes), limits); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + MANAGED_ARTIFACT_FIXTURE_LIMITS, + ); const members = inspectManagedMembersBytes( bytes, - target(bytes), - memberLimits, + managedPeFixtureTarget(bytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); const boundaries = inspectManagedNativeBoundariesBytes( bytes, - target(bytes), - nativeBoundaryLimits, + managedPeFixtureTarget(bytes), + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, ); expect(result.module?.mvid).toBe("0ec6eb3a-4adc-4b54-1f45-8b4ed40b33b1"); @@ -146,8 +120,8 @@ describe("managed artifact inventory", () => { }); const result = inspectManagedMembersBytes( bytes, - target(bytes), - memberLimits, + managedPeFixtureTarget(bytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); expect(result.fields.items[0]?.signature).toMatchObject({ @@ -161,13 +135,3 @@ describe("managed artifact inventory", () => { }); }); }); -const target = (bytes: Buffer): BinaryTarget => ({ - path: "/fixture.exe", - sha256: createHash("sha256").update(bytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}); diff --git a/tests/boundary/providers/managed/managedArtifactProvider.cilDecoding.test.ts b/src/dotnet/ManagedCilDecoding.test.ts similarity index 73% rename from tests/boundary/providers/managed/managedArtifactProvider.cilDecoding.test.ts rename to src/dotnet/ManagedCilDecoding.test.ts index 6d233664c..d61f20b77 100644 --- a/tests/boundary/providers/managed/managedArtifactProvider.cilDecoding.test.ts +++ b/src/dotnet/ManagedCilDecoding.test.ts @@ -2,28 +2,12 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; -import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { buildManagedPeFixture } from "../../../fixtures/managedPe.js"; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; +import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; +import { + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_MEMBER_FIXTURE_LIMITS, +} from "./ManagedPe.fixture.js"; describe("managed CIL decoding", () => { it("reads fat method header size from the full flags-and-size word", () => { @@ -41,8 +25,8 @@ describe("managed CIL decoding", () => { }); const result = inspectManagedMembersBytes( bytes, - target(bytes), - memberLimits, + managedPeFixtureTarget(bytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); expect(result.methods.items[0]?.body).toMatchObject({ @@ -68,8 +52,8 @@ describe("managed CIL decoding", () => { }); const result = inspectManagedMembersBytes( bytes, - target(bytes), - memberLimits, + managedPeFixtureTarget(bytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); expect(result.methods.items[0]?.body).toMatchObject({ @@ -96,13 +80,13 @@ describe("managed CIL decoding", () => { }); const tiny = inspectManagedMembersBytes( tinyBytes, - target(tinyBytes), - memberLimits, + managedPeFixtureTarget(tinyBytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); const fat = inspectManagedMembersBytes( fatBytes, - target(fatBytes), - memberLimits, + managedPeFixtureTarget(fatBytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); expect(tiny.methods.items[0]?.body).toMatchObject({ @@ -128,13 +112,3 @@ describe("managed CIL decoding", () => { }); }); }); -const target = (bytes: Buffer): BinaryTarget => ({ - path: "/fixture.exe", - sha256: createHash("sha256").update(bytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}); diff --git a/tests/boundary/providers/managed/managedArtifactProvider.cilLimits.test.ts b/src/dotnet/ManagedCilLimits.test.ts similarity index 53% rename from tests/boundary/providers/managed/managedArtifactProvider.cilLimits.test.ts rename to src/dotnet/ManagedCilLimits.test.ts index e67e7c01f..577ee0d83 100644 --- a/tests/boundary/providers/managed/managedArtifactProvider.cilLimits.test.ts +++ b/src/dotnet/ManagedCilLimits.test.ts @@ -2,36 +2,24 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; -import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { buildManagedPeFixture } from "../../../fixtures/managedPe.js"; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; +import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; +import { + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_MEMBER_FIXTURE_LIMITS, +} from "./ManagedPe.fixture.js"; describe("managed CIL limits", () => { it("marks instruction-limited CIL as partial without assigning normalized identity", () => { const bytes = buildManagedPeFixture(); - const result = inspectManagedMembersBytes(bytes, target(bytes), { - ...memberLimits, - maxMethodInstructions: 1, - }); + const result = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + { + ...MANAGED_MEMBER_FIXTURE_LIMITS, + maxMethodInstructions: 1, + }, + ); const il = bytes.subarray(0x0a01, 0x0a0d); expect(result.metadata.status).toBe("complete"); @@ -63,13 +51,3 @@ describe("managed CIL limits", () => { ); }); }); -const target = (bytes: Buffer): BinaryTarget => ({ - path: "/fixture.exe", - sha256: createHash("sha256").update(bytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}); diff --git a/tests/boundary/providers/managed/managedArtifactProvider.members.test.ts b/src/dotnet/ManagedMemberInspector.test.ts similarity index 74% rename from tests/boundary/providers/managed/managedArtifactProvider.members.test.ts rename to src/dotnet/ManagedMemberInspector.test.ts index 8ef73935f..64c134233 100644 --- a/tests/boundary/providers/managed/managedArtifactProvider.members.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -1,42 +1,24 @@ -import { createHash } from "node:crypto"; - import { describe, expect, it } from "vitest"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; -import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { buildManagedPeFixture } from "../../../fixtures/managedPe.js"; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; +import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; +import { + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_MEMBER_FIXTURE_LIMITS, +} from "./ManagedPe.fixture.js"; describe("managed member inspection", () => { it("inspects metadata members, signatures, CIL hashes, call edges, and field anchors", () => { const bytes = buildManagedPeFixture(); const result = inspectManagedMembersBytes( bytes, - target(bytes), - memberLimits, + managedPeFixtureTarget(bytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); expect(result.identity_scope).toEqual({ token_identity: "build-local", - requires_artifact_sha256: target(bytes).sha256, + requires_artifact_sha256: managedPeFixtureTarget(bytes).sha256, requires_mvid: "00112233-4455-6677-8899-aabbccddeeff", }); expect(result.types.items).toEqual([ @@ -130,13 +112,3 @@ describe("managed member inspection", () => { expect(result.coverage).toMatchObject({ state: "complete", issues: [] }); }); }); -const target = (bytes: Buffer): BinaryTarget => ({ - path: "/fixture.exe", - sha256: createHash("sha256").update(bytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}); diff --git a/tests/boundary/providers/managed/managedArtifactProvider.nativeBoundaries.test.ts b/src/dotnet/ManagedNativeBoundaryInspector.test.ts similarity index 65% rename from tests/boundary/providers/managed/managedArtifactProvider.nativeBoundaries.test.ts rename to src/dotnet/ManagedNativeBoundaryInspector.test.ts index db914b6d2..b84b7f47a 100644 --- a/tests/boundary/providers/managed/managedArtifactProvider.nativeBoundaries.test.ts +++ b/src/dotnet/ManagedNativeBoundaryInspector.test.ts @@ -1,45 +1,14 @@ -import { createHash } from "node:crypto"; - import { describe, expect, it } from "vitest"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; -import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { inspectManagedNativeBoundariesBytes } from "../../../../src/dotnet/ManagedNativeBoundaryInspector.js"; +import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js"; +import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js"; import { buildManagedPeFixture, buildNativePeFixture, -} from "../../../fixtures/managedPe.js"; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; - -const nativeBoundaryLimits = { - moduleRefOffset: 0, - moduleRefLimit: 100, - importOffset: 0, - importLimit: 100, - implementationOffset: 0, - implementationLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, -}; + managedPeFixtureTarget, + MANAGED_MEMBER_FIXTURE_LIMITS, + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, +} from "./ManagedPe.fixture.js"; describe("managed native boundaries", () => { it("inspects managed/native PInvoke declarations without verifying native exports", () => { @@ -53,8 +22,8 @@ describe("managed native boundaries", () => { }); const result = inspectManagedNativeBoundariesBytes( bytes, - target(bytes), - nativeBoundaryLimits, + managedPeFixtureTarget(bytes), + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, ); expect(result.cli_native).toMatchObject({ @@ -109,11 +78,15 @@ describe("managed native boundaries", () => { it("keeps member evidence bounded and typed for pagination and unavailable metadata", () => { const bytes = buildManagedPeFixture(); - const paged = inspectManagedMembersBytes(bytes, target(bytes), { - ...memberLimits, - methodLimit: 0 + 1, - instructionAnchorLimit: 1, - }); + const paged = inspectManagedMembersBytes( + bytes, + managedPeFixtureTarget(bytes), + { + ...MANAGED_MEMBER_FIXTURE_LIMITS, + methodLimit: 0 + 1, + instructionAnchorLimit: 1, + }, + ); expect(paged.methods).toMatchObject({ total: 1, returned: 1, @@ -124,8 +97,8 @@ describe("managed native boundaries", () => { const nativeBytes = buildNativePeFixture(); const native = inspectManagedMembersBytes( nativeBytes, - target(nativeBytes), - memberLimits, + managedPeFixtureTarget(nativeBytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); expect(native.metadata.status).toBe("absent"); expect(native.coverage.state).toBe("unavailable"); @@ -135,8 +108,8 @@ describe("managed native boundaries", () => { }); const malformed = inspectManagedMembersBytes( malformedBytes, - target(malformedBytes), - memberLimits, + managedPeFixtureTarget(malformedBytes), + MANAGED_MEMBER_FIXTURE_LIMITS, ); expect(malformed.metadata.status).toBe("malformed"); expect(malformed.coverage.issues).toEqual([ @@ -144,13 +117,3 @@ describe("managed native boundaries", () => { ]); }); }); -const target = (bytes: Buffer): BinaryTarget => ({ - path: "/fixture.exe", - sha256: createHash("sha256").update(bytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}); diff --git a/tests/fixtures/managedPe.ts b/src/dotnet/ManagedPe.fixture.ts similarity index 87% rename from tests/fixtures/managedPe.ts rename to src/dotnet/ManagedPe.fixture.ts index 04d654062..a5f9725a6 100644 --- a/tests/fixtures/managedPe.ts +++ b/src/dotnet/ManagedPe.fixture.ts @@ -1,3 +1,7 @@ +import { createHash } from "node:crypto"; + +import type { BinaryTarget } from "../domain/binaryTarget.js"; + interface ManagedPeFixtureOptions { readonly cliFlags?: number; readonly corruptMetadataSignature?: boolean; @@ -69,6 +73,73 @@ const DEFAULT_MVID = Buffer.from([ 0xdd, 0xee, 0xff, ]); +/** Generous bounds for inventory assertions over the source-owned PE fixture. */ +export const MANAGED_ARTIFACT_FIXTURE_LIMITS = { + referenceOffset: 0, + referenceLimit: 100, + resourceOffset: 0, + resourceLimit: 100, + attributeOffset: 0, + attributeLimit: 100, + maxMetadataBytes: 1024 * 1024, + maxTableRows: 1_000, + maxHeapItemBytes: 1024 * 1024, +}; + +/** Generous bounds for member assertions over the source-owned PE fixture. */ +export const MANAGED_MEMBER_FIXTURE_LIMITS = { + typeOffset: 0, + typeLimit: 100, + methodOffset: 0, + methodLimit: 100, + fieldOffset: 0, + fieldLimit: 100, + memberRefOffset: 0, + memberRefLimit: 100, + edgeOffset: 0, + edgeLimit: 100, + instructionAnchorLimit: 100, + maxMetadataBytes: 1024 * 1024, + maxTableRows: 1_000, + maxHeapItemBytes: 1024 * 1024, + maxMethodBodyBytes: 1024 * 1024, + maxMethodInstructions: 1_000, +}; + +/** Member bounds for fixture tests that inspect a real local path. */ +export const MANAGED_MEMBER_PATH_FIXTURE_LIMITS = { + ...MANAGED_MEMBER_FIXTURE_LIMITS, + maxFileBytes: 1024 * 1024, +}; + +/** Generous native-boundary bounds for the source-owned PE fixture. */ +export const MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS = { + moduleRefOffset: 0, + moduleRefLimit: 100, + importOffset: 0, + importLimit: 100, + implementationOffset: 0, + implementationLimit: 100, + maxMetadataBytes: 1024 * 1024, + maxTableRows: 1_000, + maxHeapItemBytes: 1024 * 1024, +}; + +/** Describe fixture bytes as the managed PE target consumed by static inspectors. */ +export const managedPeFixtureTarget = ( + bytes: Buffer, + path = "/fixture.exe", +): BinaryTarget => ({ + path, + sha256: createHash("sha256").update(bytes).digest("hex"), + kind: "executable", + format: "pe", + architecture: "x86", + availableArchitectures: ["x86"], + executableRole: "application", + managed: true, +}); + const guidHeap = (mvid: Buffer = DEFAULT_MVID): Buffer => { if (mvid.length !== 16) throw new RangeError("Managed PE fixture MVID must be 16 bytes"); diff --git a/tests/boundary/providers/ghidra/ghidraDoctor.test.ts b/src/ghidra/GhidraDoctor.test.ts similarity index 75% rename from tests/boundary/providers/ghidra/ghidraDoctor.test.ts rename to src/ghidra/GhidraDoctor.test.ts index efcd71c75..b97c7b3b7 100644 --- a/tests/boundary/providers/ghidra/ghidraDoctor.test.ts +++ b/src/ghidra/GhidraDoctor.test.ts @@ -1,17 +1,13 @@ import { describe, expect, it } from "vitest"; -import { - runDoctor, - type DoctorHost, -} from "../../../../src/application/Doctor.js"; +import { runDoctor, type DoctorHost } from "../application/Doctor.js"; +import { createDoctorHostFixture } from "../application/Doctor.fixture.js"; import { inspectGhidraInstallation, type GhidraInstallationHost, type GhidraInstallationInspection, -} from "../../../../src/ghidra/GhidraInstallation.js"; -import { projectGhidraDoctorInspection } from "../../../../src/ghidra/GhidraDoctor.js"; -import { CATALOG_IDENTITY } from "../../../../src/catalogIdentity.js"; -import { PRODUCT_IDENTITY } from "../../../../src/identity.js"; +} from "./GhidraInstallation.js"; +import { projectGhidraDoctorInspection } from "./GhidraDoctor.js"; const INSTALL = "/opt/ghidra_12.1.2_PUBLIC"; const installationHost = ( @@ -51,39 +47,34 @@ const inspection = ( const doctorHost = ( ghidra: GhidraInstallationInspection, hopperAvailable = false, -): DoctorHost => ({ - platform: "linux", - architecture: "x64", - nodeVersion: "24.18.0", - macosVersion: () => Promise.resolve(undefined), - linuxDistribution: () => - Promise.resolve({ - id: "ubuntu", - versionId: "24.04", - packageFamily: "deb", - supported: true, - }), - validTarget: () => Promise.resolve(true), - executable: (path) => - Promise.resolve(hopperAvailable && path.includes("Hopper")), - supportedLinuxHopper: () => Promise.resolve(true), - linuxDemoRuntimeCheck: () => - Promise.resolve({ - name: "hopper-demo-runtime", - ok: true, - classification: "healthy", - }), - brewHopperPath: () => Promise.resolve(undefined), - manualHopperPaths: () => Promise.resolve([]), - providerInspections: () => - Promise.resolve([projectGhidraDoctorInspection(ghidra)]), - installedSkillIdentity: () => - Promise.resolve({ - version: PRODUCT_IDENTITY.skillVersion, - toolCount: CATALOG_IDENTITY.counts.mcp_tools, - catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, - }), -}); +): DoctorHost => + createDoctorHostFixture({ + platform: "linux", + architecture: "x64", + nodeVersion: "24.18.0", + macosVersion: () => Promise.resolve(undefined), + linuxDistribution: () => + Promise.resolve({ + id: "ubuntu", + versionId: "24.04", + packageFamily: "deb", + supported: true, + }), + validTarget: () => Promise.resolve(true), + executable: (path) => + Promise.resolve(hopperAvailable && path.includes("Hopper")), + supportedLinuxHopper: () => Promise.resolve(true), + linuxDemoRuntimeCheck: () => + Promise.resolve({ + name: "hopper-demo-runtime", + ok: true, + classification: "healthy", + }), + brewHopperPath: () => Promise.resolve(undefined), + manualHopperPaths: () => Promise.resolve([]), + providerInspections: () => + Promise.resolve([projectGhidraDoctorInspection(ghidra)]), + }); describe("Ghidra doctor integration", () => { it("accepts a valid BYO Ghidra engine when Hopper is absent", async () => { diff --git a/tests/boundary/providers/ghidra/ghidraFunctionValues.test.ts b/src/ghidra/GhidraFunctionValues.test.ts similarity index 97% rename from tests/boundary/providers/ghidra/ghidraFunctionValues.test.ts rename to src/ghidra/GhidraFunctionValues.test.ts index a296be220..01ed6dd60 100644 --- a/tests/boundary/providers/ghidra/ghidraFunctionValues.test.ts +++ b/src/ghidra/GhidraFunctionValues.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from "vitest"; -import type { JsonValue } from "../../../../src/domain/jsonValue.js"; +import type { JsonValue } from "../domain/jsonValue.js"; import { GHIDRA_FUNCTION_OPERATIONS, isGhidraFunctionOperation, parseGhidraFunctionInput, parseGhidraFunctionResult, type GhidraFunctionOperation, -} from "../../../../src/ghidra/GhidraFunctionValues.js"; +} from "./GhidraFunctionValues.js"; import { ghidraBounded, ghidraFunctionClassification, @@ -15,7 +15,7 @@ import { ghidraFunctionIdentity, ghidraNativeApiBoundary, ghidraReferenceEdge, -} from "../../../fixtures/ghidraFunction.js"; +} from "../domain/hopperValues.fixture.js"; describe("Ghidra function-analysis boundary values", () => { it("defaults bounded inputs and rejects undeclared or implicit addresses", () => { diff --git a/tests/boundary/providers/ghidra/ghidraInstallation.test.ts b/src/ghidra/GhidraInstallation.test.ts similarity index 99% rename from tests/boundary/providers/ghidra/ghidraInstallation.test.ts rename to src/ghidra/GhidraInstallation.test.ts index cddcd17f5..337c9113c 100644 --- a/tests/boundary/providers/ghidra/ghidraInstallation.test.ts +++ b/src/ghidra/GhidraInstallation.test.ts @@ -7,7 +7,7 @@ import { SUPPORTED_GHIDRA_VERSION, type GhidraInstallationHost, type GhidraJavaObservation, -} from "../../../../src/ghidra/GhidraInstallation.js"; +} from "./GhidraInstallation.js"; const INSTALL = "/opt/ghidra"; const PROPERTIES = `${INSTALL}/Ghidra/application.properties`; diff --git a/tests/boundary/providers/ghidra/ghidraInventoryValues.test.ts b/src/ghidra/GhidraInventoryValues.test.ts similarity index 97% rename from tests/boundary/providers/ghidra/ghidraInventoryValues.test.ts rename to src/ghidra/GhidraInventoryValues.test.ts index 2a4d843e6..9a76de60c 100644 --- a/tests/boundary/providers/ghidra/ghidraInventoryValues.test.ts +++ b/src/ghidra/GhidraInventoryValues.test.ts @@ -6,8 +6,8 @@ import { parseGhidraInventoryInput, parseGhidraInventoryResult, type GhidraInventoryOperation, -} from "../../../../src/ghidra/GhidraInventoryValues.js"; -import type { JsonValue } from "../../../../src/domain/jsonValue.js"; +} from "./GhidraInventoryValues.js"; +import type { JsonValue } from "../domain/jsonValue.js"; describe("Ghidra inventory boundary values", () => { it("defaults bounded provider inputs without accepting undeclared fields", () => { diff --git a/tests/boundary/providers/ghidra/ghidraProvider.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts similarity index 95% rename from tests/boundary/providers/ghidra/ghidraProvider.test.ts rename to src/ghidra/GhidraProvider.behavior.test.ts index 7830be700..5faac24cb 100644 --- a/tests/boundary/providers/ghidra/ghidraProvider.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -1,18 +1,18 @@ import { describe, expect, it } from "vitest"; -import { parseConfig } from "../../../../src/config.js"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; +import { parseConfig } from "../config.js"; +import type { BinaryTarget } from "../domain/binaryTarget.js"; import { GHIDRA_PROVIDER_IDENTITY, GHIDRA_PROVIDER_TOOL_CONTRACTS, GhidraProvider, type GhidraProviderClientFactory, -} from "../../../../src/ghidra/GhidraProvider.js"; -import type { GhidraInstallationHost } from "../../../../src/ghidra/GhidraInstallation.js"; -import { GHIDRA_SESSION_CAPABILITIES } from "../../../../src/ghidra/GhidraSessionValues.js"; -import { err, ok } from "../../../../src/domain/result.js"; -import { GhidraSessionError } from "../../../../src/ghidra/GhidraSessionError.js"; -import { silentLogger } from "../../../../src/logger.js"; +} from "./GhidraProvider.js"; +import type { GhidraInstallationHost } from "./GhidraInstallation.js"; +import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js"; +import { err, ok } from "../domain/result.js"; +import { GhidraSessionError } from "./GhidraSessionError.js"; +import { silentLogger } from "../logger.js"; const INSTALL = "/opt/ghidra_12.1.2_PUBLIC"; const installationHost = (): GhidraInstallationHost => ({ @@ -61,7 +61,6 @@ describe("Ghidra provider", () => { GHIDRA_PROVIDER_TOOL_CONTRACTS.map(({ name }) => name), ); expect(GHIDRA_PROVIDER_TOOL_CONTRACTS).toHaveLength(19); - expect(Object.isFrozen(ghidra.capabilities())).toBe(true); expect(ghidra.capabilities()).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/tests/composition/analysis-sessions/hopperProviderSelection.test.ts b/src/hopper/HopperProviderDiscovery.test.ts similarity index 92% rename from tests/composition/analysis-sessions/hopperProviderSelection.test.ts rename to src/hopper/HopperProviderDiscovery.test.ts index 5afcc23a5..e3177464a 100644 --- a/tests/composition/analysis-sessions/hopperProviderSelection.test.ts +++ b/src/hopper/HopperProviderDiscovery.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { parseConfig } from "../../../src/config.js"; -import type { BinaryTarget } from "../../../src/domain/binaryTarget.js"; -import { HopperProvider } from "../../../src/hopper/HopperProvider.js"; -import { silentLogger } from "../../../src/logger.js"; +import { parseConfig } from "../config.js"; +import type { BinaryTarget } from "../domain/binaryTarget.js"; +import { silentLogger } from "../logger.js"; +import { HopperProvider } from "./HopperProvider.js"; describe("Hopper provider discovery", () => { it("reports bounded launcher availability without starting Hopper", () => { diff --git a/tests/boundary/process/processOwnershipPart2.test.ts b/src/process/ProcessOwnership.part2.test.ts similarity index 99% rename from tests/boundary/process/processOwnershipPart2.test.ts rename to src/process/ProcessOwnership.part2.test.ts index 4aef4fb65..b2f8fdb2e 100644 --- a/tests/boundary/process/processOwnershipPart2.test.ts +++ b/src/process/ProcessOwnership.part2.test.ts @@ -4,7 +4,7 @@ import { observeOwnedProcessLineage, verifyNoTokenOwnedProcesses, type ProcessOwnershipHost, -} from "../../../src/process/ProcessOwnership.js"; +} from "./ProcessOwnership.js"; const ownership = { runId: "run-token", leaderPid: 100, diff --git a/tests/boundary/process/processOwnershipPart3.test.ts b/src/process/ProcessOwnership.part3.test.ts similarity index 97% rename from tests/boundary/process/processOwnershipPart3.test.ts rename to src/process/ProcessOwnership.part3.test.ts index 77dfa8f75..6f638367e 100644 --- a/tests/boundary/process/processOwnershipPart3.test.ts +++ b/src/process/ProcessOwnership.part3.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { observeOwnedProcessLineage, type ProcessOwnershipHost, -} from "../../../src/process/ProcessOwnership.js"; +} from "./ProcessOwnership.js"; const ownership = { runId: "run-token", leaderPid: 100, diff --git a/tests/boundary/process/processOwnership.test.ts b/src/process/ProcessOwnership.test.ts similarity index 97% rename from tests/boundary/process/processOwnership.test.ts rename to src/process/ProcessOwnership.test.ts index 49a7e0cd6..c1e86cf80 100644 --- a/tests/boundary/process/processOwnership.test.ts +++ b/src/process/ProcessOwnership.test.ts @@ -4,8 +4,8 @@ import { parseProcessEnvironment, selectCapturedProcessGroupIds, type ProcessOwnershipHost, -} from "../../../src/process/ProcessOwnership.js"; -import { matchesOwnedProcessCommand } from "../../../src/process/ProcessCommandIdentity.js"; +} from "./ProcessOwnership.js"; +import { matchesOwnedProcessCommand } from "./ProcessCommandIdentity.js"; const ownership = { runId: "run-token", leaderPid: 100, diff --git a/tests/boundary/process/processOwnershipValidation.test.ts b/src/process/ProcessOwnership.validation.test.ts similarity index 99% rename from tests/boundary/process/processOwnershipValidation.test.ts rename to src/process/ProcessOwnership.validation.test.ts index 4c7519f18..aab21fcb1 100644 --- a/tests/boundary/process/processOwnershipValidation.test.ts +++ b/src/process/ProcessOwnership.validation.test.ts @@ -3,7 +3,7 @@ import { cleanupOwnedProcessGroup, observeOwnedProcessLineage, type ProcessOwnershipHost, -} from "../../../src/process/ProcessOwnership.js"; +} from "./ProcessOwnership.js"; const ownership = { runId: "run-token", leaderPid: 100, diff --git a/tests/boundary/process/processOwnershipValidationPart2.test.ts b/src/process/ProcessOwnership.validationPart2.test.ts similarity index 98% rename from tests/boundary/process/processOwnershipValidationPart2.test.ts rename to src/process/ProcessOwnership.validationPart2.test.ts index f1e89a00e..a802c2c7d 100644 --- a/tests/boundary/process/processOwnershipValidationPart2.test.ts +++ b/src/process/ProcessOwnership.validationPart2.test.ts @@ -3,7 +3,7 @@ import { cleanupOwnedProcessGroup, observeOwnedProcessGroup, type ProcessOwnershipHost, -} from "../../../src/process/ProcessOwnership.js"; +} from "./ProcessOwnership.js"; const ownership = { runId: "run-token", leaderPid: 100, diff --git a/tests/boundary/process/processOwnershipValidationPart3.test.ts b/src/process/ProcessOwnership.validationPart3.test.ts similarity index 96% rename from tests/boundary/process/processOwnershipValidationPart3.test.ts rename to src/process/ProcessOwnership.validationPart3.test.ts index 11d72954f..7671d947e 100644 --- a/tests/boundary/process/processOwnershipValidationPart3.test.ts +++ b/src/process/ProcessOwnership.validationPart3.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { cleanupWindowsProcessTree, type WindowsProcessTreeHost, -} from "../../../src/process/ProcessOwnership.js"; +} from "./ProcessOwnership.js"; describe("Windows P0 process-tree cleanup", () => { it("reports whether taskkill signaled or found an exited tree", async () => { const terminated: WindowsProcessTreeHost = { diff --git a/tests/boundary/providers/replay/replayWorkerProtocol.test.ts b/src/replay/ReplayWorkerProtocol.test.ts similarity index 94% rename from tests/boundary/providers/replay/replayWorkerProtocol.test.ts rename to src/replay/ReplayWorkerProtocol.test.ts index ae8306509..a3877eeb3 100644 --- a/tests/boundary/providers/replay/replayWorkerProtocol.test.ts +++ b/src/replay/ReplayWorkerProtocol.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { parseReplayWorkerResponse } from "../../../../src/replay/ReplayWorkerProtocol.js"; +import { parseReplayWorkerResponse } from "./ReplayWorkerProtocol.js"; const digest = "1".repeat(64); const cases = [{ case_id: "case-1", sha256: digest }]; diff --git a/tests/boundary/mcp/derivedOperation.test.ts b/src/server/runDerivedOperation.test.ts similarity index 96% rename from tests/boundary/mcp/derivedOperation.test.ts rename to src/server/runDerivedOperation.test.ts index c4328bf11..e1705cdc0 100644 --- a/tests/boundary/mcp/derivedOperation.test.ts +++ b/src/server/runDerivedOperation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { runDerivedOperation } from "../../../src/server/runDerivedOperation.js"; +import { runDerivedOperation } from "./runDerivedOperation.js"; const context = ( controller: AbortController, diff --git a/tests/acceptance/setup/setupLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts similarity index 98% rename from tests/acceptance/setup/setupLifecycle.test.ts rename to tests/boundary/filesystem/clientLifecycle.test.ts index 8321ae89a..4c5023f0b 100644 --- a/tests/acceptance/setup/setupLifecycle.test.ts +++ b/tests/boundary/filesystem/clientLifecycle.test.ts @@ -30,7 +30,7 @@ afterEach(async () => { ); }); -describe("agent lifecycle", () => { +describe("client configuration filesystem lifecycle", () => { it("detects every supported client and skips absent clients", async () => { const home = await createTestTempDirectory("rea-detect-"); roots.push(home); @@ -170,7 +170,7 @@ describe("agent lifecycle", () => { }); }); -describe("agent lifecycle removal", () => { +describe("client configuration filesystem removal", () => { it("uninstalls only owned entries and refuses purge symlinks", async () => { const home = await createTestTempDirectory("rea-uninstall-"); roots.push(home); @@ -280,7 +280,7 @@ describe("agent lifecycle removal", () => { }); }); -describe("agent lifecycle failure reporting", () => { +describe("client configuration filesystem failure reporting", () => { it.each([ [ "read", diff --git a/tests/composition/setup-lifecycle/directAnalysisPermissions.test.ts b/tests/boundary/filesystem/directAnalysisPermissions.test.ts similarity index 98% rename from tests/composition/setup-lifecycle/directAnalysisPermissions.test.ts rename to tests/boundary/filesystem/directAnalysisPermissions.test.ts index c16b0ff5e..23d19f547 100644 --- a/tests/composition/setup-lifecycle/directAnalysisPermissions.test.ts +++ b/tests/boundary/filesystem/directAnalysisPermissions.test.ts @@ -27,7 +27,7 @@ afterEach(async () => { vi.unstubAllEnvs(); }); -describe("direct analysis snapshot permissions", () => { +describe("direct analysis snapshot filesystem permissions", () => { it("forwards the CLI provider selector ahead of the environment preference", async () => { const directory = await createTestTempDirectory("rea-direct-provider-"); const targetPath = join(directory, "fixture.hop"); diff --git a/tests/boundary/filesystem/managedRuntimeCorrelation.test.ts b/tests/boundary/filesystem/managedRuntimeCorrelation.test.ts index 1858944a2..47f3d96ba 100644 --- a/tests/boundary/filesystem/managedRuntimeCorrelation.test.ts +++ b/tests/boundary/filesystem/managedRuntimeCorrelation.test.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -15,29 +14,13 @@ import { MANAGED_STATIC_PROVIDER } from "../../../src/application/InvestigationP import { createPermissionAuthority } from "../../../src/application/PermissionAuthority.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { managedRuntimeCorrelationResultSchema } from "../../../src/domain/managedRuntimeCorrelation.js"; -import type { BinaryTarget } from "../../../src/domain/binaryTarget.js"; import type { PermissionCeiling } from "../../../src/domain/permissionPolicy.js"; import { inspectManagedMembersBytes } from "../../../src/dotnet/ManagedMemberInspector.js"; -import { buildManagedPeFixture } from "../../fixtures/managedPe.js"; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; +import { + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_MEMBER_FIXTURE_LIMITS, +} from "../../../src/dotnet/ManagedPe.fixture.js"; describe("managed runtime correlation planning", () => { it("fails closed while the managed runtime authority is disabled", async () => { @@ -157,7 +140,7 @@ describe("managed runtime correlation planning", () => { await writeFile(artifactPath, bytes); await writeFile(executablePath, "#!/bin/sh\n"); const fixture = inspect(bytes, artifactPath, { - ...memberLimits, + ...MANAGED_MEMBER_FIXTURE_LIMITS, maxMethodInstructions: 1, }); const authority = await authorityFor(directory, executablePath, true); @@ -247,18 +230,9 @@ const inputFor = ( const inspect = ( bytes: Buffer, path: string, - limits: typeof memberLimits = memberLimits, + limits: typeof MANAGED_MEMBER_FIXTURE_LIMITS = MANAGED_MEMBER_FIXTURE_LIMITS, ) => { - const target: BinaryTarget = { - path, - sha256: hash(bytes), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, - }; + const target = managedPeFixtureTarget(bytes, path); const result = inspectManagedMembersBytes(bytes, target, limits); const method = result.methods.items[0]; if (method === undefined) throw new Error("fixture has no method"); @@ -274,6 +248,3 @@ const inspect = ( }), }; }; - -const hash = (bytes: Buffer): string => - createHash("sha256").update(bytes).digest("hex"); diff --git a/tests/composition/setup-lifecycle/permissionAuthority.test.ts b/tests/boundary/filesystem/permissionAuthority.test.ts similarity index 99% rename from tests/composition/setup-lifecycle/permissionAuthority.test.ts rename to tests/boundary/filesystem/permissionAuthority.test.ts index 5a8980bcc..6b80d48a2 100644 --- a/tests/composition/setup-lifecycle/permissionAuthority.test.ts +++ b/tests/boundary/filesystem/permissionAuthority.test.ts @@ -17,7 +17,7 @@ import { type PermissionScope, } from "../../../src/domain/permissionPolicy.js"; -describe("permission authority", () => { +describe("permission authority filesystem boundaries", () => { it("distinguishes an elicitable grant from administrator reconfiguration", async () => { const sandbox = await createTestTempDirectory( "rea-permission-remediation-", diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts index eba3c3020..82b2a774d 100644 --- a/tests/boundary/mcp/electronMcp.test.ts +++ b/tests/boundary/mcp/electronMcp.test.ts @@ -21,7 +21,7 @@ import { type FakeCdpBrowser, } from "../../fixtures/fakeCdpBrowser.js"; import { writeElectronBoundaryFixture } from "../../fixtures/electronBoundaryApplication.js"; -import { createElectronActiveObservationFixtureResult } from "../../fixtures/electronActiveObservationResult.js"; +import { createElectronActiveObservationFixtureResult } from "../../../src/domain/electronActiveObservation.fixture.js"; const browsers: FakeCdpBrowser[] = []; const resources: Array<{ close(): Promise }> = []; diff --git a/tests/boundary/mcp/ghidraMcpHarness.ts b/tests/boundary/mcp/ghidraMcpHarness.ts index 39f8c59b4..513a9fa8d 100644 --- a/tests/boundary/mcp/ghidraMcpHarness.ts +++ b/tests/boundary/mcp/ghidraMcpHarness.ts @@ -21,7 +21,7 @@ import { ghidraFunctionDossier, ghidraFunctionIdentity, ghidraReferenceEdge, -} from "../../fixtures/ghidraFunction.js"; +} from "../../../src/domain/hopperValues.fixture.js"; const INSTALL = "/opt/ghidra_12.1.2_PUBLIC"; diff --git a/tests/boundary/mcp/managedMcp.test.ts b/tests/boundary/mcp/managedMcp.test.ts index 724627199..3ca4f9127 100644 --- a/tests/boundary/mcp/managedMcp.test.ts +++ b/tests/boundary/mcp/managedMcp.test.ts @@ -17,7 +17,7 @@ import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../../src/contracts/mana import type { PermissionCeiling } from "../../../src/domain/permissionPolicy.js"; import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js"; import { createServer } from "../../../src/server/createServer.js"; -import { buildManagedPeFixture } from "../../fixtures/managedPe.js"; +import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; it("opens a managed PE and executes the managed static provider through MCP", async () => { const directory = await createTestTempDirectory("rea-managed-mcp-"); diff --git a/tests/boundary/process/processCaptureMultisource.test.ts b/tests/boundary/process/processCaptureMultisource.test.ts index 050af18a5..5bdd7f97f 100644 --- a/tests/boundary/process/processCaptureMultisource.test.ts +++ b/tests/boundary/process/processCaptureMultisource.test.ts @@ -12,7 +12,7 @@ import { parseProcessScenario, type ProcessCapture, } from "../../../src/domain/processCapture.js"; -import { processCaptureIssues } from "../../fixtures/processCapture.js"; +import { processCaptureIssues } from "../../../src/domain/processCapture.fixture.js"; type EventSource = "process" | "filesystem" | "http" | "websocket" | "shim"; diff --git a/tests/boundary/process/processCaptureReactive.test.ts b/tests/boundary/process/processCaptureReactive.test.ts index 51b7f371d..0f6374151 100644 --- a/tests/boundary/process/processCaptureReactive.test.ts +++ b/tests/boundary/process/processCaptureReactive.test.ts @@ -11,7 +11,7 @@ import { parseProcessScenario, type ProcessCapture, } from "../../../src/domain/processCapture.js"; -import { processCaptureIssues } from "../../fixtures/processCapture.js"; +import { processCaptureIssues } from "../../../src/domain/processCapture.fixture.js"; function createInteractiveScenario(root: string, script: string) { return parseProcessScenario({ diff --git a/tests/boundary/providers/ghidra/ghidraClient.test.ts b/tests/boundary/providers/ghidra/ghidraClient.test.ts index 42018059e..9f96b2e93 100644 --- a/tests/boundary/providers/ghidra/ghidraClient.test.ts +++ b/tests/boundary/providers/ghidra/ghidraClient.test.ts @@ -1,6 +1,5 @@ import { spawn, type ChildProcess } from "node:child_process"; import { createHash } from "node:crypto"; -import { getEventListeners } from "node:events"; import { readFileSync } from "node:fs"; import { access, mkdir } from "node:fs/promises"; import { join } from "node:path"; @@ -394,12 +393,6 @@ describe("GhidraClient cleanup and diagnostics", () => { expect(exited(process_)).toBe(true); if (process_ === undefined) throw new Error("Fixture process was not captured"); - expect(getEventListeners(process_, "exit")).toHaveLength(0); - expect(getEventListeners(process_, "close")).toHaveLength(0); - expect(getEventListeners(process_, "error")).toHaveLength(0); - expect(process_.stdout).not.toBeNull(); - if (process_.stdout !== null) - expect(getEventListeners(process_.stdout, "data")).toHaveLength(0); }); it("retains actionable runtime coordinates after successful cleanup", async () => { diff --git a/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts b/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts index 41ea16118..6cfcd7f40 100644 --- a/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.cleanup.test.ts @@ -1,5 +1,4 @@ import { spawn, type ChildProcess } from "node:child_process"; -import { getEventListeners } from "node:events"; import { access } from "node:fs/promises"; import { fileURLToPath } from "node:url"; @@ -286,12 +285,6 @@ describe("HopperClient cleanup", () => { if (child === undefined) throw new Error("Fixture process was not captured"); expect(child.exitCode !== null || child.signalCode !== null).toBe(true); - expect(getEventListeners(child, "exit")).toHaveLength(0); - expect(getEventListeners(child, "close")).toHaveLength(0); - expect(getEventListeners(child, "error")).toHaveLength(0); - expect(child.stderr).not.toBeNull(); - if (child.stderr === null) throw new Error("Fixture stderr is unavailable"); - expect(getEventListeners(child.stderr, "data")).toHaveLength(0); }); }); const stopUnrelatedFixture = async (child: ChildProcess): Promise => { diff --git a/tests/boundary/providers/managed/managedArtifactProvider.cliEvidence.test.ts b/tests/boundary/providers/managed/managedDirectAnalysis.test.ts similarity index 89% rename from tests/boundary/providers/managed/managedArtifactProvider.cliEvidence.test.ts rename to tests/boundary/providers/managed/managedDirectAnalysis.test.ts index 8efb327b2..1eadc52c9 100644 --- a/tests/boundary/providers/managed/managedArtifactProvider.cliEvidence.test.ts +++ b/tests/boundary/providers/managed/managedDirectAnalysis.test.ts @@ -7,10 +7,10 @@ import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js"; import { parseEvidence } from "../../../../src/domain/evidence.js"; -import { buildManagedPeFixture } from "../../../fixtures/managedPe.js"; +import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js"; -describe("managed CLI evidence", () => { - it("has the same evidence shape through direct CLI analysis plumbing", async () => { +describe("managed direct-analysis path boundary", () => { + it("returns provider evidence through the shared analysis service", async () => { const directory = await createTestTempDirectory("rea-managed-cli-"); const path = join(directory, "fixture.exe"); await writeFile(path, buildManagedPeFixture()); diff --git a/tests/boundary/providers/managed/managedMemberComparison.test.ts b/tests/boundary/providers/managed/managedMemberComparison.test.ts index 73d9df979..154c341da 100644 --- a/tests/boundary/providers/managed/managedMemberComparison.test.ts +++ b/tests/boundary/providers/managed/managedMemberComparison.test.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -7,292 +6,11 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { compareManagedMemberPaths } from "../../../../src/application/ManagedMemberComparisonService.js"; -import { MANAGED_STATIC_PROVIDER } from "../../../../src/application/InvestigationProviders.js"; +import { managedMemberComparisonResultSchema } from "../../../../src/domain/managedMemberComparison.js"; import { - compareManagedMembers, - managedMemberComparisonResultSchema, -} from "../../../../src/domain/managedMemberComparison.js"; -import { createEvidence } from "../../../../src/domain/evidence.js"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; -import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { buildManagedPeFixture } from "../../../fixtures/managedPe.js"; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; - -const pathMemberLimits = { - ...memberLimits, - maxFileBytes: 1024 * 1024, -}; - -const comparisonLimits = { - max_method_matches: 100, - max_field_matches: 100, - max_candidates: 10, -}; - -describe("managed member comparison", () => { - it("remaps renamed methods by exact CIL/signature without using names", () => { - const leftBytes = buildManagedPeFixture(); - const rightBytes = buildManagedPeFixture({ - mvid: Buffer.from("00112233445566778899aabbccddeefe", "hex"), - typeName: "A", - typeNamespace: "X", - methodName: "b", - fieldName: "c", - }); - const left = inspect(leftBytes, "/tmp/left.dll"); - const right = inspect(rightBytes, "/tmp/right.dll"); - const result = compareManagedMembers( - { evidenceId: left.evidence.evidence_id, result: left.result }, - { evidenceId: right.evidence.evidence_id, result: right.result }, - comparisonLimits, - ); - - expect(result.algorithm.name_matching).toBe("not-used"); - expect(result.left.mvid).toBe("00112233-4455-6677-8899-aabbccddeeff"); - expect(result.right.mvid).toBe("33221100-5544-7766-8899-aabbccddeefe"); - expect(result.matching.exact_il_signature).toBe(1); - expect(result.methods).toEqual([ - expect.objectContaining({ - status: "unchanged", - left: expect.objectContaining({ token: "0x06000001", name: "Main" }), - right: expect.objectContaining({ token: "0x06000001", name: "b" }), - match: expect.objectContaining({ - status: "matched", - basis: "exact-il-signature", - confidence: "exact", - }), - dimensions: [], - }), - ]); - expect(result.limitations).toContain( - "Names are reported as observations but are not used as a matching basis.", - ); - }); - - it("uses structural method shape when build-local token operands drift", () => { - const leftBytes = buildManagedPeFixture(); - const rightBytes = buildManagedPeFixture({ - ilBody: Buffer.from([ - 0x32, 0x02, 0x7b, 0x02, 0x00, 0x00, 0x04, 0x28, 0x02, 0x00, 0x00, 0x0a, - 0x2a, - ]), - }); - const left = inspect(leftBytes, "/tmp/left.dll"); - const right = inspect(rightBytes, "/tmp/right.dll"); - const result = compareManagedMembers( - { evidenceId: left.evidence.evidence_id, result: left.result }, - { evidenceId: right.evidence.evidence_id, result: right.result }, - comparisonLimits, - ); - - expect(result.matching.exact_il_signature).toBe(0); - expect(result.matching.structural_method_shape).toBe(1); - expect(result.methods[0]).toMatchObject({ - status: "changed", - match: { status: "matched", basis: "structural-method-shape" }, - dimensions: ["cil"], - }); - }); - - it("rejects member states that disagree with their observed sides or match", () => { - const left = inspect(buildManagedPeFixture(), "/tmp/left.dll"); - const right = inspect(buildManagedPeFixture(), "/tmp/right.dll"); - const result = compareManagedMembers( - { evidenceId: left.evidence.evidence_id, result: left.result }, - { evidenceId: right.evidence.evidence_id, result: right.result }, - comparisonLimits, - ); - const method = result.methods[0]; - expect(method).toBeDefined(); - if (method === undefined) return; - - expect( - managedMemberComparisonResultSchema.safeParse({ - ...result, - methods: [{ ...method, status: "unchanged", right: null }], - }).success, - ).toBe(false); - expect( - managedMemberComparisonResultSchema.safeParse({ - ...result, - methods: [ - { - ...method, - match: { - ...method.match, - basis: "none", - confidence: "unknown", - }, - }, - ], - }).success, - ).toBe(false); - }); - - it("does not match identical instruction-limited method prefixes", () => { - const limited = { ...memberLimits, maxMethodInstructions: 1 }; - const left = inspect( - buildManagedPeFixture(), - "/tmp/left-partial.dll", - limited, - ); - const right = inspect( - buildManagedPeFixture(), - "/tmp/right-partial.dll", - limited, - ); - const result = compareManagedMembers( - { evidenceId: left.evidence.evidence_id, result: left.result }, - { evidenceId: right.evidence.evidence_id, result: right.result }, - comparisonLimits, - ); - - expect(result.matching.exact_il_signature).toBe(0); - expect(result.matching.structural_method_shape).toBe(0); - expect(result.coverage).toMatchObject({ - status: "partial", - left_status: "partial", - right_status: "partial", - }); - }); -}); - -describe("managed member comparison uncertainty", () => { - it("keeps unmatched members unknown when the opposite page is incomplete", () => { - const left = inspect(buildManagedPeFixture(), "/tmp/left-paged.dll"); - const right = inspect(buildManagedPeFixture(), "/tmp/right-paged.dll"); - const leftPartial = { - ...left.result, - fields: { - ...left.result.fields, - items: [], - returned: 0, - dropped: 1, - complete: false, - }, - }; - const rightPartial = { - ...right.result, - methods: { - ...right.result.methods, - items: [], - returned: 0, - dropped: 1, - complete: false, - }, - }; - const result = compareManagedMembers( - { evidenceId: left.evidence.evidence_id, result: leftPartial }, - { evidenceId: right.evidence.evidence_id, result: rightPartial }, - comparisonLimits, - ); - - expect(result.summary).toMatchObject({ - added: 0, - removed: 0, - unknown: 2, - }); - expect(result.methods[0]).toMatchObject({ - status: "unknown", - left: { token: "0x06000001" }, - right: null, - limitations: [expect.stringContaining("unknown-within-unobserved-page")], - }); - expect(result.fields[0]).toMatchObject({ - status: "unknown", - left: null, - right: { token: "0x04000001" }, - limitations: [expect.stringContaining("unknown-within-unobserved-page")], - }); - expect(result.coverage).toEqual({ - status: "truncated", - left_status: "partial", - right_status: "partial", - omitted_methods: 1, - omitted_fields: 1, - omitted_candidates: 0, - }); - }); - - it("does not guess ambiguous field signature matches", () => { - const left = inspect(buildManagedPeFixture(), "/tmp/left.dll"); - const leftField = left.result.fields.items[0]; - expect(leftField).toBeDefined(); - if (leftField === undefined) return; - const duplicatedLeft = { - ...left.result, - fields: { - ...left.result.fields, - items: [ - leftField, - { - ...leftField, - token: "0x04000002", - name: "other", - }, - ], - total: 2, - returned: 2, - }, - }; - const right = inspect(buildManagedPeFixture(), "/tmp/right.dll"); - const rightField = right.result.fields.items[0]; - expect(rightField).toBeDefined(); - if (rightField === undefined) return; - const duplicatedRight = { - ...right.result, - fields: { - ...right.result.fields, - items: [ - rightField, - { - ...rightField, - token: "0x04000002", - name: "renamed", - }, - ], - total: 2, - returned: 2, - }, - }; - const result = compareManagedMembers( - { evidenceId: left.evidence.evidence_id, result: duplicatedLeft }, - { evidenceId: right.evidence.evidence_id, result: duplicatedRight }, - comparisonLimits, - ); - - expect(result.matching.ambiguous).toBe(1); - expect(result.fields).toEqual([ - expect.objectContaining({ - status: "unknown", - match: expect.objectContaining({ - status: "ambiguous", - basis: "field-signature", - candidate_left_tokens: ["0x04000001", "0x04000002"], - candidate_right_tokens: ["0x04000001", "0x04000002"], - }), - }), - ]); - }); -}); + buildManagedPeFixture, + MANAGED_MEMBER_PATH_FIXTURE_LIMITS, +} from "../../../../src/dotnet/ManagedPe.fixture.js"; describe("managed member comparison path workflow", () => { it("compares two local paths and returns derived Evidence", async () => { @@ -308,49 +26,24 @@ describe("managed member comparison path workflow", () => { const result = await compareManagedMemberPaths({ leftPath, rightPath, - memberLimits: pathMemberLimits, - comparisonLimits, + memberLimits: MANAGED_MEMBER_PATH_FIXTURE_LIMITS, + comparisonLimits: { + max_method_matches: 100, + max_field_matches: 100, + max_candidates: 10, + }, }); expect(result.ok).toBe(true); if (!result.ok) return; - expect(result.value.operation).toBe("compare_managed_members"); - expect(result.value.confidence).toBe("inferred"); - expect(result.value.authority).toBe("analyst-inference"); + expect(result.value).toMatchObject({ + operation: "compare_managed_members", + confidence: "inferred", + authority: "analyst-inference", + }); expect( managedMemberComparisonResultSchema.parse(result.value.normalized_result) .matching.exact_il_signature, ).toBe(1); }); }); - -const inspect = ( - bytes: Buffer, - path: string, - limits: typeof memberLimits = memberLimits, -) => { - const target: BinaryTarget = { - path, - sha256: hash(bytes), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, - }; - const result = inspectManagedMembersBytes(bytes, target, limits); - return { - result, - evidence: createEvidence(target, MANAGED_STATIC_PROVIDER, { - operation: "inspect_managed_members", - parameters: {}, - result, - rawResult: null, - limitations: result.limitations, - }), - }; -}; - -const hash = (bytes: Buffer): string => - createHash("sha256").update(bytes).digest("hex"); diff --git a/tests/boundary/providers/managed/managedArtifactProvider.failures.test.ts b/tests/boundary/providers/managed/managedStaticProvider.test.ts similarity index 61% rename from tests/boundary/providers/managed/managedArtifactProvider.failures.test.ts rename to tests/boundary/providers/managed/managedStaticProvider.test.ts index c29018cac..9c03f43d0 100644 --- a/tests/boundary/providers/managed/managedArtifactProvider.failures.test.ts +++ b/tests/boundary/providers/managed/managedStaticProvider.test.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -7,79 +6,16 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import type { AnalysisExecution } from "../../../../src/application/AnalysisProvider.js"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; import { parseBinaryTarget } from "../../../../src/domain/binaryTarget.js"; import { managedArtifactInspectionSchema, managedMemberInspectionSchema, managedNativeBoundaryInspectionSchema, } from "../../../../src/domain/managedArtifact.js"; -import { inspectManagedArtifactBytes } from "../../../../src/dotnet/ManagedArtifactInspector.js"; import { ManagedStaticProvider } from "../../../../src/dotnet/ManagedStaticProvider.js"; -import { - buildManagedPeFixture, - buildNativePeFixture, -} from "../../../fixtures/managedPe.js"; - -const limits = { - referenceOffset: 0, - referenceLimit: 100, - resourceOffset: 0, - resourceLimit: 100, - attributeOffset: 0, - attributeLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, -}; - -describe("managed artifact failures", () => { - it("reports native PE, malformed metadata, and bounded metadata failures as typed results", () => { - const native = inspectManagedArtifactBytes( - buildNativePeFixture(), - target(buildNativePeFixture()), - limits, - ); - expect(native.classification.status).toBe("not-managed"); - expect(native.metadata.status).toBe("absent"); - expect(native.coverage.state).toBe("unavailable"); - - const malformedBytes = buildManagedPeFixture({ - corruptMetadataSignature: true, - }); - const malformed = inspectManagedArtifactBytes( - malformedBytes, - target(malformedBytes), - limits, - ); - expect(malformed.classification.status).toBe("malformed"); - expect(malformed.coverage.issues).toEqual([ - expect.objectContaining({ code: "invalid-metadata-root" }), - ]); - - const unsupportedTableBytes = buildManagedPeFixture({ - metadataValidMaskExtra: 1n << 50n, - }); - const unsupportedTable = inspectManagedArtifactBytes( - unsupportedTableBytes, - target(unsupportedTableBytes), - limits, - ); - expect(unsupportedTable.coverage.issues).toEqual([ - expect.objectContaining({ code: "invalid-tables" }), - ]); - - const limited = inspectManagedArtifactBytes( - buildManagedPeFixture(), - target(buildManagedPeFixture()), - { ...limits, maxMetadataBytes: 256 }, - ); - expect(limited.metadata.status).toBe("partial"); - expect(limited.coverage.issues).toEqual([ - expect.objectContaining({ code: "limit-exceeded" }), - ]); - }); +import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js"; +describe("managed static provider path boundary", () => { it("executes as a read-only auxiliary provider with digest, cancellation, and format boundaries", async () => { const directory = await createTestTempDirectory("rea-managed-provider-"); const bytes = buildManagedPeFixture(); @@ -161,17 +97,6 @@ describe("managed artifact failures", () => { }); }); }); -const target = (bytes: Buffer): BinaryTarget => ({ - path: "/fixture.exe", - sha256: createHash("sha256").update(bytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}); - const asManagedResult = (execution: AnalysisExecution) => managedArtifactInspectionSchema.parse(execution.result); diff --git a/tests/composition/analysis-sessions/binarySession.01-returns-detached-provider-target-and.test.ts b/tests/composition/analysis-sessions/binarySession.01-returns-detached-provider-target-and.test.ts index d77b356d5..9dc264810 100644 --- a/tests/composition/analysis-sessions/binarySession.01-returns-detached-provider-target-and.test.ts +++ b/tests/composition/analysis-sessions/binarySession.01-returns-detached-provider-target-and.test.ts @@ -1,96 +1,17 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import type { - AnalysisProvider, - CapabilityDescriptor, -} from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; +import { + createBinarySessionTargets, + createCacheProvider, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; import { createInvestigationWorkspace } from "../../../src/domain/investigationWorkspace.js"; -import { ok as resultOk } from "../../../src/domain/result.js"; -import { observed as ok } from "../../fixtures/analysisExecution.js"; - -const cacheProvider = ( - calls: string[], - mayWriteFilesystem = false, -): AnalysisProvider => { - const identity = { - id: "fixture", - name: "Fixture analysis provider", - version: "1", - } as const; - return { - identity: () => identity, - resolveAnalysisProfile: () => - Promise.resolve( - resultOk({ - profile: createAnalysisProfile(identity, 1, { fixture: true }), - compatibility: {}, - }), - ), - capabilities: () => [ - cacheCapability(identity, "address_name", false, mayWriteFilesystem), - cacheCapability(identity, "set_address_name", true), - ], - createClient: () => ({ - execute: (operation) => { - calls.push(operation); - return Promise.resolve(ok(operation)); - }, - close: () => Promise.resolve(), - }), - }; -}; - -const cacheCapability = ( - provider: CapabilityDescriptor["provider"], - operation: "address_name" | "set_address_name", - mutatesArtifact: boolean, - mayWriteFilesystem = false, -): CapabilityDescriptor => ({ - provider, - operation, - inputContractVersion: 1, - outputContractVersion: 1, - available: true, - reason: null, - pagination: "none", - exhaustive: true, - effects: { - mutatesArtifact, - launchesProcess: false, - mayShowUi: false, - mayAccessNetwork: false, - mayWriteFilesystem, - changesPermissions: false, - requiresRoot: false, - }, - limits: { - maxResults: null, - maxPayloadBytes: null, - timeoutMs: null, - }, - limitations: [], -}); - -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; describe("binary session", () => { it("returns detached provider, target, and workspace metadata", async () => { - const [first] = await targets(); - const session = createTestBinarySession(cacheProvider([])); + const [first] = await createBinarySessionTargets(); + const session = createTestBinarySession(createCacheProvider([])); expect((await session.open(first)).ok).toBe(true); expect(session.status()).toMatchObject({ analysis_run: { diff --git a/tests/composition/analysis-sessions/binarySession.02-runs-through-a-non-hopper.test.ts b/tests/composition/analysis-sessions/binarySession.02-runs-through-a-non-hopper.test.ts index 7f486a05b..4f9957784 100644 --- a/tests/composition/analysis-sessions/binarySession.02-runs-through-a-non-hopper.test.ts +++ b/tests/composition/analysis-sessions/binarySession.02-runs-through-a-non-hopper.test.ts @@ -1,22 +1,12 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - import type { AnalysisProvider } from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + createBinarySessionTargets, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; - const nonHopperProvider = (operations: string[]): AnalysisProvider => { const provider: AnalysisProvider = { identity: () => ({ id: "fixture", name: "Fixture", version: "1" }), @@ -96,7 +86,7 @@ const nonHopperProvider = (operations: string[]): AnalysisProvider => { describe("binary session", () => { it("runs through a non-Hopper analysis provider", async () => { - const [first] = await targets(); + const [first] = await createBinarySessionTargets(); const operations: string[] = []; const provider = nonHopperProvider(operations); const session = createTestBinarySession(provider); diff --git a/tests/composition/analysis-sessions/binarySession.03-allocates-one-fresh-run-identity.test.ts b/tests/composition/analysis-sessions/binarySession.03-allocates-one-fresh-run-identity.test.ts index 9507212b4..b941acd9b 100644 --- a/tests/composition/analysis-sessions/binarySession.03-allocates-one-fresh-run-identity.test.ts +++ b/tests/composition/analysis-sessions/binarySession.03-allocates-one-fresh-run-identity.test.ts @@ -1,94 +1,15 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import type { - AnalysisProvider, - CapabilityDescriptor, -} from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; -import { ok as resultOk } from "../../../src/domain/result.js"; -import { observed as ok } from "../../fixtures/analysisExecution.js"; - -const cacheProvider = ( - calls: string[], - mayWriteFilesystem = false, -): AnalysisProvider => { - const identity = { - id: "fixture", - name: "Fixture analysis provider", - version: "1", - } as const; - return { - identity: () => identity, - resolveAnalysisProfile: () => - Promise.resolve( - resultOk({ - profile: createAnalysisProfile(identity, 1, { fixture: true }), - compatibility: {}, - }), - ), - capabilities: () => [ - cacheCapability(identity, "address_name", false, mayWriteFilesystem), - cacheCapability(identity, "set_address_name", true), - ], - createClient: () => ({ - execute: (operation) => { - calls.push(operation); - return Promise.resolve(ok(operation)); - }, - close: () => Promise.resolve(), - }), - }; -}; - -const cacheCapability = ( - provider: CapabilityDescriptor["provider"], - operation: "address_name" | "set_address_name", - mutatesArtifact: boolean, - mayWriteFilesystem = false, -): CapabilityDescriptor => ({ - provider, - operation, - inputContractVersion: 1, - outputContractVersion: 1, - available: true, - reason: null, - pagination: "none", - exhaustive: true, - effects: { - mutatesArtifact, - launchesProcess: false, - mayShowUi: false, - mayAccessNetwork: false, - mayWriteFilesystem, - changesPermissions: false, - requiresRoot: false, - }, - limits: { - maxResults: null, - maxPayloadBytes: null, - timeoutMs: null, - }, - limitations: [], -}); - -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; +import { + createBinarySessionTargets, + createCacheProvider, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; describe("binary session", () => { it("allocates one fresh run identity per provider client lifetime", async () => { - const [first, second] = await targets(); - const provider = cacheProvider([]); + const [first, second] = await createBinarySessionTargets(); + const provider = createCacheProvider([]); const createClient = provider.createClient.bind(provider); const runIds: string[] = []; provider.createClient = (target, profile, context) => { diff --git a/tests/composition/analysis-sessions/binarySession.04-replays-exact-immutable-calls-from.test.ts b/tests/composition/analysis-sessions/binarySession.04-replays-exact-immutable-calls-from.test.ts index d91b91a07..56da16443 100644 --- a/tests/composition/analysis-sessions/binarySession.04-replays-exact-immutable-calls-from.test.ts +++ b/tests/composition/analysis-sessions/binarySession.04-replays-exact-immutable-calls-from.test.ts @@ -1,95 +1,18 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import type { - AnalysisProvider, - CapabilityDescriptor, -} from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + createBinarySessionTargets, + createCacheProvider, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; import { ok as resultOk } from "../../../src/domain/result.js"; -import { observed as ok } from "../../fixtures/analysisExecution.js"; - -const cacheProvider = ( - calls: string[], - mayWriteFilesystem = false, -): AnalysisProvider => { - const identity = { - id: "fixture", - name: "Fixture analysis provider", - version: "1", - } as const; - return { - identity: () => identity, - resolveAnalysisProfile: () => - Promise.resolve( - resultOk({ - profile: createAnalysisProfile(identity, 1, { fixture: true }), - compatibility: {}, - }), - ), - capabilities: () => [ - cacheCapability(identity, "address_name", false, mayWriteFilesystem), - cacheCapability(identity, "set_address_name", true), - ], - createClient: () => ({ - execute: (operation) => { - calls.push(operation); - return Promise.resolve(ok(operation)); - }, - close: () => Promise.resolve(), - }), - }; -}; - -const cacheCapability = ( - provider: CapabilityDescriptor["provider"], - operation: "address_name" | "set_address_name", - mutatesArtifact: boolean, - mayWriteFilesystem = false, -): CapabilityDescriptor => ({ - provider, - operation, - inputContractVersion: 1, - outputContractVersion: 1, - available: true, - reason: null, - pagination: "none", - exhaustive: true, - effects: { - mutatesArtifact, - launchesProcess: false, - mayShowUi: false, - mayAccessNetwork: false, - mayWriteFilesystem, - changesPermissions: false, - requiresRoot: false, - }, - limits: { - maxResults: null, - maxPayloadBytes: null, - timeoutMs: null, - }, - limitations: [], -}); - -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; describe("binary session", () => { it("replays exact immutable calls from a matching provider-neutral snapshot", async () => { - const [first, second] = await targets(); + const [first, second] = await createBinarySessionTargets(); const initialCalls: string[] = []; - const initial = createTestBinarySession(cacheProvider(initialCalls)); + const initial = createTestBinarySession(createCacheProvider(initialCalls)); expect((await initial.open(first)).ok).toBe(true); expect( ( @@ -106,7 +29,7 @@ describe("binary session", () => { await initial.close(); const replayCalls: string[] = []; - const replay = createTestBinarySession(cacheProvider(replayCalls)); + const replay = createTestBinarySession(createCacheProvider(replayCalls)); expect(replay.importAnalysisSnapshot(snapshot.value)).toEqual({ ok: true, value: 1, @@ -147,7 +70,7 @@ describe("binary session", () => { }); await replay.close(); - const mismatch = createTestBinarySession(cacheProvider([])); + const mismatch = createTestBinarySession(createCacheProvider([])); expect(mismatch.importAnalysisSnapshot(snapshot.value).ok).toBe(true); const opened = await mismatch.open(second); expect(opened.ok).toBe(false); @@ -155,7 +78,7 @@ describe("binary session", () => { await mismatch.close(); const profileMismatchCalls: string[] = []; - const profileMismatchProvider = cacheProvider(profileMismatchCalls); + const profileMismatchProvider = createCacheProvider(profileMismatchCalls); const identity = profileMismatchProvider.identity(); profileMismatchProvider.resolveAnalysisProfile = () => Promise.resolve( @@ -199,9 +122,9 @@ describe("binary session", () => { }); it("does not snapshot reads that depend on the provider cursor", async () => { - const [first] = await targets(); + const [first] = await createBinarySessionTargets(); const calls: string[] = []; - const session = createTestBinarySession(cacheProvider(calls)); + const session = createTestBinarySession(createCacheProvider(calls)); expect((await session.open(first)).ok).toBe(true); expect((await session.execute("address_name", {})).ok).toBe(true); expect((await session.execute("address_name", {})).ok).toBe(true); diff --git a/tests/composition/analysis-sessions/binarySession.05-publishes-provider-health-availability-changes.test.ts b/tests/composition/analysis-sessions/binarySession.05-publishes-provider-health-availability-changes.test.ts index a652b129e..8ca20de01 100644 --- a/tests/composition/analysis-sessions/binarySession.05-publishes-provider-health-availability-changes.test.ts +++ b/tests/composition/analysis-sessions/binarySession.05-publishes-provider-health-availability-changes.test.ts @@ -1,95 +1,18 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import type { - AnalysisProvider, - CapabilityDescriptor, -} from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; import { ProviderAdapterError } from "../../../src/domain/errors.js"; -import { err, ok as resultOk } from "../../../src/domain/result.js"; +import { err } from "../../../src/domain/result.js"; +import { + createBinarySessionTargets, + createCacheProvider, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; -const cacheProvider = ( - calls: string[], - mayWriteFilesystem = false, -): AnalysisProvider => { - const identity = { - id: "fixture", - name: "Fixture analysis provider", - version: "1", - } as const; - return { - identity: () => identity, - resolveAnalysisProfile: () => - Promise.resolve( - resultOk({ - profile: createAnalysisProfile(identity, 1, { fixture: true }), - compatibility: {}, - }), - ), - capabilities: () => [ - cacheCapability(identity, "address_name", false, mayWriteFilesystem), - cacheCapability(identity, "set_address_name", true), - ], - createClient: () => ({ - execute: (operation) => { - calls.push(operation); - return Promise.resolve(ok(operation)); - }, - close: () => Promise.resolve(), - }), - }; -}; - -const cacheCapability = ( - provider: CapabilityDescriptor["provider"], - operation: "address_name" | "set_address_name", - mutatesArtifact: boolean, - mayWriteFilesystem = false, -): CapabilityDescriptor => ({ - provider, - operation, - inputContractVersion: 1, - outputContractVersion: 1, - available: true, - reason: null, - pagination: "none", - exhaustive: true, - effects: { - mutatesArtifact, - launchesProcess: false, - mayShowUi: false, - mayAccessNetwork: false, - mayWriteFilesystem, - changesPermissions: false, - requiresRoot: false, - }, - limits: { - maxResults: null, - maxPayloadBytes: null, - timeoutMs: null, - }, - limitations: [], -}); - -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; - describe("binary session", () => { it("publishes provider-health availability changes and resets on target switch", async () => { - const [first, second] = await targets(); - const provider = cacheProvider([]); + const [first, second] = await createBinarySessionTargets(); + const provider = createCacheProvider([]); provider.createClient = () => ({ execute: (operation) => Promise.resolve( @@ -130,8 +53,8 @@ describe("binary session", () => { }); it("isolates availability observers from execution results and session state", async () => { - const [first] = await targets(); - const provider = cacheProvider([]); + const [first] = await createBinarySessionTargets(); + const provider = createCacheProvider([]); let providerCalls = 0; provider.createClient = () => ({ execute: (operation) => { @@ -179,9 +102,9 @@ describe("binary session", () => { }); it("does not replay operations with filesystem side effects", async () => { - const [first] = await targets(); + const [first] = await createBinarySessionTargets(); const calls: string[] = []; - const session = createTestBinarySession(cacheProvider(calls, true)); + const session = createTestBinarySession(createCacheProvider(calls, true)); expect((await session.open(first)).ok).toBe(true); const input = { address: "0x1000", document: "first" }; expect((await session.execute("address_name", input)).ok).toBe(true); diff --git a/tests/composition/analysis-sessions/binarySession.06-returns-typed-unavailability-without-dispatching.test.ts b/tests/composition/analysis-sessions/binarySession.06-returns-typed-unavailability-without-dispatching.test.ts index 0534ccd25..3ba62fe43 100644 --- a/tests/composition/analysis-sessions/binarySession.06-returns-typed-unavailability-without-dispatching.test.ts +++ b/tests/composition/analysis-sessions/binarySession.06-returns-typed-unavailability-without-dispatching.test.ts @@ -1,14 +1,14 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - import type { AnalysisClient, AnalysisProvider, } from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + ControllableAnalysisClient, + createBinarySessionTargets, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { HopperStartError } from "../../../src/domain/errors.js"; import { err } from "../../../src/domain/result.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; @@ -18,38 +18,9 @@ const client = (fail = false): AnalysisClient => ({ close: () => Promise.resolve(), }); -class TestClient implements AnalysisClient { - closed = 0; - constructor( - readonly pendingHealth?: Promise>, - readonly failHealth = false, - readonly pendingCall?: Promise>, - ) {} - execute(name: string) { - if (name === "health") - return this.failHealth - ? Promise.resolve(err(new HopperStartError())) - : (this.pendingHealth ?? Promise.resolve(ok(null))); - return this.pendingCall ?? Promise.resolve(ok(null)); - } - close(): Promise { - this.closed += 1; - return Promise.resolve(); - } -} - -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; - describe("binary session", () => { it("returns typed unavailability without dispatching a partial provider", async () => { - const [first] = await targets(); + const [first] = await createBinarySessionTargets(); const operations: string[] = []; const provider: AnalysisProvider = { identity: () => ({ id: "partial", name: "Partial", version: "1" }), @@ -111,11 +82,7 @@ describe("binary session", () => { }); it("keeps the active client when a switch fails", async () => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); + const [first, second] = await createBinarySessionTargets(); let created = 0; const session = createTestBinarySession(() => client(created++ === 1)); expect((await session.open(first)).ok).toBe(true); @@ -131,10 +98,10 @@ describe("binary session", () => { }); it("serializes concurrent opens and leaves the last target active", async () => { - const [first, second] = await targets(); - const clients: TestClient[] = []; + const [first, second] = await createBinarySessionTargets(); + const clients: ControllableAnalysisClient[] = []; const session = createTestBinarySession(() => { - const value = new TestClient(); + const value = new ControllableAnalysisClient(); clients.push(value); return value; }); diff --git a/tests/composition/analysis-sessions/binarySession.07-replaces-the-active-client-when.test.ts b/tests/composition/analysis-sessions/binarySession.07-replaces-the-active-client-when.test.ts index 8b9290b4a..78a0dff3b 100644 --- a/tests/composition/analysis-sessions/binarySession.07-replaces-the-active-client-when.test.ts +++ b/tests/composition/analysis-sessions/binarySession.07-replaces-the-active-client-when.test.ts @@ -4,57 +4,14 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import type { AnalysisClient } from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { HopperStartError } from "../../../src/domain/errors.js"; -import { err } from "../../../src/domain/result.js"; +import { + ControllableAnalysisClient, + createBinarySessionTargets, + createDeferred, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; -class TestClient implements AnalysisClient { - closed = 0; - constructor( - readonly pendingHealth?: Promise>, - readonly failHealth = false, - readonly pendingCall?: Promise>, - ) {} - execute(name: string) { - if (name === "health") - return this.failHealth - ? Promise.resolve(err(new HopperStartError())) - : (this.pendingHealth ?? Promise.resolve(ok(null))); - return this.pendingCall ?? Promise.resolve(ok(null)); - } - close(): Promise { - this.closed += 1; - return Promise.resolve(); - } -} - -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; - -const deferred = (): { - readonly promise: Promise; - resolve(value: T): void; -} => { - let resolvePromise: ((value: T) => void) | undefined; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - return { - promise, - resolve(value) { - resolvePromise?.(value); - }, - }; -}; - describe("binary session", () => { it("replaces the active client when a canonical path changes contents", async () => { const directory = await createTestTempDirectory("bb-session-"); @@ -115,11 +72,11 @@ describe("binary session", () => { }); it("waits for an active call before closing its client during a switch", async () => { - const [first, second] = await targets(); - const active = deferred>(); - const clients: TestClient[] = []; + const [first, second] = await createBinarySessionTargets(); + const active = createDeferred>(); + const clients: ControllableAnalysisClient[] = []; const session = createTestBinarySession(() => { - const value = new TestClient( + const value = new ControllableAnalysisClient( undefined, false, clients.length === 0 ? active.promise : undefined, @@ -139,12 +96,14 @@ describe("binary session", () => { }); it("cancels an open queued behind another transition without creating a client", async () => { - const [first, second] = await targets(); - const health = deferred>(); + const [first, second] = await createBinarySessionTargets(); + const health = createDeferred>(); let created = 0; const session = createTestBinarySession(() => { created += 1; - return new TestClient(created === 1 ? health.promise : undefined); + return new ControllableAnalysisClient( + created === 1 ? health.promise : undefined, + ); }); const opening = session.open(first); const controller = new AbortController(); diff --git a/tests/composition/analysis-sessions/binarySession.08-cancels-legacy-profile-resolution-even.test.ts b/tests/composition/analysis-sessions/binarySession.08-cancels-legacy-profile-resolution-even.test.ts index 654b35c49..df9131a35 100644 --- a/tests/composition/analysis-sessions/binarySession.08-cancels-legacy-profile-resolution-even.test.ts +++ b/tests/composition/analysis-sessions/binarySession.08-cancels-legacy-profile-resolution-even.test.ts @@ -1,133 +1,20 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import type { - AnalysisClient, - AnalysisProvider, - CapabilityDescriptor, -} from "../../../src/application/AnalysisProvider.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; -import { HopperStartError } from "../../../src/domain/errors.js"; +import { + ControllableAnalysisClient, + createBinarySessionTargets, + createCacheProvider, + createDeferred, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; -import { err, ok as resultOk } from "../../../src/domain/result.js"; +import { err } from "../../../src/domain/result.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; -const cacheProvider = ( - calls: string[], - mayWriteFilesystem = false, -): AnalysisProvider => { - const identity = { - id: "fixture", - name: "Fixture analysis provider", - version: "1", - } as const; - return { - identity: () => identity, - resolveAnalysisProfile: () => - Promise.resolve( - resultOk({ - profile: createAnalysisProfile(identity, 1, { fixture: true }), - compatibility: {}, - }), - ), - capabilities: () => [ - cacheCapability(identity, "address_name", false, mayWriteFilesystem), - cacheCapability(identity, "set_address_name", true), - ], - createClient: () => ({ - execute: (operation) => { - calls.push(operation); - return Promise.resolve(ok(operation)); - }, - close: () => Promise.resolve(), - }), - }; -}; - -const cacheCapability = ( - provider: CapabilityDescriptor["provider"], - operation: "address_name" | "set_address_name", - mutatesArtifact: boolean, - mayWriteFilesystem = false, -): CapabilityDescriptor => ({ - provider, - operation, - inputContractVersion: 1, - outputContractVersion: 1, - available: true, - reason: null, - pagination: "none", - exhaustive: true, - effects: { - mutatesArtifact, - launchesProcess: false, - mayShowUi: false, - mayAccessNetwork: false, - mayWriteFilesystem, - changesPermissions: false, - requiresRoot: false, - }, - limits: { - maxResults: null, - maxPayloadBytes: null, - timeoutMs: null, - }, - limitations: [], -}); - -class TestClient implements AnalysisClient { - closed = 0; - constructor( - readonly pendingHealth?: Promise>, - readonly failHealth = false, - readonly pendingCall?: Promise>, - ) {} - execute(name: string) { - if (name === "health") - return this.failHealth - ? Promise.resolve(err(new HopperStartError())) - : (this.pendingHealth ?? Promise.resolve(ok(null))); - return this.pendingCall ?? Promise.resolve(ok(null)); - } - close(): Promise { - this.closed += 1; - return Promise.resolve(); - } -} - -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; - -const deferred = (): { - readonly promise: Promise; - resolve(value: T): void; -} => { - let resolvePromise: ((value: T) => void) | undefined; - const promise = new Promise((resolve) => { - resolvePromise = resolve; - }); - return { - promise, - resolve(value) { - resolvePromise?.(value); - }, - }; -}; - describe("binary session", () => { it("cancels legacy profile resolution even when the provider ignores its signal", async () => { - const [first] = await targets(); - const provider = cacheProvider([]); + const [first] = await createBinarySessionTargets(); + const provider = createCacheProvider([]); let observedSignal: AbortSignal | undefined; let created = 0; provider.resolveAnalysisProfile = (_target, options) => { @@ -136,7 +23,7 @@ describe("binary session", () => { }; provider.createClient = () => { created += 1; - return new TestClient(); + return new ControllableAnalysisClient(); }; const session = createTestBinarySession(provider); const controller = new AbortController(); @@ -155,10 +42,10 @@ describe("binary session", () => { }); it("cancels a call while it waits for a transition", async () => { - const [first] = await targets(); - const health = deferred>(); + const [first] = await createBinarySessionTargets(); + const health = createDeferred>(); const session = createTestBinarySession( - () => new TestClient(health.promise), + () => new ControllableAnalysisClient(health.promise), ); const opening = session.open(first); const controller = new AbortController(); @@ -178,10 +65,13 @@ describe("binary session", () => { }); it("closes a failed candidate and reopens the previous target", async () => { - const [first, second] = await targets(); - const clients: TestClient[] = []; + const [first, second] = await createBinarySessionTargets(); + const clients: ControllableAnalysisClient[] = []; const session = createTestBinarySession(() => { - const value = new TestClient(undefined, clients.length === 1); + const value = new ControllableAnalysisClient( + undefined, + clients.length === 1, + ); clients.push(value); return value; }); @@ -197,7 +87,7 @@ describe("binary session", () => { }); it("closes the active bridge before starting a replacement", async () => { - const [first, second] = await targets(); + const [first, second] = await createBinarySessionTargets(); let liveClients = 0; let overlapped = false; const session = createTestBinarySession(() => ({ @@ -218,7 +108,7 @@ describe("binary session", () => { }); it("clears session state while preserving a typed provider cleanup failure", async () => { - const [first] = await targets(); + const [first] = await createBinarySessionTargets(); const cleanupError = new ProviderCleanupError( "fixture", ["fixture-document"], diff --git a/tests/composition/analysis-sessions/binarySession.09-does-not-start-a-replacement.test.ts b/tests/composition/analysis-sessions/binarySession.09-does-not-start-a-replacement.test.ts index dd30ad6cd..ab0dddcf6 100644 --- a/tests/composition/analysis-sessions/binarySession.09-does-not-start-a-replacement.test.ts +++ b/tests/composition/analysis-sessions/binarySession.09-does-not-start-a-replacement.test.ts @@ -1,26 +1,16 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; - -import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + createBinarySessionTargets, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; import { err } from "../../../src/domain/result.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; -const targets = async (): Promise => { - const directory = await createTestTempDirectory("bb-session-"); - const first = join(directory, "first.hop"); - const second = join(directory, "second.hop"); - await writeFile(first, "one"); - await writeFile(second, "two"); - return [first, second]; -}; - describe("binary session", () => { it("does not start a replacement after the active provider cleanup is unconfirmed", async () => { - const [first, second] = await targets(); + const [first, second] = await createBinarySessionTargets(); const cleanupError = new ProviderCleanupError( "fixture", ["fixture-document"], diff --git a/tests/composition/analysis-sessions/errors.test.ts b/tests/composition/analysis-sessions/errors.test.ts deleted file mode 100644 index 3ad152b87..000000000 --- a/tests/composition/analysis-sessions/errors.test.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - AnalysisCancelledError, - AnalysisCapabilityUnavailableError, - AnalysisInputError, - AnalysisOutputError, - AnalysisProtocolError, - AnalysisTimeoutError, - ArtifactOperationError, - BinaryTargetError, - BrowserObservationError, - ConfigurationError, - EvidenceFileError, - EvidenceIntegrityError, - EvidenceLimitError, - HopperCancelledError, - HopperProcessError, - HopperProtocolError, - HopperRemoteError, - HopperStartError, - HopperTimeoutError, - InvestigationWorkspaceError, - NoBinaryOpenError, - ProviderAdapterError, - ProviderSelectionError, - PermissionRequiredError, - ReplayPlanStaleError, - UnknownRegistryError, - projectAnalysisError, - type AnalysisError, - type AnalysisErrorTag, -} from "../../../src/domain/errors.js"; -import { ProcessCaptureError } from "../../../src/application/ProcessHarness.js"; -import { - analysisErrorJsonSchema, - analysisErrorProjectionSchema, -} from "../../../src/contracts/errorSchemas.js"; - -const expectSecretsHidden = (serialized: string): void => { - for (const hidden of [ - "secret-token", - "AnalysisCapabilityUnavailableError", - "declaredSha256", - ]) - expect(serialized).not.toContain(hidden); -}; - -describe("analysis error projection: provider failures", () => { - it("uses the correct provider-neutral protocol tag", () => { - expect(new AnalysisProtocolError("invalid")._tag).toBe( - "AnalysisProtocolError", - ); - }); - - it("projects bounded provider diagnostics without dropping local coordinates", () => { - const diagnostics = { - runtime_root: "/tmp/rea-ghidra-fixture", - profile_digest: "a".repeat(64), - exit_code: 1, - }; - const error = new ProviderAdapterError("ghidra", "health", { - diagnostics, - }); - - expect(error.diagnostics).not.toBe(diagnostics); - expect(projectAnalysisError(error)).toMatchObject({ - details: { - provider_id: "ghidra", - operation: "health", - diagnostics, - }, - }); - }); - - it("projects stable, safe Linux startup failure codes", () => { - const expected = [ - [70, "private_display_unavailable"], - [71, "x11_authorization_failed"], - [72, "unsupported_hopper_build"], - [73, "invalid_launch_command"], - [74, "process_ownership_mismatch"], - [75, "hopper_exited_during_startup"], - [76, "unsupported_demo_dialog"], - [77, "unexpected_display_geometry"], - [78, "x11_input_failed"], - [79, "runtime_dependency_unavailable"], - [80, "x11_socket_directory_unusable"], - ] as const; - for (const [exitCode, code] of expected) { - const projected = projectAnalysisError(new HopperProcessError(exitCode)); - expect(projected).toMatchObject({ - code: "provider_unavailable", - details: { failure_code: code, exit_code: exitCode }, - }); - expect(projected.message.length).toBeGreaterThan(20); - expect(JSON.stringify(projected)).not.toContain("/proc/"); - } - }); -}); - -describe("analysis error projection: typed failures", () => { - it("projects every typed failure without secrets", () => { - const secretCause = new Error("secret-token"); - const byTag = { - AnalysisProtocolError: new AnalysisProtocolError("protocol failed"), - AnalysisInputError: new AnalysisInputError("overview", { - cause: secretCause, - }), - AnalysisOutputError: new AnalysisOutputError( - "overview", - "invalid shape", - { - cause: secretCause, - }, - ), - AnalysisCapabilityUnavailableError: - new AnalysisCapabilityUnavailableError("fixture", "overview", "absent"), - AnalysisCancelledError: new AnalysisCancelledError("overview"), - AnalysisTimeoutError: new AnalysisTimeoutError("overview", 50), - ProviderSelectionError: new ProviderSelectionError("overview"), - ProviderAdapterError: new ProviderAdapterError("fixture", "overview", { - cause: secretCause, - }), - BrowserObservationError: new BrowserObservationError( - "inspect_web_page", - "endpoint_unreachable", - { cause: secretCause }, - ), - ArtifactOperationError: new ArtifactOperationError( - "inventory_artifact", - "integrity", - ), - ProcessCaptureError: new ProcessCaptureError("capture failed", { - cause: secretCause, - }), - EvidenceIntegrityError: new EvidenceIntegrityError("integrity failed", { - cause: secretCause, - }), - EvidenceLimitError: new EvidenceLimitError("records", 10), - EvidenceFileError: new EvidenceFileError("read", "outside-root", { - cause: secretCause, - }), - InvestigationWorkspaceError: new InvestigationWorkspaceError( - "update", - "revision-conflict", - { cause: secretCause }, - ), - UnknownRegistryError: new UnknownRegistryError("revision-conflict", { - cause: secretCause, - }), - HopperTimeoutError: new HopperTimeoutError(100), - HopperCancelledError: new HopperCancelledError(), - HopperProtocolError: new HopperProtocolError("wire failed", { - cause: secretCause, - }), - HopperRemoteError: new HopperRemoteError(7, "safe"), - HopperProcessError: new HopperProcessError(1), - HopperStartError: new HopperStartError({ cause: secretCause }), - ConfigurationError: new ConfigurationError("configuration failed", { - cause: secretCause, - }), - NoBinaryOpenError: new NoBinaryOpenError(), - BinaryTargetError: new BinaryTargetError( - "/secret/local/path", - "invalid", - { - cause: secretCause, - }, - ), - PermissionRequiredError: new PermissionRequiredError({ - requested: { - capability: "evidence_read", - roots: ["/workspace/evidence.json"], - executables: [], - environment_names: ["TOKEN_NAME"], - network: "none", - mount: false, - operation_identity: "read:evidence", - }, - missing: { environment_names: ["TOKEN_NAME"] }, - ceiling: null, - remediation: "restart", - }), - ReplayPlanStaleError: new ReplayPlanStaleError( - "a".repeat(64), - "b".repeat(64), - ), - } satisfies Readonly>; - const errors = Object.values(byTag); - const projected = errors.map(projectAnalysisError); - expect(projected).toHaveLength(27); - const serialized = JSON.stringify(projected); - expectSecretsHidden(serialized); - expect(serialized).toContain("/secret/local/path"); - expect(serialized).toContain('"environment_names":["TOKEN_NAME"]'); - expect( - projectAnalysisError(byTag.AnalysisCapabilityUnavailableError), - ).toMatchObject({ - category: "unsupported_provider", - }); - expect( - projectAnalysisError( - new ArtifactOperationError("inventory_artifact", "integrity", { - logicalPath: "main.js", - declaredSha256: "a".repeat(64), - calculatedSha256: "b".repeat(64), - unpacked: true, - }), - ), - ).toMatchObject({ - category: "integrity_mismatch", - details: { - logical_path: "main.js", - declared_sha256: "a".repeat(64), - calculated_sha256: "b".repeat(64), - unpacked: true, - }, - }); - expect(projected.every(({ message }) => message.length > 0)).toBe(true); - expect( - projected.every( - (value) => analysisErrorProjectionSchema.safeParse(value).success, - ), - ).toBe(true); - expect(analysisErrorJsonSchema).toMatchObject({ oneOf: expect.any(Array) }); - expect( - projectAnalysisError( - new ProcessCaptureError("terminal cleanup failed", { - reason: "cleanup_incomplete", - cleanupResources: ["process_group"], - }), - ), - ).toMatchObject({ - code: "cleanup_incomplete", - details: { cleanup: "incomplete", resources: ["process_group"] }, - }); - expect( - projected - .map(({ message }) => message) - .filter( - (message) => - !/try again|run `rea doctor`|current target|when ready|smaller request|smaller artifact|fresh copy|re-import|reduce|inline evidence|configured evidence directory|evidence file|allow overwrite|refresh the current state|reported setting|call open_binary|supported file|review capture policy|review the requested scope/iu.test( - message, - ), - ), - ).toEqual([]); - }); -}); - -describe("analysis error projection: remediation", () => { - it.each([ - [ - "configure", - false, - false, - "Add the exact missing scope beneath the administrator ceiling, then retry.", - ], - [ - "elicit", - false, - true, - "Approve the exact missing scope, then retry the operation.", - ], - [ - "restart", - true, - false, - "Add the exact missing scope to the administrator configuration, then restart the registered MCP server or client.", - ], - ] as const)( - "projects %s permission remediation into the stable protocol fields", - (remediation, restartRequired, elicitationSupported, action) => { - const projected = projectAnalysisError( - new PermissionRequiredError({ - requested: { - capability: "evidence_read", - roots: ["/workspace/evidence.json"], - executables: [], - environment_names: [], - network: "none", - mount: false, - operation_identity: "read:evidence", - }, - missing: { roots: ["/workspace/evidence.json"] }, - ceiling: null, - remediation, - }), - ); - - expect(projected.remediation).toEqual({ - action, - restart_required: restartRequired, - elicitation_supported: elicitationSupported, - }); - }, - ); - - it("gives missing residual unknowns a lookup-specific remediation", () => { - expect( - projectAnalysisError(new UnknownRegistryError("not-found")), - ).toMatchObject({ - code: "execution_failure", - message: - "The requested residual unknown does not exist in this session. Check the unknown_id and try again.", - remediation: { - action: - "Check that the unknown_id belongs to this session, then retry.", - }, - details: { reason: "not-found" }, - }); - }); - - it("retains custom input guidance in the projected error details", () => { - expect( - projectAnalysisError( - new AnalysisInputError("find_changed_behavior", undefined, [ - { - path: [], - reason: "invalid_value", - message: - "Supply either existing comparisons or one investigation_run", - }, - ]), - ), - ).toMatchObject({ - details: { - issues: [ - { - path: [], - reason: "invalid_value", - message: - "Supply either existing comparisons or one investigation_run", - }, - ], - }, - }); - }); - - it("distinguishes disabled integrity continuation from format support", () => { - expect( - projectAnalysisError( - new ArtifactOperationError("inventory_artifact", "policy"), - ), - ).toMatchObject({ - code: "artifact_operation_failed", - category: "unavailable", - message: expect.stringContaining( - "REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true", - ), - }); - }); -}); - -describe("analysis error projection: contract variants", () => { - it("validates every closed error-reason variant against the generated contract", () => { - const variants: AnalysisError[] = [ - ...( - [ - "cancelled", - "format", - "integrity", - "limit", - "path", - "policy", - "unavailable", - "io", - ] as const - ).map( - (reason) => new ArtifactOperationError("inventory_artifact", reason), - ), - ...( - [ - "disabled", - "outside-root", - "not-file", - "too-large", - "exists", - "invalid-json", - "io", - ] as const - ).map((reason) => new EvidenceFileError("read", reason)), - ...( - [ - "disabled", - "outside-root", - "not-file", - "too-large", - "invalid-json", - "integrity", - "locked", - "revision-conflict", - "name-conflict", - "io", - ] as const - ).map((reason) => new InvestigationWorkspaceError("update", reason)), - ...( - [ - "not-found", - "already-exists", - "revision-conflict", - "invalid-transition", - "integrity", - "limit", - ] as const - ).map((reason) => new UnknownRegistryError(reason)), - ...( - [ - "capture_failed", - "cleanup_incomplete", - "permission_required", - "cancelled", - ] as const - ).map( - (reason) => - new ProcessCaptureError("SECRET capture diagnostic", { reason }), - ), - ...( - [ - "remote", - "authorization", - "invalid_request", - "bridge_exception", - ] as const - ).map((diagnostic) => new HopperRemoteError(9, "safe", diagnostic)), - ]; - - expect(variants).toHaveLength(39); - for (const variant of variants) { - const projected = projectAnalysisError(variant); - const parsed = analysisErrorProjectionSchema.safeParse(projected); - expect(parsed, JSON.stringify({ variant, projected })).toMatchObject({ - success: true, - }); - expect(projected.code).toMatch(/^[a-z][a-z0-9_]*$/u); - expect(JSON.stringify(projected)).not.toContain("SECRET"); - } - }); -}); diff --git a/tests/boundary/providers/managed/managedApplicationGraph.test.ts b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts similarity index 76% rename from tests/boundary/providers/managed/managedApplicationGraph.test.ts rename to tests/composition/analysis-sessions/managedApplicationGraph.test.ts index daffb3e15..ecf21072e 100644 --- a/tests/boundary/providers/managed/managedApplicationGraph.test.ts +++ b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts @@ -1,64 +1,21 @@ -import { createHash } from "node:crypto"; - import { describe, expect, it } from "vitest"; -import { projectManagedApplicationGraphEvidence } from "../../../../src/application/ManagedApplicationGraphService.js"; -import { traceApplicationFeatureEvidence } from "../../../../src/application/JavaScriptApplicationWorkflowService.js"; -import { MANAGED_STATIC_PROVIDER } from "../../../../src/application/InvestigationProviders.js"; -import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js"; +import { projectManagedApplicationGraphEvidence } from "../../../src/application/ManagedApplicationGraphService.js"; +import { traceApplicationFeatureEvidence } from "../../../src/application/JavaScriptApplicationWorkflowService.js"; +import { MANAGED_STATIC_PROVIDER } from "../../../src/application/InvestigationProviders.js"; +import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; +import { parseJavaScriptApplicationGraph } from "../../../src/domain/javascriptApplicationGraph.js"; +import { managedApplicationGraphResultSchema } from "../../../src/domain/managedApplicationGraph.js"; +import { inspectManagedArtifactBytes } from "../../../src/dotnet/ManagedArtifactInspector.js"; +import { inspectManagedMembersBytes } from "../../../src/dotnet/ManagedMemberInspector.js"; +import { inspectManagedNativeBoundariesBytes } from "../../../src/dotnet/ManagedNativeBoundaryInspector.js"; import { - createEvidence, - parseEvidence, -} from "../../../../src/domain/evidence.js"; -import { parseJavaScriptApplicationGraph } from "../../../../src/domain/javascriptApplicationGraph.js"; -import { managedApplicationGraphResultSchema } from "../../../../src/domain/managedApplicationGraph.js"; -import { inspectManagedArtifactBytes } from "../../../../src/dotnet/ManagedArtifactInspector.js"; -import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { inspectManagedNativeBoundariesBytes } from "../../../../src/dotnet/ManagedNativeBoundaryInspector.js"; -import { buildManagedPeFixture } from "../../../fixtures/managedPe.js"; - -const artifactLimits = { - referenceOffset: 0, - referenceLimit: 100, - resourceOffset: 0, - resourceLimit: 100, - attributeOffset: 0, - attributeLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, -}; - -const memberLimits = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; - -const boundaryLimits = { - moduleRefOffset: 0, - moduleRefLimit: 100, - importOffset: 0, - importLimit: 100, - implementationOffset: 0, - implementationLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, -}; + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_ARTIFACT_FIXTURE_LIMITS, + MANAGED_MEMBER_FIXTURE_LIMITS, + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, +} from "../../../src/dotnet/ManagedPe.fixture.js"; describe("managed application graph projection", () => { it("projects managed metadata and native declarations into authenticated graph Evidence", () => { @@ -69,17 +26,21 @@ describe("managed application graph projection", () => { mappingFlags: 0x0345, }, }); - const binary = target(bytes); + const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe"); const managedArtifact = inspectManagedArtifactBytes( bytes, binary, - artifactLimits, + MANAGED_ARTIFACT_FIXTURE_LIMITS, + ); + const members = inspectManagedMembersBytes( + bytes, + binary, + MANAGED_MEMBER_FIXTURE_LIMITS, ); - const members = inspectManagedMembersBytes(bytes, binary, memberLimits); const boundaries = inspectManagedNativeBoundariesBytes( bytes, binary, - boundaryLimits, + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, ); const artifactEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, { operation: "inspect_managed_artifact", @@ -213,8 +174,12 @@ describe("managed application graph projection", () => { describe("managed application graph coverage", () => { it("preserves source-page omissions in graph and per-fact coverage", () => { const bytes = buildManagedPeFixture(); - const binary = target(bytes); - const members = inspectManagedMembersBytes(bytes, binary, memberLimits); + const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe"); + const members = inspectManagedMembersBytes( + bytes, + binary, + MANAGED_MEMBER_FIXTURE_LIMITS, + ); const partialMembers = { ...members, methods: { @@ -337,14 +302,3 @@ describe("managed application graph coverage", () => { }); }); }); - -const target = (bytes: Buffer): BinaryTarget => ({ - path: "/fixture/ManagedInterop.exe", - sha256: createHash("sha256").update(bytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}); diff --git a/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts b/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts index 57e0f4ea5..11d1bf11d 100644 --- a/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts +++ b/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts @@ -14,7 +14,7 @@ import { electronActiveObservationInputSchema } from "../../../src/domain/electr import { inspectElectronPageInputSchema } from "../../../src/domain/electronObservation.js"; import { createWebTextArtifact } from "../../../src/domain/webContentArtifact.js"; import { permissionAuthorityForRoot } from "../../fixtures/permissionAuthority.js"; -import { createElectronActiveObservationFixtureResult } from "../../fixtures/electronActiveObservationResult.js"; +import { createElectronActiveObservationFixtureResult } from "../../../src/domain/electronActiveObservation.fixture.js"; const SOURCE = `const worker = new Worker("./worker.js");\nexport const observed = worker;\n`; diff --git a/tests/conformance/contracts/analysisErrorProjection.test.ts b/tests/conformance/contracts/analysisErrorProjection.test.ts new file mode 100644 index 000000000..2d0b17771 --- /dev/null +++ b/tests/conformance/contracts/analysisErrorProjection.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from "vitest"; + +import { ProcessCaptureError } from "../../../src/application/ProcessCaptureError.js"; +import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js"; +import { + ArtifactOperationError, + EvidenceFileError, + HopperRemoteError, + InvestigationWorkspaceError, + UnknownRegistryError, + projectAnalysisError, + type AnalysisError, +} from "../../../src/domain/errors.js"; + +describe("analysis error projection contract", () => { + it("accepts every closed error-reason variant without exposing diagnostics", () => { + const variants: AnalysisError[] = [ + ...( + [ + "cancelled", + "format", + "integrity", + "limit", + "path", + "policy", + "unavailable", + "io", + ] as const + ).map( + (reason) => new ArtifactOperationError("inventory_artifact", reason), + ), + ...( + [ + "disabled", + "outside-root", + "not-file", + "too-large", + "exists", + "invalid-json", + "io", + ] as const + ).map((reason) => new EvidenceFileError("read", reason)), + ...( + [ + "disabled", + "outside-root", + "not-file", + "too-large", + "invalid-json", + "integrity", + "locked", + "revision-conflict", + "name-conflict", + "io", + ] as const + ).map((reason) => new InvestigationWorkspaceError("update", reason)), + ...( + [ + "not-found", + "already-exists", + "revision-conflict", + "invalid-transition", + "integrity", + "limit", + ] as const + ).map((reason) => new UnknownRegistryError(reason)), + ...( + [ + "capture_failed", + "cleanup_incomplete", + "permission_required", + "cancelled", + ] as const + ).map( + (reason) => + new ProcessCaptureError("SECRET capture diagnostic", { reason }), + ), + ...( + [ + "remote", + "authorization", + "invalid_request", + "bridge_exception", + ] as const + ).map((diagnostic) => new HopperRemoteError(9, "safe", diagnostic)), + ]; + + expect(variants).toHaveLength(39); + for (const variant of variants) { + const projected = projectAnalysisError(variant); + expect( + analysisErrorProjectionSchema.safeParse(projected), + JSON.stringify({ variant, projected }), + ).toMatchObject({ success: true }); + expect(projected.code).toMatch(/^[a-z][a-z0-9_]*$/u); + expect(JSON.stringify(projected)).not.toContain("SECRET"); + } + }); +}); diff --git a/tests/conformance/providers/coverageMonotonicity.test.ts b/tests/conformance/providers/coverageMonotonicity.test.ts index 4978d8897..5e5ad1b3f 100644 --- a/tests/conformance/providers/coverageMonotonicity.test.ts +++ b/tests/conformance/providers/coverageMonotonicity.test.ts @@ -1,9 +1,6 @@ -import { createHash } from "node:crypto"; - import { fc, it } from "@fast-check/vitest"; import { describe, expect } from "vitest"; -import type { BinaryTarget } from "../../../src/domain/binaryTarget.js"; import { assessManagedGraphOmissions, managedGraphEvidenceCoverage, @@ -16,38 +13,12 @@ import { } from "../../../src/domain/knownPageCoverage.js"; import { inspectManagedMembersBytes } from "../../../src/dotnet/ManagedMemberInspector.js"; import { inspectManagedNativeBoundariesBytes } from "../../../src/dotnet/ManagedNativeBoundaryInspector.js"; -import { buildManagedPeFixture } from "../../fixtures/managedPe.js"; - -const MEMBER_LIMITS = { - typeOffset: 0, - typeLimit: 100, - methodOffset: 0, - methodLimit: 100, - fieldOffset: 0, - fieldLimit: 100, - memberRefOffset: 0, - memberRefLimit: 100, - edgeOffset: 0, - edgeLimit: 100, - instructionAnchorLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, - maxMethodBodyBytes: 1024 * 1024, - maxMethodInstructions: 1_000, -}; - -const BOUNDARY_LIMITS = { - moduleRefOffset: 0, - moduleRefLimit: 100, - importOffset: 0, - importLimit: 100, - implementationOffset: 0, - implementationLimit: 100, - maxMetadataBytes: 1024 * 1024, - maxTableRows: 1_000, - maxHeapItemBytes: 1024 * 1024, -}; +import { + buildManagedPeFixture, + managedPeFixtureTarget, + MANAGED_MEMBER_FIXTURE_LIMITS, + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, +} from "../../../src/dotnet/ManagedPe.fixture.js"; const limitPair = fc .tuple(fc.integer({ min: 1, max: 12 }), fc.integer({ min: 1, max: 12 })) @@ -182,25 +153,19 @@ const fixtureBytes = buildManagedPeFixture({ }, readyToRun: true, }); -const fixtureTarget: BinaryTarget = { - path: "/fixture/ManagedCoverage.exe", - sha256: createHash("sha256").update(fixtureBytes).digest("hex"), - kind: "executable", - format: "pe", - architecture: "x86", - availableArchitectures: ["x86"], - executableRole: "application", - managed: true, -}; +const fixtureTarget = managedPeFixtureTarget( + fixtureBytes, + "/fixture/ManagedCoverage.exe", +); const fixtureMembers = inspectManagedMembersBytes( fixtureBytes, fixtureTarget, - MEMBER_LIMITS, + MANAGED_MEMBER_FIXTURE_LIMITS, ); const fixtureBoundaries = inspectManagedNativeBoundariesBytes( fixtureBytes, fixtureTarget, - BOUNDARY_LIMITS, + MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS, ); const managedSources = (totals: readonly number[]) => ({ diff --git a/tests/fixtures/binarySession.ts b/tests/fixtures/binarySession.ts index 0af3fa68d..e38c272f8 100644 --- a/tests/fixtures/binarySession.ts +++ b/tests/fixtures/binarySession.ts @@ -1,14 +1,24 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { createAnalysisProfile } from "../../src/domain/analysisProfile.js"; import type { BinaryTarget } from "../../src/domain/binaryTarget.js"; import type { AnalysisError } from "../../src/domain/errors.js"; import type { Result } from "../../src/domain/result.js"; import type { + AnalysisClient, AnalysisClientFactory, + CapabilityDescriptor, AnalysisProfileResolution, AnalysisProfileResolutionOptions, AnalysisProvider, } from "../../src/application/AnalysisProvider.js"; import { BinarySession } from "../../src/application/BinarySession.js"; import { SessionProviderRouter } from "../../src/application/SessionProviderRouter.js"; +import { HopperStartError } from "../../src/domain/errors.js"; +import { err, ok as resultOk } from "../../src/domain/result.js"; +import { observed } from "./analysisExecution.js"; +import { createTestTempDirectory } from "./temporaryDirectory.js"; type TestBinarySessionOptions = { readonly resolveAnalysisProfile?: ( @@ -32,3 +42,119 @@ export const createTestBinarySession = ( : (optionsOrResolver ?? {}); return new BinarySession(SessionProviderRouter.single(provider, options)); }; + +/** Materialize two distinct targets for session lifecycle tests. */ +export const createBinarySessionTargets = async (): Promise< + readonly [string, string] +> => { + const directory = await createTestTempDirectory("rea-binary-session-"); + const first = join(directory, "first.hop"); + const second = join(directory, "second.hop"); + await Promise.all([writeFile(first, "one"), writeFile(second, "two")]); + return [first, second]; +}; + +/** Create a provider whose calls and declared effects are observable by tests. */ +export const createCacheProvider = ( + calls: string[], + mayWriteFilesystem = false, +): AnalysisProvider => { + const identity = { + id: "fixture", + name: "Fixture analysis provider", + version: "1", + } as const; + return { + identity: () => identity, + resolveAnalysisProfile: () => + Promise.resolve( + resultOk({ + profile: createAnalysisProfile(identity, 1, { fixture: true }), + compatibility: {}, + }), + ), + capabilities: () => [ + cacheCapability(identity, "address_name", false, mayWriteFilesystem), + cacheCapability(identity, "set_address_name", true), + ], + createClient: () => ({ + execute: (operation) => { + calls.push(operation); + return Promise.resolve(observed(operation)); + }, + close: () => Promise.resolve(), + }), + }; +}; + +/** Controllable client used to observe session replacement and cancellation. */ +export class ControllableAnalysisClient implements AnalysisClient { + closed = 0; + + constructor( + readonly pendingHealth?: Promise>, + readonly failHealth = false, + readonly pendingCall?: Promise>, + ) {} + + execute(name: string) { + if (name === "health") + return this.failHealth + ? Promise.resolve(err(new HopperStartError())) + : (this.pendingHealth ?? Promise.resolve(observed(null))); + return this.pendingCall ?? Promise.resolve(observed(null)); + } + + close(): Promise { + this.closed += 1; + return Promise.resolve(); + } +} + +/** Create an explicitly resolved promise for lifecycle interleaving tests. */ +export const createDeferred = (): { + readonly promise: Promise; + resolve(value: T): void; +} => { + let resolvePromise: ((value: T) => void) | undefined; + const promise = new Promise((resolve) => { + resolvePromise = resolve; + }); + return { + promise, + resolve(value) { + resolvePromise?.(value); + }, + }; +}; + +const cacheCapability = ( + provider: CapabilityDescriptor["provider"], + operation: "address_name" | "set_address_name", + mutatesArtifact: boolean, + mayWriteFilesystem = false, +): CapabilityDescriptor => ({ + provider, + operation, + inputContractVersion: 1, + outputContractVersion: 1, + available: true, + reason: null, + pagination: "none", + exhaustive: true, + effects: { + mutatesArtifact, + launchesProcess: false, + mayShowUi: false, + mayAccessNetwork: false, + mayWriteFilesystem, + changesPermissions: false, + requiresRoot: false, + }, + limits: { + maxResults: null, + maxPayloadBytes: null, + timeoutMs: null, + }, + limitations: [], +}); diff --git a/tests/process-global/cliLogging.test.ts b/tests/process-global/cliLogging.test.ts new file mode 100644 index 000000000..d703c7fcb --- /dev/null +++ b/tests/process-global/cliLogging.test.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { logCliCommand } from "../../src/cliLogging.js"; +import { silentLogger } from "../../src/logger.js"; + +const originalExitCode = process.exitCode; + +afterEach(() => { + process.exitCode = originalExitCode; +}); + +describe("CLI operation status", () => { + it("sets a nonzero process status without replacing structured output", async () => { + const output = { + error: "Analysis failed", + category: "integrity_mismatch", + message: "Artifact integrity check failed.", + details: { logical_path: "main.js" }, + }; + + await expect( + logCliCommand(silentLogger, "inventory-artifact", () => + Promise.resolve(output), + ), + ).resolves.toBe(output); + expect(process.exitCode).toBe(1); + }); +}); diff --git a/tests/process-global/vitestConfiguration.test.ts b/tests/process-global/vitestConfiguration.test.ts index cd9540259..21e6ac65f 100644 --- a/tests/process-global/vitestConfiguration.test.ts +++ b/tests/process-global/vitestConfiguration.test.ts @@ -53,7 +53,11 @@ describe("Vitest project configuration", () => { expect(TEST_PROJECTS.map(({ name }) => name).sort()).toEqual( EXPECTED_PROJECTS, ); - const sharedModuleProjects = new Set(["domain", "services"]); + const sharedModuleProjects = new Set([ + "domain", + "mcp-boundary", + "services", + ]); expect( TEST_PROJECTS.every(({ name, isolate }) => sharedModuleProjects.has(name) diff --git a/vitest.config.ts b/vitest.config.ts index d46c9591e..9d21db260 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -41,8 +41,8 @@ export const TEST_PROJECTS = [ { name: "adapters", include: [ - "src/config*.test.ts", - "src/{artifacts,browser,dotnet,ghidra,hopper,native,process,reference,replay}/**/*.test.ts", + "src/*.test.ts", + "src/{artifacts,browser,dotnet,ghidra,hopper,native,process,reference,replay,server}/**/*.test.ts", ], pool: "forks" as const, maxWorkers: MAX_TEST_WORKERS, @@ -65,6 +65,9 @@ export const TEST_PROJECTS = [ include: ["tests/boundary/mcp/**/*.test.ts"], pool: "threads" as const, maxWorkers: MAX_TEST_WORKERS, + // Each file creates and closes an explicit in-memory MCP session. Reuse the + // immutable server graph so module startup does not outweigh transport work. + isolate: false, }, { name: "acceptance",