@@ -132,3 +132,111 @@ describe("redis-only fallback gate is org-aware", () => {
132132 expect ( h . delegateTouched ) . not . toContain ( "findSnapshotCompletedWaitpointIds" ) ;
133133 } ) ;
134134} ) ;
135+
136+ // The other half of the gate: a Redis MISS (not an error). At redis-only Postgres holds nothing,
137+ // so a miss must THROW rather than delegate into an empty Postgres. Below redis-only a miss still
138+ // falls back, because Postgres legitimately holds the pre-cutover data.
139+ function missHarness ( opts : {
140+ globalMode : SnapshotStoreMode ;
141+ readModeFor ?: ( runId : string , environmentId ?: string ) => SnapshotStoreMode | undefined ;
142+ anyOrgRedisOnly ?: ( ) => boolean ;
143+ anyOrgReadEnabled ?: ( ) => boolean ;
144+ } ) {
145+ // Every Redis read reports a MISS: null for the single/by-id reads, a miss window, an absent
146+ // waitpoint set.
147+ const redis = new Proxy ( { } as RedisSnapshotStore , {
148+ get : ( _t , prop ) => {
149+ if ( prop === "getSinceCreatedAt" ) return ( ) => Promise . resolve ( { kind : "miss" } ) ;
150+ if ( prop === "getSnapshotWaitpointIds" )
151+ return ( ) => Promise . resolve ( { present : false , distinctIds : [ ] , order : [ ] } ) ;
152+ return ( ) => Promise . resolve ( null ) ;
153+ } ,
154+ } ) ;
155+
156+ const delegateTouched : string [ ] = [ ] ;
157+ const delegate = new Proxy ( { } as Record < string , unknown > , {
158+ get : ( _t , prop ) => {
159+ return ( ...__ : unknown [ ] ) => {
160+ delegateTouched . push ( String ( prop ) ) ;
161+ return Promise . resolve ( { id : "pg_fallback" } ) ;
162+ } ;
163+ } ,
164+ } ) as unknown as RunStore ;
165+
166+ const modeResolver : SnapshotStoreModeResolver = {
167+ resolve : ( ) => opts . globalMode ,
168+ ...( opts . readModeFor && {
169+ readModeFor : opts . readModeFor as SnapshotStoreModeResolver [ "readModeFor" ] ,
170+ } ) ,
171+ ...( opts . anyOrgRedisOnly && { anyOrgRedisOnly : opts . anyOrgRedisOnly } ) ,
172+ ...( opts . anyOrgReadEnabled && { anyOrgReadEnabled : opts . anyOrgReadEnabled } ) ,
173+ } ;
174+
175+ const decorated = new TaskRunExecutionSnapshotStore ( delegate , {
176+ store : redis ,
177+ mode : opts . globalMode ,
178+ modeResolver,
179+ readPercent : 100 ,
180+ } ) ;
181+
182+ return { decorated, delegateTouched } ;
183+ }
184+
185+ describe ( "redis-only throws on a miss instead of serving empty Postgres" , ( ) => {
186+ it ( "findLatestExecutionSnapshot throws at global redis-only, never delegating" , async ( ) => {
187+ const h = missHarness ( { globalMode : "redis-only" } ) ;
188+
189+ await expect (
190+ h . decorated . findLatestExecutionSnapshot ( "run_x" , undefined , "env_x" )
191+ ) . rejects . toThrow ( / r e d i s - o n l y / ) ;
192+ expect ( h . delegateTouched ) . not . toContain ( "findLatestExecutionSnapshot" ) ;
193+ } ) ;
194+
195+ it ( "findLatestExecutionSnapshot falls back at redis-read (Postgres holds pre-cutover data)" , async ( ) => {
196+ const h = missHarness ( { globalMode : "redis-read" } ) ;
197+
198+ const result = await h . decorated . findLatestExecutionSnapshot ( "run_x" , undefined , "env_x" ) ;
199+ expect ( result ) . toEqual ( { id : "pg_fallback" } ) ;
200+ expect ( h . delegateTouched ) . toContain ( "findLatestExecutionSnapshot" ) ;
201+ } ) ;
202+
203+ it ( "throws for a run whose org resolves to redis-only, falls back for a redis-read org" , async ( ) => {
204+ const h = missHarness ( {
205+ globalMode : "redis-read" ,
206+ anyOrgReadEnabled : ( ) => true ,
207+ readModeFor : ( runId ) => ( runId === "run_ro" ? "redis-only" : "redis-read" ) ,
208+ } ) ;
209+
210+ await expect (
211+ h . decorated . findLatestExecutionSnapshot ( "run_ro" , undefined , "env_a" )
212+ ) . rejects . toThrow ( / r e d i s - o n l y / ) ;
213+ expect ( h . delegateTouched ) . not . toContain ( "findLatestExecutionSnapshot" ) ;
214+
215+ const result = await h . decorated . findLatestExecutionSnapshot ( "run_rr" , undefined , "env_b" ) ;
216+ expect ( result ) . toEqual ( { id : "pg_fallback" } ) ;
217+ expect ( h . delegateTouched ) . toContain ( "findLatestExecutionSnapshot" ) ;
218+ } ) ;
219+
220+ it ( "findManyExecutionSnapshots throws on a miss window at redis-only" , async ( ) => {
221+ const h = missHarness ( { globalMode : "redis-only" } ) ;
222+
223+ await expect (
224+ h . decorated . findManyExecutionSnapshots ( {
225+ where : { runId : "run_x" , isValid : true , createdAt : { gt : new Date ( 0 ) } } ,
226+ include : { checkpoint : true } ,
227+ orderBy : { createdAt : "desc" } ,
228+ take : 10 ,
229+ } as never )
230+ ) . rejects . toThrow ( / r e d i s - o n l y / ) ;
231+ expect ( h . delegateTouched ) . not . toContain ( "findManyExecutionSnapshots" ) ;
232+ } ) ;
233+
234+ it ( "findSnapshotCompletedWaitpointIds throws on an absent set at redis-only" , async ( ) => {
235+ const h = missHarness ( { globalMode : "redis-only" } ) ;
236+
237+ await expect (
238+ h . decorated . findSnapshotCompletedWaitpointIds ( "snap_1" , undefined , "run_x" )
239+ ) . rejects . toThrow ( / r e d i s - o n l y / ) ;
240+ expect ( h . delegateTouched ) . not . toContain ( "findSnapshotCompletedWaitpointIds" ) ;
241+ } ) ;
242+ } ) ;
0 commit comments