@@ -29,14 +29,14 @@ describe("session.message-v2.fromError — SSEStallError", () => {
2929 } )
3030
3131 test ( "detects SSE stall by timeout message without SSEStallError name" , ( ) => {
32- const error = new Error ( "SSE chunk timeout after 120000ms" )
32+ const error = new Error ( "SSE read timed out after 120000ms" )
3333
3434 const result = MessageV2 . fromError ( error , { providerID } )
3535
3636 expect ( result . name ) . toBe ( "SSEStallError" )
3737 expect ( MessageV2 . SSEStallError . isInstance ( result ) ) . toBe ( true )
3838 if ( ! MessageV2 . SSEStallError . isInstance ( result ) ) throw new Error ( "Expected SSEStallError" )
39- expect ( result . data . message ) . toBe ( "SSE chunk timeout after 120000ms" )
39+ expect ( result . data . message ) . toBe ( "SSE read timed out after 120000ms" )
4040 } )
4141
4242 test ( "detects SSE stall through two-deep cause chain" , ( ) => {
@@ -65,4 +65,44 @@ describe("session.message-v2.fromError — SSEStallError", () => {
6565 expect ( result . name ) . toBe ( "SSEStallError" )
6666 expect ( MessageV2 . APIError . isInstance ( result ) ) . toBe ( false )
6767 } )
68+
69+ test ( "hasSSEStallCause: tag-based detection still works" , ( ) => {
70+ const tagged = Object . assign ( new Error ( "anything" ) , { _tag : "SSEStallError" } )
71+ const result = MessageV2 . fromError ( tagged , { providerID } )
72+ expect ( MessageV2 . SSEStallError . isInstance ( result ) ) . toBe ( true )
73+ } )
74+
75+ test ( "hasSSEStallCause: exact wrapSSE-format message is detected through cause chain" , ( ) => {
76+ const taggedless = new Error ( "SSE read timed out after 120000ms" )
77+ const outer = new Error ( "outer" )
78+ outer . cause = taggedless
79+ const result = MessageV2 . fromError ( outer , { providerID } )
80+ expect ( MessageV2 . SSEStallError . isInstance ( result ) ) . toBe ( true )
81+ } )
82+
83+ test ( "hasSSEStallCause: message-regex fallback rejects speculative 'chunk timeout' variants" , ( ) => {
84+ const speculative = new Error ( "SSE chunk timeout" )
85+ const result = MessageV2 . fromError ( speculative , { providerID } )
86+ expect ( MessageV2 . SSEStallError . isInstance ( result ) ) . toBe ( false )
87+ } )
88+
89+ test ( "hasSSEStallCause: narrowed regex rejects shapes the old loose regex accepted" , ( ) => {
90+ // The previous regex /SSE (read|chunk) time(d out|out)/ matched all of these;
91+ // the narrowed /^SSE read timed out after \d+ms$/ rejects them all.
92+ // Any future wrapSSE format change must update the regex in lockstep.
93+ // Note: V8's `$` without the `m` flag is true end-of-string and does NOT
94+ // match before a trailing "\n", so newline-suffixed messages are rejected.
95+ const cases = [
96+ "SSE read timed out" , // missing "after Nms" suffix
97+ "SSE chunk timed out after 120000ms" , // wrong verb ("chunk" never emitted)
98+ "SSE read timeout after 120000ms" , // "timeout" not "timed out"
99+ "prefix: SSE read timed out after 120000ms" , // non-anchored prefix
100+ "SSE read timed out after 120000ms " , // trailing whitespace
101+ "SSE read timed out after 120000ms\n" , // trailing newline ($ does not match before \n)
102+ ]
103+ for ( const message of cases ) {
104+ const result = MessageV2 . fromError ( new Error ( message ) , { providerID } )
105+ expect ( MessageV2 . SSEStallError . isInstance ( result ) ) . toBe ( false )
106+ }
107+ } )
68108} )
0 commit comments