import { test, expect } from "vitest";
import { execFileSync } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
lintCommitSubject,
parseGitLogOutput,
parseNameOnlyOutput,
isExempt,
runCommitMessageLint,
type CommitRecord,
} from "../commit-message-lint.ts";
import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-shared/kernel";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
/*
RFC-0265: fixture tests for commit.message.lint. Written BEFORE the git
integration was wired up, per the RFC's rollout note — message-string
fixtures exercise the rules with no real git needed; one integration test
runs over a temp git repository for range resolution + merge exemption.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- Fixtures stay inline and minimal so each case states its own setup.
- RFC-1311: Compass header sweep — declares the commit suite's contract for test-file scaffold coverage.
*/
// @ai-invariant fixtures must not couple to internals beyond the public surface under test
function commit(overrides: Partial): CommitRecord {
return { hash: "abcdef1234567890", parents: ["parent1"], subject: "", body: "", ...overrides };
}
test("COMMIT-01: fails a subject over 120 characters", () => {
const subject = "feat: " + "x".repeat(120);
const findings = lintCommitSubject(commit({ subject }));
expect(findings.some((f) => f.ruleId === "COMMIT-01")).toBeTruthy();
});
test("COMMIT-01: a subject at 120 characters passes", () => {
const subject = "feat: " + "x".repeat(114); // 6 + 114 = 120
expect(subject.length).toBe(120);
const findings = lintCommitSubject(commit({ subject }));
expect(!findings.some((f) => f.ruleId === "COMMIT-01")).toBeTruthy();
});
test("COMMIT-02/03: three real offending subjects from the RFC each fail", () => {
const offenders = [
"Found the bug — `app.qa.validate` passes `args:` incorrectly to the handler",
"Good — `PlanetCatalog` and `MoonCatalog` are exported from the cosmic barrel",
"**05-audit — пройдено (з очікуваними build-deferred блокерами).**",
];
for (const subject of offenders) {
const findings = lintCommitSubject(commit({ subject }));
expect(findings.some((f) => f.ruleId === "COMMIT-02" || f.ruleId === "COMMIT-03")).toBeTruthy();
}
});
test("five well-formed subjects pass with zero findings", () => {
const wellFormed = [
"feat: add maintenance debt queues",
"fix(kernel): resolve flag parsing edge case",
"docs: update AGENTS.md subpath map",
"refactor(share): split barrel into subpaths",
"chore: bump lockfile",
];
for (const subject of wellFormed) {
const findings = lintCommitSubject(commit({ subject }));
expect(findings).toEqual([]);
}
});
test("COMMIT-04: warns when packages/os/** is touched without an RFC id reference", () => {
const findings = lintCommitSubject(commit({ subject: "fix(kernel): patch the flag resolver" }), [
"packages/os/site-kernel/src/runtime.ts",
]);
expect(findings.some((f) => f.ruleId === "COMMIT-04" && f.severity === "warning")).toBeTruthy();
});
test("COMMIT-04: silent when the subject references an RFC id", () => {
const findings = lintCommitSubject(
commit({ subject: "feat(kernel): implement rfc-0265 commit lint" }),
["packages/os/site-kernel/src/commit-message-lint.ts"],
);
expect(!findings.some((f) => f.ruleId === "COMMIT-04")).toBeTruthy();
});
test("COMMIT-04: silent when body references an RFC id", () => {
const findings = lintCommitSubject(
commit({ subject: "fix(kernel): patch the flag resolver", body: "See rfc-0260 for context." }),
["packages/os/site-kernel/src/runtime.ts"],
);
expect(!findings.some((f) => f.ruleId === "COMMIT-04")).toBeTruthy();
});
test("isExempt: a merge commit (>1 parent) is exempt regardless of subject shape", () => {
const findings = lintCommitSubject(
commit({ parents: ["p1", "p2"], subject: "Found the bug and merged the fix" }),
);
expect(findings).toEqual([]);
expect(isExempt({ subject: "Merge branch x", parents: ["p1", "p2"] })).toBe(true);
});
test("isExempt: a git-generated revert is exempt", () => {
expect(isExempt({ subject: 'Revert "feat: add thing"', parents: ["p1"] })).toBe(true);
});
test("parseGitLogOutput: parses hash, parents, subject, and body", () => {
const raw = [
"\x1eabc123\x1fparent1 parent2\x1ffeat: do a thing\x1fBody line one.\nBody line two.",
].join("");
const parsed = parseGitLogOutput(raw);
expect(parsed.length).toBe(1);
expect(parsed[0]?.hash).toBe("abc123");
expect(parsed[0]?.parents).toEqual(["parent1", "parent2"]);
expect(parsed[0]?.subject).toBe("feat: do a thing");
expect(parsed[0]?.body).toBe("Body line one.\nBody line two.");
});
test("parseGitLogOutput: parses multiple records", () => {
const raw = ["\x1eabc123\x1fp1\x1ffeat: one\x1f", "\x1edef456\x1fp2\x1ffeat: two\x1f"].join("");
const parsed = parseGitLogOutput(raw);
expect(parsed.length).toBe(2);
expect(parsed[0]?.hash).toBe("abc123");
expect(parsed[1]?.hash).toBe("def456");
});
test("parseNameOnlyOutput: groups changed files by commit hash", () => {
const marker = "\x02COMMIT:";
const raw = [
`${marker}abc123\npackages/os/site-kernel/src/a.ts\npackages/os/site-kernel/src/b.ts\n`,
`${marker}def456\ndocs/rfcs/rfc-0265.md\n`,
].join("");
const parsed = parseNameOnlyOutput(raw, marker);
expect(parsed.get("abc123")).toEqual([
"packages/os/site-kernel/src/a.ts",
"packages/os/site-kernel/src/b.ts",
]);
expect(parsed.get("def456")).toEqual(["docs/rfcs/rfc-0265.md"]);
});
// ---------------------------------------------------------------------------
// Integration: a real temp git repository
// ---------------------------------------------------------------------------
const logger = {
section() {},
info() {},
warn() {},
error() {},
success() {},
getEvents() {
return [];
},
};
function ctx(root: string): KernelRuntimeContext {
return {
io: createDefaultIO().io,
workspaceRoot: root,
siteExplicit: false,
logger,
dryRun: false,
outputFormat: "json",
} as unknown as KernelRuntimeContext;
}
function git(root: string, args: string[]): void {
execFileSync("git", args, { cwd: root, stdio: "pipe" });
}
test("runCommitMessageLint: integration over a temp git repo — range resolution + merge exemption", async () => {
const root = await mkdtemp(join(tmpdir(), "commit-lint-repo-"));
try {
git(root, ["init", "-q", "-b", "main"]);
git(root, ["config", "user.email", "test@example.com"]);
git(root, ["config", "user.name", "Test"]);
await writeFile(join(root, "a.txt"), "1", "utf8");
git(root, ["add", "."]);
git(root, ["commit", "-q", "-m", "chore: initial commit"]);
git(root, ["remote", "add", "origin", root]);
git(root, ["fetch", "-q", "origin"]);
// pretend origin/main tracks the initial commit
git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
await writeFile(join(root, "b.txt"), "2", "utf8");
git(root, ["add", "."]);
git(root, ["commit", "-q", "-m", "Found the issue and fixed it immediately"]);
const input = { argv: [], flags: {} } as unknown as KernelCommandInput;
const result = await runCommitMessageLint(input, ctx(root));
expect(result.exitCode).toBe(1);
const diags = (result.data as { diagnostics: Array<{ ruleId: string }> }).diagnostics;
expect(diags.some((d) => d.ruleId === "COMMIT-03")).toBeTruthy();
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("runCommitMessageLint: passes when the range has only well-formed commits", async () => {
const root = await mkdtemp(join(tmpdir(), "commit-lint-repo-"));
try {
git(root, ["init", "-q", "-b", "main"]);
git(root, ["config", "user.email", "test@example.com"]);
git(root, ["config", "user.name", "Test"]);
await writeFile(join(root, "a.txt"), "1", "utf8");
git(root, ["add", "."]);
git(root, ["commit", "-q", "-m", "chore: initial commit"]);
git(root, ["remote", "add", "origin", root]);
git(root, ["fetch", "-q", "origin"]);
git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
await writeFile(join(root, "b.txt"), "2", "utf8");
git(root, ["add", "."]);
git(root, ["commit", "-q", "-m", "feat: add a widget"]);
const input = { argv: [], flags: {} } as unknown as KernelCommandInput;
const result = await runCommitMessageLint(input, ctx(root));
expect(result.exitCode ?? 0).toBe(0);
} finally {
await rm(root, { recursive: true, force: true });
}
});