import * as fs from "fs" import { describe, it, expect } from "vitest" import { parseModules, parseBugzillaTracker } from "../../lib/tracker" import { parseTodoFile } from "../../lib/file" import { serializeTodoFile, serializeModules, serializeBugzillaTracker } from "../../lib/serializer" describe("parseModules", () => { it("parses modules list", () => { const text = `Modules: - Name: General Path: . - Name: Frontend Path: src/frontend` const modules = parseModules(text) expect(modules).toHaveLength(2) expect(modules[0]).toEqual({ name: "General", path: "." }) expect(modules[1]).toEqual({ name: "Frontend", path: "src/frontend" }) }) }) describe("parseBugzillaTracker", () => { it("parses bugzilla tracker with mappings", () => { const text = `URL: https://bugzilla.example.com Mappings: - Module: General Product: MyProject Component: General - Module: Frontend Product: MyProject Component: FrontendUI` const tracker = parseBugzillaTracker(text) expect(tracker.url).toBe("https://bugzilla.example.com") expect(tracker.mappings).toHaveLength(2) expect(tracker.mappings[0]).toEqual({ module: "General", product: "MyProject", component: "General", }) expect(tracker.mappings[1]).toEqual({ module: "Frontend", product: "MyProject", component: "FrontendUI", }) }) }) describe("parseTodoFile with modules and bugzilla", () => { it("parses TODO with modules and bugzilla parts", async () => { const todo = await parseTodoFile("tests/_mocks/todo-with-tracker.txt") expect(todo.modules).toBeDefined() expect(todo.modules).toHaveLength(3) expect(todo.modules![0].name).toBe("General") expect(todo.bugzilla).toBeDefined() expect(todo.bugzilla!.url).toBe("https://bugzilla.example.com") expect(todo.bugzilla!.mappings).toHaveLength(3) expect(todo.issues).toHaveLength(2) expect(todo.issues[0].module).toBe("Frontend") expect(todo.issues[1].module).toBe("Backend") }) it("rejects issue with invalid module", async () => { const raw = `--ISSUE Content-Type: application/modules Modules: - Name: General Path: . --ISSUE Content-Type: application/issue ID: 1 Type: feature Title: Bad module Status: open Priority: low Created: 2026-01-01 Module: NonExistent Relationships: Description: Test` const tmpPath = "tests/_mocks/_bad-module.txt" fs.writeFileSync(tmpPath, raw) try { await expect(parseTodoFile(tmpPath)).rejects.toThrow("NonExistent") } finally { fs.unlinkSync(tmpPath) } }) it("rejects bugzilla mapping referencing undefined module", async () => { const raw = `--ISSUE Content-Type: application/modules Modules: - Name: General Path: . --ISSUE Content-Type: application/bugzilla URL: https://bz.example.com Mappings: - Module: DoesNotExist Product: P Component: C --ISSUE Content-Type: application/issue ID: 1 Type: feature Title: Test Status: open Priority: low Created: 2026-01-01 Relationships: Description: Test` const tmpPath = "tests/_mocks/_bad-bz-mapping.txt" fs.writeFileSync(tmpPath, raw) try { await expect(parseTodoFile(tmpPath)).rejects.toThrow("DoesNotExist") } finally { fs.unlinkSync(tmpPath) } }) it("rejects dangling relationship targets", async () => { const raw = `--ISSUE Content-Type: application/issue ID: 1 Type: feature Title: Test Status: open Priority: low Created: 2026-01-01 Relationships: dependsOn:99 Description: Test` const tmpPath = "tests/_mocks/_bad-rel.txt" fs.writeFileSync(tmpPath, raw) try { await expect(parseTodoFile(tmpPath)).rejects.toThrow("#99") } finally { fs.unlinkSync(tmpPath) } }) }) describe("serialize modules and bugzilla", () => { it("serializes modules part", () => { const result = serializeModules([ { name: "General", path: "." }, { name: "Frontend", path: "src/frontend" }, ]) expect(result).toContain("Modules:") expect(result).toContain("- Name: General") expect(result).toContain("Path: .") expect(result).toContain("- Name: Frontend") }) it("serializes bugzilla tracker part", () => { const result = serializeBugzillaTracker({ url: "https://bugzilla.example.com", mappings: [ { module: "General", product: "MyProject", component: "General" }, ], }) expect(result).toContain("URL: https://bugzilla.example.com") expect(result).toContain("- Module: General") expect(result).toContain("Product: MyProject") expect(result).toContain("Component: General") }) }) describe("round-trip with modules and bugzilla", () => { it("parse → serialize → parse preserves modules, bugzilla, and issue modules", async () => { const todo1 = await parseTodoFile("tests/_mocks/todo-with-tracker.txt") const serialized = serializeTodoFile(todo1) const tmpPath = "tests/_mocks/_roundtrip-tracker.txt" fs.writeFileSync(tmpPath, serialized) try { const todo2 = await parseTodoFile(tmpPath) expect(todo2.modules).toHaveLength(todo1.modules!.length) for (let i = 0; i < todo1.modules!.length; i++) { expect(todo2.modules![i]).toEqual(todo1.modules![i]) } expect(todo2.bugzilla!.url).toBe(todo1.bugzilla!.url) expect(todo2.bugzilla!.mappings).toHaveLength(todo1.bugzilla!.mappings.length) for (let i = 0; i < todo1.bugzilla!.mappings.length; i++) { expect(todo2.bugzilla!.mappings[i]).toEqual(todo1.bugzilla!.mappings[i]) } for (let i = 0; i < todo1.issues.length; i++) { expect(todo2.issues[i].module).toBe(todo1.issues[i].module) } } finally { fs.unlinkSync(tmpPath) } }) })