syntaxai/tdd.md · main · src / c14_real_reports.test.ts
// Sibling test for c32_real_reports.ts. buildLiveReports itself fans out
// to fetchRepoCommits (network) so its end-to-end shape is covered by
// the live /reports/live route. The pure helpers underneath — agent
// attribution from commit messages, and the 30-day daily sparkline —
// are unit-testable here.
import { describe, test, expect } from "bun:test";
import {
detectAgent,
buildTrend,
buildLiveReports,
} from "./c14_real_reports.ts";
import type { GithubCommit } from "./c14_github.ts";
const mkCommit = (date: string, message = ""): GithubCommit => ({
sha: "0".repeat(40),
commit: {
message,
author: { name: "test", email: "[email protected]", date },
committer: { name: "test", email: "[email protected]", date },
},
author: null,
committer: null,
} as unknown as GithubCommit);
describe("c32_real_reports — detectAgent", () => {
test("recognises a Claude Code commit via Co-Authored-By: Claude", () => {
expect(detectAgent("Add feature\n\nCo-Authored-By: Claude <noreply>")).toBe("claude-code");
});
test("recognises a Cursor commit", () => {
expect(detectAgent("Fix bug\n\nCo-Authored-By: Cursor <[email protected]>")).toBe("cursor");
});
test("recognises an Aider commit", () => {
expect(detectAgent("Refactor x\n\nCo-Authored-By: aider")).toBe("aider");
});
test("returns unknown when no recognised footer is present", () => {
expect(detectAgent("Just a commit")).toBe("unknown");
expect(detectAgent("")).toBe("unknown");
});
test("the regex is case-insensitive on the agent token", () => {
expect(detectAgent("Co-Authored-By: CLAUDE")).toBe("claude-code");
expect(detectAgent("co-authored-by: CURSOR")).toBe("cursor");
});
});
describe("c32_real_reports — buildTrend (30-day daily sparkline)", () => {
// Use today (UTC) as the anchor — the function compares against UTC
// midnight, so we need ISO strings that fall on the right days.
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const iso = (daysAgo: number): string => {
const d = new Date(today.getTime() - daysAgo * 24 * 60 * 60 * 1000);
return d.toISOString();
};
test("returns an array of `days` length", () => {
expect(buildTrend([], 30)).toHaveLength(30);
expect(buildTrend([], 7)).toHaveLength(7);
});
test("empty input flat-lines at zero", () => {
const trend = buildTrend([], 7);
expect(trend.every((n) => n === 0)).toBe(true);
});
test("a single commit today increments the last bucket", () => {
const trend = buildTrend([mkCommit(iso(0))], 7);
expect(trend[trend.length - 1]).toBe(1);
expect(trend.slice(0, -1).every((n) => n === 0)).toBe(true);
});
test("multiple commits on the same day stack in the same bucket", () => {
const trend = buildTrend([mkCommit(iso(0)), mkCommit(iso(0)), mkCommit(iso(0))], 7);
expect(trend[trend.length - 1]).toBe(3);
});
test("commits older than the window are dropped", () => {
const trend = buildTrend([mkCommit(iso(99))], 7);
expect(trend.every((n) => n === 0)).toBe(true);
});
test("a commit `daysAgo` lands at index `days - 1 - daysAgo`", () => {
const trend = buildTrend([mkCommit(iso(2))], 7);
// index 6 = today, 5 = yesterday, 4 = 2 days ago
expect(trend[4]).toBe(1);
});
});
describe("c32_real_reports — orchestrator entry point", () => {
test("buildLiveReports is exported as an async function", () => {
expect(typeof buildLiveReports).toBe("function");
// End-to-end coverage lives on /reports/live; this is the structural
// smoke that the export shape didn't drift. `.length` counts only
// non-default params (owner, repo) — perPage carries a default.
expect(buildLiveReports.length).toBe(2);
});
});