mirror of
https://github.com/Heretek-OpenCode/heretek-opencode.git
synced 2026-11-07 10:05:58 -10:00
- state/: SDK type re-exports + JSONL prev-hash journal, snapshot, resume - dag/: topo sort, cycle detect, ready-set - scheduler/: run loop, rework cap=2 -> waiting_human, judge-split fix tasks, goalUnchanged drift guard - lanes/: bwrap|docker|dagger select, worktree lifecycle, refs/fleet-salvage/* - host/: opencode serve REST adapter (prompt/steer/wait/diff, polling fallback) - gates/: mechanical runner + budget enforcement, SDK GateReport shape - judges/: qa-functional + qa-adversarial prompts and report parsers - pipeline/: grill->research->spec->build->dualQA->humanPR stages + artifacts - cli/: grill|research|spec|run|status|approve|resume|steer - trust: git SHAs + journal chains + branch protection + human PR; no ceremony - tests: 29 pass (dag, journal, scheduler, goalUnchanged)
82 lines
3.0 KiB
TypeScript
82 lines
3.0 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import {
|
|
Journal,
|
|
appendJournalEvent,
|
|
loadJournal,
|
|
resumeMission,
|
|
saveJournal,
|
|
saveSnapshot,
|
|
verifyJournalFile,
|
|
} from "../src/state/index.js";
|
|
import { makeTask } from "../src/state/index.js";
|
|
|
|
async function missionDir(): Promise<string> {
|
|
return mkdtemp(path.join(tmpdir(), "fleet-journal-"));
|
|
}
|
|
|
|
describe("Journal", () => {
|
|
test("append then verify passes", async () => {
|
|
const dir = await missionDir();
|
|
const j = new Journal();
|
|
j.append("mission.grilled", { missionId: "m-1" }, "operator");
|
|
j.append("task.started", { taskId: "t-1" });
|
|
expect(j.verify().ok).toBe(true);
|
|
await saveJournal(dir, j);
|
|
expect((await verifyJournalFile(dir)).ok).toBe(true);
|
|
const loaded = await loadJournal(dir);
|
|
expect(loaded.length).toBe(2);
|
|
expect(loaded.verify().ok).toBe(true);
|
|
});
|
|
|
|
test("tamper detection: edited payload fails verify", async () => {
|
|
const dir = await missionDir();
|
|
const j = new Journal();
|
|
j.append("task.done", { taskId: "t-1" });
|
|
await saveJournal(dir, j);
|
|
const file = path.join(dir, "journal.jsonl");
|
|
const text = await readFile(file, "utf8");
|
|
const tampered = text.replace("task.done", "task.done-hacked");
|
|
await writeFile(file, tampered, "utf8");
|
|
expect((await verifyJournalFile(dir)).ok).toBe(false);
|
|
await expect(loadJournal(dir)).rejects.toThrow(/corrupt/);
|
|
});
|
|
|
|
test("tamper detection: truncated chain prefix still verifies, appended forgery fails", () => {
|
|
const j = new Journal();
|
|
j.append("a");
|
|
j.append("b");
|
|
const clone = Journal.fromJSONL(j.toJSONL());
|
|
expect(clone.verify().ok).toBe(true);
|
|
// Forge: splice in an entry with a wrong prevHash.
|
|
const entries = clone.list();
|
|
const forged = Journal.fromJSONL(
|
|
`${JSON.stringify(entries[0])}\n${JSON.stringify({ ...entries[1]!, prevHash: "00".repeat(32) })}\n`,
|
|
);
|
|
expect(forged.verify().ok).toBe(false);
|
|
});
|
|
|
|
test("snapshot round-trip + resume", async () => {
|
|
const dir = await missionDir();
|
|
const j = new Journal();
|
|
j.append("mission.grilled", { missionId: "m-1" }, "operator");
|
|
const mission = { id: "m-1", intent: "ship it", acceptance: [], state: "active" as const };
|
|
const taskList = [makeTask({ id: "t-1", missionId: "m-1" })];
|
|
await saveJournal(dir, j);
|
|
await saveSnapshot(dir, mission, taskList, j);
|
|
// Post-snapshot event: resume must surface it as tail.
|
|
await appendJournalEvent(dir, "task.started", { taskId: "t-1" });
|
|
const resumed = await resumeMission(dir);
|
|
expect(resumed.mission.id).toBe("m-1");
|
|
expect(resumed.tasks.map((t) => t.id)).toEqual(["t-1"]);
|
|
expect(resumed.tail.map((e) => e.event)).toEqual(["task.started"]);
|
|
expect(resumed.snapshotDiverged).toBe(true);
|
|
});
|
|
|
|
test("resume without snapshot throws a helpful error", async () => {
|
|
await expect(resumeMission(await missionDir())).rejects.toThrow(/no snapshot/);
|
|
});
|
|
});
|