From 65b83a3559c7ce260b6d8248ee73aaaad24987a7 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Tue, 3 Mar 2026 11:15:37 -0700 Subject: [PATCH] test: add comprehensive tests for state and files lib modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state.test.ts: 9 tests covering loadState/saveState round-trip, corrupt JSON handling, auto-directory creation, appendLog/readLog with lastN, corrupt JSONL line skipping, and now() ISO format - files.test.ts: 9 tests covering readIfExists (missing, text, maxLines, binary detection), findWorkspaceDocs (missing dir, md filtering, nested scan, metadataOnly, node_modules/hidden dir skipping) Brings test count from 43 → 61 across 7 test files. --- tests/lib/files.test.ts | 106 ++++++++++++++++++++++++++++++++++++++++ tests/lib/state.test.ts | 93 +++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 tests/lib/files.test.ts create mode 100644 tests/lib/state.test.ts diff --git a/tests/lib/files.test.ts b/tests/lib/files.test.ts new file mode 100644 index 0000000..d8da11e --- /dev/null +++ b/tests/lib/files.test.ts @@ -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); + }); +}); diff --git a/tests/lib/state.test.ts b/tests/lib/state.test.ts new file mode 100644 index 0000000..03edb39 --- /dev/null +++ b/tests/lib/state.test.ts @@ -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); + }); +});