import { execSync } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { generateChangelog, parseCommitLine } from "./index.js"; // Helper to create temp git repos function createTmpGitRepo(): string { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "changelog-test-")); execSync("git init", { cwd: tmpDir, stdio: "pipe" }); execSync("git config user.email test@test.com", { cwd: tmpDir, stdio: "pipe", }); execSync("git config user.name Test", { cwd: tmpDir, stdio: "pipe" }); return tmpDir; } function commit(dir: string, message: string, file = "test.txt"): void { const filePath = path.join(dir, file); fs.writeFileSync(filePath, `${message}\n`, "utf-8"); execSync(`git add ${file}`, { cwd: dir, stdio: "pipe" }); execSync(`git commit -m "${message}"`, { cwd: dir, stdio: "pipe" }); } function tag(dir: string, tagName: string): void { execSync(`git tag ${tagName}`, { cwd: dir, stdio: "pipe" }); } // ========================================= // parseCommitLine tests // ========================================= describe("parseCommitLine", () => { it("should parse a standard conventional commit", () => { const line = "abc1234|Author|2026-01-01|feat: add new feature"; const result = parseCommitLine(line); expect(result).not.toBeNull(); expect(result?.type).toBe("feat"); expect(result?.scope).toBeNull(); expect(result?.subject).toBe("add new feature"); expect(result?.breaking).toBe(false); expect(result?.hash).toBe("abc1234"); }); it("should parse a scoped commit", () => { const line = "def5678|Author|2026-01-01|fix(api): handle null response"; const result = parseCommitLine(line); expect(result).not.toBeNull(); expect(result?.type).toBe("fix"); expect(result?.scope).toBe("api"); expect(result?.subject).toBe("handle null response"); }); it("should detect breaking changes with bang", () => { const line = "ghi9012|Author|2026-01-01|feat!: redesign config format"; const result = parseCommitLine(line); expect(result).not.toBeNull(); expect(result?.type).toBe("feat"); expect(result?.breaking).toBe(true); expect(result?.subject).toBe("redesign config format"); }); it("should parse scoped breaking change", () => { const line = "jkl3456|Author|2026-01-01|feat(core)!: change API surface"; const result = parseCommitLine(line); expect(result).not.toBeNull(); expect(result?.type).toBe("feat"); expect(result?.scope).toBe("core"); expect(result?.breaking).toBe(true); }); it("should handle non-conventional commits", () => { const line = "mno7890|Author|2026-01-01|random commit message"; const result = parseCommitLine(line); expect(result).not.toBeNull(); expect(result?.type).toBe("other"); expect(result?.subject).toBe("random commit message"); }); it("should return null for malformed lines", () => { const result = parseCommitLine("bad"); expect(result).toBeNull(); }); }); // ========================================= // generateChangelog tests // ========================================= describe("generateChangelog", () => { let tmpDir: string; beforeEach(() => { tmpDir = createTmpGitRepo(); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("should handle empty git history", () => { const result = generateChangelog(tmpDir); expect(result.total_commits).toBe(0); expect(result.markdown).toContain("No commits found"); }); it("should group commits by type", () => { commit(tmpDir, "feat: add login", "login.txt"); commit(tmpDir, "fix: fix crash on startup", "fix.txt"); commit(tmpDir, "docs: update README", "docs.txt"); const result = generateChangelog(tmpDir); expect(result.total_commits).toBe(3); expect(result.entries.length).toBeGreaterThanOrEqual(3); const types = result.entries.map((e) => e.type); expect(types).toContain("feat"); expect(types).toContain("fix"); expect(types).toContain("docs"); }); it("should detect breaking changes", () => { commit(tmpDir, "feat: normal feature", "a.txt"); commit(tmpDir, "feat!: breaking change", "b.txt"); const result = generateChangelog(tmpDir); expect(result.breaking_changes.length).toBe(1); expect(result.breaking_changes[0].subject).toBe("breaking change"); expect(result.markdown).toContain("BREAKING CHANGES"); }); it("should use version label when provided", () => { commit(tmpDir, "feat: something", "a.txt"); const result = generateChangelog(tmpDir, undefined, undefined, "v2.0.0"); expect(result.version).toBe("v2.0.0"); expect(result.markdown).toContain("v2.0.0"); }); it("should detect version from latest tag", () => { commit(tmpDir, "feat: first", "a.txt"); tag(tmpDir, "v1.0.0"); commit(tmpDir, "feat: second", "b.txt"); const result = generateChangelog(tmpDir); expect(result.version).toBe("v1.0.0"); }); it("should respect from_ref and to_ref", () => { commit(tmpDir, "feat: first", "a.txt"); tag(tmpDir, "v0.1.0"); commit(tmpDir, "feat: second", "b.txt"); commit(tmpDir, "fix: third", "c.txt"); const result = generateChangelog(tmpDir, "v0.1.0"); expect(result.total_commits).toBe(2); }); it("should produce valid markdown", () => { commit(tmpDir, "feat(auth): add OAuth support", "a.txt"); commit(tmpDir, "fix: resolve memory leak", "b.txt"); const result = generateChangelog(tmpDir); expect(result.markdown).toContain("# Changelog"); expect(result.markdown).toContain("### Features"); expect(result.markdown).toContain("### Bug Fixes"); expect(result.markdown).toContain("add OAuth support"); }); it("should include scoped commits correctly", () => { commit(tmpDir, "feat(api): new endpoint", "a.txt"); const result = generateChangelog(tmpDir); expect(result.markdown).toContain("**api:**"); expect(result.markdown).toContain("new endpoint"); }); }); // ========================================= // Extension registration tests // ========================================= describe("extension registration", () => { it("should export a default function", async () => { const mod = await import("./index.js"); expect(typeof mod.default).toBe("function"); }); it("should export helper functions directly", async () => { const mod = await import("./index.js"); expect(typeof mod.parseCommitLine).toBe("function"); expect(typeof mod.generateChangelog).toBe("function"); expect(typeof mod.getGitLog).toBe("function"); expect(typeof mod.getLatestTag).toBe("function"); expect(typeof mod.getRepoUrl).toBe("function"); }); });