test(omo-codex): batch 101 (18 files)
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
normalizeSteeringProposal,
|
||||
parseSteeringKind,
|
||||
parseSteeringProposal,
|
||||
parseSteeringSource,
|
||||
printSteerResult,
|
||||
} from "../src/cli-steering.js";
|
||||
import type { SteerUlwLoopResult, UlwLoopPlan } from "../src/types.js";
|
||||
import { UlwLoopError } from "../src/types.js";
|
||||
|
||||
const NOW = "2026-05-23T00:00:00.000Z";
|
||||
|
||||
function plan(): UlwLoopPlan {
|
||||
return {
|
||||
version: 1,
|
||||
createdAt: NOW,
|
||||
updatedAt: NOW,
|
||||
briefPath: ".omo/ulw-loop/brief.md",
|
||||
goalsPath: ".omo/ulw-loop/goals.json",
|
||||
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||
goals: [],
|
||||
};
|
||||
}
|
||||
|
||||
function steerResult(overrides: Partial<SteerUlwLoopResult> = {}): SteerUlwLoopResult {
|
||||
return {
|
||||
plan: plan(),
|
||||
accepted: true,
|
||||
audit: {
|
||||
kind: "add_subgoal",
|
||||
source: "cli",
|
||||
targetGoalIds: ["G001"],
|
||||
evidence: "x",
|
||||
rationale: "y",
|
||||
invariant: {
|
||||
accepted: true,
|
||||
structuralInvariantAccepted: true,
|
||||
evidenceBackedNecessity: true,
|
||||
noEasierCompletion: true,
|
||||
rejectedReasons: [],
|
||||
},
|
||||
},
|
||||
rejectedReasons: [],
|
||||
deduped: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function captureStdout(action: () => void): string {
|
||||
let output = "";
|
||||
const write = vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
|
||||
output += chunk.toString();
|
||||
return true;
|
||||
});
|
||||
action();
|
||||
write.mockRestore();
|
||||
return output;
|
||||
}
|
||||
|
||||
describe("parseSteeringKind", () => {
|
||||
it("returns valid kind from --kind", () => {
|
||||
expect(parseSteeringKind(["--kind", "add_subgoal"])).toBe("add_subgoal");
|
||||
});
|
||||
|
||||
it("accepts revise_criterion", () => {
|
||||
expect(parseSteeringKind(["--kind", "revise_criterion"])).toBe("revise_criterion");
|
||||
});
|
||||
|
||||
it("throws when --kind missing", () => {
|
||||
expect(() => parseSteeringKind([])).toThrow(UlwLoopError);
|
||||
});
|
||||
|
||||
it("throws when kind unknown", () => {
|
||||
expect(() => parseSteeringKind(["--kind", "bogus"])).toThrow(UlwLoopError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSteeringSource", () => {
|
||||
it("defaults to cli", () => {
|
||||
expect(parseSteeringSource([])).toBe("cli");
|
||||
});
|
||||
|
||||
it("returns explicit value", () => {
|
||||
expect(parseSteeringSource(["--source", "user_prompt_submit"])).toBe("user_prompt_submit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSteeringProposal add_subgoal", () => {
|
||||
it("builds proposal from required flags", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"add_subgoal",
|
||||
"--title",
|
||||
" New ",
|
||||
"--objective",
|
||||
" Build ",
|
||||
"--evidence",
|
||||
" x ",
|
||||
"--rationale",
|
||||
" y ",
|
||||
]);
|
||||
|
||||
expect(p).toMatchObject({
|
||||
kind: "add_subgoal",
|
||||
source: "cli",
|
||||
title: "New",
|
||||
objective: "Build",
|
||||
evidence: "x",
|
||||
rationale: "y",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws when --title missing", async () => {
|
||||
await expect(
|
||||
parseSteeringProposal([
|
||||
"--kind",
|
||||
"add_subgoal",
|
||||
"--objective",
|
||||
"Build",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]),
|
||||
).rejects.toThrow(UlwLoopError);
|
||||
});
|
||||
|
||||
it("throws when --evidence missing", async () => {
|
||||
await expect(
|
||||
parseSteeringProposal(["--kind", "add_subgoal", "--title", "New", "--objective", "Build", "--rationale", "y"]),
|
||||
).rejects.toThrow(UlwLoopError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSteeringProposal revise_criterion", () => {
|
||||
it("builds proposal with goal, criterion, scenario, evidence, and rationale", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"revise_criterion",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--criterion-id",
|
||||
"C002",
|
||||
"--scenario",
|
||||
"new scenario",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p.kind).toBe("revise_criterion");
|
||||
expect(p.goalId).toBe("G001");
|
||||
expect(p.targetGoalId).toBe("G001");
|
||||
expect(p.criterionId).toBe("C002");
|
||||
expect(p.scenario).toBe("new scenario");
|
||||
});
|
||||
|
||||
it("accepts --expected-evidence as an update field", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"revise_criterion",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--criterion-id",
|
||||
"C002",
|
||||
"--expected-evidence",
|
||||
"new evidence",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p.expectedEvidence).toBe("new evidence");
|
||||
});
|
||||
|
||||
it("accepts --user-model as an update field", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"revise_criterion",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--criterion-id",
|
||||
"C002",
|
||||
"--user-model",
|
||||
"edge",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p.userModel).toBe("edge");
|
||||
});
|
||||
|
||||
it("throws when none of scenario/expected-evidence/user-model provided", async () => {
|
||||
await expect(
|
||||
parseSteeringProposal([
|
||||
"--kind",
|
||||
"revise_criterion",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--criterion-id",
|
||||
"C002",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]),
|
||||
).rejects.toThrow(UlwLoopError);
|
||||
});
|
||||
|
||||
it("throws when goal-id missing", async () => {
|
||||
await expect(
|
||||
parseSteeringProposal([
|
||||
"--kind",
|
||||
"revise_criterion",
|
||||
"--criterion-id",
|
||||
"C002",
|
||||
"--scenario",
|
||||
"s",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]),
|
||||
).rejects.toThrow(UlwLoopError);
|
||||
});
|
||||
|
||||
it("throws when criterion-id missing", async () => {
|
||||
await expect(
|
||||
parseSteeringProposal([
|
||||
"--kind",
|
||||
"revise_criterion",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--scenario",
|
||||
"s",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]),
|
||||
).rejects.toThrow(UlwLoopError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSteeringProposal split_subgoal", () => {
|
||||
it("reads --children from inline JSON", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"split_subgoal",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--children",
|
||||
'[{"title":"A","objective":"Do A"}]',
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p.childGoals).toEqual([{ title: "A", objective: "Do A" }]);
|
||||
});
|
||||
|
||||
it("reads --children from JSON file path", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "ug-steer-"));
|
||||
try {
|
||||
const file = join(dir, "children.json");
|
||||
await writeFile(file, '[{"title":"B","objective":"Do B"}]', "utf8");
|
||||
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"split_subgoal",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--children",
|
||||
file,
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p.childGoals).toEqual([{ title: "B", objective: "Do B" }]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSteeringProposal reorder_pending", () => {
|
||||
it("reads --order from inline JSON array", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"reorder_pending",
|
||||
"--order",
|
||||
'["G002","G001"]',
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p.pendingOrder).toEqual(["G002", "G001"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSteeringProposal remaining kinds", () => {
|
||||
it("builds revise_pending_wording proposal", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"revise_pending_wording",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--title",
|
||||
"New",
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p).toMatchObject({ kind: "revise_pending_wording", targetGoalId: "G001", revisedTitle: "New" });
|
||||
});
|
||||
|
||||
it("builds mark_blocked_superseded proposal with replacements", async () => {
|
||||
const p = await parseSteeringProposal([
|
||||
"--kind",
|
||||
"mark_blocked_superseded",
|
||||
"--goal-id",
|
||||
"G001",
|
||||
"--replacements",
|
||||
'[{"title":"C","objective":"Do C"}]',
|
||||
"--evidence",
|
||||
"x",
|
||||
"--rationale",
|
||||
"y",
|
||||
]);
|
||||
|
||||
expect(p).toMatchObject({
|
||||
kind: "mark_blocked_superseded",
|
||||
targetGoalId: "G001",
|
||||
childGoals: [{ title: "C", objective: "Do C" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSteeringProposal annotate_ledger", () => {
|
||||
it("builds minimal proposal", async () => {
|
||||
const p = await parseSteeringProposal(["--kind", "annotate_ledger", "--evidence", "x", "--rationale", "y"]);
|
||||
|
||||
expect(p).toMatchObject({ kind: "annotate_ledger", source: "cli", evidence: "x", rationale: "y" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeSteeringProposal", () => {
|
||||
it("trims string fields", () => {
|
||||
const p = normalizeSteeringProposal({
|
||||
kind: "revise_criterion",
|
||||
source: "cli",
|
||||
goalId: " G001 ",
|
||||
targetGoalId: " G001 ",
|
||||
criterionId: " C002 ",
|
||||
evidence: " x ",
|
||||
rationale: " y ",
|
||||
scenario: " z ",
|
||||
});
|
||||
|
||||
expect(p).toMatchObject({
|
||||
goalId: "G001",
|
||||
targetGoalId: "G001",
|
||||
criterionId: "C002",
|
||||
evidence: "x",
|
||||
rationale: "y",
|
||||
scenario: "z",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects empty evidence after trim", () => {
|
||||
expect(() =>
|
||||
normalizeSteeringProposal({ kind: "annotate_ledger", source: "cli", evidence: " ", rationale: "y" }),
|
||||
).toThrow(UlwLoopError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("printSteerResult", () => {
|
||||
it("prints JSON when json=true", () => {
|
||||
const output = captureStdout(() => printSteerResult(steerResult(), true));
|
||||
|
||||
expect(JSON.parse(output)).toMatchObject({ accepted: true, deduped: false, audit: { kind: "add_subgoal" } });
|
||||
});
|
||||
|
||||
it("prints human-readable when json=false", () => {
|
||||
const output = captureStdout(() => printSteerResult(steerResult(), false));
|
||||
|
||||
expect(output).toContain("ulw-loop steer: accepted add_subgoal");
|
||||
expect(output).toContain("ulw-loop status");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user