Compare commits

...
Author SHA1 Message Date
morluto 6ba881d243 test: strengthen evidence and cleanup checks 2026-10-27 21:11:46 +08:00
23 changed files with 365 additions and 162 deletions

No files matched your search

+5
View File
@@ -1630,6 +1630,11 @@ results after a failure. Available failed-shard reports remain diagnostic only:
failed, cancelled, skipped, or missing selected shards cannot pass the gate or
publish successful timing history. This deliberately avoids relying on an
import graph for generated files, filesystem inputs, subprocesses, or bridges.
The portable report rejects an entirely skipped module inventory and lists
fully skipped modules in its summary. A mixed passed/skipped inventory is
allowed: Vitest's skip state does not establish whether a skipped module was
required on that runner. Review the listed skips against platform and provider
prerequisites; the inventory alone does not prove every required case ran.
Scheduled qualification is admitted hourly at minute 23. A forced full run at
03:23 UTC refreshes runner and dependency drift every day. Other hourly runs
+9
View File
@@ -92,6 +92,8 @@ if (process.env.CI_TEST_SELECTION) {
}
if (passed && verifyInventory) {
const actual = files.map(({ name }) => name);
if (actual.length === 0)
throw new Error("No portable test modules were reported");
const expectedSet = new Set(expected);
const actualSet = new Set(actual);
const missing = expected.filter((path) => !actualSet.has(path));
@@ -100,7 +102,10 @@ if (passed && verifyInventory) {
throw new Error(
`Incomplete portable test inventory: ${JSON.stringify({ missing, unexpected, duplicateFiles: actual.length - actualSet.size })}`,
);
if (files.every(({ state }) => state === "skipped"))
throw new Error("Every portable test module was skipped");
}
const skippedFiles = files.filter(({ state }) => state === "skipped");
const summary = [
groupsCompleted
? "## Merged test diagnostics"
@@ -120,6 +125,10 @@ const summary = [
...(process.env.SOURCE_REQUIRED === "true"
? [`Source result: ${process.env.SOURCE_RESULT ?? "unknown"}.`]
: []),
`Skipped test modules: ${skippedFiles.length}. Their skip reasons and required coverage are not established by this report.`,
...(skippedFiles.length ? [""] : []),
...skippedFiles.map(({ name }) => `- ${name.replaceAll("|", "\\|")}`),
"",
passed
? "Successful timing history is available."
: "This report is diagnostic only and is not a successful timing baseline.",
@@ -416,29 +416,31 @@ describe("buildEnvironment cancellation and input checks", () => {
});
});
it("refuses an unreadable package tarball on a dry run as a build does", async () => {
if (process.getuid?.() === 0) return;
const root = await createTestTempDirectory("rea-environment-build-tgz-");
const tarball = join(root, "rea-agents.tgz");
await writeFile(tarball, "packed");
await chmod(tarball, 0o000);
try {
const planned = await buildEnvironment(
await buildInput(root, undefined, {
dryRun: true,
packageTarball: tarball,
}),
);
const built = await buildEnvironment(
await buildInput(root, undefined, { packageTarball: tarball }),
);
if (planned.ok || built.ok) throw new Error("Expected both refusals");
expect(projectAnalysisError(planned.error).code).toBe("access_denied");
expect(projectAnalysisError(built.error).code).toBe("access_denied");
} finally {
await chmod(tarball, 0o600);
}
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"refuses an unreadable package tarball on a dry run as a build does",
async () => {
const root = await createTestTempDirectory("rea-environment-build-tgz-");
const tarball = join(root, "rea-agents.tgz");
await writeFile(tarball, "packed");
await chmod(tarball, 0o000);
try {
const planned = await buildEnvironment(
await buildInput(root, undefined, {
dryRun: true,
packageTarball: tarball,
}),
);
const built = await buildEnvironment(
await buildInput(root, undefined, { packageTarball: tarball }),
);
if (planned.ok || built.ok) throw new Error("Expected both refusals");
expect(projectAnalysisError(planned.error).code).toBe("access_denied");
expect(projectAnalysisError(built.error).code).toBe("access_denied");
} finally {
await chmod(tarball, 0o600);
}
},
);
it("refuses a loopback capture whose file name is too long, and quotes the next command", async () => {
const root = await createTestTempDirectory("rea-environment-build-long-");
@@ -970,7 +970,7 @@ it("inventories and extracts a Windows member name under its canonical path", as
).toBe(0);
});
it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ data", async () => {
const posixPathFixture = async () => {
const backslashName = String.raw`a\b.txt`;
const slashName = "a/b.txt";
const backslashPayload = Buffer.from("literal-backslash");
@@ -1010,6 +1010,18 @@ it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ
}),
);
const snapshot = await scanCanonicalArtifactInventory(path);
return {
path,
snapshot,
backslashName,
slashName,
backslashPayload,
slashPayload,
};
};
it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ data", async () => {
const { snapshot, backslashName, slashName } = await posixPathFixture();
const backslashEntry = snapshot.occurrences.find(
(occurrence) => occurrence.logical_path === backslashName,
);
@@ -1034,39 +1046,53 @@ it("preserves distinct POSIX backslash and slash paths in CArchive files and PYZ
},
});
expect(snapshot.coverage.status).toBe("complete");
if (process.platform === "win32") return;
const directory = await createTestTempDirectory("rea-pyinstaller-posix-out-");
const output = join(directory, "extracted");
await materializeArtifactInventory(
{
inputPath: path,
inputFormat: "elf",
outputRoot: output,
environment: process.env,
integrityPolicy: "fail",
},
path,
snapshot,
);
expect(
Buffer.compare(
await readFile(join(output, backslashName)),
backslashPayload,
),
).toBe(0);
expect(
Buffer.compare(await readFile(join(output, "a", "b.txt")), slashPayload),
).toBe(0);
for (const [name, body] of [
[backslashName, backslashPayload],
[slashName, slashPayload],
] as const)
expect((await readFile(join(output, "PYZ.pyz", name))).equals(body)).toBe(
true,
);
});
it.skipIf(process.platform === "win32")(
"extracts distinct POSIX backslash and slash paths in CArchive files and PYZ data",
async () => {
const {
path,
snapshot,
backslashName,
slashName,
backslashPayload,
slashPayload,
} = await posixPathFixture();
const directory = await createTestTempDirectory(
"rea-pyinstaller-posix-out-",
);
const output = join(directory, "extracted");
await materializeArtifactInventory(
{
inputPath: path,
inputFormat: "elf",
outputRoot: output,
environment: process.env,
integrityPolicy: "fail",
},
path,
snapshot,
);
expect(
Buffer.compare(
await readFile(join(output, backslashName)),
backslashPayload,
),
).toBe(0);
expect(
Buffer.compare(await readFile(join(output, "a", "b.txt")), slashPayload),
).toBe(0);
for (const [name, body] of [
[backslashName, backslashPayload],
[slashName, slashPayload],
] as const)
expect((await readFile(join(output, "PYZ.pyz", name))).equals(body)).toBe(
true,
);
},
);
it("reads a PE signature beyond the carrier prefix before projecting paths", async () => {
const path = await writeArchive(
"distant-pe",
@@ -102,7 +102,8 @@ describe("APK signing block locator", () => {
if (!tail.ok) return;
const block = locateApkSigningBlock(bytes, tail.value);
expect(block.ok).toBe(true);
if (!block.ok || block.value === null) return expect(block).toBeTruthy();
if (!block.ok || block.value === null)
throw new Error("Expected an APK signing block");
expect(block.value.pairs.map((p) => p.id)).toEqual([
APK_BLOCK_ID.signatureV2,
APK_BLOCK_ID.signatureV3,
@@ -47,6 +47,9 @@ it("retains complete coverage for an inline Electron factory export", () => {
load(9);
})();`);
expect(result.parse_status).toBe("complete");
expect(result.references).toContainEqual(
expect.objectContaining({ kind: "require", specifier: "electron" }),
);
expect(result.references.every(({ specifier }) => specifier !== null)).toBe(
true,
);
+29 -27
View File
@@ -76,33 +76,35 @@ it.skipIf(process.platform === "win32")(
},
);
it("marks an unusable system temporary parent unavailable when workspaceRoot is omitted", async () => {
if (process.platform === "win32") return;
const root = await mkdtemp(join(tmpdir(), "rea-ida-default-parent-"));
roots.push(root);
const path = join(root, "registration.json");
const unusableDefault = join(root, "not-a-directory");
await writeFile(unusableDefault, "file");
await writeFile(
path,
JSON.stringify({ command: "python", mode: "headless" }),
);
const previous = process.env.TMPDIR;
process.env.TMPDIR = unusableDefault;
try {
const inspection = await inspectIdaRegistration(path, resourceScope);
expect(inspection.available).toBe(false);
expect(
inspection.checks.find(({ name }) => name === "private_workspace"),
).toMatchObject({
ok: false,
code: "invalid_configuration",
});
} finally {
if (previous === undefined) delete process.env.TMPDIR;
else process.env.TMPDIR = previous;
}
});
it.skipIf(process.platform === "win32")(
"marks an unusable system temporary parent unavailable when workspaceRoot is omitted",
async () => {
const root = await mkdtemp(join(tmpdir(), "rea-ida-default-parent-"));
roots.push(root);
const path = join(root, "registration.json");
const unusableDefault = join(root, "not-a-directory");
await writeFile(unusableDefault, "file");
await writeFile(
path,
JSON.stringify({ command: "python", mode: "headless" }),
);
const previous = process.env.TMPDIR;
process.env.TMPDIR = unusableDefault;
try {
const inspection = await inspectIdaRegistration(path, resourceScope);
expect(inspection.available).toBe(false);
expect(
inspection.checks.find(({ name }) => name === "private_workspace"),
).toMatchObject({
ok: false,
code: "invalid_configuration",
});
} finally {
if (previous === undefined) delete process.env.TMPDIR;
else process.env.TMPDIR = previous;
}
},
);
it("keeps IDA configuration for setup without copying transport credentials or claiming runtime verification", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-ida-doctor-"));
roots.push(root);
@@ -101,8 +101,7 @@ describe.skipIf(process.platform !== "darwin")(
reported_limits: { maximum_nesting_depth: 998 },
},
remediation: {
action:
"The 998-level native plist nesting allowance (no runtime override) is a fixed allowance. Worker memory limits, inherited limits, and additional host memory do not raise it. Select another complete input within the allowance when that preserves the needed observation. Repeating the unchanged request fails the same way.",
action: expect.stringMatching(/\S/u),
},
};
expect(output.json).toMatchObject(expectedFailure);
@@ -139,7 +139,9 @@ for (const scenario of cases)
appliedActions: [],
});
if ("syntax" in scenario) {
expect(setup.json).toMatchObject({ remediation: expect.any(String) });
expect(setup.json).toMatchObject({
remediation: expect.stringMatching(/\S/u),
});
if (scenario.syntax === "JSON")
expect(setup.stdout).toContain("Invalid JSON/JSONC");
} else {
+44 -6
View File
@@ -5,6 +5,7 @@ import type { Socket } from "node:net";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it, onTestFinished } from "vitest";
import { z } from "zod";
import { parseEvidence } from "../../src/domain/evidence.js";
import { webEventListenersSchema } from "../../src/domain/webEventListeners.js";
import { startRuntimeBrowser } from "../fixtures/webRuntime.js";
@@ -18,6 +19,45 @@ const environment = () => ({
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
});
const executionProgress = z.object({
rea_progress: z.object({
phase: z.literal("browser_execution"),
completed: z.literal(1),
}),
});
/** Buffer complete stderr lines so a progress event cannot borrow fields from another line. */
const executionReadyReader = () => {
let pending = "";
return (chunk: string): boolean => {
const lines = (pending + chunk).split("\n");
pending = lines.pop() ?? "";
return lines.some((line) => {
try {
return executionProgress.safeParse(JSON.parse(line)).success;
} catch {
// Other stderr lines are not necessarily JSON progress events.
return false;
}
});
};
};
it("arms only for a complete matching browser execution progress event", () => {
const ready = executionReadyReader();
expect(
ready('{"rea_progress":{"phase":"browser_execution","completed":0}}\n'),
).toBe(false);
expect(ready('{"rea_progress":{"phase":"other","completed":1}}\n')).toBe(
false,
);
expect(
ready('{"rea_progress":{"phase":"browser_execution","completed":10}}\n'),
).toBe(false);
expect(ready('other stderr\n{"rea_progress":{"phase":"browser_')).toBe(false);
expect(ready('execution","completed":1}}\n')).toBe(true);
});
it("advertises the canonical observation duration bounds through the compiled CLI", async () => {
const { stdout } = await promisify(execFile)(
process.execPath,
@@ -86,17 +126,15 @@ it("translates SIGINT after real CLI arming into awaited instrumentation cleanup
let stderr = "";
let stdout = "";
let armed = false;
const executionReady = executionReadyReader();
const timer = setTimeout(() => child.kill("SIGKILL"), 20_000);
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
child.stderr.on("data", (chunk: string) => {
stderr += chunk;
if (
!armed &&
stderr.includes('"completed":1') &&
stderr.includes("browser_execution")
) {
if (!armed && executionReady(chunk)) {
armed = true;
child.kill("SIGINT");
}
@@ -50,9 +50,15 @@ cliTest(
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
details: { operation: "environment-export" },
code: "cleanup_incomplete",
details: {
operation: "environment-export",
cleanup: "incomplete",
resources: [files.manifestPath],
diagnostics: { input_path: files.manifestPath, input_role: "manifest" },
cleanup_reason: "controlled persistent close failure",
},
});
expect(JSON.stringify(result.json)).toContain("cleanup");
await expect(readFile(join(directory, "README.md"))).rejects.toMatchObject({
code: "ENOENT",
});
@@ -256,9 +256,24 @@ it.each([true, false])(
partial_observation: { evidence_id: evidence.evidence_id },
},
});
expect(JSON.stringify(result)).toContain("comparison close denied");
if (!success)
expect(JSON.stringify(result)).toContain("primary comparison failure");
expect(result).toMatchObject({
details: {
diagnostics: success
? { reason: "comparison close denied" }
: {
primary_error: {
code: "unreadable_output",
details: { reason: "primary comparison failure" },
},
cleanup_error: {
code: "cleanup_incomplete",
details: {
diagnostics: { reason: "comparison close denied" },
},
},
},
},
});
expect(close).toHaveBeenCalledOnce();
} finally {
run.mockRestore();
@@ -326,6 +326,56 @@ it("rejects successful shard reports with missing or duplicate portable files",
await expect(
readFile(join(root, ".cache/ci-test-history/test-results.json")),
).rejects.toMatchObject({ code: "ENOENT" });
const empty = await createTestTempDirectory("rea-ci-report-empty-");
await workflowGit(empty, ["init"]);
await writeFile(
join(empty, "test-timings.json"),
JSON.stringify({ version: 1, success: true, files: [] }),
);
await expect(
executeTimingReport(empty, { CI_VERIFY_TEST_INVENTORY: "true" }),
).rejects.toMatchObject({
stderr: expect.stringContaining("No portable test modules were reported"),
});
});
it("reports skipped modules without treating an entirely skipped suite as successful", async () => {
const root = await createTestTempDirectory("rea-ci-report-skips-");
await workflowGit(root, ["init"]);
await mkdir(join(root, "tests"));
for (const name of ["required", "platform"])
await writeFile(join(root, `tests/${name}.test.ts`), "// fixture\n");
await workflowGit(root, ["add", "tests"]);
const run = async (requiredState: string) => {
await writeFile(
join(root, "test-timings.json"),
JSON.stringify({
version: 1,
success: true,
files: [
{
name: "tests/required.test.ts",
durationMs: 1,
state: requiredState,
},
{ name: "tests/platform.test.ts", durationMs: 1, state: "skipped" },
],
}),
);
return executeTimingReport(root, { CI_VERIFY_TEST_INVENTORY: "true" });
};
const mixed = await run("passed");
expect(mixed.summary).toContain("Skipped test modules: 1");
expect(mixed.summary).toContain("- tests/platform.test.ts");
expect(mixed.summary).toContain("required coverage are not established");
await expect(run("skipped")).rejects.toMatchObject({
stderr: expect.stringContaining("Every portable test module was skipped"),
});
await expect(
readFile(join(root, ".cache/ci-test-history/test-results.json")),
).rejects.toMatchObject({ code: "ENOENT" });
});
it("runs the existing onboarding and Python producer guards from their selected CI owners", async () => {
@@ -87,19 +87,19 @@ describe("client registration status", () => {
client: "cursor",
state: "stale",
command: [foreignEntry, "mcp"],
remediation: expect.any(String),
remediation: expect.stringMatching(/\S/u),
}),
expect.objectContaining({
client: "gemini_cli",
state: "invalid",
command: [],
remediation: expect.any(String),
remediation: expect.stringMatching(/\S/u),
}),
expect.objectContaining({
client: "claude_code",
state: "missing",
command: [],
remediation: expect.any(String),
remediation: expect.stringMatching(/\S/u),
}),
]),
);
@@ -758,7 +758,7 @@ it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
config_path: path,
state: "invalid",
command: [],
remediation: expect.any(String),
remediation: expect.stringMatching(/\S/u),
});
expect(
(await existingMaintenanceScope(home, "/current/rea", process.env))
+14 -6
View File
@@ -13,6 +13,7 @@ import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/a
import { fridaSessionStatusSchema } from "../../../src/domain/fridaSessionStatus.js";
import { evidenceBundleSummarySchema } from "../../../src/domain/evidenceRecordSummary.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js";
const createFridaTestProvider = (
overrides: Partial<FridaInstrumentationPort> = {},
@@ -241,9 +242,7 @@ it("delivers Frida status summaries with complete retained Evidence for both tru
arguments: {},
});
expect(bundle.isError).not.toBe(true);
expect(JSON.stringify(bundle.structuredContent)).toContain(
"caller-selected-nonsecret",
);
expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(parent);
expect(session.evidenceById(parent.evidence_id)).toEqual(parent);
// Inline/file projection validates the owning result and exact source selection.
const malformed = createEvidence(undefined, parent.provider ?? undefined, {
@@ -651,8 +650,17 @@ it("retains cancellation Evidence after the MCP client stops waiting", async ()
name: "get_evidence_bundle",
arguments: {},
});
expect(JSON.stringify(bundle.structuredContent)).toContain("cancel-observed");
expect(JSON.stringify(bundle.structuredContent)).toContain(
"instrument_with_frida",
expect(bundle.isError).not.toBe(true);
expect(parseMcpEvidenceBundle(bundle).records).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: "instrument_with_frida",
normalized_result: expect.objectContaining({
messages: expect.arrayContaining([
expect.objectContaining({ payload: "cancel-observed" }),
]),
}),
}),
]),
);
});
@@ -1,7 +1,6 @@
import { chmod, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { z } from "zod";
@@ -9,9 +8,9 @@ import {
expandToolAvailability,
sessionProvider,
} from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
// The server enables default packet tools only on Linux, where they can run.
const linux = process.platform === "linux";
@@ -23,33 +22,24 @@ const packetCaptureAvailability = async (
const session = createTestBinarySession(() => {
throw new Error("binary analysis must not start during status discovery");
});
const server = createServer(
{ kind: "session", session },
{ environment: { ...environment, REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" } },
);
const client = new Client({
name: "packet-capture-availability",
version: "1",
const { client } = await createApplicationMcpHarness({
session,
serverOptions: {
environment: { ...environment, REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" },
},
clientIdentity: { name: "packet-capture-availability", version: "1" },
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
try {
const response = await client.callTool({
name: "binary_session",
arguments: {},
});
if (response.isError === true) throw new Error("binary_session failed");
const parsed = z
.object({ result: sessionProvider })
.parse(response.structuredContent).result;
return expandToolAvailability(parsed.tool_availability).find(
(tool) => tool.name === name,
);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
const response = await client.callTool({
name: "binary_session",
arguments: {},
});
if (response.isError === true) throw new Error("binary_session failed");
const parsed = z
.object({ result: sessionProvider })
.parse(response.structuredContent).result;
return expandToolAvailability(parsed.tool_availability).find(
(tool) => tool.name === name,
);
};
const executableTshark = async (prefix: string): Promise<string> => {
+2 -3
View File
@@ -7,6 +7,7 @@ import { expect, it, onTestFinished } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js";
it.each(["inspect_packet_capture", "inspect_packet_fields"])(
"advertises valid target-free schemas and missing-tool diagnostics: %s",
@@ -122,9 +123,7 @@ it.each(["cancelled", "cleanup"])(
arguments: {},
});
expect(bundle.isError).not.toBe(true);
expect(JSON.stringify(bundle.structuredContent)).toContain(
evidence.evidence_id,
);
expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(evidence);
expect(harness.session.evidenceById(evidence.evidence_id)).toEqual(
evidence,
);
@@ -1,4 +1,5 @@
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, expect, it } from "vitest";
@@ -208,9 +209,7 @@ it.runIf(SQLITE_NATIVE_LIMITS_AVAILABLE)(
arguments: {},
});
expect(bundle.isError).not.toBe(true);
expect(JSON.stringify(bundle.structuredContent)).toContain(
error.details.partial_observation.evidence_id,
);
expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(retained);
} finally {
removalAllowed = true;
await client.close();
@@ -7,6 +7,7 @@ import { expect, it, onTestFinished } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js";
it.each(["decode_signal_recording", "inspect_signal_decoders"])(
"advertises valid target-free schemas and missing-tool diagnostics: %s",
@@ -117,9 +118,7 @@ it.each(["cancelled", "cleanup"])(
arguments: {},
});
expect(bundle.isError).not.toBe(true);
expect(JSON.stringify(bundle.structuredContent)).toContain(
evidence.evidence_id,
);
expect(parseMcpEvidenceBundle(bundle).records).toContainEqual(evidence);
expect(harness.session.evidenceById(evidence.evidence_id)).toEqual(
evidence,
);
+21 -8
View File
@@ -554,14 +554,27 @@ it.skipIf(process.platform === "win32")(
expect(remainsLive).toBe(false);
expect(() => process.kill(unrelated.process.pid!, 0)).not.toThrow();
} finally {
if (descendantOwner !== undefined)
expect(
(
await (descendantOwner.cleanup?.() ??
cleanupOwnedProcessGroup(descendantOwner.ownership))
).cleaned,
).toBe(true);
await cleanupOwnedProcessGroup(unrelated.ownership);
const descendant = descendantOwner;
const results = await Promise.allSettled([
...(descendant === undefined
? []
: [
Promise.resolve().then(
() =>
descendant.cleanup?.() ??
cleanupOwnedProcessGroup(descendant.ownership),
),
]),
Promise.resolve().then(() =>
cleanupOwnedProcessGroup(unrelated.ownership),
),
]);
const failures = results.flatMap((result) => {
if (result.status === "rejected") return [result.reason];
return result.value.cleaned ? [] : [new Error(result.value.reason)];
});
if (failures.length > 0)
throw new AggregateError(failures, "Fixture process cleanup failed");
}
},
);
@@ -67,11 +67,26 @@ const waitForExit = async (pid: number): Promise<void> => {
const cleanupFixture = async (marker: Marker | undefined): Promise<void> => {
if (marker === undefined) return;
// Teardown uses the fixture's capture run token, never a bare PID kill.
await cleanupOwnedProcessGroup({
const result = await cleanupOwnedProcessGroup({
runId: marker.runId,
leaderPid: marker.rootPid,
processGroupId: marker.rootPid,
});
if (!result.cleaned)
throw new Error(`Fixture cleanup failed: ${result.reason}`);
};
const finishCleanup = async (steps: readonly (() => Promise<unknown>)[]) => {
const failures: unknown[] = [];
for (const step of steps) {
try {
await step();
} catch (cause: unknown) {
failures.push(cause);
}
}
if (failures.length > 0)
throw new AggregateError(failures, "Fixture cleanup failed");
};
itWithCaptureCapability.each(["SIGINT", "SIGTERM"] as const)(
@@ -110,9 +125,14 @@ itWithCaptureCapability.each(["SIGINT", "SIGTERM"] as const)(
});
let marker: Marker | undefined;
onTestFinished(async () => {
await cleanupFixture(marker);
if (cli.exitCode === null && cli.signalCode === null) cli.kill("SIGKILL");
await closed;
await finishCleanup([
() => cleanupFixture(marker),
async () => {
if (cli.exitCode === null && cli.signalCode === null)
cli.kill("SIGKILL");
await closed;
},
]);
});
marker = await readMarker(root);
expect(await isRunning(marker.rootPid)).toBe(true);
@@ -256,10 +276,12 @@ itWithCaptureCapability(
});
let marker: Marker | undefined;
onTestFinished(async () => {
await cleanupFixture(marker);
await client.close();
await server.close();
await session.close();
await finishCleanup([
() => cleanupFixture(marker),
() => client.close(),
() => server.close(),
() => session.close(),
]);
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
+12
View File
@@ -0,0 +1,12 @@
import type { CallToolResult } from "@modelcontextprotocol/server";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { parseEvidenceBundle } from "../../src/domain/evidenceBundle.js";
/** Parse the full bundle through the advertised result and its owning contract. */
export const parseMcpEvidenceBundle = (response: CallToolResult) =>
parseEvidenceBundle(
toolContract("get_evidence_bundle").outputSchema.parse(
response.structuredContent,
).result,
);
+5 -2
View File
@@ -188,9 +188,12 @@ it("stops and joins metadata discovery when the update host closes", async () =>
const probing = host.installation();
await ready;
expect(child?.exitCode).toBeNull();
expect(await host.close?.()).toBeUndefined();
await probing;
expect(host.close).toBeTypeOf("function");
if (host.close === undefined) throw new Error("Missing update host close");
expect(await host.close()).toBeUndefined();
// Close must join the probe itself, not rely on the later probe deadline.
expect(child?.signalCode).not.toBeNull();
await probing;
});
it("bounds a stalled metadata probe and keeps the runner usable", async () => {