Files
rea/src/domain/replayMachineRuntime.test.ts
T
morluto d4dc940bd8 refactor(test): keep coverage at behavioral boundaries (#474)
* refactor(test): keep coverage at behavioral boundaries

* fix(ci): restore package workflow temp-root classification
2026-08-11 18:34:32 +08:00

334 lines
9.3 KiB
TypeScript

import { expect, it } from "vitest";
import { replayMachineSchema } from "./replayMachine.js";
import { ReplayMachineRuntime } from "./replayMachineRuntime.js";
const machine = replayMachineSchema.parse({
initial_state: "login",
states: [
{ name: "login" },
{ name: "api" },
{ name: "complete", terminal: true },
],
transitions: [
{
id: "capture_token",
from: "login",
to: "api",
priority: 10,
trigger: { protocol: "http", method: "POST", path: "/login" },
captures: [
{
variable: "session_value",
value: { source: "request_json", path: ["session_value"] },
sensitive: true,
},
],
actions: [{ type: "http_response", status: 204, headers: {}, body: "" }],
max_uses: 1,
},
{
id: "authorize_api",
from: "api",
to: "complete",
priority: 10,
trigger: { protocol: "http", method: "GET", path: "/api" },
guards: [
{
variable: "session_value",
value: { source: "request_header", name: "authorization" },
},
],
actions: [
{ type: "http_response", status: 200, headers: {}, body: "done" },
],
max_uses: 1,
},
],
max_transitions: 2,
});
const event = (
method: string,
path: string,
body = "",
headers: Readonly<Record<string, string>> = {},
) => ({
protocol: "http" as const,
connection: "not_applicable" as const,
at_ms: 0,
method,
path,
body,
headers,
});
it("captures secret values in memory and journals aliases only", () => {
const runtime = new ReplayMachineRuntime(machine);
expect(
runtime.dispatch(
event("POST", "/login", '{"session_value":"test-auth-token"}'),
),
).toMatchObject({
outcome: "matched",
transition: {
transition_id: "capture_token",
sensitive_aliases: ["session_value"],
},
});
const expectedRedaction = ["Bearer <", "secret", ":session_value>"].join("");
expect(runtime.redact("Bearer test-auth-token")).toBe(expectedRedaction);
expect(
runtime.dispatch(
event("GET", "/api", "", { Authorization: "test-auth-token" }),
),
).toMatchObject({ outcome: "matched" });
expect(JSON.stringify(runtime.timeline)).not.toContain("test-auth-token");
expect(runtime.state).toBe("complete");
});
it("classifies wrong-state, guard, capture, and exhaustion failures", () => {
expect(
new ReplayMachineRuntime(machine).dispatch(event("GET", "/api")),
).toMatchObject({ outcome: "invalid_state" });
expect(
new ReplayMachineRuntime(machine).dispatch(event("POST", "/login", "{}")),
).toMatchObject({ outcome: "invalid_capture" });
const runtime = new ReplayMachineRuntime(machine);
runtime.dispatch(
event("POST", "/login", '{"session_value":"test-auth-token"}'),
);
expect(
runtime.dispatch(
event("GET", "/api", "", {
authorization: "test-token-placeholder",
}),
),
).toMatchObject({
outcome: "guard_failed",
});
const bounded = new ReplayMachineRuntime(
replayMachineSchema.parse({
initial_state: "active",
states: [{ name: "active" }, { name: "done", terminal: true }],
transitions: [
{
id: "once",
from: "active",
to: "active",
trigger: { protocol: "http", method: "GET", path: "/once" },
actions: [
{ type: "http_response", status: 200, headers: {}, body: "ok" },
],
max_uses: 1,
},
{
id: "finish",
from: "active",
to: "done",
trigger: { protocol: "http", method: "GET", path: "/done" },
actions: [
{ type: "http_response", status: 200, headers: {}, body: "ok" },
],
max_uses: 1,
},
],
max_transitions: 2,
}),
);
expect(bounded.dispatch(event("GET", "/once"))).toMatchObject({
outcome: "matched",
});
expect(bounded.dispatch(event("GET", "/once"))).toMatchObject({
outcome: "transition_exhausted",
});
});
it("matches bounded headers and bodies and distinguishes wrong-state reconnects", () => {
const runtime = new ReplayMachineRuntime(
replayMachineSchema.parse({
initial_state: "connected",
states: [{ name: "connected" }, { name: "done", terminal: true }],
transitions: [
{
id: "finish",
from: "connected",
to: "done",
trigger: {
protocol: "http",
method: "POST",
path: "/finish",
headers: { "content-type": "application/json" },
body: '{"done":true}',
},
actions: [
{ type: "http_response", status: 204, headers: {}, body: "" },
],
max_uses: 1,
},
{
id: "initial_socket",
from: "connected",
to: "connected",
trigger: { protocol: "websocket_connect", path: "/ws" },
actions: [{ type: "disconnect" }],
max_uses: 1,
},
],
max_transitions: 2,
}),
);
expect(runtime.dispatch(event("POST", "/finish", "wrong"))).toMatchObject({
outcome: "unmatched",
});
expect(
runtime.dispatch({
protocol: "websocket_connect",
connection: "initial",
at_ms: 0,
method: null,
path: "/ws",
headers: {},
body: "",
}),
).toMatchObject({ outcome: "matched" });
expect(
runtime.dispatch({
protocol: "websocket_connect",
connection: "reconnect",
at_ms: 0,
method: null,
path: "/ws",
headers: {},
body: "",
}),
).toMatchObject({ outcome: "transition_exhausted" });
expect(
runtime.dispatch(
event("POST", "/finish", '{"done":true}', {
"content-type": "application/json",
}),
),
).toMatchObject({ outcome: "matched" });
expect(
runtime.dispatch({
protocol: "websocket_connect",
connection: "reconnect",
at_ms: 0,
method: null,
path: "/ws",
headers: {},
body: "",
}),
).toMatchObject({ outcome: "unexpected_reconnect" });
});
it("enforces state, connection, message, byte, and duration limits", () => {
const limitedMachine = replayMachineSchema.parse({
initial_state: "active",
states: [
{ name: "active", max_visits: 1 },
{ name: "done", terminal: true },
],
transitions: [
{
id: "loop",
from: "active",
to: "active",
trigger: { protocol: "websocket_message", path: "/ws" },
actions: [{ type: "websocket_send", data: "ack" }],
max_uses: 2,
},
{
id: "finish",
from: "active",
to: "done",
trigger: { protocol: "http", method: "GET", path: "/done" },
actions: [
{ type: "http_response", status: 200, headers: {}, body: "ok" },
],
max_uses: 1,
},
],
max_transitions: 3,
limits: { connections: 1, messages: 1, bytes: 100, duration_ms: 10 },
});
const stateLimited = new ReplayMachineRuntime(limitedMachine);
expect(
stateLimited.dispatch({
protocol: "websocket_message",
connection: "not_applicable",
at_ms: 0,
method: null,
path: "/ws",
headers: {},
body: "one",
}),
).toMatchObject({ outcome: "limit_exhausted" });
const messageLimited = new ReplayMachineRuntime(
replayMachineSchema.parse({
...limitedMachine,
states: limitedMachine.states.map((state) =>
state.name === "active" ? { ...state, max_visits: 3 } : state,
),
}),
);
const message = {
protocol: "websocket_message" as const,
connection: "not_applicable" as const,
at_ms: 0,
method: null,
path: "/ws",
headers: {},
body: "one",
};
expect(messageLimited.dispatch(message)).toMatchObject({
outcome: "matched",
});
expect(messageLimited.dispatch(message)).toMatchObject({
outcome: "limit_exhausted",
});
const durationLimited = new ReplayMachineRuntime(limitedMachine);
expect(
durationLimited.dispatch({ ...event("GET", "/done"), at_ms: 11 }),
).toMatchObject({ outcome: "limit_exhausted" });
expect(
new ReplayMachineRuntime(limitedMachine).dispatch({
...event("GET", "/done"),
at_ms: Number.NaN,
}),
).toMatchObject({ outcome: "limit_exhausted" });
const monotonic = new ReplayMachineRuntime(limitedMachine);
expect(
monotonic.dispatch({ ...event("GET", "/missing"), at_ms: 5 }),
).toMatchObject({ outcome: "unmatched" });
expect(
monotonic.dispatch({ ...event("GET", "/missing"), at_ms: 4 }),
).toMatchObject({ outcome: "limit_exhausted" });
const byteLimited = new ReplayMachineRuntime(
replayMachineSchema.parse({
...limitedMachine,
limits: { ...limitedMachine.limits, bytes: 4 },
}),
);
expect(byteLimited.dispatch(event("GET", "/missing", "12345"))).toMatchObject(
{ outcome: "limit_exhausted" },
);
const connectionLimited = new ReplayMachineRuntime(limitedMachine);
const connect = {
protocol: "websocket_connect" as const,
connection: "initial" as const,
at_ms: 0,
method: null,
path: "/unknown",
headers: {},
body: "",
};
expect(connectionLimited.dispatch(connect)).toMatchObject({
outcome: "unmatched",
});
expect(
connectionLimited.dispatch({ ...connect, connection: "reconnect" }),
).toMatchObject({ outcome: "limit_exhausted" });
});