import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { loadConfig } from "./config.js"; function withTempDir(fn: (dir: string) => void) { const dir = join(tmpdir(), `tdd-test-${Date.now()}`); mkdirSync(dir, { recursive: true }); try { fn(dir); } finally { rmSync(dir, { recursive: true, force: true }); } } describe("loadConfig", () => { it("throws when no config exists", () => { withTempDir((dir) => { expect(() => loadConfig(dir)).toThrow(); }); }); it("loads config from rules.json", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: ["src/**/*.ts"], testCommands: ["npm run test"], timeoutSeconds: 60, }), "utf-8", ); const config = loadConfig(dir); expect(config.blockedInRed).toEqual(["tests/**/*.test.ts"]); expect(config.timeoutSeconds).toBe(60); }); }); it("supports multiple test commands", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: ["src/**/*.ts"], testCommands: ["npm run test:unit", "npm run test:integration"], }), "utf-8", ); const config = loadConfig(dir); expect(config.testCommands).toHaveLength(2); }); }); it("defaults timeoutSeconds to 120 when omitted", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: ["src/**/*.ts"], testCommands: ["npm test"], }), "utf-8", ); const config = loadConfig(dir); expect(config.timeoutSeconds).toBe(120); }); }); describe("validation — throws on invalid content", () => { it("throws when blockedInRed is not an array", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: "not-an-array", blockedInGreen: ["src/**/*.ts"], testCommands: ["npm test"], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when blockedInGreen is not an array", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: null, testCommands: ["npm test"], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when testCommands is not an array", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: ["src/**/*.ts"], testCommands: "npm test", }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws on malformed JSON", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync(join(tddDir, "rules.json"), "not json{{", "utf-8"); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when blockedInRed is empty", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: [], blockedInGreen: ["src/**/*.ts"], testCommands: ["npm test"], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when blockedInGreen is empty", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: [], testCommands: ["npm test"], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when testCommands is empty", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: ["src/**/*.ts"], testCommands: [], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when blockedInRed contains non-strings", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: [123], blockedInGreen: ["src/**/*.ts"], testCommands: ["npm test"], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when blockedInGreen contains non-strings", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: [null], testCommands: ["npm test"], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); it("throws when testCommands contains non-strings", () => { withTempDir((dir) => { const tddDir = join(dir, ".pi", "tdd"); mkdirSync(tddDir, { recursive: true }); writeFileSync( join(tddDir, "rules.json"), JSON.stringify({ blockedInRed: ["tests/**/*.test.ts"], blockedInGreen: ["src/**/*.ts"], testCommands: ["npm test", 456], }), "utf-8", ); expect(() => loadConfig(dir)).toThrow(); }); }); }); });