2b480f7da1
fixed retained-kernel restart and owner cleanup edge cases during recovery and disposal tracked async user_python hooks during disposal-sensitive execution paths and hardened startup warmup tracking strengthened cleanup and kernel lifecycle regressions to remove deadlocks, false positives, and timing flakes
105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
import { disposeAllKernelSessions, executePython } from "@oh-my-pi/pi-coding-agent/ipy/executor";
|
|
import * as pythonKernel from "@oh-my-pi/pi-coding-agent/ipy/kernel";
|
|
|
|
class FakeKernel {
|
|
executeCalls = 0;
|
|
shutdownCalls = 0;
|
|
alive = true;
|
|
constructor(private readonly shouldThrow: boolean = false) {}
|
|
|
|
isAlive(): boolean {
|
|
return this.alive;
|
|
}
|
|
|
|
async execute(): Promise<{ status: "ok"; cancelled: false; timedOut: false; stdinRequested: false }> {
|
|
this.executeCalls += 1;
|
|
if (this.shouldThrow) {
|
|
this.alive = false;
|
|
throw new Error("kernel crashed");
|
|
}
|
|
return { status: "ok", cancelled: false, timedOut: false, stdinRequested: false };
|
|
}
|
|
|
|
async ping(): Promise<boolean> {
|
|
return this.alive;
|
|
}
|
|
|
|
async shutdown(): Promise<pythonKernel.KernelShutdownResult> {
|
|
this.shutdownCalls += 1;
|
|
this.alive = false;
|
|
return { confirmed: true };
|
|
}
|
|
}
|
|
|
|
describe("executePython session lifecycle", () => {
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
await disposeAllKernelSessions();
|
|
});
|
|
|
|
it("restarts session when kernel is not alive", async () => {
|
|
const kernel1 = new FakeKernel();
|
|
kernel1.alive = false;
|
|
const kernel2 = new FakeKernel();
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi
|
|
.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValueOnce(kernel1 as unknown as pythonKernel.PythonKernel)
|
|
.mockResolvedValueOnce(kernel2 as unknown as pythonKernel.PythonKernel);
|
|
|
|
await executePython("print('hi')", { cwd: "/tmp", sessionId: "session-1", kernelMode: "session" });
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
expect(kernel1.executeCalls).toBe(0);
|
|
expect(kernel1.shutdownCalls).toBe(1);
|
|
expect(kernel2.executeCalls).toBe(1);
|
|
});
|
|
|
|
it("restarts after an execution failure when kernel is dead", async () => {
|
|
const kernel1 = new FakeKernel(true);
|
|
const kernel2 = new FakeKernel();
|
|
const starts = [kernel1, kernel2];
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi.spyOn(pythonKernel.PythonKernel, "start").mockImplementation(async () => {
|
|
const next = starts.shift();
|
|
if (!next) {
|
|
throw new Error("No kernel available");
|
|
}
|
|
return next as unknown as pythonKernel.PythonKernel;
|
|
});
|
|
|
|
await executePython("raise", { cwd: "/tmp", sessionId: "session-2", kernelMode: "session" });
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
expect(kernel1.executeCalls).toBe(1);
|
|
expect(kernel2.executeCalls).toBe(1);
|
|
});
|
|
|
|
it("resets existing session when requested", async () => {
|
|
const kernel1 = new FakeKernel();
|
|
const kernel2 = new FakeKernel();
|
|
const starts = [kernel1, kernel2];
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi.spyOn(pythonKernel.PythonKernel, "start").mockImplementation(async () => {
|
|
const next = starts.shift();
|
|
if (!next) {
|
|
throw new Error("No kernel available");
|
|
}
|
|
return next as unknown as pythonKernel.PythonKernel;
|
|
});
|
|
|
|
await executePython("print('one')", { cwd: "/tmp", sessionId: "session-3", kernelMode: "session" });
|
|
await executePython("print('two')", {
|
|
cwd: "/tmp",
|
|
sessionId: "session-3",
|
|
kernelMode: "session",
|
|
reset: true,
|
|
});
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
expect(kernel1.shutdownCalls).toBe(1);
|
|
expect(kernel2.executeCalls).toBe(1);
|
|
});
|
|
});
|