|
3 | 3 | // records, and asserts the two agree field for field. The waitpoint lane owns the |
4 | 4 | // production resolver; this reference exists so the frozen shapes are checked rather |
5 | 5 | // than asserted. |
| 6 | +import { isDeepStrictEqual } from "node:util"; |
6 | 7 | import { describe, expect, it } from "vitest"; |
7 | 8 | import type { Waitpoint } from "@trigger.dev/database"; |
8 | 9 | import { BatchId, RunId } from "@trigger.dev/core/v3/isomorphic"; |
9 | 10 | import type { CompletedWaitpoint } from "@trigger.dev/core/v3"; |
10 | 11 | import type { |
11 | 12 | CompletedWaitpointRecord, |
12 | 13 | CompletedWaitpointResolver, |
13 | | - CompletedWaitpointsPointer, |
14 | 14 | ResolveCompletedWaitpointsArgs, |
15 | 15 | } from "@internal/run-store"; |
16 | 16 | import { enhanceExecutionSnapshotWithWaitpoints } from "./executionSnapshotSystem.js"; |
@@ -166,9 +166,8 @@ async function assertParity( |
166 | 166 | order, |
167 | 167 | records: waitpoints.map(toRecord), |
168 | 168 | }; |
169 | | - // The frozen rule: count is order.length, NOT the record count. Binding it here means every |
170 | | - // parity case enforces it, not only the dedicated "the frozen pointer shape" cases. |
171 | | - expect(args.pointer.count).toBe(order.length); |
| 169 | + // count-carried-forward behaviour (order.length, not the record count) is covered by |
| 170 | + // the run-store Redis suite, not here -- this line only constructs `args`, not asserts. |
172 | 171 | const resolved = await referenceResolver(args, async (id) => runOutputs[id]); |
173 | 172 | expect(resolved).toEqual(enhanced.completedWaitpoints); |
174 | 173 | return { enhanced, resolved }; |
@@ -311,19 +310,8 @@ describe("the frozen record shape", () => { |
311 | 310 | }); |
312 | 311 | }); |
313 | 312 |
|
314 | | -describe("the frozen pointer shape", () => { |
315 | | - it("pins count to order.length, not the record count", () => { |
316 | | - const order = ["wp_a", "wp_b", "wp_a"]; |
317 | | - const pointer: CompletedWaitpointsPointer = { cycleSeq: 7, count: order.length }; |
318 | | - expect(pointer).toEqual({ cycleSeq: 7, count: 3 }); |
319 | | - }); |
320 | | - |
321 | | - it("pins count at 0 when order is empty, even if records exist", () => { |
322 | | - const order: string[] = []; |
323 | | - const pointer: CompletedWaitpointsPointer = { cycleSeq: 7, count: order.length }; |
324 | | - expect(pointer).toEqual({ cycleSeq: 7, count: 0 }); |
325 | | - }); |
326 | | -}); |
| 313 | +// The pointer's shape is pinned by CompletedWaitpointsPointer and tsconfig.freeze-test.json, |
| 314 | +// not by a runtime assertion here -- a value that only echoes its own construction can't fail. |
327 | 315 |
|
328 | 316 | describe("the completed-waitpoints freeze", () => { |
329 | 317 | it("expands a repeated id at each of its positions", async () => { |
@@ -379,11 +367,6 @@ describe("the completed-waitpoints freeze", () => { |
379 | 367 | expect(resolved[0]!.output).toBe('{"type":"STRING_ERROR"}'); |
380 | 368 | }); |
381 | 369 |
|
382 | | - it("returns an empty list for no waitpoints", async () => { |
383 | | - const { resolved } = await assertParity([], [], "batch_1"); |
384 | | - expect(resolved).toEqual([]); |
385 | | - }); |
386 | | - |
387 | 370 | it("resolves through the frozen hook signature", async () => { |
388 | 371 | // Exercises resolverUnderTest, so the declared CompletedWaitpointResolver type is |
389 | 372 | // proved implementable at runtime, on top of the compile-time proof at its |
@@ -496,6 +479,144 @@ describe("the completed-waitpoints freeze", () => { |
496 | 479 | }); |
497 | 480 | }); |
498 | 481 |
|
| 482 | +describe("the exhaustive parity grid", () => { |
| 483 | + // Dimensions mirror every Waitpoint column the oracle reads (type, output, outputType, |
| 484 | + // outputIsError, completedByTaskRunId, completedByBatchId, completedAfter, |
| 485 | + // userProvidedIdempotencyKey, inactiveIdempotencyKey), plus order-membership and the |
| 486 | + // reading entry's batchId. A new column the oracle reads must widen a dimension here, |
| 487 | + // so the pinned combination count below fails instead of coverage silently shrinking. |
| 488 | + const TYPES: Waitpoint["type"][] = ["RUN", "BATCH", "DATETIME", "MANUAL"]; |
| 489 | + const OUTPUTS: (string | null)[] = [null, '{"value":42}']; |
| 490 | + const OUTPUT_TYPES = ["application/json", "application/store"]; |
| 491 | + const OUTPUT_IS_ERRORS = [false, true]; |
| 492 | + const TASK_RUN_IDS: (string | null)[] = [null, "run_child"]; |
| 493 | + const BATCH_IDS: (string | null)[] = [null, "batch_child"]; |
| 494 | + const COMPLETED_AFTERS: (Date | null)[] = [null, new Date("2026-02-02T00:00:00.000Z")]; |
| 495 | + const IDEMPOTENCY_COMBOS: Array<[boolean, string | null]> = [ |
| 496 | + [false, null], |
| 497 | + [false, "cleared"], |
| 498 | + [true, null], |
| 499 | + [true, "cleared"], |
| 500 | + ]; |
| 501 | + const ORDER_MEMBERSHIPS = ["absent", "once", "twice"] as const; |
| 502 | + const READING_BATCH_IDS: (string | null)[] = [null, "batch_reading_entry"]; |
| 503 | + |
| 504 | + // Only reached when type is RUN, output is set, outputIsError is false, and |
| 505 | + // completedByTaskRunId is "run_child": the deriveFromRun branch. The value matches |
| 506 | + // OUTPUTS' non-null entry so a correct resolver is byte-identical to the oracle. |
| 507 | + const RUN_OUTPUT_LOOKUP: Record<string, string> = { run_child: '{"value":42}' }; |
| 508 | + |
| 509 | + it("agrees with the oracle across every combination", async () => { |
| 510 | + type Combo = { |
| 511 | + type: Waitpoint["type"]; |
| 512 | + output: string | null; |
| 513 | + outputType: string; |
| 514 | + outputIsError: boolean; |
| 515 | + completedByTaskRunId: string | null; |
| 516 | + completedByBatchId: string | null; |
| 517 | + completedAfter: Date | null; |
| 518 | + userProvidedIdempotencyKey: boolean; |
| 519 | + inactiveIdempotencyKey: string | null; |
| 520 | + orderMembership: (typeof ORDER_MEMBERSHIPS)[number]; |
| 521 | + readingBatchId: string | null; |
| 522 | + }; |
| 523 | + const failures: Array<{ combo: Combo; oracle: unknown; resolver: unknown }> = []; |
| 524 | + let cases = 0; |
| 525 | + |
| 526 | + for (const type of TYPES) { |
| 527 | + for (const output of OUTPUTS) { |
| 528 | + for (const outputType of OUTPUT_TYPES) { |
| 529 | + for (const outputIsError of OUTPUT_IS_ERRORS) { |
| 530 | + for (const completedByTaskRunId of TASK_RUN_IDS) { |
| 531 | + for (const completedByBatchId of BATCH_IDS) { |
| 532 | + for (const completedAfter of COMPLETED_AFTERS) { |
| 533 | + for (const [ |
| 534 | + userProvidedIdempotencyKey, |
| 535 | + inactiveIdempotencyKey, |
| 536 | + ] of IDEMPOTENCY_COMBOS) { |
| 537 | + for (const orderMembership of ORDER_MEMBERSHIPS) { |
| 538 | + for (const readingBatchId of READING_BATCH_IDS) { |
| 539 | + cases++; |
| 540 | + const combo: Combo = { |
| 541 | + type, |
| 542 | + output, |
| 543 | + outputType, |
| 544 | + outputIsError, |
| 545 | + completedByTaskRunId, |
| 546 | + completedByBatchId, |
| 547 | + completedAfter, |
| 548 | + userProvidedIdempotencyKey, |
| 549 | + inactiveIdempotencyKey, |
| 550 | + orderMembership, |
| 551 | + readingBatchId, |
| 552 | + }; |
| 553 | + |
| 554 | + const id = "wp_grid"; |
| 555 | + const w = makeWaitpoint({ |
| 556 | + id, |
| 557 | + type, |
| 558 | + output, |
| 559 | + outputType, |
| 560 | + outputIsError, |
| 561 | + completedByTaskRunId, |
| 562 | + completedByBatchId, |
| 563 | + completedAfter, |
| 564 | + idempotencyKey: "idem_user", |
| 565 | + userProvidedIdempotencyKey, |
| 566 | + inactiveIdempotencyKey, |
| 567 | + }); |
| 568 | + const order = |
| 569 | + orderMembership === "absent" |
| 570 | + ? ["wp_other"] |
| 571 | + : orderMembership === "once" |
| 572 | + ? [id] |
| 573 | + : [id, id]; |
| 574 | + |
| 575 | + const enhanced = enhanceExecutionSnapshotWithWaitpoints( |
| 576 | + makeSnapshot(readingBatchId), |
| 577 | + [w], |
| 578 | + order |
| 579 | + ); |
| 580 | + const args: ResolveCompletedWaitpointsArgs = { |
| 581 | + runId: "run_1", |
| 582 | + batchId: readingBatchId ?? undefined, |
| 583 | + pointer: { cycleSeq: 1, count: order.length }, |
| 584 | + order, |
| 585 | + records: [toRecord(w)], |
| 586 | + }; |
| 587 | + const resolved = await referenceResolver( |
| 588 | + args, |
| 589 | + async (runId) => RUN_OUTPUT_LOOKUP[runId] |
| 590 | + ); |
| 591 | + |
| 592 | + if (!isDeepStrictEqual(resolved, enhanced.completedWaitpoints)) { |
| 593 | + failures.push({ |
| 594 | + combo, |
| 595 | + oracle: enhanced.completedWaitpoints, |
| 596 | + resolver: resolved, |
| 597 | + }); |
| 598 | + } |
| 599 | + } |
| 600 | + } |
| 601 | + } |
| 602 | + } |
| 603 | + } |
| 604 | + } |
| 605 | + } |
| 606 | + } |
| 607 | + } |
| 608 | + } |
| 609 | + |
| 610 | + expect(cases).toBe(6144); |
| 611 | + expect( |
| 612 | + failures.length, |
| 613 | + failures.length > 0 |
| 614 | + ? `${failures.length}/${cases} combinations diverged. First: ${JSON.stringify(failures[0], null, 2)}` |
| 615 | + : undefined |
| 616 | + ).toBe(0); |
| 617 | + }); |
| 618 | +}); |
| 619 | + |
499 | 620 | describe("the freeze's two deliberate divergences", () => { |
500 | 621 | it("pins completedAt at write time, where the oracle samples the clock", () => { |
501 | 622 | // The oracle applies `w.completedAt ?? new Date()`, so a null value changes on every |
|
0 commit comments