Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
32 changes: 32 additions & 0 deletions src/application/Doctor.fixture.ts
Original file line number Diff line number Diff line change
@@ -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> = {},
): 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,
});
Original file line number Diff line number Diff line change
@@ -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> = {}): 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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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> = {}): 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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
33 changes: 33 additions & 0 deletions src/application/ProcessCaptureError.test.ts
Original file line number Diff line number Diff line change
@@ -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" },
});
});
});
35 changes: 35 additions & 0 deletions src/application/ProcessCaptureJournal.test.ts
Original file line number Diff line number Diff line change
@@ -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"]);
});
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/application/ProcessPairedExperiment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading