mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
test(cli): avoid global timer patching
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user