166 lines
6.9 KiB
TypeScript
166 lines
6.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
import { disposeAllKernelSessions, executePython } from "@oh-my-pi/pi-coding-agent/eval/py/executor";
|
|
import type { KernelExecuteResult } from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
|
|
import * as pythonKernel from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
|
|
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
|
class FakeKernel {
|
|
execute = vi.fn(async () => this.result);
|
|
shutdown = vi.fn(async () => {
|
|
return { confirmed: true };
|
|
});
|
|
ping = vi.fn(async () => true);
|
|
alive = true;
|
|
|
|
constructor(private readonly result: KernelExecuteResult) {}
|
|
|
|
isAlive(): boolean {
|
|
return this.alive;
|
|
}
|
|
}
|
|
|
|
const OK_RESULT: KernelExecuteResult = {
|
|
status: "ok",
|
|
cancelled: false,
|
|
timedOut: false,
|
|
stdinRequested: false,
|
|
};
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
await disposeAllKernelSessions();
|
|
});
|
|
|
|
describe("executePython lifecycle", () => {
|
|
it("starts and shuts down per-call kernels", async () => {
|
|
const kernel = new FakeKernel(OK_RESULT);
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi
|
|
.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValue(kernel as unknown as pythonKernel.PythonKernel);
|
|
|
|
await executePython("print('hi')", { kernelMode: "per-call", cwd: getProjectDir() });
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(1);
|
|
expect(kernel.execute).toHaveBeenCalledTimes(1);
|
|
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("reuses session kernels until reset", async () => {
|
|
const kernel = new FakeKernel(OK_RESULT);
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi
|
|
.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValue(kernel as unknown as pythonKernel.PythonKernel);
|
|
|
|
await executePython("1 + 1", { kernelMode: "session", sessionId: "test-session", cwd: getProjectDir() });
|
|
await executePython("2 + 2", { kernelMode: "session", sessionId: "test-session", cwd: getProjectDir() });
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(1);
|
|
expect(kernel.execute).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("resets session kernels when requested", async () => {
|
|
const kernel = new FakeKernel(OK_RESULT);
|
|
const kernelNext = new FakeKernel(OK_RESULT);
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi
|
|
.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValueOnce(kernel as unknown as pythonKernel.PythonKernel)
|
|
.mockResolvedValueOnce(kernelNext as unknown as pythonKernel.PythonKernel);
|
|
|
|
await executePython("1 + 1", { kernelMode: "session", sessionId: "reset-session", cwd: getProjectDir() });
|
|
await executePython("2 + 2", {
|
|
kernelMode: "session",
|
|
sessionId: "reset-session",
|
|
reset: true,
|
|
cwd: getProjectDir(),
|
|
});
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
|
|
expect(kernelNext.execute).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("restarts session kernels when they are dead", async () => {
|
|
const kernel = new FakeKernel(OK_RESULT);
|
|
const kernelNext = new FakeKernel(OK_RESULT);
|
|
kernel.alive = false;
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi
|
|
.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValueOnce(kernel as unknown as pythonKernel.PythonKernel)
|
|
.mockResolvedValueOnce(kernelNext as unknown as pythonKernel.PythonKernel);
|
|
|
|
await executePython("1 + 1", { kernelMode: "session", sessionId: "dead-session", cwd: getProjectDir() });
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
|
|
expect(kernel.execute).toHaveBeenCalledTimes(0);
|
|
expect(kernelNext.execute).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("restarts after an execution failure when kernel is dead", async () => {
|
|
const kernel = new FakeKernel(OK_RESULT);
|
|
kernel.execute.mockImplementation(async () => {
|
|
kernel.alive = false;
|
|
throw new Error("kernel crashed");
|
|
});
|
|
const kernelNext = new FakeKernel(OK_RESULT);
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi
|
|
.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValueOnce(kernel as unknown as pythonKernel.PythonKernel)
|
|
.mockResolvedValueOnce(kernelNext as unknown as pythonKernel.PythonKernel);
|
|
|
|
await executePython("1 + 1", { kernelMode: "session", sessionId: "crash-session", cwd: getProjectDir() });
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
expect(kernel.execute).toHaveBeenCalledTimes(1);
|
|
expect(kernelNext.execute).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("restarts dead retained sessions even when shutdown confirmation is missing", async () => {
|
|
const kernel = new FakeKernel(OK_RESULT);
|
|
const kernelNext = new FakeKernel(OK_RESULT);
|
|
kernel.alive = false;
|
|
kernel.shutdown.mockResolvedValueOnce({ confirmed: false });
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
const startSpy = vi
|
|
.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValueOnce(kernel as unknown as pythonKernel.PythonKernel)
|
|
.mockResolvedValueOnce(kernelNext as unknown as pythonKernel.PythonKernel);
|
|
|
|
await executePython("1 + 1", { kernelMode: "session", sessionId: "retry-dead-session", cwd: getProjectDir() });
|
|
await executePython("2 + 2", { kernelMode: "session", sessionId: "retry-dead-session", cwd: getProjectDir() });
|
|
|
|
expect(startSpy).toHaveBeenCalledTimes(2);
|
|
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
|
|
expect(kernel.execute).toHaveBeenCalledTimes(0);
|
|
expect(kernelNext.execute).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("coalesces concurrent reset requests instead of throwing 'reset already in progress'", async () => {
|
|
// Two cells from the same session asking for reset in flight at once
|
|
// previously crashed the second one with "Python kernel reset already
|
|
// in progress" — the user reported this as eval returning only the
|
|
// status line and no executed output. The executor now waits for the
|
|
// in-flight reset and then proceeds.
|
|
const kernelA = new FakeKernel(OK_RESULT);
|
|
const kernelB = new FakeKernel(OK_RESULT);
|
|
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
|
vi.spyOn(pythonKernel.PythonKernel, "start")
|
|
.mockResolvedValueOnce(kernelA as unknown as pythonKernel.PythonKernel)
|
|
.mockResolvedValueOnce(kernelB as unknown as pythonKernel.PythonKernel);
|
|
// Seed a live session that both reset cells will tear down.
|
|
await executePython("1 + 1", { kernelMode: "session", sessionId: "coalesce", cwd: getProjectDir() });
|
|
|
|
const [r1, r2] = await Promise.all([
|
|
executePython("2 + 2", { kernelMode: "session", sessionId: "coalesce", reset: true, cwd: getProjectDir() }),
|
|
executePython("3 + 3", { kernelMode: "session", sessionId: "coalesce", reset: true, cwd: getProjectDir() }),
|
|
]);
|
|
expect(r1.exitCode).toBe(0);
|
|
expect(r2.exitCode).toBe(0);
|
|
});
|
|
});
|