test(run-engine): prove the frozen waitpoint record matches the enhance oracle

Runs the real enhanceExecutionSnapshotWithWaitpoints against a reference
resolver over equivalent records and asserts field-for-field parity, so the
frozen shape is machine-checked instead of asserted.
This commit is contained in:
Daniel Sutton
2026-08-21 18:30:33 +01:00
parent 0cc05df6b4
commit 08c0d82904
2 changed files with 488 additions and 1 deletions
@@ -0,0 +1,487 @@
// The freeze's executable definition. It runs the real oracle,
// enhanceExecutionSnapshotWithWaitpoints, against a reference resolver over equivalent
// records, and asserts the two agree field for field. The waitpoint lane owns the
// production resolver; this reference exists so the frozen shapes are checked rather
// than asserted.
import { describe, expect, it } from "vitest";
import type { Waitpoint } from "@trigger.dev/database";
import { BatchId, RunId } from "@trigger.dev/core/v3/isomorphic";
import type {
CompletedWaitpoint,
} from "@trigger.dev/core/v3";
import type {
CompletedWaitpointRecord,
CompletedWaitpointResolver,
ResolveCompletedWaitpointsArgs,
} from "@internal/run-store";
import { enhanceExecutionSnapshotWithWaitpoints } from "./executionSnapshotSystem.js";
function makeWaitpoint(overrides: Partial<Waitpoint>): Waitpoint {
return {
id: "wp_default",
friendlyId: "waitpoint_default",
type: "MANUAL",
status: "COMPLETED",
completedAt: new Date("2026-01-01T00:00:00.000Z"),
idempotencyKey: "idem_generated",
userProvidedIdempotencyKey: false,
inactiveIdempotencyKey: null,
idempotencyKeyExpiresAt: null,
completedByTaskRunId: null,
completedByBatchId: null,
completedAfter: null,
output: null,
outputType: "application/json",
outputIsError: false,
projectId: "proj_1",
environmentId: "env_1",
tags: [],
createdAt: new Date("2026-01-01T00:00:00.000Z"),
updatedAt: new Date("2026-01-01T00:00:00.000Z"),
...overrides,
} as Waitpoint;
}
// The WRITE side of the freeze: one Waitpoint row becomes one record.
function toRecord(w: Waitpoint): CompletedWaitpointRecord {
return {
id: w.id,
friendlyId: w.friendlyId,
type: w.type,
completedAt: (w.completedAt ?? new Date()).toISOString(),
outputType: w.outputType,
outputIsError: w.outputIsError,
output: recordOutputFor(w),
completedByTaskRunId: w.completedByTaskRunId ?? undefined,
completedByBatchId: w.completedByBatchId ?? undefined,
completedAfter: w.completedAfter?.toISOString(),
idempotencyKey:
w.userProvidedIdempotencyKey && !w.inactiveIdempotencyKey ? w.idempotencyKey : undefined,
};
}
function recordOutputFor(w: Waitpoint): CompletedWaitpointRecord["output"] {
if (w.output === null) return null;
// A RUN success re-derives byte-identically from TaskRun.output. A RUN error cannot,
// because TaskRun.error is jsonb, so it carries inline.
//
// This branch is deliberately BEFORE the application/store branch. An offloaded RUN
// success is still deriveFromRun, and that is correct: completeAttemptSuccess receives
// the same `output` and `outputType` the waitpoint got, so TaskRun.output holds the
// same ref string. The re-read stays byte-identical either way.
if (w.type === "RUN" && !w.outputIsError) return { deriveFromRun: true };
if (w.outputType === "application/store") return { ref: w.output };
return { inline: w.output };
}
// The READ side of the freeze. Iterates `records`, never `order`.
async function referenceResolver(
args: ResolveCompletedWaitpointsArgs,
lookupRunOutput: (runId: string) => Promise<string | undefined>
): Promise<CompletedWaitpoint[]> {
const out: CompletedWaitpoint[] = [];
for (const record of args.records) {
const indexes: (number | undefined)[] = [];
for (let i = 0; i < args.order.length; i++) {
if (args.order[i] === record.id) indexes.push(i);
}
if (indexes.length === 0) indexes.push(undefined);
let output: string | undefined;
if (record.output === null) {
output = undefined;
} else if ("inline" in record.output) {
output = record.output.inline;
} else if ("ref" in record.output) {
output = record.output.ref;
} else {
output = record.completedByTaskRunId
? await lookupRunOutput(record.completedByTaskRunId)
: undefined;
}
for (const index of indexes) {
out.push({
id: record.id,
// Unreachable: the oracle's own loop pushes a non-negative integer or undefined.
// Reproduced because the frozen index-expansion rule names it.
index: index === -1 ? undefined : index,
friendlyId: record.friendlyId,
type: record.type,
completedAt: new Date(record.completedAt),
idempotencyKey: record.idempotencyKey,
completedByTaskRun: record.completedByTaskRunId
? {
id: record.completedByTaskRunId,
friendlyId: RunId.toFriendlyId(record.completedByTaskRunId),
batch: args.batchId
? { id: args.batchId, friendlyId: BatchId.toFriendlyId(args.batchId) }
: undefined,
}
: undefined,
completedAfter: record.completedAfter ? new Date(record.completedAfter) : undefined,
completedByBatch: record.completedByBatchId
? {
id: record.completedByBatchId,
friendlyId: BatchId.toFriendlyId(record.completedByBatchId),
}
: undefined,
output,
outputType: record.outputType,
outputIsError: record.outputIsError,
});
}
}
return out;
}
// Proves the frozen hook signature is implementable exactly as declared. The reference
// resolver takes its TaskRun lookup as a second parameter, so the production shape is
// the curried form -- which is what the waitpoint lane will bind to a Prisma client.
// If this assignment stops compiling, the frozen signature has drifted.
const resolverUnderTest: CompletedWaitpointResolver = (args) =>
referenceResolver(args, async () => undefined);
// The oracle spreads the snapshot, so it needs the two fields the mapping reads.
function makeSnapshot(batchId: string | null) {
return { id: "snap_1", runId: "run_1", batchId, checkpoint: null } as never;
}
async function assertParity(
waitpoints: Waitpoint[],
order: string[],
batchId: string | null,
runOutputs: Record<string, string> = {}
) {
const enhanced = enhanceExecutionSnapshotWithWaitpoints(
makeSnapshot(batchId),
waitpoints,
order
);
const resolved = await referenceResolver(
{
runId: "run_1",
batchId: batchId ?? undefined,
pointer: { cycleSeq: 1, count: order.length },
order,
records: waitpoints.map(toRecord),
},
async (id) => runOutputs[id]
);
expect(resolved).toEqual(enhanced.completedWaitpoints);
return { enhanced, resolved };
}
describe("the frozen record shape", () => {
// Literals, not a mirror of the writer. A field rename or an encoding change must
// fail HERE, because the parity suite cannot see it.
it("pins the RUN record", () => {
expect(
toRecord(
makeWaitpoint({
id: "wp_run",
friendlyId: "waitpoint_run",
type: "RUN",
completedByTaskRunId: "run_child",
output: '{"value":42}',
})
)
).toEqual({
id: "wp_run",
friendlyId: "waitpoint_run",
type: "RUN",
completedAt: "2026-01-01T00:00:00.000Z",
outputType: "application/json",
outputIsError: false,
output: { deriveFromRun: true },
completedByTaskRunId: "run_child",
completedByBatchId: undefined,
completedAfter: undefined,
idempotencyKey: undefined,
});
});
it("pins the BATCH record", () => {
expect(
toRecord(
makeWaitpoint({
id: "wp_batch",
friendlyId: "waitpoint_batch",
type: "BATCH",
completedByBatchId: "batch_child",
output: "Batch waitpoint completed",
})
)
).toEqual({
id: "wp_batch",
friendlyId: "waitpoint_batch",
type: "BATCH",
completedAt: "2026-01-01T00:00:00.000Z",
outputType: "application/json",
outputIsError: false,
output: { inline: "Batch waitpoint completed" },
completedByTaskRunId: undefined,
completedByBatchId: "batch_child",
completedAfter: undefined,
idempotencyKey: undefined,
});
});
it("pins the DATETIME record", () => {
expect(
toRecord(
makeWaitpoint({
id: "wp_dt",
friendlyId: "waitpoint_dt",
type: "DATETIME",
completedAfter: new Date("2026-02-02T00:00:00.000Z"),
})
)
).toEqual({
id: "wp_dt",
friendlyId: "waitpoint_dt",
type: "DATETIME",
completedAt: "2026-01-01T00:00:00.000Z",
outputType: "application/json",
outputIsError: false,
output: null,
completedByTaskRunId: undefined,
completedByBatchId: undefined,
completedAfter: "2026-02-02T00:00:00.000Z",
idempotencyKey: undefined,
});
});
it("pins the MANUAL record, with a user idempotency key and an offloaded output", () => {
expect(
toRecord(
makeWaitpoint({
id: "wp_manual",
friendlyId: "waitpoint_manual",
type: "MANUAL",
idempotencyKey: "idem_user",
userProvidedIdempotencyKey: true,
output: "s3://bucket/key",
outputType: "application/store",
})
)
).toEqual({
id: "wp_manual",
friendlyId: "waitpoint_manual",
type: "MANUAL",
completedAt: "2026-01-01T00:00:00.000Z",
outputType: "application/store",
outputIsError: false,
output: { ref: "s3://bucket/key" },
completedByTaskRunId: undefined,
completedByBatchId: undefined,
completedAfter: undefined,
idempotencyKey: "idem_user",
});
});
it("keeps an offloaded RUN success on deriveFromRun, not ref", () => {
// Branch precedence. TaskRun.output holds the same ref string, so the re-read is
// still byte-identical. A later edit that reorders the branches must fail here.
const record = toRecord(
makeWaitpoint({
id: "wp_run_offloaded",
type: "RUN",
completedByTaskRunId: "run_child",
output: "s3://bucket/key",
outputType: "application/store",
})
);
expect(record.output).toEqual({ deriveFromRun: true });
});
it("carries a RUN error inline, never deriveFromRun", () => {
const record = toRecord(
makeWaitpoint({
id: "wp_run_err",
type: "RUN",
completedByTaskRunId: "run_child",
output: '{"type":"BUILT_IN_ERROR"}',
outputIsError: true,
})
);
expect(record.output).toEqual({ inline: '{"type":"BUILT_IN_ERROR"}' });
});
});
describe("the completed-waitpoints freeze", () => {
it("expands a repeated id at each of its positions", async () => {
const w = makeWaitpoint({ id: "wp_a", type: "RUN", completedByTaskRunId: "run_child" });
const { resolved } = await assertParity([w], ["wp_a", "wp_other", "wp_a"], "batch_1");
expect(resolved.map((r) => r.index)).toEqual([0, 2]);
});
it("yields one entry with an undefined index for a record absent from order", async () => {
const w = makeWaitpoint({ id: "wp_absent", type: "MANUAL" });
const { resolved } = await assertParity([w], ["wp_other"], null);
expect(resolved).toHaveLength(1);
expect(resolved[0]!.index).toBeUndefined();
});
it("resolves a non-batch wait, where order is empty and one record exists", async () => {
// The commonest resume. Postgres's join holds the id while order does not, which is
// why `records` is authoritative and the mint comparison never reads `order`.
// batchId is null here on purpose: a triggerAndWait outside a batch is the shape
// this case is named for, and it exercises the oracle's `batchId ? ... : undefined`
// false branch, which no other case reaches.
// output stays null, so no TaskRun lookup is involved: both halves yield undefined.
const w = makeWaitpoint({ id: "wp_single", type: "RUN", completedByTaskRunId: "run_child" });
const { resolved } = await assertParity([w], [], null);
expect(resolved).toHaveLength(1);
expect(resolved[0]!.index).toBeUndefined();
expect(resolved[0]!.completedByTaskRun?.id).toBe("run_child");
expect(resolved[0]!.completedByTaskRun?.batch).toBeUndefined();
});
it("keys completedByBatch on the id alone, not on the type", async () => {
// The oracle checks completedByBatchId without looking at `type`, at
// executionSnapshotSystem.ts:107-113. A resolver keyed on type would pass every
// other case here and diverge in production.
const w = makeWaitpoint({
id: "wp_manual_with_batch",
type: "MANUAL",
completedByBatchId: "batch_child",
});
const { resolved } = await assertParity([w], ["wp_manual_with_batch"], null);
expect(resolved[0]!.completedByBatch?.id).toBe("batch_child");
});
it("carries outputIsError on a non-RUN type", async () => {
const w = makeWaitpoint({
id: "wp_manual_err",
type: "MANUAL",
output: '{"type":"STRING_ERROR"}',
outputIsError: true,
});
const { resolved } = await assertParity([w], ["wp_manual_err"], null);
expect(resolved[0]!.outputIsError).toBe(true);
expect(resolved[0]!.output).toBe('{"type":"STRING_ERROR"}');
});
it("returns an empty list for no waitpoints", async () => {
const { resolved } = await assertParity([], [], "batch_1");
expect(resolved).toEqual([]);
});
it("resolves through the frozen hook signature", async () => {
// Exercises resolverUnderTest, so the declared CompletedWaitpointResolver type is
// proved implementable at runtime and not only at compile time.
const w = makeWaitpoint({ id: "wp_hook", type: "MANUAL" });
const resolved = await resolverUnderTest({
runId: "run_1",
batchId: undefined,
pointer: { cycleSeq: 1, count: 1 },
order: ["wp_hook"],
records: [toRecord(w)],
});
expect(resolved).toHaveLength(1);
expect(resolved[0]!.id).toBe("wp_hook");
expect(resolved[0]!.index).toBe(0);
});
it("round-trips all four waitpoint types", async () => {
const waitpoints = [
makeWaitpoint({ id: "wp_run", type: "RUN", completedByTaskRunId: "run_child" }),
makeWaitpoint({ id: "wp_batch", type: "BATCH", completedByBatchId: "batch_child" }),
makeWaitpoint({
id: "wp_dt",
type: "DATETIME",
completedAfter: new Date("2026-02-02T00:00:00.000Z"),
}),
makeWaitpoint({ id: "wp_manual", type: "MANUAL" }),
];
const { resolved } = await assertParity(waitpoints, ["wp_run", "wp_batch"], "batch_1");
expect(resolved.map((r) => r.type)).toEqual(["RUN", "BATCH", "DATETIME", "MANUAL"]);
});
it("applies the idempotency-key rule in all four combinations", async () => {
const combos: Array<[boolean, string | null, string | undefined]> = [
[true, null, "idem_user"],
[true, "cleared", undefined],
[false, null, undefined],
[false, "cleared", undefined],
];
for (const [userProvided, inactive, expected] of combos) {
const w = makeWaitpoint({
id: "wp_idem",
idempotencyKey: "idem_user",
userProvidedIdempotencyKey: userProvided,
inactiveIdempotencyKey: inactive,
});
const { resolved } = await assertParity([w], ["wp_idem"], null);
expect(resolved[0]!.idempotencyKey).toBe(expected);
}
});
it("forwards completedAfter on a MANUAL waitpoint with a timeout", async () => {
// The plan comment scopes completedAfter to DATETIME. The oracle forwards it for any
// type, so the resolver must too.
const w = makeWaitpoint({
id: "wp_timeout",
type: "MANUAL",
completedAfter: new Date("2026-03-03T00:00:00.000Z"),
});
const { resolved } = await assertParity([w], ["wp_timeout"], null);
expect(resolved[0]!.completedAfter).toEqual(new Date("2026-03-03T00:00:00.000Z"));
});
it("maps every output variant", async () => {
const runSuccess = makeWaitpoint({
id: "wp_run_ok",
type: "RUN",
completedByTaskRunId: "run_ok",
output: '{"value":42}',
});
const runError = makeWaitpoint({
id: "wp_run_err",
type: "RUN",
completedByTaskRunId: "run_err",
output: '{"type":"BUILT_IN_ERROR"}',
outputIsError: true,
});
const offloaded = makeWaitpoint({
id: "wp_ref",
type: "MANUAL",
output: "s3://bucket/key",
outputType: "application/store",
});
const empty = makeWaitpoint({ id: "wp_none", type: "MANUAL", output: null });
const { resolved } = await assertParity(
[runSuccess, runError, offloaded, empty],
["wp_run_ok", "wp_run_err", "wp_ref", "wp_none"],
"batch_1",
// deriveFromRun: the same string TaskRun.output holds verbatim.
{ run_ok: '{"value":42}' }
);
expect(resolved.map((r) => r.output)).toEqual([
'{"value":42}',
'{"type":"BUILT_IN_ERROR"}',
"s3://bucket/key",
undefined,
]);
});
});
describe("the freeze's two deliberate divergences", () => {
it("pins completedAt at write time, where the oracle samples the clock", async () => {
// The oracle applies `w.completedAt ?? new Date()`, so a null value changes on every
// read. No deterministic record can match that. The record pins it once instead.
const w = makeWaitpoint({ id: "wp_null_at", completedAt: null });
const record = toRecord(w);
expect(record.completedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(new Date(record.completedAt).getTime()).not.toBeNaN();
});
it("takes batch{} from the reading entry, not the completing run's own batch", async () => {
// A known, deliberate conflation in the oracle. Byte-compatibility requires it.
const w = makeWaitpoint({ id: "wp_run", type: "RUN", completedByTaskRunId: "run_child" });
const { resolved } = await assertParity([w], ["wp_run"], "batch_reading_entry");
expect(resolved[0]!.completedByTaskRun?.batch?.id).toBe("batch_reading_entry");
});
});
@@ -58,7 +58,7 @@ function enhanceExecutionSnapshot(
* Transforms a snapshot (with checkpoint but without waitpoints) into an EnhancedExecutionSnapshot
* by combining it with pre-fetched waitpoints.
*/
function enhanceExecutionSnapshotWithWaitpoints(
export function enhanceExecutionSnapshotWithWaitpoints(
snapshot: ExecutionSnapshotWithCheckpoint,
waitpoints: Waitpoint[],
completedWaitpointOrder: string[]