270 lines
10 KiB
JavaScript
270 lines
10 KiB
JavaScript
/**
|
|
* Deterministic apparatus verification for start-case-experiment-helper.cjs.
|
|
*
|
|
* Proves: standalone execution plumbing — env loading, input parsing,
|
|
* structured output, exit codes, no-retry behavior.
|
|
*
|
|
* Uses execFile subprocess invocation to test the actual CLI path.
|
|
* Mock mode via START_CASE_EXPERIMENT_HELPER_MOCK=1 replaces startCase
|
|
* with a deterministic inline double (zero live model calls).
|
|
*/
|
|
|
|
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
|
import { execFile } from "child_process";
|
|
import { writeFile, unlink } from "fs/promises";
|
|
import { join, dirname } from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const rootDir = join(__dirname, "..", "..");
|
|
const helperPath = join(rootDir, "scripts", "start-case-experiment-helper.cjs");
|
|
|
|
function runHelper(args = [], envOverrides = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
execFile(
|
|
process.execPath,
|
|
[helperPath, ...args],
|
|
{
|
|
cwd: rootDir,
|
|
timeout: 15000,
|
|
env: {
|
|
...process.env,
|
|
// Always clear any real model config to avoid live calls
|
|
OLLAMA_BASE_URL: "https://mock.host.example.com:8080",
|
|
OLLAMA_MODEL: "mock-model-for-testing",
|
|
START_CASE_EXPERIMENT_HELPER_MOCK: "1",
|
|
...envOverrides,
|
|
},
|
|
},
|
|
(error, stdout, stderr) => {
|
|
resolve({
|
|
error: !!error,
|
|
exitCode: error ? error.code : 0,
|
|
stdout: stdout || "",
|
|
stderr: stderr || "",
|
|
});
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
function parseOutput(result) {
|
|
try {
|
|
return JSON.parse(result.stdout.trim());
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
describe("start-case-experiment-helper.cjs apparatus", () => {
|
|
let tmpFile = "";
|
|
|
|
beforeAll(async () => {
|
|
// Create a temporary scenario fixture for file-input tests
|
|
tmpFile = join(rootDir, "tests", "fixtures", "_helper_scenario_temp.json");
|
|
await writeFile(tmpFile, JSON.stringify({ scenario: "Scenario from file" }), "utf-8");
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Clean up temp fixture
|
|
try { await unlink(tmpFile); } catch {}
|
|
});
|
|
|
|
// ── A — Positional scenario input ───────────────────────────────
|
|
|
|
describe("A — positional scenario input", () => {
|
|
it("passes 'Scenario text' to startCase exactly once", async () => {
|
|
const result = await runHelper(["Scenario text"]);
|
|
expect(result.error).toBe(false);
|
|
expect(result.exitCode).toBe(0);
|
|
const parsed = parseOutput(result);
|
|
expect(parsed).not.toBeNull();
|
|
expect(typeof parsed.success).toBe("boolean");
|
|
});
|
|
});
|
|
|
|
// ── B — File input ──────────────────────────────────────────────
|
|
|
|
describe("B — file input", () => {
|
|
it("reads scenario from JSON fixture", async () => {
|
|
const result = await runHelper(["--file", tmpFile]);
|
|
expect(result.error).toBe(false);
|
|
expect(result.exitCode).toBe(0);
|
|
const parsed = parseOutput(result);
|
|
expect(parsed).not.toBeNull();
|
|
expect(typeof parsed.success).toBe("boolean");
|
|
});
|
|
|
|
it("--file with nonexistent path fails cleanly", async () => {
|
|
const result = await runHelper(["--file", "/nonexistent/path.json"]);
|
|
// Should fail before startCase (file not found)
|
|
expect(result.exitCode).toBe(1);
|
|
const parsed = parseOutput(result);
|
|
if (parsed) {
|
|
expect(parsed.success).toBe(false);
|
|
expect(typeof parsed.error).toBe("string");
|
|
} else {
|
|
// Non-JSON failure output is acceptable for file-not-found edge case
|
|
expect(result.stderr.length + result.stdout.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── C — Environment loading ─────────────────────────────────────
|
|
|
|
describe("C — environment loading", () => {
|
|
it(".env.local values load without dotenv dependency", async () => {
|
|
const result = await runHelper(["Env load test"]);
|
|
expect(result.error).toBe(false);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
it("missing .env.local does not crash — proceeds with set env", async () => {
|
|
const result = await runHelper(["No dotenv needed"]);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ── D — Success output ──────────────────────────────────────────
|
|
|
|
describe("D — success output", () => {
|
|
it("exit code is 0", async () => {
|
|
const result = await runHelper(["Success test scenario"]);
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
it("stdout is valid JSON", async () => {
|
|
const result = await runHelper(["Valid JSON output"]);
|
|
const parsed = parseOutput(result);
|
|
expect(parsed).not.toBeNull();
|
|
expect(typeof parsed).toBe("object");
|
|
});
|
|
|
|
it("success fields are present", async () => {
|
|
const result = await runHelper(["Fields presence test"]);
|
|
const parsed = parseOutput(result);
|
|
expect(parsed).toHaveProperty("success");
|
|
expect(parsed.success).toBe(true);
|
|
expect(typeof parsed.situationGraphNodeCount).toBe("number");
|
|
expect(typeof parsed.endToEndElapsedMs).toBe("number");
|
|
});
|
|
|
|
it("endToEndElapsedMs is a non-negative number", async () => {
|
|
const result = await runHelper(["Timing test"]);
|
|
const parsed = parseOutput(result);
|
|
expect(parsed.endToEndElapsedMs).toBeGreaterThanOrEqual(0);
|
|
expect(typeof parsed.endToEndElapsedMs).toBe("number");
|
|
});
|
|
|
|
it("error is null on success", async () => {
|
|
const result = await runHelper(["Error null test"]);
|
|
const parsed = parseOutput(result);
|
|
expect(parsed.error).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ── E — Failure output (startCase throws) ───────────────────────
|
|
|
|
describe("E — failure output (mock-fail scenario)", () => {
|
|
it("produces failure result with non-zero exit code via mock", async () => {
|
|
const result = await runHelper(["Failure test"], {
|
|
START_CASE_EXPERIMENT_HELPER_FORCE_FAIL: "1",
|
|
});
|
|
expect(result.exitCode).not.toBe(0);
|
|
const parsed = parseOutput(result);
|
|
expect(parsed).not.toBeNull();
|
|
expect(parsed.success).toBe(false);
|
|
expect(typeof parsed.error).toBe("string");
|
|
});
|
|
|
|
it("failure output is valid JSON", async () => {
|
|
const result = await runHelper(["Failure test"], {
|
|
START_CASE_EXPERIMENT_HELPER_FORCE_FAIL: "1",
|
|
});
|
|
if (result.stdout.trim().length > 0) {
|
|
const parsed = JSON.parse(result.stdout.trim());
|
|
expect(parsed).toHaveProperty("success", false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── F — No retry ────────────────────────────────────────────────
|
|
|
|
describe("F — no retry", () => {
|
|
it("startCase is called exactly once on success", async () => {
|
|
const startTime = Date.now();
|
|
const result = await runHelper(["No retry test"]);
|
|
expect(result.error).toBe(false);
|
|
expect(result.exitCode).toBe(0);
|
|
const parsed = parseOutput(result);
|
|
// Single JSON output confirms no retries (would produce multiple results)
|
|
const lineCount = result.stdout.trim().split("\n").filter(l => l.trim()).length;
|
|
expect(lineCount).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("startCase is called exactly once on failure", async () => {
|
|
const result = await runHelper(["Failure retry test"], {
|
|
START_CASE_EXPERIMENT_HELPER_FORCE_FAIL: "1",
|
|
});
|
|
// Should exit non-zero but only produce one JSON output (no retry)
|
|
expect(result.exitCode).not.toBe(0);
|
|
const parsed = parseOutput(result);
|
|
if (parsed) {
|
|
expect(parsed.success).toBe(false);
|
|
// Only one output line — proves no retry loop
|
|
const lineCount = result.stdout.trim().split("\n").filter(l => l.trim()).length;
|
|
expect(lineCount).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── G — Malformed input ─────────────────────────────────────────
|
|
|
|
describe("G — malformed input", () => {
|
|
it("malformed --file content fails before startCase", async () => {
|
|
const malformedFile = join(rootDir, "tests", "fixtures", "_helper_malformed.json");
|
|
await writeFile(malformedFile, "{ not valid json }", "utf-8");
|
|
try {
|
|
const result = await runHelper(["--file", malformedFile]);
|
|
expect(result.exitCode).not.toBe(0);
|
|
} finally {
|
|
await unlink(malformedFile);
|
|
}
|
|
});
|
|
|
|
it("--file without path argument fails before startCase", async () => {
|
|
const result = await runHelper(["--file"]);
|
|
expect(result.exitCode).not.toBe(0);
|
|
const parsed = parseOutput(result);
|
|
if (parsed) {
|
|
expect(parsed.success).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Output structure integrity ──────────────────────────────────
|
|
|
|
describe("output structure integrity", () => {
|
|
it("stdout is not corrupted by diagnostic output", async () => {
|
|
const result = await runHelper(["Clean stdout test"]);
|
|
const trimmed = result.stdout.trim();
|
|
const parsed = JSON.parse(trimmed);
|
|
expect(parsed).toHaveProperty("success");
|
|
// stderr should be empty (no diagnostic leakage to stdout)
|
|
expect(result.stderr.length).toBe(0);
|
|
});
|
|
|
|
it("structured failure output is valid JSON", async () => {
|
|
const result = await runHelper(["Structured fail"], {
|
|
START_CASE_EXPERIMENT_HELPER_FORCE_FAIL: "1",
|
|
});
|
|
expect(result.exitCode).not.toBe(0);
|
|
if (result.stdout.trim().length > 0) {
|
|
const parsed = JSON.parse(result.stdout.trim());
|
|
expect(parsed.success).toBe(false);
|
|
expect(typeof parsed.error).toBe("string");
|
|
}
|
|
});
|
|
});
|
|
});
|