Generate chat, task and learning models from agent-models.json. Use WaterNetwork fast/expert models with 272k context and validate upstream names before readiness. Add an isolated model check command with signal-safe cleanup and plugin, Skill and MCP management docs. Verified in isolation: type checks, 26 focused tests and both model catalog checks; no model inference requests.
48 lines
2.5 KiB
TypeScript
48 lines
2.5 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { spawn } from "node:child_process";
|
|
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
|
|
const alive = (pid: number) => {
|
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
};
|
|
|
|
describe("model checker cancellation", () => {
|
|
for (const signal of ["SIGTERM", "SIGINT"] as const) {
|
|
it(`cleans its own OpenCode process and workspace when receiving ${signal} during startup`, async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "model-check-command-test-"));
|
|
const statePath = join(root, "state.json");
|
|
const command = join(root, "opencode-test");
|
|
let state: { pid: number; xdg: string } | undefined;
|
|
let checker: ReturnType<typeof spawn> | undefined;
|
|
try {
|
|
await writeFile(command, `#!${process.execPath}\nimport { writeFileSync } from "node:fs";\nwriteFileSync(process.env.MODEL_CHECK_TEST_STATE, JSON.stringify({ pid: process.pid, xdg: process.env.XDG_DATA_HOME }));\nsetInterval(() => {}, 1000);\n`, { mode: 0o700 });
|
|
checker = spawn(process.execPath, ["scripts/check-models.ts"], {
|
|
cwd: resolve("."), env: { ...process.env, OPENCODE_COMMAND: command, MODEL_CHECK_TEST_STATE: statePath },
|
|
stdio: ["ignore", "ignore", "ignore"],
|
|
});
|
|
const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
|
|
checker!.once("error", reject);
|
|
checker!.once("close", (code, signal) => resolve({ code, signal }));
|
|
});
|
|
const deadline = Date.now() + 5000;
|
|
while (!state && Date.now() < deadline) {
|
|
try { state = JSON.parse(await readFile(statePath, "utf8")); } catch { await Bun.sleep(10); }
|
|
}
|
|
expect(state).toBeDefined();
|
|
checker.kill(signal);
|
|
const result = await Promise.race([exited, Bun.sleep(5000).then(() => { throw new Error("checker did not finish cancellation"); })]);
|
|
expect(alive(state!.pid)).toBe(false);
|
|
await expect(stat(dirname(state!.xdg))).rejects.toThrow();
|
|
expect(result).toEqual({ code: signal === "SIGTERM" ? 143 : 130, signal: null });
|
|
} finally {
|
|
checker?.kill("SIGKILL");
|
|
if (state && alive(state.pid)) process.kill(state.pid, "SIGKILL");
|
|
if (state) await rm(dirname(state.xdg), { recursive: true, force: true });
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
}, 10_000);
|
|
}
|
|
});
|