@@ -1017,6 +1017,78 @@ describe("CloudTaskService", () => {
10171017 expect ( mockStreamTokenFetch . mock . calls . length ) . toBe ( 1 ) ;
10181018 } ) ;
10191019
1020+ it ( "a transient stream_token failure retries resolution instead of pinning to Django" , async ( ) => {
1021+ vi . useFakeTimers ( ) ;
1022+
1023+ // The endpoint is momentarily down (503): unlike a 404, this must not cache a Django fallback.
1024+ // The next reconnect re-resolves and the watch upgrades to the durable proxy leg.
1025+ mockStreamTokenFetch
1026+ . mockImplementationOnce ( ( ) =>
1027+ Promise . resolve ( createJsonResponse ( { detail : "unavailable" } , 503 ) ) ,
1028+ )
1029+ . mockImplementation ( ( ) =>
1030+ Promise . resolve (
1031+ createJsonResponse ( {
1032+ token : "fresh-token" ,
1033+ stream_base_url : "https://proxy.example" ,
1034+ } ) ,
1035+ ) ,
1036+ ) ;
1037+
1038+ mockNetFetch . mockImplementation ( ( input : string | Request ) => {
1039+ const url = typeof input === "string" ? input : input . url ;
1040+ if ( url . includes ( "/session_logs/" ) ) {
1041+ return Promise . resolve (
1042+ createJsonResponse ( [ ] , 200 , { "X-Has-More" : "false" } ) ,
1043+ ) ;
1044+ }
1045+ return Promise . resolve (
1046+ createJsonResponse ( {
1047+ id : "run-1" ,
1048+ status : "in_progress" ,
1049+ stage : null ,
1050+ output : null ,
1051+ error_message : null ,
1052+ branch : "main" ,
1053+ updated_at : "2026-01-01T00:00:00Z" ,
1054+ } ) ,
1055+ ) ;
1056+ } ) ;
1057+
1058+ // Django leg (transient round) just EOFs to force a reconnect; once the proxy resolves it
1059+ // emits stream-end so the watch completes, proving durable streaming engaged after the retry.
1060+ const usedProxyLeg = ( input : string | Request ) : boolean => {
1061+ const url = typeof input === "string" ? input : input . url ;
1062+ return url . includes ( "proxy.example" ) ;
1063+ } ;
1064+ mockStreamFetch . mockImplementation ( ( input : string | Request ) =>
1065+ Promise . resolve (
1066+ usedProxyLeg ( input )
1067+ ? createSseResponse ( "event: stream-end\ndata: {}\n\n" )
1068+ : createSseResponse ( "" ) ,
1069+ ) ,
1070+ ) ;
1071+
1072+ service . watch ( {
1073+ taskId : "task-1" ,
1074+ runId : "run-1" ,
1075+ apiHost : "https://app.example.com" ,
1076+ teamId : 2 ,
1077+ } ) ;
1078+
1079+ const hasWatcher = ( ) : boolean =>
1080+ ( service as unknown as { watchers : Map < string , unknown > } ) . watchers . has (
1081+ "task-1:run-1" ,
1082+ ) ;
1083+ await waitFor ( ( ) => ! hasWatcher ( ) , 10_000 ) ;
1084+
1085+ // The 503 was not cached: resolution retried and the stream switched to the durable proxy leg.
1086+ expect ( mockStreamTokenFetch . mock . calls . length ) . toBeGreaterThanOrEqual ( 2 ) ;
1087+ expect (
1088+ mockStreamFetch . mock . calls . some ( ( [ input ] ) => usedProxyLeg ( input ) ) ,
1089+ ) . toBe ( true ) ;
1090+ } ) ;
1091+
10201092 it ( "proxy 401 re-resolves the read target and resumes with a fresh token" , async ( ) => {
10211093 vi . useFakeTimers ( ) ;
10221094
0 commit comments