Merge pull request #51 from Runfusion/codex/testing-suite-quality-prd
test: harden and slim local test workflow
This commit is contained in:
60
packages/engine/src/__tests__/custom-providers.test.ts
Normal file
60
packages/engine/src/__tests__/custom-providers.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { readCustomProviders } from "../custom-providers.js";
|
||||
|
||||
describe("readCustomProviders", () => {
|
||||
let homeDir: string;
|
||||
let settingsPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
homeDir = await mkdtemp(join(tmpdir(), "fn-custom-providers-home-"));
|
||||
settingsPath = join(homeDir, ".fusion", "settings.json");
|
||||
await mkdir(join(homeDir, ".fusion"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns an empty list when settings are missing or malformed", async () => {
|
||||
expect(readCustomProviders(homeDir)).toEqual([]);
|
||||
|
||||
await writeFile(settingsPath, "{ invalid json", "utf-8");
|
||||
expect(readCustomProviders(homeDir)).toEqual([]);
|
||||
|
||||
await writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify({ customProviders: { id: "not-an-array" } }),
|
||||
"utf-8",
|
||||
);
|
||||
expect(readCustomProviders(homeDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns custom provider arrays from user settings", async () => {
|
||||
const providers = [
|
||||
{
|
||||
id: "local-openai",
|
||||
name: "Local OpenAI",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
apiKey: "local-key",
|
||||
models: [{ id: "qwen3", name: "Qwen 3" }],
|
||||
},
|
||||
{
|
||||
id: "anthropic-proxy",
|
||||
name: "Anthropic Proxy",
|
||||
apiType: "anthropic-compatible",
|
||||
baseUrl: "https://anthropic.example.test",
|
||||
},
|
||||
];
|
||||
await writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify({ customProviders: providers }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
expect(readCustomProviders(homeDir)).toEqual(providers);
|
||||
});
|
||||
});
|
||||
39
packages/engine/src/__tests__/task-completion.test.ts
Normal file
39
packages/engine/src/__tests__/task-completion.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { TaskDetail } from "@fusion/core";
|
||||
import { getTaskCompletionBlockerForStore } from "../task-completion.js";
|
||||
|
||||
function createTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: "FN-100",
|
||||
description: "Task",
|
||||
prompt: "Task prompt",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("getTaskCompletionBlockerForStore", () => {
|
||||
it("treats dependency lookup failures as unresolved dependencies", async () => {
|
||||
const getTask = vi.fn(async (taskId: string) => {
|
||||
if (taskId === "FN-DONE") {
|
||||
return createTask({ id: taskId, column: "done" });
|
||||
}
|
||||
throw new Error("database temporarily unavailable");
|
||||
});
|
||||
|
||||
await expect(getTaskCompletionBlockerForStore(
|
||||
{ getTask },
|
||||
createTask({ dependencies: ["FN-DONE", "FN-MISSING"] }),
|
||||
)).resolves.toBe("task has unresolved dependencies: FN-MISSING");
|
||||
|
||||
expect(getTask).toHaveBeenCalledWith("FN-DONE");
|
||||
expect(getTask).toHaveBeenCalledWith("FN-MISSING");
|
||||
});
|
||||
});
|
||||
81
packages/engine/src/__tests__/verification-utils.test.ts
Normal file
81
packages/engine/src/__tests__/verification-utils.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { execWithProcessGroup } from "../verification-utils.js";
|
||||
|
||||
const onPosix = process.platform !== "win32";
|
||||
const itPosix = onPosix ? it : it.skip;
|
||||
|
||||
describe("execWithProcessGroup", { timeout: 10_000 }, () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "fn-verification-utils-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("reports buffer overflow while preserving capped stdout", async () => {
|
||||
const result = await execWithProcessGroup(
|
||||
`${JSON.stringify(process.execPath)} -e "process.stdout.write('x'.repeat(128))"`,
|
||||
{ cwd: tempDir, timeout: 1_000, maxBuffer: 12 },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
stdout: "x".repeat(12),
|
||||
stderr: "",
|
||||
bufferOverflow: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects and kills the command when the abort signal fires", async () => {
|
||||
const controller = new AbortController();
|
||||
const promise = execWithProcessGroup(
|
||||
`${JSON.stringify(process.execPath)} -e "setInterval(() => {}, 1000)"`,
|
||||
{ cwd: tempDir, timeout: 5_000, maxBuffer: 1_024, signal: controller.signal },
|
||||
);
|
||||
|
||||
setTimeout(() => controller.abort(), 50);
|
||||
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
code: "ABORT_ERR",
|
||||
aborted: true,
|
||||
killed: true,
|
||||
});
|
||||
});
|
||||
|
||||
itPosix("times out and terminates child processes in the spawned process group", async () => {
|
||||
const markerPath = join(tempDir, "descendant-survived.txt");
|
||||
const parentScriptPath = join(tempDir, "spawn-descendant.cjs");
|
||||
await writeFile(
|
||||
parentScriptPath,
|
||||
`
|
||||
const { spawn } = require("node:child_process");
|
||||
spawn(process.execPath, [
|
||||
"-e",
|
||||
"setTimeout(() => require('node:fs').writeFileSync(process.env.MARKER, 'survived'), 450)",
|
||||
], {
|
||||
env: { ...process.env, MARKER: process.argv[2] },
|
||||
stdio: "ignore",
|
||||
}).unref();
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await expect(execWithProcessGroup(
|
||||
`${JSON.stringify(process.execPath)} ${JSON.stringify(parentScriptPath)} ${JSON.stringify(markerPath)}`,
|
||||
{ cwd: tempDir, timeout: 75, maxBuffer: 1_024 },
|
||||
)).rejects.toMatchObject({
|
||||
code: "ETIMEDOUT",
|
||||
killed: true,
|
||||
});
|
||||
|
||||
await delay(700);
|
||||
await expect(access(markerPath)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,9 @@ import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { CustomProvider } from "@fusion/core";
|
||||
|
||||
export function readCustomProviders(): CustomProvider[] {
|
||||
export function readCustomProviders(homeDir = homedir()): CustomProvider[] {
|
||||
try {
|
||||
const settingsPath = join(homedir(), ".fusion", "settings.json");
|
||||
const settingsPath = join(homeDir, ".fusion", "settings.json");
|
||||
const raw = readFileSync(settingsPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as { customProviders?: CustomProvider[] };
|
||||
return Array.isArray(parsed.customProviders) ? parsed.customProviders : [];
|
||||
|
||||
Reference in New Issue
Block a user