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.
This commit is contained in:
artale 2026-07-04 19:15:38 +02:00
parent d4e2d3c36b
commit 0236f351ec
4 changed files with 146 additions and 1 deletions

View File

@ -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);
});
});

View File

@ -2,7 +2,11 @@ import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import * as os from "node:os"; 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 { export interface LedgerEntry {
step: number; step: number;

View File

@ -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);
});
});

View File

@ -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");
});
});