test(cli): avoid global timer patching

This commit is contained in:
Catriel Müller
2026-05-18 14:07:02 -03:00
parent 621af97e4a
commit 928c80d8fb
2 changed files with 21 additions and 47 deletions
+19 -9
View File
@@ -108,6 +108,8 @@ export namespace SessionNetwork {
{
info: Types.Mutable<Wait>
abort: AbortSignal
ctl: AbortController
delay: number
resolve: () => void
reject: (e: unknown) => void
}
@@ -217,23 +219,23 @@ export namespace SessionNetwork {
).catch(() => false)
}
async function delay(abort: AbortSignal) {
if (abort.aborted) return false
async function delay(input: { abort: AbortSignal; ms: number }) {
if (input.abort.aborted) return false
return new Promise<boolean>((resolve) => {
const timer = setTimeout(() => {
abort.removeEventListener("abort", onAbort)
input.abort.removeEventListener("abort", onAbort)
resolve(true)
}, RESUME_MS)
}, input.ms)
function onAbort() {
clearTimeout(timer)
resolve(false)
}
abort.addEventListener("abort", onAbort, { once: true })
input.abort.addEventListener("abort", onAbort, { once: true })
})
}
async function resume(input: { requestID: QuestionID; abort: AbortSignal }) {
if (!(await delay(input.abort))) return
async function resume(input: { requestID: QuestionID; abort: AbortSignal; ms: number }) {
if (!(await delay(input))) return
const s = await state()
const req = s.pending.get(input.requestID)
if (!req || !req.info.restored) return
@@ -254,9 +256,10 @@ export namespace SessionNetwork {
}
}
export async function ask(input: { sessionID: SessionID; message: string; abort: AbortSignal }) {
export async function ask(input: { sessionID: SessionID; message: string; abort: AbortSignal; resumeMs?: number }) {
const s = await state()
const id = QuestionID.ascending()
const ctl = new AbortController()
const info: Wait = {
id,
sessionID: input.sessionID,
@@ -270,6 +273,7 @@ export namespace SessionNetwork {
const promise = new Promise<void>((resolve, reject) => {
const onAbort = () => {
if (!s.pending.has(id)) return
ctl.abort()
input.abort.removeEventListener("abort", onAbort)
s.pending.delete(id)
Bus.publish(Event.Rejected, {
@@ -281,11 +285,15 @@ export namespace SessionNetwork {
s.pending.set(id, {
info,
abort: input.abort,
ctl,
delay: input.resumeMs ?? RESUME_MS,
resolve: () => {
ctl.abort()
input.abort.removeEventListener("abort", onAbort)
resolve()
},
reject: (err) => {
ctl.abort()
input.abort.removeEventListener("abort", onAbort)
reject(err)
},
@@ -322,7 +330,7 @@ export namespace SessionNetwork {
requestID: req.info.id,
time,
})
void resume({ requestID, abort: req.abort }).catch((err) => {
void resume({ requestID, abort: AbortSignal.any([req.abort, req.ctl.signal]), ms: req.delay }).catch((err) => {
log.error("auto resume failed", { err, requestID })
})
},
@@ -340,6 +348,7 @@ export namespace SessionNetwork {
log.warn("reply for unknown request", { requestID })
return
}
req.ctl.abort()
s.pending.delete(requestID)
// kilocode_change start — reconnect failed remote MCP servers after network recovery
void MCP.status()
@@ -376,6 +385,7 @@ export namespace SessionNetwork {
log.warn("reject for unknown request", { requestID })
return
}
req.ctl.abort()
s.pending.delete(requestID)
Bus.publish(Event.Rejected, {
sessionID: req.info.sessionID,
+2 -38
View File
@@ -1,42 +1,11 @@
// kilocode_change - new file
import { afterEach, describe, expect, test } from "bun:test"
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"
import { SessionID } from "../../src/session/schema"
const timer = globalThis.setTimeout
const clear = globalThis.clearTimeout
afterEach(() => {
globalThis.setTimeout = timer
globalThis.clearTimeout = clear
})
function manual() {
const state = {
next: 0,
jobs: new Map<number, TimerHandler>(),
}
globalThis.setTimeout = ((cb: TimerHandler) => {
const id = state.next + 1
state.next = id
state.jobs.set(id, cb)
return id as unknown as ReturnType<typeof setTimeout>
}) as unknown as typeof setTimeout
globalThis.clearTimeout = ((id: ReturnType<typeof setTimeout>) => {
state.jobs.delete(id as unknown as number)
}) as unknown as typeof clearTimeout
return () => {
const jobs = Array.from(state.jobs.values())
state.jobs.clear()
for (const job of jobs) {
if (typeof job === "function") job()
}
}
}
describe("session.network", () => {
test("detects common network disconnect codes", () => {
expect(SessionNetwork.disconnected({ code: "ECONNREFUSED" })).toBe(true)
@@ -110,18 +79,17 @@ describe("session.network", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const run = manual()
const { promise } = await SessionNetwork.ask({
sessionID: SessionID.make("ses_test"),
message: "Connection refused",
abort: new AbortController().signal,
resumeMs: 0,
})
const pending = await SessionNetwork.list()
expect(pending).toHaveLength(1)
const req = pending[0]!
await SessionNetwork.restore({ requestID: req.id })
expect((await SessionNetwork.list())[0]?.restored).toBe(true)
run()
await expect(promise).resolves.toBeUndefined()
expect(await SessionNetwork.list()).toHaveLength(0)
},
@@ -133,7 +101,6 @@ describe("session.network", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const run = manual()
const { promise } = await SessionNetwork.ask({
sessionID: SessionID.make("ses_test"),
message: "Connection timed out",
@@ -143,7 +110,6 @@ describe("session.network", () => {
await SessionNetwork.restore({ requestID: req.id })
await SessionNetwork.reject({ requestID: req.id })
await expect(promise).rejects.toBeInstanceOf(SessionNetwork.RejectedError)
run()
expect(await SessionNetwork.list()).toHaveLength(0)
},
})
@@ -154,7 +120,6 @@ describe("session.network", () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
const run = manual()
const abort = new AbortController()
const { promise } = await SessionNetwork.ask({
sessionID: SessionID.make("ses_test"),
@@ -165,7 +130,6 @@ describe("session.network", () => {
await SessionNetwork.restore({ requestID: req.id })
abort.abort()
await expect(promise).rejects.toBeInstanceOf(DOMException)
run()
expect(await SessionNetwork.list()).toHaveLength(0)
},
})