init
This commit is contained in:
commit
932d4ad420
46 changed files with 5800 additions and 0 deletions
31
tests/_mocks/todo-basic.txt
Normal file
31
tests/_mocks/todo-basic.txt
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
--ISSUE
|
||||
Content-Type: application/sprints
|
||||
Sprints:
|
||||
- Name: Sprint Alpha
|
||||
Range: 2026-02-01..2026-02-14
|
||||
-
|
||||
Name: Sprint Beta
|
||||
Range: 2026-02-15..2026-02-28
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/issue
|
||||
ID: 1
|
||||
Type: feature
|
||||
Title: Add streaming parser
|
||||
Status: open
|
||||
Priority: high
|
||||
Created: 2026-02-05
|
||||
Relationships: dependsOn:2
|
||||
Description: Implement streaming JSON parser.
|
||||
Must support SAX-like events.
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/issue
|
||||
ID: 2
|
||||
Type: bugfix
|
||||
Title: Fix wraparound
|
||||
Status: in-progress
|
||||
Priority: medium
|
||||
Created: 2026-02-06
|
||||
Relationships:
|
||||
Description: Fix off-by-one in circular buffer.
|
||||
10
tests/_mocks/todo-issues-only.txt
Normal file
10
tests/_mocks/todo-issues-only.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
--ISSUE
|
||||
Content-Type: application/issue
|
||||
ID: 10
|
||||
Type: hotfix
|
||||
Title: Patch crash
|
||||
Status: done
|
||||
Priority: high
|
||||
Created: 2026-02-03
|
||||
Relationships:
|
||||
Description: Fix crash in allocator.
|
||||
26
tests/_mocks/todo-multiple-sprints.txt
Normal file
26
tests/_mocks/todo-multiple-sprints.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
--ISSUE
|
||||
Content-Type: application/sprints
|
||||
Sprints:
|
||||
- Name: Sprint Alpha
|
||||
Range: 2026-02-01..2026-02-14
|
||||
-
|
||||
Name: Sprint Beta
|
||||
Range: 2026-02-15..2026-02-28
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/sprints
|
||||
Sprints:
|
||||
- Name: Sprint Gamma
|
||||
Range: 2026-02-28..2026-03-15
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/issue
|
||||
ID: 1
|
||||
Type: feature
|
||||
Title: Add streaming parser
|
||||
Status: open
|
||||
Priority: high
|
||||
Created: 2026-02-05
|
||||
Relationships: dependsOn:3
|
||||
Description: Implement streaming JSON parser.
|
||||
Must support SAX-like events.
|
||||
1
tests/_mocks/todo-no-sprints.txt
Normal file
1
tests/_mocks/todo-no-sprints.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Sprints:
|
||||
54
tests/_mocks/todo-with-tracker.txt
Normal file
54
tests/_mocks/todo-with-tracker.txt
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
--ISSUE
|
||||
Content-Type: application/sprints
|
||||
Sprints:
|
||||
- Name: Sprint Alpha
|
||||
Range: 2026-02-01..2026-02-14
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/modules
|
||||
Modules:
|
||||
- Name: General
|
||||
Path: .
|
||||
- Name: Frontend
|
||||
Path: src/frontend
|
||||
- Name: Backend
|
||||
Path: src/backend
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/bugzilla
|
||||
URL: https://bugzilla.example.com
|
||||
Mappings:
|
||||
- Module: General
|
||||
Product: MyProject
|
||||
Component: General
|
||||
- Module: Frontend
|
||||
Product: MyProject
|
||||
Component: Frontend
|
||||
- Module: Backend
|
||||
Product: MyProject
|
||||
Component: Backend
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/issue
|
||||
ID: 1
|
||||
Type: feature
|
||||
Title: Add streaming parser
|
||||
Status: open
|
||||
Priority: high
|
||||
Created: 2026-02-05
|
||||
Module: Frontend
|
||||
Relationships:
|
||||
Description: Implement streaming JSON parser.
|
||||
Must support SAX-like events.
|
||||
|
||||
--ISSUE
|
||||
Content-Type: application/issue
|
||||
ID: 2
|
||||
Type: bugfix
|
||||
Title: Fix API crash
|
||||
Status: in-progress
|
||||
Priority: medium
|
||||
Created: 2026-02-06
|
||||
Module: Backend
|
||||
Relationships:
|
||||
Description: Fix null pointer in request handler.
|
||||
87
tests/lib/bugzilla/fieldmap.test.ts
Normal file
87
tests/lib/bugzilla/fieldmap.test.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import {
|
||||
statusToBugzilla,
|
||||
priorityToBugzilla,
|
||||
typeToBugzilla,
|
||||
issueToBugzillaCreate,
|
||||
resolveProductComponent,
|
||||
} from "../../../lib/bugzilla/fieldmap"
|
||||
import type { Issue } from "../../../lib/issue"
|
||||
import type { BugzillaTracker } from "../../../lib/tracker"
|
||||
|
||||
describe("status mapping", () => {
|
||||
it("maps TODO statuses to Bugzilla", () => {
|
||||
expect(statusToBugzilla("open")).toEqual({ status: "CONFIRMED" })
|
||||
expect(statusToBugzilla("in-progress")).toEqual({ status: "IN_PROGRESS" })
|
||||
expect(statusToBugzilla("done")).toEqual({ status: "RESOLVED", resolution: "FIXED" })
|
||||
expect(statusToBugzilla("hold")).toEqual({ status: "RESOLVED", resolution: "LATER" })
|
||||
expect(statusToBugzilla("cancelled")).toEqual({ status: "RESOLVED", resolution: "WONTFIX" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("priority mapping", () => {
|
||||
it("maps TODO priorities to Bugzilla", () => {
|
||||
expect(priorityToBugzilla("low")).toBe("Low")
|
||||
expect(priorityToBugzilla("medium")).toBe("Normal")
|
||||
expect(priorityToBugzilla("high")).toBe("Highest")
|
||||
})
|
||||
})
|
||||
|
||||
describe("type/severity mapping", () => {
|
||||
it("maps TODO types to Bugzilla severity", () => {
|
||||
expect(typeToBugzilla("feature")).toBe("enhancement")
|
||||
expect(typeToBugzilla("bugfix")).toBe("normal")
|
||||
expect(typeToBugzilla("hotfix")).toBe("critical")
|
||||
})
|
||||
})
|
||||
|
||||
describe("issueToBugzillaCreate", () => {
|
||||
it("converts a TODO issue to a Bugzilla create payload", () => {
|
||||
const issue: Issue = {
|
||||
id: 1,
|
||||
type: "feature",
|
||||
title: "Add streaming parser",
|
||||
status: "open",
|
||||
priority: "high",
|
||||
created: "2026-02-05",
|
||||
relationships: { dependsOn: [3] },
|
||||
description: "Implement streaming JSON parser.",
|
||||
body: "",
|
||||
}
|
||||
|
||||
const payload = issueToBugzillaCreate(issue, "MyProduct", "MyComponent", "https://example.com/TODO#1")
|
||||
expect(payload.product).toBe("MyProduct")
|
||||
expect(payload.component).toBe("MyComponent")
|
||||
expect(payload.summary).toBe("Add streaming parser")
|
||||
expect(payload.url).toBe("https://example.com/TODO#1")
|
||||
expect(payload.priority).toBe("Highest")
|
||||
expect(payload.severity).toBe("enhancement")
|
||||
expect(payload.depends_on).toEqual([3])
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveProductComponent", () => {
|
||||
const tracker: BugzillaTracker = {
|
||||
url: "https://bugzilla.example.com",
|
||||
mappings: [
|
||||
{ module: "General", product: "MyProject", component: "General" },
|
||||
{ module: "Frontend", product: "MyProject", component: "Frontend" },
|
||||
],
|
||||
}
|
||||
|
||||
it("resolves product/component from issue module", () => {
|
||||
const issue = { module: "Frontend" } as Issue
|
||||
const result = resolveProductComponent(issue, tracker)
|
||||
expect(result).toEqual({ product: "MyProject", component: "Frontend" })
|
||||
})
|
||||
|
||||
it("falls back to first mapping when no module set", () => {
|
||||
const issue = {} as Issue
|
||||
const result = resolveProductComponent(issue, tracker)
|
||||
expect(result).toEqual({ product: "MyProject", component: "General" })
|
||||
})
|
||||
|
||||
it("returns null when no tracker", () => {
|
||||
expect(resolveProductComponent({} as Issue, undefined)).toBeNull()
|
||||
})
|
||||
})
|
||||
44
tests/lib/bugzilla/origin.test.ts
Normal file
44
tests/lib/bugzilla/origin.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import {
|
||||
normalizeRemoteUrl,
|
||||
buildOriginUrl,
|
||||
} from "../../../lib/bugzilla/origin"
|
||||
|
||||
describe("normalizeRemoteUrl", () => {
|
||||
it("passes HTTPS URLs through", () => {
|
||||
expect(normalizeRemoteUrl("https://github.com/user/repo"))
|
||||
.toBe("https://github.com/user/repo")
|
||||
})
|
||||
|
||||
it("strips trailing .git", () => {
|
||||
expect(normalizeRemoteUrl("https://github.com/user/repo.git"))
|
||||
.toBe("https://github.com/user/repo")
|
||||
})
|
||||
|
||||
it("converts SSH URLs to HTTPS", () => {
|
||||
expect(normalizeRemoteUrl("git@github.com:user/repo.git"))
|
||||
.toBe("https://github.com/user/repo")
|
||||
})
|
||||
|
||||
it("converts Bitbucket SSH URLs", () => {
|
||||
expect(normalizeRemoteUrl("git@bitbucket.org:org/project.git"))
|
||||
.toBe("https://bitbucket.org/org/project")
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildOriginUrl", () => {
|
||||
it("builds Bitbucket URL with src/<branch>", () => {
|
||||
expect(buildOriginUrl("git@bitbucket.org:byteb4rb1e/mime-todo-spec.git", "master", "TODO", 1))
|
||||
.toBe("https://bitbucket.org/byteb4rb1e/mime-todo-spec/src/master/TODO#1")
|
||||
})
|
||||
|
||||
it("builds GitHub URL with blob/<branch>", () => {
|
||||
expect(buildOriginUrl("https://github.com/user/repo", "main", "TODO", 42))
|
||||
.toBe("https://github.com/user/repo/blob/main/TODO#42")
|
||||
})
|
||||
|
||||
it("builds GitLab URL with -/blob/<branch>", () => {
|
||||
expect(buildOriginUrl("https://gitlab.com/org/repo", "develop", "TODO", 5))
|
||||
.toBe("https://gitlab.com/org/repo/-/blob/develop/TODO#5")
|
||||
})
|
||||
})
|
||||
57
tests/lib/cli.test.ts
Normal file
57
tests/lib/cli.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { CLICommand } from "../../lib/cli/CLICommand"
|
||||
|
||||
class TestLeafCommand extends CLICommand {
|
||||
readonly name = "leaf"
|
||||
readonly help = "A leaf command"
|
||||
readonly description = "Test leaf"
|
||||
|
||||
async execute(): Promise<number> {
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
class TestChildA extends CLICommand {
|
||||
readonly name = "child-a"
|
||||
readonly help = "Child A"
|
||||
readonly description = "Test child A"
|
||||
}
|
||||
|
||||
class TestChildB extends CLICommand {
|
||||
readonly name = "child-b"
|
||||
readonly help = "Child B"
|
||||
readonly description = "Test child B"
|
||||
}
|
||||
|
||||
class TestBranchCommand extends CLICommand {
|
||||
readonly name = "branch"
|
||||
readonly help = "A branch command"
|
||||
readonly description = "Test branch"
|
||||
|
||||
static override _subcommands = [TestChildA, TestChildB]
|
||||
}
|
||||
|
||||
describe("CLICommand", () => {
|
||||
it("leaf command returns exit code from execute()", async () => {
|
||||
const cmd = new TestLeafCommand()
|
||||
expect(await cmd.execute({} as any)).toBe(42)
|
||||
})
|
||||
|
||||
it("branch command has subcommands", () => {
|
||||
expect(TestBranchCommand._subcommands).toHaveLength(2)
|
||||
expect(new TestBranchCommand().name).toBe("branch")
|
||||
})
|
||||
|
||||
it("base CLICommand.execute returns 0", async () => {
|
||||
const cmd = new TestChildA()
|
||||
expect(await cmd.execute({} as any)).toBe(0)
|
||||
})
|
||||
|
||||
it("subcommands are constructable", () => {
|
||||
for (const Sub of TestBranchCommand._subcommands) {
|
||||
const instance = new Sub()
|
||||
expect(instance.name).toBeTruthy()
|
||||
expect(instance.help).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
55
tests/lib/file.test.ts
Normal file
55
tests/lib/file.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as fs from "fs"
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { preprocessTODO, parseTodoFile } from "../../lib/file"
|
||||
|
||||
|
||||
describe("parseTodoFile", () => {
|
||||
it("parses full TODO file end-to-end", async () => {
|
||||
const todo = await parseTodoFile("tests/_mocks/todo-basic.txt")
|
||||
|
||||
expect(todo.sprints.length).toBe(2)
|
||||
|
||||
expect(todo.issues.length).toBe(2)
|
||||
|
||||
// expect(todo.issues[0].title).toBe("Add streaming parser")
|
||||
expect(todo.sprints[0].name).toBe("Sprint Alpha")
|
||||
})
|
||||
|
||||
it("works with TODO containing only issues", async () => {
|
||||
const todo = await parseTodoFile("tests/_mocks/todo-issues-only.txt")
|
||||
expect(todo.sprints.length).toBe(0)
|
||||
// expect(todo.issues.length).toBe(1) - skipping for now
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe("preprocessTODO", () => {
|
||||
it("wraps TODO into MIME and puts sprints first", () => {
|
||||
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
||||
const mime = preprocessTODO(raw)
|
||||
|
||||
expect(mime).toContain("MIME-Version: 1.0")
|
||||
expect(mime).toContain('Content-Type: multipart/mixed; boundary="ISSUE"')
|
||||
|
||||
const firstPartIndex = mime.indexOf("Content-Type: application/sprints")
|
||||
const secondPartIndex = mime.indexOf("Content-Type: application/issue")
|
||||
|
||||
expect(firstPartIndex).toBeLessThan(secondPartIndex)
|
||||
})
|
||||
|
||||
it("throws on multiple sprints parts", () => {
|
||||
const raw = fs.readFileSync("tests/_mocks/todo-multiple-sprints.txt", "utf-8")
|
||||
expect(() => preprocessTODO(raw)).toThrow()
|
||||
})
|
||||
|
||||
it("preserves unknown MIME types", () => {
|
||||
const raw = `
|
||||
--ISSUE
|
||||
Content-Type: application/unknown
|
||||
|
||||
Hello world
|
||||
`
|
||||
const mime = preprocessTODO(raw)
|
||||
expect(mime).toContain("application/unknown")
|
||||
})
|
||||
})
|
||||
57
tests/lib/issue.test.ts
Normal file
57
tests/lib/issue.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import * as fs from "fs"
|
||||
import { parseIssue, validateStatusTransition } from "../../lib/issue"
|
||||
|
||||
describe("parseIssue", () => {
|
||||
it("parses all required fields", () => {
|
||||
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
||||
const issueText = raw.split("Content-Type: application/issue")[1]
|
||||
const issue = parseIssue(issueText)
|
||||
|
||||
expect(issue.id).toBe(1)
|
||||
expect(issue.type).toBe("feature")
|
||||
expect(issue.status).toBe("open")
|
||||
expect(issue.priority).toBe("high")
|
||||
expect(issue.description).toContain("Implement streaming JSON parser.")
|
||||
})
|
||||
|
||||
it("parses empty relationships", () => {
|
||||
const raw = `
|
||||
ID: 2
|
||||
Type: bugfix
|
||||
Title: T
|
||||
Status: open
|
||||
Priority: low
|
||||
Created: 2026-02-01
|
||||
Relationships:
|
||||
Description: X
|
||||
`
|
||||
const issue = parseIssue(raw)
|
||||
expect(issue.relationships).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateStatusTransition", () => {
|
||||
it("allows valid transitions", () => {
|
||||
expect(validateStatusTransition("open", "in-progress")).toBeNull()
|
||||
expect(validateStatusTransition("open", "hold")).toBeNull()
|
||||
expect(validateStatusTransition("open", "cancelled")).toBeNull()
|
||||
expect(validateStatusTransition("in-progress", "done")).toBeNull()
|
||||
expect(validateStatusTransition("in-progress", "open")).toBeNull()
|
||||
expect(validateStatusTransition("done", "open")).toBeNull()
|
||||
expect(validateStatusTransition("cancelled", "open")).toBeNull()
|
||||
expect(validateStatusTransition("hold", "in-progress")).toBeNull()
|
||||
})
|
||||
|
||||
it("rejects invalid transitions", () => {
|
||||
expect(validateStatusTransition("open", "done")).not.toBeNull()
|
||||
expect(validateStatusTransition("done", "in-progress")).not.toBeNull()
|
||||
expect(validateStatusTransition("done", "cancelled")).not.toBeNull()
|
||||
expect(validateStatusTransition("cancelled", "done")).not.toBeNull()
|
||||
})
|
||||
|
||||
it("allows no-op (same status)", () => {
|
||||
expect(validateStatusTransition("open", "open")).toBeNull()
|
||||
expect(validateStatusTransition("done", "done")).toBeNull()
|
||||
})
|
||||
})
|
||||
129
tests/lib/serializer.test.ts
Normal file
129
tests/lib/serializer.test.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import * as fs from "fs"
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { parseTodoFile } from "../../lib/file"
|
||||
import {
|
||||
serializeTodoFile,
|
||||
serializeIssue,
|
||||
serializeSprints,
|
||||
serializeRelationships,
|
||||
} from "../../lib/serializer"
|
||||
|
||||
describe("serializeRelationships", () => {
|
||||
it("serializes empty relationships", () => {
|
||||
expect(serializeRelationships({})).toBe("")
|
||||
})
|
||||
|
||||
it("serializes single relationship", () => {
|
||||
expect(serializeRelationships({ dependsOn: [3] })).toBe("dependsOn:3")
|
||||
})
|
||||
|
||||
it("serializes multiple relationships", () => {
|
||||
const result = serializeRelationships({ dependsOn: [3, 4], blocks: [7] })
|
||||
expect(result).toContain("dependsOn:3 4")
|
||||
expect(result).toContain("blocks:7")
|
||||
})
|
||||
})
|
||||
|
||||
describe("serializeSprints", () => {
|
||||
it("serializes empty sprints", () => {
|
||||
expect(serializeSprints([])).toBe("Sprints:")
|
||||
})
|
||||
|
||||
it("serializes sprint list", () => {
|
||||
const result = serializeSprints([
|
||||
{ name: "Alpha", start: "2026-02-01", end: "2026-02-14" },
|
||||
])
|
||||
expect(result).toContain("Sprints:")
|
||||
expect(result).toContain("- Name: Alpha")
|
||||
expect(result).toContain("Range: 2026-02-01..2026-02-14")
|
||||
})
|
||||
})
|
||||
|
||||
describe("serializeIssue", () => {
|
||||
it("serializes an issue with all fields", () => {
|
||||
const result = serializeIssue({
|
||||
id: 1,
|
||||
type: "feature",
|
||||
title: "Test",
|
||||
status: "open",
|
||||
priority: "high",
|
||||
created: "2026-02-05",
|
||||
relationships: { dependsOn: [3] },
|
||||
description: "A test issue",
|
||||
body: "",
|
||||
})
|
||||
expect(result).toContain("ID: 1")
|
||||
expect(result).toContain("Type: feature")
|
||||
expect(result).toContain("Title: Test")
|
||||
expect(result).toContain("Status: open")
|
||||
expect(result).toContain("Priority: high")
|
||||
expect(result).toContain("Created: 2026-02-05")
|
||||
expect(result).toContain("Relationships: dependsOn:3")
|
||||
expect(result).toContain("Description: A test issue")
|
||||
})
|
||||
|
||||
it("serializes multi-line description", () => {
|
||||
const result = serializeIssue({
|
||||
id: 1,
|
||||
type: "feature",
|
||||
title: "Test",
|
||||
status: "open",
|
||||
priority: "high",
|
||||
created: "2026-02-05",
|
||||
relationships: {},
|
||||
description: "Line one\nLine two",
|
||||
body: "",
|
||||
})
|
||||
expect(result).toContain("Description: Line one")
|
||||
expect(result).toContain(" Line two")
|
||||
})
|
||||
})
|
||||
|
||||
describe("round-trip", () => {
|
||||
it("parse → serialize → parse produces equivalent data", async () => {
|
||||
const todo1 = await parseTodoFile("tests/_mocks/todo-basic.txt")
|
||||
const serialized = serializeTodoFile(todo1)
|
||||
// Write to temp location for re-parsing
|
||||
const tmpPath = "tests/_mocks/_roundtrip.txt"
|
||||
fs.writeFileSync(tmpPath, serialized)
|
||||
|
||||
try {
|
||||
const todo2 = await parseTodoFile(tmpPath)
|
||||
expect(todo2.sprints.length).toBe(todo1.sprints.length)
|
||||
expect(todo2.issues.length).toBe(todo1.issues.length)
|
||||
|
||||
for (let i = 0; i < todo1.sprints.length; i++) {
|
||||
expect(todo2.sprints[i].name).toBe(todo1.sprints[i].name)
|
||||
expect(todo2.sprints[i].start).toBe(todo1.sprints[i].start)
|
||||
expect(todo2.sprints[i].end).toBe(todo1.sprints[i].end)
|
||||
}
|
||||
|
||||
for (let i = 0; i < todo1.issues.length; i++) {
|
||||
expect(todo2.issues[i].id).toBe(todo1.issues[i].id)
|
||||
expect(todo2.issues[i].type).toBe(todo1.issues[i].type)
|
||||
expect(todo2.issues[i].title).toBe(todo1.issues[i].title)
|
||||
expect(todo2.issues[i].status).toBe(todo1.issues[i].status)
|
||||
expect(todo2.issues[i].priority).toBe(todo1.issues[i].priority)
|
||||
expect(todo2.issues[i].created).toBe(todo1.issues[i].created)
|
||||
expect(todo2.issues[i].description).toBe(todo1.issues[i].description)
|
||||
}
|
||||
} finally {
|
||||
fs.unlinkSync(tmpPath)
|
||||
}
|
||||
})
|
||||
|
||||
it("round-trips issues-only file", async () => {
|
||||
const todo1 = await parseTodoFile("tests/_mocks/todo-issues-only.txt")
|
||||
const serialized = serializeTodoFile(todo1)
|
||||
const tmpPath = "tests/_mocks/_roundtrip2.txt"
|
||||
fs.writeFileSync(tmpPath, serialized)
|
||||
|
||||
try {
|
||||
const todo2 = await parseTodoFile(tmpPath)
|
||||
expect(todo2.issues.length).toBe(todo1.issues.length)
|
||||
expect(todo2.sprints.length).toBe(0)
|
||||
} finally {
|
||||
fs.unlinkSync(tmpPath)
|
||||
}
|
||||
})
|
||||
})
|
||||
22
tests/lib/sprint.test.ts
Normal file
22
tests/lib/sprint.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as fs from "fs"
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { parseSprints } from "../../lib/sprint"
|
||||
|
||||
describe("parseSprints", () => {
|
||||
it("parses compact and expanded sprint entries", () => {
|
||||
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
||||
const sprintsText = raw.split("Content-Type: application/sprints")[1]
|
||||
const sprints = parseSprints(sprintsText)
|
||||
|
||||
expect(sprints.length).toBe(2)
|
||||
expect(sprints[0].name).toBe("Sprint Alpha")
|
||||
expect(sprints[0].start).toBe("2026-02-01")
|
||||
expect(sprints[0].end).toBe("2026-02-14")
|
||||
})
|
||||
|
||||
it("handles TODO with no sprints", () => {
|
||||
const raw = fs.readFileSync("tests/_mocks/todo-no-sprints.txt", "utf-8")
|
||||
const sprints = parseSprints(raw)
|
||||
expect(sprints.length).toBe(0)
|
||||
})
|
||||
})
|
||||
205
tests/lib/tracker.test.ts
Normal file
205
tests/lib/tracker.test.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue