// 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: "t@example.com", date }, committer: { name: "test", email: "t@example.com", 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 ")).toBe("claude-code"); }); test("recognises a Cursor commit", () => { expect(detectAgent("Fix bug\n\nCo-Authored-By: Cursor ")).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); }); });