From 0236f351ece80e150b7601ea4665585387780d3f Mon Sep 17 00:00:00 2001 From: artale Date: Sat, 4 Jul 2026 19:15:38 +0200 Subject: [PATCH] test: add harness tests for interrupt-gate, flat-ledger, repo-mapper interrupt-gate: 5 tests (stdin defaults, requireApproval) flat-ledger: 6 tests (write/read, lastStep, summary, clear, sessionName) repo-mapper: 4 tests (real dir, empty path, minifiedTree, treeString) Fix FlatLedger to accept setLedgerDir() for test isolation. Total: 68 files, 291 tests passing. --- src/fable5/flat-ledger.test.ts | 77 +++++++++++++++++++++++++++++++ src/fable5/flat-ledger.ts | 6 ++- src/fable5/interrupt-gate.test.ts | 29 ++++++++++++ src/fable5/repo-mapper.test.ts | 35 ++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/fable5/flat-ledger.test.ts create mode 100644 src/fable5/interrupt-gate.test.ts create mode 100644 src/fable5/repo-mapper.test.ts diff --git a/src/fable5/flat-ledger.test.ts b/src/fable5/flat-ledger.test.ts new file mode 100644 index 0000000..60dc5fc --- /dev/null +++ b/src/fable5/flat-ledger.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as os from "node:os"; +import { FlatLedger, setLedgerDir } from "./flat-ledger.js"; + +describe("flat ledger", () => { + const testDir = path.join(os.tmpdir(), "fable-ledger-test"); + + beforeEach(() => { + fs.mkdirSync(testDir, { recursive: true }); + setLedgerDir(testDir); + }); + + afterEach(() => { + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + it("writes and reads entries", () => { + const ledger = new FlatLedger("test-session"); + const entry = ledger.append({ + timestamp: new Date().toISOString(), + agent: "test-agent", + action: "build", + target: "src/main.ts", + result: "ok", + durationMs: 100, + }); + expect(entry.step).toBe(1); + expect(entry.result).toBe("ok"); + + const state = ledger.readState(); + expect(state.length).toBe(1); + expect(state[0].step).toBe(1); + }); + + it("returns last step", () => { + const ledger = new FlatLedger("test-session"); + expect(ledger.lastStep()).toBeNull(); + + ledger.append({ timestamp: new Date().toISOString(), agent: "a", action: "build", target: "x", result: "ok", durationMs: 10 }); + ledger.append({ timestamp: new Date().toISOString(), agent: "a", action: "test", target: "x", result: "fail", durationMs: 20 }); + + const last = ledger.lastStep(); + expect(last?.step).toBe(2); + expect(last?.result).toBe("fail"); + expect(ledger.lastActionResult()).toBe("fail"); + }); + + it("produces summary", () => { + const ledger = new FlatLedger("test-session"); + ledger.append({ timestamp: "t1", agent: "a", action: "b", target: "t", result: "ok", durationMs: 1 }); + ledger.append({ timestamp: "t2", agent: "a", action: "b", target: "t", result: "ok", durationMs: 1 }); + ledger.append({ timestamp: "t3", agent: "a", action: "b", target: "t", result: "fail", durationMs: 1 }); + ledger.append({ timestamp: "t4", agent: "a", action: "b", target: "t", result: "skip", durationMs: 1 }); + + const summary = ledger.summary(); + expect(summary).toContain("4 steps"); + expect(summary).toContain("2 ok"); + expect(summary).toContain("1 fail"); + expect(summary).toContain("1 skip"); + }); + + it("clear resets state", () => { + const ledger = new FlatLedger("test-session"); + ledger.append({ timestamp: "t1", agent: "a", action: "b", target: "t", result: "ok", durationMs: 1 }); + expect(ledger.readState().length).toBe(1); + ledger.clear(); + expect(ledger.readState().length).toBe(0); + }); + + it("sessionNameFromTask generates safe names", () => { + const name = FlatLedger.sessionNameFromTask("Fix: broken auth on /api/v2/users"); + expect(name).toBe("fix-broken-auth-on-api-v2-users"); + expect(name.length).toBeLessThanOrEqual(40); + }); +}); diff --git a/src/fable5/flat-ledger.ts b/src/fable5/flat-ledger.ts index 033647f..300be2e 100644 --- a/src/fable5/flat-ledger.ts +++ b/src/fable5/flat-ledger.ts @@ -2,7 +2,11 @@ import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; -const LEDGER_DIR = path.join(os.homedir(), ".fable-agent", "ledger"); +let LEDGER_DIR = path.join(os.homedir(), ".fable-agent", "ledger"); + +export function setLedgerDir(dir: string) { + LEDGER_DIR = dir; +} export interface LedgerEntry { step: number; diff --git a/src/fable5/interrupt-gate.test.ts b/src/fable5/interrupt-gate.test.ts new file mode 100644 index 0000000..d4fe9a3 --- /dev/null +++ b/src/fable5/interrupt-gate.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { humanInterruptGate, requireApproval } from "./interrupt-gate.js"; + +describe("interrupt gate", () => { + it("defaults to allow when stdin is not a TTY", async () => { + const decision = await humanInterruptGate({ defaultValue: "allow" }); + expect(decision).toBe("allow"); + }); + + it("defaults to deny when configured", async () => { + const decision = await humanInterruptGate({ defaultValue: "deny" }); + expect(decision).toBe("deny"); + }); + + it("returns skip when configured", async () => { + const decision = await humanInterruptGate({ defaultValue: "skip" }); + expect(decision).toBe("skip"); + }); + + it("requireApproval returns true by default on non-TTY", async () => { + const ok = await requireApproval("test-action"); + expect(ok).toBe(true); + }); + + it("requireApproval returns false when default is deny", async () => { + const ok = await requireApproval("test-action", { defaultValue: "deny" }); + expect(ok).toBe(false); + }); +}); diff --git a/src/fable5/repo-mapper.test.ts b/src/fable5/repo-mapper.test.ts new file mode 100644 index 0000000..d0cbdf9 --- /dev/null +++ b/src/fable5/repo-mapper.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import { buildRepoMap, minifiedTree, repoTreeString } from "./repo-mapper.js"; + +describe("repo mapper", () => { + const fixtures = path.resolve("test", "fixtures", "fake-repo"); + + it("builds a map from a real directory", () => { + const map = buildRepoMap(process.cwd(), 2, ["node_modules", ".git", "dist", ".pi"]); + expect(map.fileCount).toBeGreaterThan(0); + expect(map.dirCount).toBeGreaterThan(0); + expect(map.totalBytes).toBeGreaterThan(0); + }); + + it("returns empty for nonexistent path", () => { + const map = buildRepoMap("/nonexistent/path"); + expect(map.fileCount).toBe(0); + expect(map.tree.length).toBe(0); + }); + + it("minifiedTree truncates long lists", () => { + const map = buildRepoMap(process.cwd(), 1, ["node_modules", ".git", "dist", ".pi"]); + const result = minifiedTree(map); + expect(result).toContain("f"); + expect(result).toContain("d"); + }); + + it("repoTreeString builds readable output", () => { + const map = buildRepoMap(process.cwd(), 1, ["node_modules", ".git", "dist", ".pi"]); + const output = repoTreeString(map); + expect(output).toContain("files"); + expect(output).toContain("dirs"); + }); +});