Files
Kilo-Org_kilocode/packages/opencode/test/session/network.test.ts
T
Alex Alecu 6ad2e7a393 fix(cli): add 2-minute default request timeout for provider requests
Previously, provider HTTP requests had no application-level timeout
unless explicitly configured. Combined with Bun's fetch timeout being
disabled, requests could hang indefinitely when the network was down,
waiting for the OS TCP stack to give up.

Add REQUEST_TIMEOUT_MS (120s) as the default timeout applied via
AbortSignal.timeout() in the provider fetch wrapper. Recognize
TimeoutError as a network disconnection so it enters the offline
prompt flow.
2026-04-06 09:19:06 +03:00

136 lines
4.9 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { Bus } from "../../src/bus"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { SessionNetwork } from "../../src/session/network"
describe("session.network", () => {
test("detects common network disconnect codes", () => {
expect(SessionNetwork.disconnected({ code: "ECONNREFUSED" })).toBe(true)
expect(SessionNetwork.disconnected({ code: "ENOTFOUND" })).toBe(true)
expect(SessionNetwork.disconnected({ code: "EAI_AGAIN" })).toBe(true)
expect(SessionNetwork.disconnected({ code: "ENOENT" })).toBe(false)
})
test("detects provider unable to connect message", () => {
const err = new Error("Unable to connect. Is the computer able to access the url?")
expect(SessionNetwork.disconnected(err)).toBe(true)
expect(SessionNetwork.message(err)).toBe("Unable to connect. Is the computer able to access the url?")
})
test("detects wrapped network cause", () => {
const err = new Error("top") as Error & { cause?: unknown }
err.cause = { code: "ETIMEDOUT" }
expect(SessionNetwork.disconnected(err)).toBe(true)
expect(SessionNetwork.message(err)).toBe("Connection timed out")
})
test("detects TimeoutError as disconnected", () => {
const err = new DOMException("The operation was aborted due to timeout", "TimeoutError")
expect(SessionNetwork.disconnected(err)).toBe(true)
expect(SessionNetwork.message(err)).toBe("Request timed out")
})
test("detects wrapped TimeoutError in cause chain", () => {
const timeout = new DOMException("signal timed out", "TimeoutError")
const err = new Error("request failed", { cause: timeout })
expect(SessionNetwork.disconnected(err)).toBe(true)
expect(SessionNetwork.message(err)).toBe("Request timed out")
})
test("reply resolves pending request", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ask = SessionNetwork.ask({
sessionID: "ses_test",
message: "Connection refused",
abort: new AbortController().signal,
})
const pending = await SessionNetwork.list()
expect(pending).toHaveLength(1)
const req = pending[0]!
await SessionNetwork.reply({ requestID: req.id })
await expect(ask).resolves.toBeUndefined()
},
})
})
test("reject rejects pending request", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ask = SessionNetwork.ask({
sessionID: "ses_test",
message: "Connection timed out",
abort: new AbortController().signal,
})
const pending = await SessionNetwork.list()
expect(pending).toHaveLength(1)
const req = pending[0]!
await SessionNetwork.reject({ requestID: req.id })
await expect(ask).rejects.toBeInstanceOf(SessionNetwork.RejectedError)
},
})
})
test("aborted signal rejects without publishing asked", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const abort = new AbortController()
const seen: string[] = []
const offAsked = Bus.subscribe(SessionNetwork.Event.Asked, () => seen.push("asked"))
const offRejected = Bus.subscribe(SessionNetwork.Event.Rejected, () => seen.push("rejected"))
abort.abort()
try {
const err = await SessionNetwork.ask({
sessionID: "ses_test",
message: "Connection timed out",
abort: abort.signal,
}).catch((err) => err)
expect(err).toBeInstanceOf(DOMException)
expect(err.name).toBe("AbortError")
expect(await SessionNetwork.list()).toHaveLength(0)
expect(seen).toStrictEqual(["rejected"])
} finally {
offAsked()
offRejected()
}
},
})
})
test("abort during pending ask rejects with AbortError and cleans up", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const abort = new AbortController()
const pending = SessionNetwork.ask({
sessionID: "ses_test",
message: "Connection refused",
abort: abort.signal,
})
// wait for the ask to register
const list = await SessionNetwork.list()
expect(list).toHaveLength(1)
// abort while waiting
abort.abort()
const err = await pending.catch((e: unknown) => e)
expect(err).toBeInstanceOf(DOMException)
expect((err as DOMException).name).toBe("AbortError")
// pending entry should be cleaned up
expect(await SessionNetwork.list()).toHaveLength(0)
},
})
})
})