The SDK client fetch wrapper now applies a 30s timeout to non-streaming reads. Without it, a socket that neither resolves nor rejects keeps the directory bootstrap concurrency slot busy forever and the UI stays on "loading sessions". Long-lived streams (POST prompts, the /event SSE) are explicitly excluded so they are not cut off mid-flight. The normalized "request timed out" error is added to the retry allowlist alongside undici's "terminated" (the exact failure observed in #2470 when undici tears down a half-open upstream connection); both are transient while the managed OpenCode process restarts. Caller- initiated aborts keep their original error shape so a user-cancelled request is not retried. Tests cover: GET timeout fires after the bound, POST is not timed out, /event SSE is not timed out, caller abort wins, AbortError is not retried, and the SDK normalized error is retried 3x.
39 lines
1.1 KiB
TypeScript
39 lines
1.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
|
|
import { retry } from './retry';
|
|
|
|
describe('retry transient classification (#2470)', () => {
|
|
test("'terminated' (undici half-open socket teardown) is retried", async () => {
|
|
let attempts = 0;
|
|
await expect(
|
|
retry(async () => {
|
|
attempts += 1;
|
|
throw new TypeError('terminated');
|
|
}),
|
|
).rejects.toThrow('terminated');
|
|
expect(attempts).toBe(3);
|
|
});
|
|
|
|
test("normalized 'request timed out' (SDK read timeout) is retried", async () => {
|
|
let attempts = 0;
|
|
await expect(
|
|
retry(async () => {
|
|
attempts += 1;
|
|
throw new Error('OpenCode request timed out after 30000ms');
|
|
}),
|
|
).rejects.toThrow('request timed out');
|
|
expect(attempts).toBe(3);
|
|
});
|
|
|
|
test('caller-initiated abort (AbortError) is NOT retried', async () => {
|
|
let attempts = 0;
|
|
await expect(
|
|
retry(async () => {
|
|
attempts += 1;
|
|
throw new DOMException('Aborted', 'AbortError');
|
|
}),
|
|
).rejects.toThrow('Aborted');
|
|
expect(attempts).toBe(1);
|
|
});
|
|
});
|