import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { closeSync, openSync, readdirSync, utimesSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { appendIssue, appendPlan, appendTemplate, plansPath, readIssues, readPlans, readTemplates, templatesPath, withLock, writeIssues, writePlans, writeTemplates, } from "./store"; import type { Issue, Plan, Template } from "./types"; function makeIssue(overrides: Partial = {}): Issue { const now = new Date().toISOString(); return { id: "test-a1b2", title: "Test issue", status: "open", type: "task", priority: 2, createdAt: now, updatedAt: now, ...overrides, }; } let tmpDir: string; let seedsDir: string; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "seeds-store-test-")); seedsDir = join(tmpDir, ".seeds"); await Bun.write(join(seedsDir, ".gitignore"), "*.lock\n"); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); }); describe("readIssues", () => { test("returns empty array when issues.jsonl does not exist", async () => { const issues = await readIssues(seedsDir); expect(issues).toEqual([]); }); test("returns empty array for empty file", async () => { await Bun.write(join(seedsDir, "issues.jsonl"), ""); const issues = await readIssues(seedsDir); expect(issues).toEqual([]); }); test("reads single issue", async () => { const issue = makeIssue(); await Bun.write(join(seedsDir, "issues.jsonl"), `${JSON.stringify(issue)}\n`); const issues = await readIssues(seedsDir); expect(issues).toHaveLength(1); expect(issues[0]).toEqual(issue); }); test("reads multiple issues", async () => { const issue1 = makeIssue({ id: "test-a1b2", title: "First" }); const issue2 = makeIssue({ id: "test-c3d4", title: "Second" }); const content = [JSON.stringify(issue1), JSON.stringify(issue2), ""].join("\n"); await Bun.write(join(seedsDir, "issues.jsonl"), content); const issues = await readIssues(seedsDir); expect(issues).toHaveLength(2); expect(issues[0]?.id).toBe("test-a1b2"); expect(issues[1]?.id).toBe("test-c3d4"); }); test("deduplicates by id — last occurrence wins", async () => { const original = makeIssue({ id: "test-a1b2", title: "Original" }); const updated = makeIssue({ id: "test-a1b2", title: "Updated" }); const content = [JSON.stringify(original), JSON.stringify(updated), ""].join("\n"); await Bun.write(join(seedsDir, "issues.jsonl"), content); const issues = await readIssues(seedsDir); expect(issues).toHaveLength(1); expect(issues[0]?.title).toBe("Updated"); }); test("skips blank lines", async () => { const issue = makeIssue(); const content = `\n${JSON.stringify(issue)}\n\n`; await Bun.write(join(seedsDir, "issues.jsonl"), content); const issues = await readIssues(seedsDir); expect(issues).toHaveLength(1); }); test("round-trips Issue.extensions with mixed scalar, ISO8601, and nested values", async () => { const issue = makeIssue({ extensions: { role: "refactor-bot", queued: true, attempts: 3, scheduledFor: "2026-05-12T03:00:00.000Z", lastRun: { id: "run-9c4d", at: "2026-05-10T16:57:24.830Z", ok: false, }, tags: ["cron", "warren"], notes: null, }, }); await appendIssue(seedsDir, issue); const issues = await readIssues(seedsDir); expect(issues).toHaveLength(1); expect(issues[0]).toEqual(issue); expect(issues[0]?.extensions).toEqual(issue.extensions); }); }); describe("appendIssue", () => { test("creates issues.jsonl if it does not exist", async () => { const issue = makeIssue(); await appendIssue(seedsDir, issue); const issues = await readIssues(seedsDir); expect(issues).toHaveLength(1); expect(issues[0]).toEqual(issue); }); test("appends to existing file", async () => { const issue1 = makeIssue({ id: "test-a1b2" }); const issue2 = makeIssue({ id: "test-c3d4" }); await appendIssue(seedsDir, issue1); await appendIssue(seedsDir, issue2); const issues = await readIssues(seedsDir); expect(issues).toHaveLength(2); }); test("each appended issue is on its own line", async () => { const issue = makeIssue(); await appendIssue(seedsDir, issue); const content = await Bun.file(join(seedsDir, "issues.jsonl")).text(); const lines = content.split("\n").filter((l) => l.trim() !== ""); expect(lines).toHaveLength(1); expect(JSON.parse(lines[0] ?? "{}")).toEqual(issue); }); test("appends a newline before the new record when existing file lacks one", async () => { // Simulate an externally-edited issues.jsonl that was saved without a // trailing newline. The next appendIssue() must NOT concatenate the new // record onto the previous line. const issue1 = makeIssue({ id: "test-a1b2" }); await Bun.write(join(seedsDir, "issues.jsonl"), JSON.stringify(issue1)); const issue2 = makeIssue({ id: "test-c3d4" }); await appendIssue(seedsDir, issue2); const content = await Bun.file(join(seedsDir, "issues.jsonl")).text(); const lines = content.split("\n").filter((l) => l.trim() !== ""); expect(lines).toHaveLength(2); expect(content.endsWith("\n")).toBe(true); for (const line of lines) { expect(() => JSON.parse(line)).not.toThrow(); } const issues = await readIssues(seedsDir); expect(issues.map((i) => i.id).sort()).toEqual(["test-a1b2", "test-c3d4"]); }); }); function makeTemplate(overrides: Partial