@@ -12,7 +12,7 @@ const { redisUrl, inheritedRedisUrl } = await vi.hoisted(async () => {
1212 const url = readTestRedisUrl ( )
1313 const inheritedRedisUrl = process . env . REDIS_URL
1414 /** The real Redis module reads this at import. */
15- process . env . REDIS_URL = url
15+ if ( url ) process . env . REDIS_URL = url
1616 return { redisUrl : url , inheritedRedisUrl }
1717} )
1818
@@ -31,7 +31,7 @@ import { copilotChats, copilotRuns, permissions, user, workspace } from '@sim/db
3131import { generateId } from '@sim/utils/id'
3232import { eq , inArray } from 'drizzle-orm'
3333import { NextRequest } from 'next/server'
34- import { closeRedisConnection } from '@/lib/core/config/redis'
34+ import { closeRedisConnection , getRedisClient } from '@/lib/core/config/redis'
3535import {
3636 createRunSegment ,
3737 getLatestRunForStream ,
@@ -40,14 +40,15 @@ import {
4040import { chatPubSub } from '@/lib/mothership/chat-status'
4141import {
4242 acquirePendingChatStream ,
43+ getLocalChatStreamLease ,
4344 releasePendingChatStream ,
4445} from '@/lib/mothership/request/session/abort'
4546import { POST as wakeRoute } from '@/app/api/mothership/wake/route'
4647
4748afterAll ( async ( ) => {
4849 chatPubSub ?. dispose ( )
4950 await closeRedisConnection ( )
50- if ( inheritedRedisUrl === undefined ) process . env . REDIS_URL = undefined
51+ if ( inheritedRedisUrl === undefined ) Reflect . deleteProperty ( process . env , 'REDIS_URL' )
5152 else process . env . REDIS_URL = inheritedRedisUrl
5253} )
5354
@@ -174,4 +175,36 @@ describe.runIf(Boolean(redisUrl))('task wake retries', () => {
174175 expect ( await acquirePendingChatStream ( chatId , nextTurn , 0 ) ) . toBe ( true )
175176 await releasePendingChatStream ( chatId , nextTurn )
176177 } )
178+
179+ it ( "keeps a retry's chat lock when an earlier wake's slow lookup fails after its own lock expired" , async ( ) => {
180+ const chatId = await idleChat ( )
181+ const runId = generateId ( )
182+ let lookupStarted ! : ( ) => void
183+ const started = new Promise < void > ( ( resolve ) => {
184+ lookupStarted = resolve
185+ } )
186+ let failLookup ! : ( ) => void
187+ const failed = new Promise < void > ( ( resolve ) => {
188+ failLookup = resolve
189+ } )
190+ vi . mocked ( getLatestRunForStream ) . mockImplementationOnce ( async ( ) => {
191+ lookupStarted ( )
192+ await failed
193+ throw new Error ( 'statement timeout' )
194+ } )
195+
196+ const slowWake = wake ( chatId , runId )
197+ await started
198+ /** The first wake's lock outlives its TTL while the lookup hangs. */
199+ const firstLease = getLocalChatStreamLease ( chatId , runId )
200+ await getRedisClient ( ) ?. del ( firstLease ?. key ?? '' )
201+ expect ( ( await wake ( chatId , runId ) ) . status ) . toBe ( 202 )
202+
203+ failLookup ( )
204+ expect ( ( await slowWake ) . status ) . toBe ( 500 )
205+
206+ const nextTurn = generateId ( )
207+ expect ( await acquirePendingChatStream ( chatId , nextTurn , 0 ) ) . toBe ( false )
208+ await releasePendingChatStream ( chatId , runId )
209+ } , 15_000 )
177210} )
0 commit comments