157 lines
4.3 KiB
TypeScript
157 lines
4.3 KiB
TypeScript
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
CodexGoalSnapshotError,
|
|
formatCodexGoalReconciliation,
|
|
parseCodexGoalSnapshot,
|
|
readCodexGoalSnapshotInput,
|
|
reconcileCodexGoalSnapshot,
|
|
} from "../src/codex-goal-snapshot.ts";
|
|
|
|
describe("parseCodexGoalSnapshot", () => {
|
|
it("returns available snapshot from { goal: { ... } } JSON", () => {
|
|
// given
|
|
const payload = { goal: { objective: "X", status: "active" } };
|
|
|
|
// when
|
|
const snapshot = parseCodexGoalSnapshot(payload);
|
|
|
|
// then
|
|
expect(snapshot.available).toBe(true);
|
|
expect(snapshot.objective).toBe("X");
|
|
expect(snapshot.status).toBe("active");
|
|
});
|
|
|
|
it("ignores remaining token budget fields from goal snapshots", () => {
|
|
// given
|
|
const payload = { goal: { objective: "X", status: "active" }, remainingTokens: 123 };
|
|
|
|
// when
|
|
const snapshot = parseCodexGoalSnapshot(payload);
|
|
|
|
// then
|
|
expect("remainingTokens" in snapshot).toBe(false);
|
|
});
|
|
|
|
it("returns unavailable snapshot from null", () => {
|
|
// when
|
|
const snapshot = parseCodexGoalSnapshot(null);
|
|
|
|
// then
|
|
expect(snapshot.available).toBe(false);
|
|
});
|
|
|
|
it("returns unavailable snapshot from malformed payload", () => {
|
|
// when
|
|
const snapshot = parseCodexGoalSnapshot({ wrong: "shape" });
|
|
|
|
// then
|
|
expect(snapshot.available).toBe(false);
|
|
expect(snapshot.status).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
describe("readCodexGoalSnapshotInput", () => {
|
|
let dir = "";
|
|
|
|
beforeEach(async () => {
|
|
// given
|
|
dir = await mkdtemp(join(tmpdir(), "ug-snap-"));
|
|
});
|
|
|
|
it("parses inline JSON string", async () => {
|
|
// when
|
|
const snapshot = await readCodexGoalSnapshotInput('{"goal":{"objective":"X","status":"active"}}');
|
|
|
|
// then
|
|
expect(snapshot?.available).toBe(true);
|
|
expect(snapshot?.objective).toBe("X");
|
|
});
|
|
|
|
it("reads from file path", async () => {
|
|
// given
|
|
const filePath = join(dir, "snap.json");
|
|
await writeFile(filePath, '{"goal":{"objective":"X","status":"complete"}}', "utf8");
|
|
|
|
// when
|
|
const snapshot = await readCodexGoalSnapshotInput(filePath);
|
|
|
|
// then
|
|
expect(snapshot?.available).toBe(true);
|
|
expect(snapshot?.status).toBe("complete");
|
|
});
|
|
|
|
it("reads from sample fixture path", async () => {
|
|
// given
|
|
const filePath = join(process.cwd(), "test", "fixtures", "codex-goal-snapshot.json");
|
|
|
|
// when
|
|
const snapshot = await readCodexGoalSnapshotInput(filePath);
|
|
|
|
// then
|
|
expect(snapshot?.available).toBe(true);
|
|
expect(snapshot?.objective).toBe("Complete the durable ulw-loop plan");
|
|
});
|
|
|
|
it("throws CodexGoalSnapshotError when input is neither JSON nor a path", async () => {
|
|
// when/then
|
|
await expect(readCodexGoalSnapshotInput("not json and not a path")).rejects.toThrow(CodexGoalSnapshotError);
|
|
});
|
|
});
|
|
|
|
describe("reconcileCodexGoalSnapshot", () => {
|
|
it("returns ok=true when snapshot matches expected", () => {
|
|
// when
|
|
const reconciliation = reconcileCodexGoalSnapshot(
|
|
{ available: true, objective: "X", status: "active", raw: null },
|
|
{ expectedObjective: "X" },
|
|
);
|
|
|
|
// then
|
|
expect(reconciliation.ok).toBe(true);
|
|
expect(reconciliation.errors).toHaveLength(0);
|
|
});
|
|
|
|
it("reports error when objective mismatches", () => {
|
|
// when
|
|
const reconciliation = reconcileCodexGoalSnapshot(
|
|
{ available: true, objective: "X", status: "active", raw: null },
|
|
{ expectedObjective: "Y" },
|
|
);
|
|
|
|
// then
|
|
expect(reconciliation.ok).toBe(false);
|
|
expect(reconciliation.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("reports error when status mismatches", () => {
|
|
// when
|
|
const reconciliation = reconcileCodexGoalSnapshot(
|
|
{ available: true, objective: "X", status: "active", raw: null },
|
|
{ expectedObjective: "X", allowedStatuses: ["complete"] },
|
|
);
|
|
|
|
// then
|
|
expect(reconciliation.ok).toBe(false);
|
|
expect(reconciliation.errors.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("formatCodexGoalReconciliation", () => {
|
|
it("renders errors joined", () => {
|
|
// given
|
|
const reconciliation = reconcileCodexGoalSnapshot(
|
|
{ available: true, objective: "X", status: "active", raw: null },
|
|
{ expectedObjective: "Y", allowedStatuses: ["complete"] },
|
|
);
|
|
|
|
// when
|
|
const formatted = formatCodexGoalReconciliation(reconciliation);
|
|
|
|
// then
|
|
expect(formatted).toMatch(/objective|status/i);
|
|
});
|
|
});
|