66import { isDeepStrictEqual } from "node:util" ;
77import { describe , expect , it } from "vitest" ;
88import type { Waitpoint } from "@trigger.dev/database" ;
9- import { BatchId , RunId } from "@trigger.dev/core/v3/isomorphic" ;
10- import type { CompletedWaitpoint } from "@trigger.dev/core/v3" ;
119import type {
1210 CompletedWaitpointRecord ,
1311 CompletedWaitpointResolver ,
1412 CompletedWaitpointsPointer ,
1513 ResolveCompletedWaitpointsArgs ,
14+ SnapshotReadWaitpoint ,
1615} from "@internal/run-store" ;
1716
1817// The frozen key sets, pinned exactly and bidirectionally. Renames, removals, widenings and
@@ -103,19 +102,15 @@ function recordOutputFor(w: Waitpoint): CompletedWaitpointRecord["output"] {
103102 return { inline : w . output } ;
104103}
105104
106- // The READ side of the freeze. Iterates `records`, never `order`.
105+ // The READ side of the freeze. Iterates `records`, never `order`, and returns ONE unenhanced row per
106+ // distinct record: no index, no nested completion objects. Index expansion and those objects belong
107+ // to the single enhancement step, which both backends then go through identically.
107108async function referenceResolver (
108109 args : ResolveCompletedWaitpointsArgs ,
109110 lookupRunOutput : ( runId : string ) => Promise < string | undefined >
110- ) : Promise < CompletedWaitpoint [ ] > {
111- const out : CompletedWaitpoint [ ] = [ ] ;
111+ ) : Promise < SnapshotReadWaitpoint [ ] > {
112+ const out : SnapshotReadWaitpoint [ ] = [ ] ;
112113 for ( const record of args . records ) {
113- const indexes : ( number | undefined ) [ ] = [ ] ;
114- for ( let i = 0 ; i < args . order . length ; i ++ ) {
115- if ( args . order [ i ] === record . id ) indexes . push ( i ) ;
116- }
117- if ( indexes . length === 0 ) indexes . push ( undefined ) ;
118-
119114 let output : string | undefined ;
120115 if ( record . output === null ) {
121116 output = undefined ;
@@ -132,37 +127,23 @@ async function referenceResolver(
132127 throw new Error ( `unknown record output variant: ${ JSON . stringify ( _never ) } ` ) ;
133128 }
134129
135- for ( const index of indexes ) {
136- out . push ( {
137- id : record . id ,
138- // Unreachable: the oracle's own loop pushes a non-negative integer or undefined.
139- // Reproduced because the frozen index-expansion rule names it.
140- index : index === - 1 ? undefined : index ,
141- friendlyId : record . friendlyId ,
142- type : record . type ,
143- completedAt : new Date ( record . completedAt ) ,
144- idempotencyKey : record . idempotencyKey ,
145- completedByTaskRun : record . completedByTaskRunId
146- ? {
147- id : record . completedByTaskRunId ,
148- friendlyId : RunId . toFriendlyId ( record . completedByTaskRunId ) ,
149- batch : args . batchId
150- ? { id : args . batchId , friendlyId : BatchId . toFriendlyId ( args . batchId ) }
151- : undefined ,
152- }
153- : undefined ,
154- completedAfter : record . completedAfter ? new Date ( record . completedAfter ) : undefined ,
155- completedByBatch : record . completedByBatchId
156- ? {
157- id : record . completedByBatchId ,
158- friendlyId : BatchId . toFriendlyId ( record . completedByBatchId ) ,
159- }
160- : undefined ,
161- output,
162- outputType : record . outputType ,
163- outputIsError : record . outputIsError ,
164- } ) ;
165- }
130+ out . push ( {
131+ id : record . id ,
132+ friendlyId : record . friendlyId ,
133+ type : record . type ,
134+ completedAt : new Date ( record . completedAt ) ,
135+ completedByTaskRunId : record . completedByTaskRunId ?? null ,
136+ completedByBatchId : record . completedByBatchId ?? null ,
137+ completedAfter : record . completedAfter ? new Date ( record . completedAfter ) : null ,
138+ output : output ?? null ,
139+ outputType : record . outputType ,
140+ outputIsError : record . outputIsError ,
141+ // The record already carries the resolved user-visible key, so it is re-expressed as the
142+ // triple the enhancement step reads: a key present means user-provided and active.
143+ idempotencyKey : record . idempotencyKey ?? "" ,
144+ userProvidedIdempotencyKey : record . idempotencyKey !== undefined ,
145+ inactiveIdempotencyKey : null ,
146+ } ) ;
166147 }
167148 return out ;
168149}
@@ -181,13 +162,18 @@ function makeSnapshot(batchId: string | null) {
181162 return { id : "snap_1" , runId : "run_1" , batchId, checkpoint : null } as never ;
182163}
183164
165+ // Parity is asserted on the FINAL runner-facing payload, not on the resolver's intermediate rows:
166+ // both sides go through the one enhancement step, the Postgres side over its own waitpoint rows and
167+ // the Redis side over the resolver's rows. Comparing the intermediate is what let a Redis read that
168+ // dropped every RUN/BATCH completion association still look correct here.
184169async function assertParity (
185170 waitpoints : Waitpoint [ ] ,
186171 order : string [ ] ,
187172 batchId : string | null ,
188173 runOutputs : Record < string , string > = { }
189174) {
190- const enhanced = enhanceExecutionSnapshotWithWaitpoints ( makeSnapshot ( batchId ) , waitpoints , order ) ;
175+ const snapshot = makeSnapshot ( batchId ) ;
176+ const enhanced = enhanceExecutionSnapshotWithWaitpoints ( snapshot , waitpoints , order ) ;
191177 const args : ResolveCompletedWaitpointsArgs = {
192178 runId : "run_1" ,
193179 batchId : batchId ?? undefined ,
@@ -197,7 +183,9 @@ async function assertParity(
197183 } ;
198184 // count-carried-forward behaviour (order.length, not the record count) is covered by
199185 // the run-store Redis suite, not here -- this line only constructs `args`, not asserts.
200- const resolved = await referenceResolver ( args , async ( id ) => runOutputs [ id ] ) ;
186+ const rows = await referenceResolver ( args , async ( id ) => runOutputs [ id ] ) ;
187+ const resolvedEnhanced = enhanceExecutionSnapshotWithWaitpoints ( snapshot , rows , order ) ;
188+ const resolved = resolvedEnhanced . completedWaitpoints ;
201189 expect ( resolved ) . toEqual ( enhanced . completedWaitpoints ) ;
202190 return { enhanced, resolved } ;
203191}
@@ -410,7 +398,11 @@ describe("the completed-waitpoints freeze", () => {
410398 } ) ;
411399 expect ( resolved ) . toHaveLength ( 1 ) ;
412400 expect ( resolved [ 0 ] ! . id ) . toBe ( "wp_hook" ) ;
413- expect ( resolved [ 0 ] ! . index ) . toBe ( 0 ) ;
401+ // The hook returns an UNENHANCED row even though the id sits at order position 0: no index and
402+ // no nested completion objects. Enhancement is the caller's single step.
403+ expect ( resolved [ 0 ] ! ) . not . toHaveProperty ( "index" ) ;
404+ expect ( resolved [ 0 ] ! ) . not . toHaveProperty ( "completedByTaskRun" ) ;
405+ expect ( resolved [ 0 ] ! ) . not . toHaveProperty ( "completedByBatch" ) ;
414406 } ) ;
415407
416408 it ( "round-trips all four waitpoint types" , async ( ) => {
@@ -601,8 +593,9 @@ describe("the exhaustive parity grid", () => {
601593 ? [ id ]
602594 : [ id , id ] ;
603595
596+ const snapshot = makeSnapshot ( readingBatchId ) ;
604597 const enhanced = enhanceExecutionSnapshotWithWaitpoints (
605- makeSnapshot ( readingBatchId ) ,
598+ snapshot ,
606599 [ w ] ,
607600 order
608601 ) ;
@@ -613,10 +606,15 @@ describe("the exhaustive parity grid", () => {
613606 order,
614607 records : [ toRecord ( w ) ] ,
615608 } ;
616- const resolved = await referenceResolver (
609+ const rows = await referenceResolver (
617610 args ,
618611 async ( runId ) => RUN_OUTPUT_LOOKUP [ runId ]
619612 ) ;
613+ const resolved = enhanceExecutionSnapshotWithWaitpoints (
614+ snapshot ,
615+ rows ,
616+ order
617+ ) . completedWaitpoints ;
620618
621619 if ( ! isDeepStrictEqual ( resolved , enhanced . completedWaitpoints ) ) {
622620 failures . push ( {
0 commit comments