Prove total orders with Kahn traversal and use ranks; stream forward and reverse reachability for general partial orders instead of retaining every closure. Aggregate unordered conflicts by declaration while preserving missing-pair counts and independent malformed-input diagnostics. Validation: 33 focused trace domain and CLI/MCP boundary tests passed. Six bounded Flameox executions preserved answer digests and cycle rejection; combined median peak RSS fell from 391.13 to 127.72 MiB. General partial-order traversal costs more CPU; the experiment records that tradeoff. Full suite and real-provider lanes were not run.
309 lines
9.1 KiB
TypeScript
309 lines
9.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
processTraceSpecificationSchema,
|
|
type ProcessTraceSpecification,
|
|
} from "./processTraceSpecification.js";
|
|
|
|
const ready = { sequence: 0, at_ms: 10, data: "ready" };
|
|
const worker = {
|
|
at_ms: 20,
|
|
pid: 1,
|
|
parent_pid: 0,
|
|
command: "worker",
|
|
process_group_id: 1,
|
|
session_id: 1,
|
|
};
|
|
const specification = (): ProcessTraceSpecification => ({
|
|
events: [
|
|
{
|
|
id: "ready",
|
|
source: "terminal_raw",
|
|
exact: ready,
|
|
cardinality: { kind: "required" },
|
|
},
|
|
{
|
|
id: "worker",
|
|
source: "process",
|
|
exact: worker,
|
|
cardinality: { kind: "required" },
|
|
},
|
|
],
|
|
language: {
|
|
kind: "partial_order",
|
|
happens_before: [{ before: "ready", after: "worker" }],
|
|
not_before: [],
|
|
unordered_groups: [],
|
|
prefix: ["ready"],
|
|
suffix: ["worker"],
|
|
},
|
|
});
|
|
|
|
describe("process trace specification", () => {
|
|
it("reports independent declaration errors alongside a cycle", () => {
|
|
const base = specification();
|
|
const result = processTraceSpecificationSchema.safeParse({
|
|
...base,
|
|
language: {
|
|
...base.language,
|
|
happens_before: [
|
|
{ before: "ready", after: "worker" },
|
|
{ before: "worker", after: "ready" },
|
|
],
|
|
not_before: [{ event: "missing", anchor: "ready" }],
|
|
unordered_groups: [{ events: ["missing", "missing"] }],
|
|
prefix: ["missing"],
|
|
suffix: ["missing"],
|
|
},
|
|
});
|
|
if (result.success) throw new Error("Expected invalid trace declarations");
|
|
expect(result.error.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
message: "happens-before graph must be acyclic",
|
|
}),
|
|
expect.objectContaining({
|
|
path: ["language", "not_before", 0, "event"],
|
|
}),
|
|
expect.objectContaining({
|
|
path: ["language", "unordered_groups", 0, "events"],
|
|
}),
|
|
expect.objectContaining({ path: ["language", "prefix", 0] }),
|
|
expect.objectContaining({ path: ["language", "suffix", 0] }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("accepts an acyclic order with shared successors and explicit unordered peers", () => {
|
|
const result = processTraceSpecificationSchema.parse({
|
|
events: ["a", "b", "c", "d"].map((id) => ({
|
|
id,
|
|
source: "lifecycle",
|
|
exact: { marker: id },
|
|
})),
|
|
language: {
|
|
kind: "partial_order",
|
|
happens_before: [
|
|
{ before: "a", after: "b" },
|
|
{ before: "a", after: "c" },
|
|
{ before: "b", after: "d" },
|
|
{ before: "c", after: "d" },
|
|
],
|
|
unordered_groups: [{ events: ["b", "c"] }],
|
|
prefix: ["a"],
|
|
suffix: ["d"],
|
|
},
|
|
});
|
|
expect(result.language.kind).toBe("partial_order");
|
|
});
|
|
});
|
|
|
|
describe("process trace ordering validation", () => {
|
|
it("uses a unique topological order for transitive declarations and conflicts", () => {
|
|
const events = ["a", "b", "c"].map((id) => ({
|
|
id,
|
|
source: "lifecycle" as const,
|
|
exact: { marker: id },
|
|
cardinality: { kind: "optional" as const },
|
|
}));
|
|
const accepted = processTraceSpecificationSchema.safeParse({
|
|
events,
|
|
language: {
|
|
kind: "partial_order",
|
|
happens_before: [
|
|
{ before: "a", after: "b" },
|
|
{ before: "b", after: "c" },
|
|
],
|
|
},
|
|
});
|
|
expect(accepted.success).toBe(true);
|
|
|
|
const rejected = processTraceSpecificationSchema.safeParse({
|
|
events,
|
|
language: {
|
|
kind: "partial_order",
|
|
happens_before: [
|
|
{ before: "a", after: "b" },
|
|
{ before: "b", after: "c" },
|
|
],
|
|
not_before: [{ event: "a", anchor: "c" }],
|
|
unordered_groups: [{ events: ["a", "b", "c"] }],
|
|
},
|
|
});
|
|
if (rejected.success) throw new Error("Expected ordered pair conflicts");
|
|
expect(rejected.error.issues).toEqual([
|
|
expect.objectContaining({
|
|
path: ["language", "not_before", 0],
|
|
message: "not-before constraint conflicts with happens-before order",
|
|
}),
|
|
expect.objectContaining({
|
|
path: ["language", "unordered_groups", 0],
|
|
message: "ordered events cannot also be declared unordered",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("counts only undeclared pairs across transitive and disconnected relations", () => {
|
|
const events = ["a", "b", "c", "d"].map((id) => ({
|
|
id,
|
|
source: "lifecycle" as const,
|
|
exact: { fixture: id },
|
|
cardinality: { kind: "optional" as const },
|
|
}));
|
|
const result = processTraceSpecificationSchema.safeParse({
|
|
events,
|
|
language: {
|
|
kind: "partial_order",
|
|
happens_before: [
|
|
{ before: "a", after: "b" },
|
|
{ before: "b", after: "c" },
|
|
],
|
|
not_before: [{ event: "d", anchor: "c" }],
|
|
unordered_groups: [{ events: ["a", "d"] }],
|
|
},
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (result.success) throw new Error("Expected the missing b/d relation");
|
|
expect(result.error.issues).toEqual([
|
|
expect.objectContaining({
|
|
path: ["language"],
|
|
message: "event pair b/d must be explicitly ordered or unordered",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("reports one duplicate-pair diagnostic per overlapping group", () => {
|
|
const result = processTraceSpecificationSchema.safeParse({
|
|
events: ["z", "a", "b"].map((id) => ({
|
|
id,
|
|
source: "lifecycle",
|
|
exact: { fixture: id },
|
|
cardinality: { kind: "optional" },
|
|
})),
|
|
language: {
|
|
kind: "partial_order",
|
|
unordered_groups: [
|
|
{ events: ["z", "a", "b"] },
|
|
{ events: ["b", "a", "z"] },
|
|
],
|
|
},
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (result.success) throw new Error("Expected a duplicate unordered pair");
|
|
expect(result.error.issues).toEqual([
|
|
expect.objectContaining({
|
|
path: ["language", "unordered_groups", 1],
|
|
message: "unordered event pairs must be declared once",
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("malformed process trace declarations", () => {
|
|
it("preserves independent diagnostics when event ids are duplicated", () => {
|
|
const result = processTraceSpecificationSchema.safeParse({
|
|
events: [
|
|
{
|
|
id: "a",
|
|
source: "lifecycle",
|
|
exact: { fixture: "first" },
|
|
cardinality: { kind: "optional" },
|
|
},
|
|
{
|
|
id: "a",
|
|
source: "lifecycle",
|
|
exact: { fixture: "second" },
|
|
cardinality: { kind: "optional" },
|
|
},
|
|
],
|
|
language: {
|
|
kind: "partial_order",
|
|
unordered_groups: [{ events: ["a", "b"] }],
|
|
},
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (result.success) throw new Error("Expected a duplicate event id");
|
|
expect(result.error.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: ["events", 1, "id"],
|
|
message: "event ids must be unique",
|
|
}),
|
|
expect.objectContaining({
|
|
path: ["language", "unordered_groups", 0, "events", 1],
|
|
message: "unknown event id: b",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("keeps reachability diagnostics for unknown referenced successors", () => {
|
|
const result = processTraceSpecificationSchema.safeParse({
|
|
events: ["a", "b"].map((id) => ({
|
|
id,
|
|
source: "lifecycle",
|
|
exact: { fixture: id },
|
|
cardinality: { kind: "optional" },
|
|
})),
|
|
language: {
|
|
kind: "partial_order",
|
|
happens_before: [{ before: "a", after: "missing" }],
|
|
not_before: [{ event: "a", anchor: "missing" }],
|
|
},
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (result.success) throw new Error("Expected an unknown event reference");
|
|
expect(result.error.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: ["language", "happens_before", 0, "after"],
|
|
message: "unknown event id: missing",
|
|
}),
|
|
expect.objectContaining({
|
|
path: ["language", "not_before", 0, "anchor"],
|
|
message: "unknown event id: missing",
|
|
}),
|
|
expect.objectContaining({
|
|
path: ["language", "not_before", 0],
|
|
message: "not-before constraint conflicts with happens-before order",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("finite trace specification validation", () => {
|
|
it("rejects invalid ignores and unsatisfiable variants", () => {
|
|
const base = specification();
|
|
expect(
|
|
processTraceSpecificationSchema.safeParse({
|
|
events: [
|
|
{
|
|
id: "bad",
|
|
source: "terminal_raw",
|
|
exact: "ready",
|
|
ignore_fields: ["at_ms"],
|
|
},
|
|
],
|
|
language: {
|
|
kind: "finite_traces",
|
|
variants: [{ id: "one", trace: ["bad"] }],
|
|
},
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
processTraceSpecificationSchema.safeParse({
|
|
events: [
|
|
{
|
|
...base.events[0],
|
|
cardinality: { kind: "exact", count: 2 },
|
|
},
|
|
],
|
|
language: {
|
|
kind: "finite_traces",
|
|
variants: [{ id: "once", trace: ["ready"] }],
|
|
},
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
});
|