Browse Source

Merge pull request #967 from MyGO-Mujica/fix/foreground-fallback-cannot-connect

fix(foreground-fallback): detect cannot-connect transport errors
Alvin 1 week ago
parent
commit
042e091d47

+ 47 - 0
src/hooks/foreground-fallback/index.test.ts

@@ -229,6 +229,18 @@ describe('isFailoverError', () => {
     ).toBe(true);
   });
 
+  test('returns true for "cannot connect to API" transport errors', () => {
+    expect(isRetryableError('Cannot connect to API')).toBe(true);
+    expect(isRetryableError('stream error: Cannot connect to API')).toBe(true);
+    expect(
+      isRetryableError({ message: 'stream error: Cannot connect to API' }),
+    ).toBe(true);
+  });
+
+  test('returns false for non-API connection errors', () => {
+    expect(isRetryableError('Cannot connect to database')).toBe(false);
+  });
+
   test('returns false for permanent channel-not-found errors', () => {
     expect(
       isRetryableError({
@@ -386,6 +398,41 @@ describe('ForegroundFallbackManager session.error', () => {
     expect(call[0].model.modelID).toBe('gpt-4o');
   });
 
+  test('triggers fallback on cannot-connect session.error', async () => {
+    await mgr.handleEvent({
+      type: 'message.updated',
+      properties: {
+        info: {
+          sessionID: 'sess-1',
+          providerID: 'anthropic',
+          modelID: 'claude-opus-4-5',
+          role: 'assistant',
+        },
+      },
+    });
+
+    await mgr.handleEvent({
+      type: 'session.error',
+      properties: {
+        sessionID: 'sess-1',
+        error: {
+          message: 'stream error: Cannot connect to API',
+        },
+      },
+    });
+
+    expect(mocks.abort).not.toHaveBeenCalled();
+    expect(mocks.promptAsync).toHaveBeenCalledTimes(1);
+
+    const call = mocks.promptAsync.mock.calls[0] as [
+      {
+        model: { providerID: string; modelID: string };
+      },
+    ];
+    expect(call[0].model.providerID).toBe('openai');
+    expect(call[0].model.modelID).toBe('gpt-4o');
+  });
+
   test('marks the replayed user prompt as an internal initiator', async () => {
     await mgr.handleEvent({
       type: 'message.updated',

+ 4 - 0
src/hooks/foreground-fallback/index.ts

@@ -78,6 +78,10 @@ const TRANSPORT_MESSAGE_PATTERNS = [
   /^request timeout$/i,
   /^connect ECONNREFUSED\b/i,
   /^getaddrinfo ENOTFOUND\b/i,
+  // Provider SDKs also report connection failures with natural-language
+  // messages (e.g. "stream error: Cannot connect to API") that carry no
+  // transport code. Match the narrow phrase only.
+  /cannot connect to api/i,
 ];
 const PROVIDER_OUTAGE_PATTERNS = [
   /\binternal server error\b/i,