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:
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { loadPythonModules, type PythonModuleExecutor } from "@oh-my-pi/pi-coding-agent/ipy/modules";
|
||||
@@ -22,32 +22,112 @@ describe("python modules", () => {
|
||||
tempRoot.removeSync();
|
||||
}
|
||||
tempRoot = null;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("loads modules in sorted order with silent execution", async () => {
|
||||
it("loads modules in sorted order and forwards startup execute options", async () => {
|
||||
tempRoot = TempDir.createSync("@omp-python-modules-");
|
||||
const agentDir = path.join(tempRoot.path(), "agent");
|
||||
const cwd = path.join(tempRoot.path(), "project");
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await writeModule(getAgentModulesDir(agentDir), "beta.py", "user-omp");
|
||||
await writeModule(getAgentModulesDir(agentDir), "alpha.py", "user-omp");
|
||||
|
||||
const calls: Array<{ name: string; options?: { silent?: boolean; storeHistory?: boolean } }> = [];
|
||||
const calls: Array<{
|
||||
name: string;
|
||||
options?: { signal?: AbortSignal; timeoutMs?: number; silent?: boolean; storeHistory?: boolean };
|
||||
}> = [];
|
||||
const executor: PythonModuleExecutor = {
|
||||
execute: async (code: string, options?: { silent?: boolean; storeHistory?: boolean }) => {
|
||||
execute: async (code: string, options) => {
|
||||
const name = code.includes("def alpha") ? "alpha" : "beta";
|
||||
calls.push({ name, options });
|
||||
return { status: "ok", cancelled: false };
|
||||
},
|
||||
};
|
||||
|
||||
await loadPythonModules(executor, { cwd, agentDir });
|
||||
await loadPythonModules(executor, { cwd, agentDir, signal, timeoutMs: 987 });
|
||||
expect(calls.map(call => call.name)).toEqual(["alpha", "beta"]);
|
||||
for (const call of calls) {
|
||||
expect(call.options).toEqual({ silent: true, storeHistory: false });
|
||||
expect(call.options).toEqual({ signal, timeoutMs: 987, silent: true, storeHistory: false });
|
||||
}
|
||||
});
|
||||
|
||||
it("derives module execution timeout from the remaining deadline", async () => {
|
||||
tempRoot = TempDir.createSync("@omp-python-modules-");
|
||||
const agentDir = path.join(tempRoot.path(), "agent");
|
||||
const cwd = path.join(tempRoot.path(), "project");
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await writeModule(getProjectModulesDir(cwd), "alpha.py", "project-omp");
|
||||
|
||||
const execute = vi.fn(
|
||||
async (
|
||||
_code: string,
|
||||
_options?: {
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
silent?: boolean;
|
||||
storeHistory?: boolean;
|
||||
},
|
||||
) => ({ status: "ok" as const, cancelled: false }),
|
||||
);
|
||||
const executor: PythonModuleExecutor = { execute };
|
||||
vi.spyOn(Date, "now").mockReturnValue(10_000);
|
||||
|
||||
await loadPythonModules(executor, { cwd, agentDir, signal, deadlineMs: 10_250 });
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
expect(execute).toHaveBeenCalledWith(expect.any(String), {
|
||||
signal,
|
||||
timeoutMs: 250,
|
||||
silent: true,
|
||||
storeHistory: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails fast when the module deadline expires before the next execution starts", async () => {
|
||||
tempRoot = TempDir.createSync("@omp-python-modules-");
|
||||
const agentDir = path.join(tempRoot.path(), "agent");
|
||||
const cwd = path.join(tempRoot.path(), "project");
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await writeModule(getProjectModulesDir(cwd), "alpha.py", "project-omp");
|
||||
await writeModule(getProjectModulesDir(cwd), "beta.py", "project-omp");
|
||||
|
||||
const execute = vi.fn(async () => ({ status: "ok" as const, cancelled: false }));
|
||||
const executor: PythonModuleExecutor = { execute };
|
||||
vi.spyOn(Date, "now").mockReturnValueOnce(10_000).mockReturnValueOnce(10_300);
|
||||
|
||||
await expect(loadPythonModules(executor, { cwd, agentDir, signal, deadlineMs: 10_250 })).rejects.toMatchObject({
|
||||
name: "TimeoutError",
|
||||
message: "Python module loading timed out",
|
||||
});
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
expect(execute).toHaveBeenCalledWith(expect.any(String), {
|
||||
signal,
|
||||
timeoutMs: 250,
|
||||
silent: true,
|
||||
storeHistory: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves timeout classification when module execution is cancelled", async () => {
|
||||
tempRoot = TempDir.createSync("@omp-python-modules-");
|
||||
const agentDir = path.join(tempRoot.path(), "agent");
|
||||
const cwd = path.join(tempRoot.path(), "project");
|
||||
|
||||
await writeModule(getProjectModulesDir(cwd), "alpha.py", "project-omp");
|
||||
|
||||
const execute = vi.fn(async () => ({ status: "ok" as const, cancelled: true, timedOut: true }));
|
||||
const executor: PythonModuleExecutor = { execute };
|
||||
|
||||
await expect(loadPythonModules(executor, { cwd, agentDir })).rejects.toMatchObject({
|
||||
name: "TimeoutError",
|
||||
message: expect.stringContaining("Failed to load Python module"),
|
||||
});
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("fails fast when a module fails to execute", async () => {
|
||||
tempRoot = TempDir.createSync("@omp-python-modules-");
|
||||
const agentDir = path.join(tempRoot.path(), "agent");
|
||||
|
||||
Reference in New Issue
Block a user