fix(coding-agent): fixed python kernel startup hangs (#548)

* fix(coding-agent): fixed python kernel startup hangs

* fix(coding-agent): fixed startup timeout regressions

* fix(coding-agent): preserved startup cancellation typing
This commit is contained in:
zamo
2026-03-27 11:16:26 +01:00
committed by GitHub
parent c7989526c1
commit 0c4a7c4f89
11 changed files with 902 additions and 157 deletions
@@ -186,6 +186,138 @@ describe("PythonKernel gateway lifecycle", () => {
expect(kernel.isAlive()).toBe(false);
});
it("aborts stalled startup after websocket connect and cleans up the kernel", async () => {
vi.spyOn(gatewayCoordinator, "acquireSharedGateway").mockResolvedValue({
url: "http://127.0.0.1:9999",
isShared: true,
});
let executeCallCount = 0;
const preludeStarted = Promise.withResolvers<void>();
vi.spyOn(PythonKernel.prototype, "execute").mockImplementation(async (_code, options) => {
executeCallCount += 1;
if (executeCallCount === 1) {
return { status: "ok", cancelled: false, timedOut: false, stdinRequested: false };
}
preludeStarted.resolve();
return await new Promise((_, reject) => {
const onAbort = () => {
const reason = options?.signal?.reason;
reject(reason instanceof Error ? reason : new Error("Python kernel startup aborted"));
};
if (options?.signal?.aborted) {
onAbort();
return;
}
options?.signal?.addEventListener("abort", onAbort, { once: true });
});
});
using _hook = hookFetch((input, init) => {
const url = String(input);
env.fetchCalls.push({ url, init });
if (url.endsWith("/api/kernels") && init?.method === "POST") {
return createResponse({ ok: true, json: { id: "kernel-stalled" } }) as unknown as Response;
}
return createResponse({ ok: true }) as unknown as Response;
});
const abortController = new AbortController();
const startPromise = PythonKernel.start({ cwd: tempDir.path(), signal: abortController.signal });
await preludeStarted.promise;
abortController.abort(new Error("cancel startup"));
const pending = Symbol("pending");
const settled = await Promise.race([
startPromise.then(
() => "resolved",
error => error,
),
Bun.sleep(50).then(() => pending),
]);
expect(settled).toBeInstanceOf(Error);
expect(settled).not.toBe(pending);
expect((settled as Error).message).toContain("cancel startup");
await expect(startPromise).rejects.toThrow("cancel startup");
expect(
env.fetchCalls.some(
call => call.url.endsWith("/api/kernels/kernel-stalled") && call.init?.method === "DELETE",
),
).toBe(true);
expect(FakeWebSocket.instances.at(-1)?.readyState).toBe(FakeWebSocket.CLOSED);
});
it("preserves timeout classification when startup environment initialization is cancelled", async () => {
vi.spyOn(gatewayCoordinator, "acquireSharedGateway").mockResolvedValue({
url: "http://127.0.0.1:9999",
isShared: true,
});
vi.spyOn(PythonKernel.prototype, "execute").mockResolvedValue({
status: "ok",
cancelled: true,
timedOut: true,
stdinRequested: false,
});
using _hook = hookFetch((input, init) => {
const url = String(input);
env.fetchCalls.push({ url, init });
if (url.endsWith("/api/kernels") && init?.method === "POST") {
return createResponse({ ok: true, json: { id: "kernel-init-timeout" } }) as unknown as Response;
}
return createResponse({ ok: true }) as unknown as Response;
});
await expect(PythonKernel.start({ cwd: tempDir.path() })).rejects.toMatchObject({
name: "TimeoutError",
message: "Failed to initialize Python kernel environment",
});
expect(
env.fetchCalls.some(
call => call.url.endsWith("/api/kernels/kernel-init-timeout") && call.init?.method === "DELETE",
),
).toBe(true);
expect(FakeWebSocket.instances.at(-1)?.readyState).toBe(FakeWebSocket.CLOSED);
});
it("preserves timeout classification when startup prelude execution is cancelled", async () => {
vi.spyOn(gatewayCoordinator, "acquireSharedGateway").mockResolvedValue({
url: "http://127.0.0.1:9999",
isShared: true,
});
let executeCallCount = 0;
vi.spyOn(PythonKernel.prototype, "execute").mockImplementation(async () => {
executeCallCount += 1;
if (executeCallCount === 1) {
return { status: "ok", cancelled: false, timedOut: false, stdinRequested: false };
}
return { status: "ok", cancelled: true, timedOut: true, stdinRequested: false };
});
using _hook = hookFetch((input, init) => {
const url = String(input);
env.fetchCalls.push({ url, init });
if (url.endsWith("/api/kernels") && init?.method === "POST") {
return createResponse({ ok: true, json: { id: "kernel-prelude-timeout" } }) as unknown as Response;
}
return createResponse({ ok: true }) as unknown as Response;
});
await expect(PythonKernel.start({ cwd: tempDir.path() })).rejects.toMatchObject({
name: "TimeoutError",
message: "Failed to initialize Python kernel prelude",
});
expect(
env.fetchCalls.some(
call => call.url.endsWith("/api/kernels/kernel-prelude-timeout") && call.init?.method === "DELETE",
),
).toBe(true);
expect(FakeWebSocket.instances.at(-1)?.readyState).toBe(FakeWebSocket.CLOSED);
});
it("throws when shared gateway kernel creation never succeeds", async () => {
using _runtime = stubKernelRuntime();
vi.spyOn(gatewayCoordinator, "acquireSharedGateway").mockResolvedValue({