rea/tests/processPairedExperiment.tes...

248 lines
7.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
runPairedProcessExperiment,
type ProcessCapturePort,
} from "../src/application/ProcessPairedExperiment.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../src/contracts/processCaptureExample.js";
import { parseProcessCapture } from "../src/domain/processCapture.js";
import {
analyzeProcessRepeatability,
bindProcessScenario,
processPairedExperimentSchema,
} from "../src/domain/processPairedExperiment.js";
import { ok } from "../src/domain/result.js";
const experiment = () =>
processPairedExperimentSchema.parse({
approved: true,
shared_scenario: {
approved: true,
executable: "/bin/template",
arguments: ["shared"],
working_directory: "/tmp",
environment: { SHARED: "yes" },
},
authority: {
id: "authority",
executable: "/bin/authority",
environment: { SIDE: "authority" },
},
candidate: {
id: "candidate",
executable: "/bin/candidate",
arguments: ["candidate"],
environment: { SIDE: "candidate" },
},
repeat_count: 3,
freshness_policy: { max_capture_age_ms: 60_000 },
required_dimensions: [
"terminal",
"interaction",
"exit",
"filesystem",
"protocol",
"process",
"shim",
],
});
const policy = {
enabled: true,
executableRoots: ["/bin"],
workingRoots: ["/tmp"],
allowedEnvironment: ["SHARED", "SIDE"],
allowExternalNetwork: false,
};
describe("paired process experiments", () => {
it("binds only declared side inputs onto the shared observation contract", () => {
const parsed = experiment();
const authority = bindProcessScenario(
parsed.shared_scenario,
parsed.authority,
);
const candidate = bindProcessScenario(
parsed.shared_scenario,
parsed.candidate,
);
expect(authority).toMatchObject({
executable: "/bin/authority",
arguments: ["shared"],
environment: { SHARED: "yes", SIDE: "authority" },
});
expect(candidate).toMatchObject({
executable: "/bin/candidate",
arguments: ["candidate"],
environment: { SHARED: "yes", SIDE: "candidate" },
});
});
it("runs same-side comparisons first and compares stable sides", async () => {
const observedExecutables: string[] = [];
const capture = parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
const capturePort: ProcessCapturePort = {
capture: (scenario) => {
observedExecutables.push(scenario.executable);
return Promise.resolve(ok(structuredClone(capture)));
},
};
const result = await runPairedProcessExperiment(experiment(), policy, {
capturePort,
now: () => Date.parse("2026-01-01T00:00:01.000Z"),
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(observedExecutables).toEqual([
"/bin/authority",
"/bin/authority",
"/bin/authority",
"/bin/candidate",
"/bin/candidate",
"/bin/candidate",
]);
expect(result.value.authority_repeatability.stable).toBe(true);
expect(result.value.candidate_repeatability.stable).toBe(true);
expect(result.value.cross_side?.status).toBe("unchanged");
});
it("blocks cross-side claims when a candidate has a flaky exit", async () => {
const base = parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
let candidateRun = 0;
const capturePort: ProcessCapturePort = {
capture: (scenario) => {
let capture = structuredClone(base);
if (scenario.executable === "/bin/candidate") {
candidateRun += 1;
if (candidateRun === 2)
capture = {
...capture,
exit: { code: 1, signal: null, reason: "exited" },
};
}
return Promise.resolve(ok(capture));
},
};
const result = await runPairedProcessExperiment(experiment(), policy, {
capturePort,
now: () => Date.parse("2026-01-01T00:00:01.000Z"),
});
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.candidate_repeatability).toMatchObject({
stable: false,
classification: "flaky_exit",
unstable_dimensions: ["exit"],
});
expect(result.value.cross_side).toBeNull();
expect(result.value.cross_side_blocked_reason).toMatch(/unstable/u);
});
it("distinguishes normalized timing variance from semantic output drift", () => {
const base = parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
const early = {
...base,
frames: [{ sequence: 0, at_ms: 10, data: "ready" }],
};
const late = {
...base,
frames: [{ sequence: 0, at_ms: 20, data: "ready" }],
};
expect(
analyzeProcessRepeatability([early, late], ["terminal"]),
).toMatchObject({
stable: false,
classification: "timing_variance",
unstable_dimensions: ["terminal"],
});
const changed = {
...base,
frames: [{ sequence: 0, at_ms: 20, data: "failed" }],
};
expect(
analyzeProcessRepeatability([early, changed], ["terminal"]),
).toMatchObject({ classification: "nondeterministic_output" });
});
it("stops before capture when the experiment is cancelled", async () => {
const controller = new AbortController();
controller.abort();
const capturePort: ProcessCapturePort = {
capture: () => {
throw new Error("capture must not start after cancellation");
},
};
const result = await runPairedProcessExperiment(experiment(), policy, {
capturePort,
signal: controller.signal,
});
expect(result).toMatchObject({
ok: false,
error: { _tag: "ProcessCaptureError", reason: "cancelled" },
});
});
it("blocks full cross-side output when only a subset was repeatable", async () => {
const capture = parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
const capturePort: ProcessCapturePort = {
capture: () => Promise.resolve(ok(structuredClone(capture))),
};
const subset = processPairedExperimentSchema.parse({
...experiment(),
required_dimensions: ["exit"],
});
const result = await runPairedProcessExperiment(subset, policy, {
capturePort,
});
expect(result).toMatchObject({
ok: true,
value: {
cross_side: null,
cross_side_blocked_reason: expect.stringMatching(/every returned/u),
},
});
});
it("compares the newest stable captures under the freshness policy", async () => {
const base = parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
let captureIndex = 0;
const capturePort: ProcessCapturePort = {
capture: () => {
const completedAt =
captureIndex === 0
? "2026-01-01T00:00:00.000Z"
: "2026-01-01T00:00:09.500Z";
captureIndex += 1;
return Promise.resolve(
ok({
...structuredClone(base),
manifest: {
...base.manifest,
started_at: completedAt,
completed_at: completedAt,
},
}),
);
},
};
const shortFreshness = processPairedExperimentSchema.parse({
...experiment(),
freshness_policy: { max_capture_age_ms: 1_000 },
});
const result = await runPairedProcessExperiment(shortFreshness, policy, {
capturePort,
now: () => Date.parse("2026-01-01T00:00:10.000Z"),
});
expect(result).toMatchObject({
ok: true,
value: { cross_side: { status: "unchanged" } },
});
});
});