@@ -198,7 +198,7 @@ describe("conversation work execution", () => {
198198 expect ( queue . sent ) . toEqual ( [
199199 {
200200 conversationId : CONVERSATION_ID ,
201- idempotencyKey : `heartbeat:pending:${ CONVERSATION_ID } ` ,
201+ idempotencyKey : `heartbeat:pending:${ CONVERSATION_ID } :62000 ` ,
202202 } ,
203203 ] ) ;
204204 } ) ;
@@ -243,30 +243,73 @@ describe("conversation work execution", () => {
243243 await expect ( first ) . resolves . toEqual ( { status : "completed" } ) ;
244244 } ) ;
245245
246- it ( "preserves work requested while a lease is running" , async ( ) => {
246+ it ( "requeues work requested while a lease is running" , async ( ) => {
247247 const queue = new FakeQueue ( ) ;
248+ let currentNowMs = 1_000 ;
248249 await appendInboundMessage ( { message : inboundMessage ( "m1" ) , nowMs : 1_000 } ) ;
249250
250251 await expect (
251252 processConversationWork ( CONVERSATION_ID , {
253+ nowMs : ( ) => currentNowMs ,
252254 queue,
253255 run : async ( context ) => {
254256 await context . drainMailbox ( async ( ) => { } ) ;
257+ currentNowMs = 2_000 ;
255258 await requestConversationWork ( {
256259 conversationId : context . conversationId ,
257- nowMs : 2_000 ,
260+ nowMs : currentNowMs ,
258261 } ) ;
259262 return { status : "completed" } ;
260263 } ,
261264 } ) ,
262- ) . resolves . toEqual ( { status : "completed " } ) ;
265+ ) . resolves . toEqual ( { status : "pending_requeued " } ) ;
263266
264267 const state = await getConversationWorkState ( {
265268 conversationId : CONVERSATION_ID ,
266269 } ) ;
267270 expect ( state ?. lease ) . toBeUndefined ( ) ;
268271 expect ( state ?. needsRun ) . toBe ( true ) ;
269272 expect ( state ? countPendingConversationMessages ( state ) : 0 ) . toBe ( 0 ) ;
273+ expect ( queue . sent ) . toMatchObject ( [
274+ {
275+ conversationId : CONVERSATION_ID ,
276+ idempotencyKey : `pending:${ CONVERSATION_ID } :2000` ,
277+ } ,
278+ ] ) ;
279+ } ) ;
280+
281+ it ( "uses fresh queue idempotency keys for repeated worker requeues" , async ( ) => {
282+ const queue = new FakeQueue ( ) ;
283+ let currentNowMs = 1_000 ;
284+ await requestConversationWork ( {
285+ conversationId : CONVERSATION_ID ,
286+ nowMs : currentNowMs ,
287+ } ) ;
288+
289+ async function runSlice ( nowMs : number ) : Promise < void > {
290+ currentNowMs = nowMs ;
291+ await expect (
292+ processConversationWork ( CONVERSATION_ID , {
293+ nowMs : ( ) => currentNowMs ,
294+ queue,
295+ run : async ( context ) => {
296+ await requestConversationWork ( {
297+ conversationId : context . conversationId ,
298+ nowMs : currentNowMs ,
299+ } ) ;
300+ return { status : "completed" } ;
301+ } ,
302+ } ) ,
303+ ) . resolves . toEqual ( { status : "pending_requeued" } ) ;
304+ }
305+
306+ await runSlice ( 2_000 ) ;
307+ await runSlice ( 63_000 ) ;
308+
309+ expect ( queue . sent . map ( ( send ) => send . idempotencyKey ) ) . toEqual ( [
310+ `pending:${ CONVERSATION_ID } :2000` ,
311+ `pending:${ CONVERSATION_ID } :63000` ,
312+ ] ) ;
270313 } ) ;
271314
272315 it ( "drains pending messages and completes the leased conversation" , async ( ) => {
@@ -353,7 +396,7 @@ describe("conversation work execution", () => {
353396 expect ( queue . sent ) . toMatchObject ( [
354397 {
355398 conversationId : CONVERSATION_ID ,
356- idempotencyKey : `heartbeat:lease:${ CONVERSATION_ID } ` ,
399+ idempotencyKey : `heartbeat:lease:${ CONVERSATION_ID } :92000 ` ,
357400 } ,
358401 ] ) ;
359402 } ) ;
@@ -371,6 +414,29 @@ describe("conversation work execution", () => {
371414 expect ( queue . sent ) . toHaveLength ( 1 ) ;
372415 } ) ;
373416
417+ it ( "uses fresh queue idempotency keys for repeated heartbeat recovery" , async ( ) => {
418+ const queue = new FakeQueue ( ) ;
419+ await appendInboundMessage ( { message : inboundMessage ( "m1" ) , nowMs : 1_000 } ) ;
420+
421+ await expect (
422+ recoverConversationWork ( {
423+ nowMs : 62_000 ,
424+ queue,
425+ } ) ,
426+ ) . resolves . toEqual ( { expiredLeaseCount : 0 , pendingCount : 1 } ) ;
427+ await expect (
428+ recoverConversationWork ( {
429+ nowMs : 122_001 ,
430+ queue,
431+ } ) ,
432+ ) . resolves . toEqual ( { expiredLeaseCount : 0 , pendingCount : 1 } ) ;
433+
434+ expect ( queue . sent . map ( ( send ) => send . idempotencyKey ) ) . toEqual ( [
435+ `heartbeat:pending:${ CONVERSATION_ID } :62000` ,
436+ `heartbeat:pending:${ CONVERSATION_ID } :122001` ,
437+ ] ) ;
438+ } ) ;
439+
374440 it ( "runs conversation work recovery from the core heartbeat" , async ( ) => {
375441 const queue = new FakeQueue ( ) ;
376442 await appendInboundMessage ( { message : inboundMessage ( "m1" ) , nowMs : 1_000 } ) ;
@@ -383,7 +449,7 @@ describe("conversation work execution", () => {
383449 expect ( queue . sent ) . toEqual ( [
384450 {
385451 conversationId : CONVERSATION_ID ,
386- idempotencyKey : `heartbeat:pending:${ CONVERSATION_ID } ` ,
452+ idempotencyKey : `heartbeat:pending:${ CONVERSATION_ID } :62000 ` ,
387453 } ,
388454 ] ) ;
389455 } ) ;
@@ -447,19 +513,22 @@ describe("conversation work execution", () => {
447513
448514 it ( "requeues instead of completing when final mailbox work remains" , async ( ) => {
449515 const queue = new FakeQueue ( ) ;
516+ let currentNowMs = 1_000 ;
450517 await appendInboundMessage ( { message : inboundMessage ( "m1" ) , nowMs : 1_000 } ) ;
451518
452519 await expect (
453520 processConversationWork ( CONVERSATION_ID , {
521+ nowMs : ( ) => currentNowMs ,
454522 queue,
455523 run : async ( context ) => {
456524 await context . drainMailbox ( async ( ) => { } ) ;
525+ currentNowMs = 2_100 ;
457526 await appendInboundMessage ( {
458527 message : inboundMessage ( "m2" , {
459528 createdAtMs : 2_000 ,
460529 receivedAtMs : 2_100 ,
461530 } ) ,
462- nowMs : 2_100 ,
531+ nowMs : currentNowMs ,
463532 } ) ;
464533 return { status : "completed" } ;
465534 } ,
@@ -468,7 +537,7 @@ describe("conversation work execution", () => {
468537 expect ( queue . sent ) . toMatchObject ( [
469538 {
470539 conversationId : CONVERSATION_ID ,
471- idempotencyKey : `pending:${ CONVERSATION_ID } ` ,
540+ idempotencyKey : `pending:${ CONVERSATION_ID } :2100 ` ,
472541 } ,
473542 ] ) ;
474543 } ) ;
@@ -499,7 +568,7 @@ describe("conversation work execution", () => {
499568 expect ( queue . sent ) . toMatchObject ( [
500569 {
501570 conversationId : CONVERSATION_ID ,
502- idempotencyKey : `yield:${ CONVERSATION_ID } ` ,
571+ idempotencyKey : `yield:${ CONVERSATION_ID } :242000 ` ,
503572 } ,
504573 ] ) ;
505574 } ) ;
0 commit comments