Files
rea/src/domain/process/processScenario.test.ts
T

330 lines
11 KiB
TypeScript

import { expect, it } from "vitest";
import { z } from "zod";
import { Ajv2020 } from "ajv/dist/2020.js";
import { SESSION_TOOL_CONTRACTS } from "../../contracts/sessionToolContracts.js";
import { projectInputIssues } from "../inputIssueProjection.js";
import { nativeCallObservationInputSchema } from "../native/nativeCallObservation.js";
import {
digestProcessCommitment,
PROCESS_TERMINAL_CELL_BUDGET,
processComparisonContract,
processScenarioCommitment,
processScenarioSchema,
} from "./processScenario.js";
const baseScenario = { executable: "/usr/bin/true" };
it("advertises the same unsupported PTY environment name as runtime admission", () => {
const schema = z.toJSONSchema(processScenarioSchema.shape.environment, {
io: "input",
});
const ajv = new Ajv2020({ strict: false });
const validate = ajv.compile(schema);
expect(validate(Object.fromEntries([["__proto__", "selected"]]))).toBe(false);
expect(
validate({
constructor: "ordinary",
prototype: "ordinary",
NORMAL_CAPTURE_KEY: "control",
}),
).toBe(true);
expect(validate({ REA_PROCESS_RUN_ID: "reserved" })).toBe(false);
});
it("defaults the finalization interval to zero without changing committed identity", () => {
const scenario = processScenarioSchema.parse(baseScenario);
expect(scenario, "parsed scenario carries the default").toMatchObject({
finalization_ms: 0,
});
expect(
Object.hasOwn(processScenarioCommitment(scenario), "finalization_ms"),
"full scenario commitment omits a zero interval",
).toBe(false);
expect(
Object.hasOwn(processComparisonContract(scenario), "finalization_ms"),
"comparison contract omits a zero interval",
).toBe(false);
});
it("accepts an explicit zero finalization interval", () => {
expect(
processScenarioSchema.parse({ ...baseScenario, finalization_ms: 0 }),
"zero is the immediate-kill default, not an invalid budget",
).toMatchObject({ finalization_ms: 0 });
});
it("commits a positive finalization interval in both identity projections", () => {
const scenario = processScenarioSchema.parse({
...baseScenario,
finalization_ms: 500,
});
expect(
processScenarioCommitment(scenario),
"full scenario commitment records the interval",
).toMatchObject({ finalization_ms: 500 });
expect(
processComparisonContract(scenario),
"comparison contract records the interval",
).toMatchObject({ finalization_ms: 500 });
});
it.each([-1, 1.5, Number.MAX_SAFE_INTEGER + 1])(
"rejects the finalization interval %j",
(finalization_ms) => {
expect(() =>
processScenarioSchema.parse({ ...baseScenario, finalization_ms }),
).toThrow();
},
);
it("accepts a combined lifecycle budget up to the safe-integer limit", () => {
const limit = Number.MAX_SAFE_INTEGER;
const accepted = processScenarioSchema.parse({
...baseScenario,
timeout_ms: limit - 100,
idle_timeout_ms: 1,
settle_ms: 90,
finalization_ms: 10,
});
expect(
accepted.timeout_ms + accepted.finalization_ms + accepted.settle_ms,
"a total exactly at the representable limit is accepted",
).toBe(limit);
expect(
processScenarioSchema.parse(baseScenario),
"the default scenario stays far below the limit",
).toMatchObject({ timeout_ms: 30_000, settle_ms: 100, finalization_ms: 0 });
});
it.each([
["timeout alone over the sum", { timeout_ms: Number.MAX_SAFE_INTEGER }],
[
"components that only overflow together",
{
timeout_ms: Number.MAX_SAFE_INTEGER - 10,
settle_ms: 6,
finalization_ms: 5,
},
],
[
"a large settle and finalization",
{
timeout_ms: 5,
settle_ms: Number.MAX_SAFE_INTEGER,
finalization_ms: 1,
},
],
])("rejects %s with a representability message", (_label, budget) => {
const parse = () =>
processScenarioSchema.parse({
...baseScenario,
idle_timeout_ms: 1,
...budget,
});
expect(parse, "the combined budget is rejected").toThrow(
"timeout_ms + finalization_ms + settle_ms",
);
expect(parse, "the message names the representability limit").toThrow(
"exactly representable",
);
});
it("keeps scenario identity without finalization while versioning corrected port semantics", () => {
const scenario = processScenarioSchema.parse({
executable: "/usr/bin/true",
arguments: ["-x"],
working_directory: "/tmp",
environment: { A: "b" },
filesystem_observation_paths: ["/tmp"],
timeout_ms: 5_000,
idle_timeout_ms: 2_000,
settle_ms: 50,
});
// Digests computed from the compiled base commit before finalization_ms existed.
expect(
digestProcessCommitment(processScenarioCommitment(scenario)),
"the full scenario commitment is unchanged",
).toBe("b50005d26269bd1db4a49c79e05bca67c3a18d837b45cd1c8ff591c01eb7623a");
const comparison = processComparisonContract(scenario);
expect(comparison.port_normalization_version).toBe("contextual-endpoints-v3");
const {
pid_normalization_version: pidVersion,
terminal_observation_version: terminalVersion,
...rootOnlyComparison
} = comparison;
expect(terminalVersion).toBe("deferred-rendered-journal-v1");
expect(pidVersion).toBe("sampled-identifiers-v2");
expect(
digestProcessCommitment({
...rootOnlyComparison,
port_normalization_version: "contextual-endpoints-v2",
}),
"the earlier comparison contract had no finalization field",
).toBe("675b759347a737a4f1cbfbf2fc759258d9c877216513f2f4dad3ed1e9e6e7631");
expect(
digestProcessCommitment(rootOnlyComparison),
"corrected endpoint normalization has its own comparison commitment",
).toBe("301f70ed68bd81b67d07c88b44c85ed0cf7d583967d2f769e2cb193d4db61c20");
expect(digestProcessCommitment(comparison)).not.toBe(
digestProcessCommitment(rootOnlyComparison),
);
});
it("admits the default and the exact combined terminal-cell budget", () => {
expect(
processScenarioSchema.parse({ executable: "/bin/true" }).terminal,
).toEqual({
columns: 80,
rows: 24,
scrollback: 1_000,
});
expect(
processScenarioSchema.safeParse({
executable: "/bin/true",
terminal: {
columns: 1,
rows: 1,
scrollback: PROCESS_TERMINAL_CELL_BUDGET - 1,
},
}).success,
).toBe(true);
});
it("tells the agent the terminal cell budget has no runtime override", () => {
const description = SESSION_TOOL_CONTRACTS.find(
(tool) => tool.name === "capture_process_scenario",
)?.description;
expect(PROCESS_TERMINAL_CELL_BUDGET).toBe(1_000_000);
expect(description).toContain("1,000,000 combined terminal cells");
expect(description).toContain(
"This release has no runtime override for that cell budget.",
);
expect(processScenarioSchema.shape.terminal.unwrap().description).toContain(
"This release has no runtime override.",
);
});
it("rejects an oversized initial buffer without clamping caller dimensions", () => {
const input = {
executable: "/bin/true",
terminal: { columns: 1_000, rows: 1, scrollback: 1_000 },
};
const parsed = processScenarioSchema.safeParse(input);
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("expected terminal capacity rejection");
expect(parsed.error.issues).toContainEqual(
expect.objectContaining({
code: "custom",
path: ["terminal"],
message: expect.stringContaining(
`${String(PROCESS_TERMINAL_CELL_BUDGET)} renderer cells. This release has no runtime override.`,
),
}),
);
});
it("applies the selected scrollback budget to every scheduled resize", () => {
const parsed = processScenarioSchema.safeParse({
executable: "/bin/true",
events: [{ type: "resize", at_ms: 10, columns: 1_000, rows: 1 }],
});
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("expected resize capacity rejection");
expect(parsed.error.issues).toContainEqual(
expect.objectContaining({
code: "custom",
path: ["events", 0],
message: expect.stringContaining("resize event at 10 ms"),
}),
);
expect(
processScenarioSchema.safeParse({
executable: "/bin/true",
terminal: { columns: 1, rows: 1, scrollback: 0 },
events: [{ type: "resize", at_ms: 10, columns: 1_000, rows: 1 }],
}).success,
).toBe(true);
});
it("identifies each event scheduled after the scenario timeout", () => {
const parsed = processScenarioSchema.safeParse({
executable: "/bin/true",
timeout_ms: 1_000,
events: [
{ type: "input", at_ms: 1_000, data: "a" },
{ type: "input", at_ms: 1_500, data: "b" },
{ type: "signal", at_ms: 2_000, signal: "SIGINT" },
],
});
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("expected late event rejection");
expect(parsed.error.issues).toEqual([
expect.objectContaining({
code: "custom",
path: ["events", 1],
message: "event at 1500 ms occurs after the 1000 ms scenario timeout",
}),
expect.objectContaining({
code: "custom",
path: ["events", 2],
message: "event at 2000 ms occurs after the 1000 ms scenario timeout",
}),
]);
});
it.each(["selected-value", 7])(
"rejects a selected __proto__ PTY environment value instead of omitting %j",
(value) => {
const environment = Object.fromEntries([
["__proto__", value],
["NORMAL_CAPTURE_KEY", "control"],
]);
const parsed = processScenarioSchema.safeParse({
...baseScenario,
environment,
});
expect(parsed.success).toBe(false);
if (parsed.success)
throw new Error("Expected the unsupported PTY name to be rejected");
expect(
projectInputIssues(parsed.error.issues, { ...baseScenario, environment }),
).toContainEqual(
expect.objectContaining({
path: ["environment", "__proto__"],
message: expect.stringContaining("PTY backend cannot preserve"),
}),
);
},
);
it("keeps native and PTY environment admission distinct through Standard Schema export", () => {
const environment = Object.fromEntries([
["__proto__", "selected"],
["constructor", "ordinary"],
]);
const native = nativeCallObservationInputSchema.shape.environment;
const parsed = native.parse(environment);
expect(Object.hasOwn(parsed, "__proto__")).toBe(true);
expect(JSON.stringify(parsed)).toBe(JSON.stringify(environment));
const ajv = new Ajv2020({ strict: false });
const options = { target: "draft-2020-12" as const };
const nativePublished = native["~standard"].jsonSchema.input(options);
const ptyPublished =
processScenarioSchema.shape.environment["~standard"].jsonSchema.input(
options,
);
expect(ajv.compile(nativePublished)(environment)).toBe(true);
expect(ajv.compile(ptyPublished)(environment)).toBe(false);
});