Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions tests/lib/files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdirSync, rmSync, writeFileSync } from "fs";
import { join } from "path";

// Must set env BEFORE any import of files.ts
const TEST_DIR = join(__dirname, ".tmp-files-test");

// We need to mock the PROJECT_DIR since it's resolved at import time
vi.stubEnv("CLAUDE_PROJECT_DIR", TEST_DIR);

// Force re-evaluation by resetting module registry
vi.resetModules();

let readIfExists: typeof import("../../src/lib/files.js")["readIfExists"];
let findWorkspaceDocs: typeof import("../../src/lib/files.js")["findWorkspaceDocs"];

beforeEach(async () => {
rmSync(TEST_DIR, { recursive: true, force: true });
mkdirSync(TEST_DIR, { recursive: true });
vi.resetModules();
vi.stubEnv("CLAUDE_PROJECT_DIR", TEST_DIR);
const mod = await import("../../src/lib/files.js");
readIfExists = mod.readIfExists;
findWorkspaceDocs = mod.findWorkspaceDocs;
});

afterEach(() => {
rmSync(TEST_DIR, { recursive: true, force: true });
vi.unstubAllEnvs();
});

describe("readIfExists", () => {
it("returns null for missing file", () => {
expect(readIfExists("nope.txt")).toBeNull();
});

it("reads a text file", () => {
writeFileSync(join(TEST_DIR, "hello.txt"), "line1\nline2\nline3");
const content = readIfExists("hello.txt");
expect(content).toBe("line1\nline2\nline3");
});

it("respects maxLines", () => {
const lines = Array.from({ length: 100 }, (_, i) => `line ${i}`);
writeFileSync(join(TEST_DIR, "big.txt"), lines.join("\n"));
const content = readIfExists("big.txt", 5);
expect(content!.split("\n")).toHaveLength(5);
});

it("returns null for binary files (null bytes)", () => {
const buf = Buffer.alloc(100);
buf[50] = 0;
buf.write("hello", 0);
writeFileSync(join(TEST_DIR, "binary.bin"), buf);
expect(readIfExists("binary.bin")).toBeNull();
});
});

describe("findWorkspaceDocs", () => {
it("returns empty when .claude dir missing", () => {
expect(findWorkspaceDocs()).toEqual({});
});

it("finds markdown files in .claude/", () => {
const claudeDir = join(TEST_DIR, ".claude");
mkdirSync(claudeDir, { recursive: true });
writeFileSync(join(claudeDir, "notes.md"), "# Notes\nSome content");
writeFileSync(join(claudeDir, "other.txt"), "not markdown");

const docs = findWorkspaceDocs();
expect(Object.keys(docs)).toEqual(["notes.md"]);
expect(docs["notes.md"].content).toContain("# Notes");
expect(docs["notes.md"].size).toBeGreaterThan(0);
});

it("scans nested directories", () => {
const subDir = join(TEST_DIR, ".claude", "sub");
mkdirSync(subDir, { recursive: true });
writeFileSync(join(subDir, "deep.md"), "# Deep");

const docs = findWorkspaceDocs();
expect(docs["sub/deep.md"]).toBeDefined();
});

it("supports metadataOnly mode", () => {
const claudeDir = join(TEST_DIR, ".claude");
mkdirSync(claudeDir, { recursive: true });
writeFileSync(join(claudeDir, "doc.md"), "# Content here");

const docs = findWorkspaceDocs({ metadataOnly: true });
expect(docs["doc.md"].content).toBe("");
expect(docs["doc.md"].size).toBeGreaterThan(0);
});

it("skips node_modules and hidden dirs", () => {
const nmDir = join(TEST_DIR, ".claude", "node_modules");
const hiddenDir = join(TEST_DIR, ".claude", ".hidden");
mkdirSync(nmDir, { recursive: true });
mkdirSync(hiddenDir, { recursive: true });
writeFileSync(join(nmDir, "skip.md"), "skip");
writeFileSync(join(hiddenDir, "skip.md"), "skip");

const docs = findWorkspaceDocs();
expect(Object.keys(docs)).toHaveLength(0);
});
});
93 changes: 93 additions & 0 deletions tests/lib/state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "fs";
import { join } from "path";

// Override PROJECT_DIR before importing state module
const TEST_DIR = join(__dirname, ".tmp-state-test");
process.env.CLAUDE_PROJECT_DIR = TEST_DIR;

// Dynamic import to pick up env override
let state: typeof import("../../src/lib/state.js");

beforeEach(async () => {
rmSync(TEST_DIR, { recursive: true, force: true });
mkdirSync(TEST_DIR, { recursive: true });
// Re-import to get fresh module (PROJECT_DIR is read at import time via files.ts)
state = await import("../../src/lib/state.js");
});

afterEach(() => {
rmSync(TEST_DIR, { recursive: true, force: true });
});

describe("loadState / saveState", () => {
it("returns empty object for missing file", () => {
expect(state.loadState("nonexistent")).toEqual({});
});

it("round-trips JSON data", () => {
state.saveState("test", { foo: "bar", count: 42 });
const loaded = state.loadState("test");
expect(loaded).toEqual({ foo: "bar", count: 42 });
});

it("returns empty object for corrupt JSON", () => {
const stateDir = join(TEST_DIR, ".claude", "preflight-state");
mkdirSync(stateDir, { recursive: true });
writeFileSync(join(stateDir, "corrupt.json"), "not json {{{");
expect(state.loadState("corrupt")).toEqual({});
});

it("creates state directory if missing", () => {
const stateDir = join(TEST_DIR, ".claude", "preflight-state");
expect(existsSync(stateDir)).toBe(false);
state.saveState("auto", { x: 1 });
expect(existsSync(stateDir)).toBe(true);
});
});

describe("appendLog / readLog", () => {
it("returns empty array for missing log", () => {
expect(state.readLog("missing.jsonl")).toEqual([]);
});

it("appends and reads JSONL entries", () => {
state.appendLog("test.jsonl", { action: "start", ts: 1 });
state.appendLog("test.jsonl", { action: "end", ts: 2 });
const entries = state.readLog("test.jsonl");
expect(entries).toHaveLength(2);
expect(entries[0]).toEqual({ action: "start", ts: 1 });
expect(entries[1]).toEqual({ action: "end", ts: 2 });
});

it("respects lastN parameter", () => {
for (let i = 0; i < 10; i++) {
state.appendLog("many.jsonl", { i });
}
const last3 = state.readLog("many.jsonl", 3);
expect(last3).toHaveLength(3);
expect(last3[0]).toEqual({ i: 7 });
expect(last3[2]).toEqual({ i: 9 });
});

it("skips corrupt lines gracefully", () => {
const stateDir = join(TEST_DIR, ".claude", "preflight-state");
mkdirSync(stateDir, { recursive: true });
writeFileSync(
join(stateDir, "mixed.jsonl"),
'{"ok":true}\nnot json\n{"also":"ok"}\n'
);
const entries = state.readLog("mixed.jsonl");
expect(entries).toHaveLength(2);
expect(entries[0]).toEqual({ ok: true });
expect(entries[1]).toEqual({ also: "ok" });
});
});

describe("now", () => {
it("returns a valid ISO timestamp", () => {
const ts = state.now();
expect(() => new Date(ts)).not.toThrow();
expect(new Date(ts).toISOString()).toBe(ts);
});
});
Loading