141 lines
No EOL
4.6 KiB
TypeScript
141 lines
No EOL
4.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { computeProjectMetrics, type MetricCommit } from "@/lib/metrics/compute";
|
|
import { DEFAULT_SESSION_CONSTANTS } from "@/lib/metrics/sessions";
|
|
|
|
function commit(overrides: Partial<MetricCommit>): MetricCommit {
|
|
return {
|
|
type: "feat",
|
|
scope: "stock",
|
|
authoredAt: "2026-09-01T10:00:00Z",
|
|
isMerge: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("computeProjectMetrics", () => {
|
|
it("is empty and safe for a project with no commits", () => {
|
|
const m = computeProjectMetrics([], DEFAULT_SESSION_CONSTANTS);
|
|
expect(m.totalCommits).toBe(0);
|
|
expect(m.features).toBe(0);
|
|
expect(m.activeDays).toBe(0);
|
|
expect(m.workdays).toBe(0);
|
|
expect(m.estimatedMinutes).toBe(0);
|
|
expect(m.firstCommit).toBe("");
|
|
});
|
|
|
|
it("excludes merges from every count", () => {
|
|
const m = computeProjectMetrics(
|
|
[commit({}), commit({ isMerge: true, type: "merge" })],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
expect(m.totalCommits).toBe(1);
|
|
expect(m.mergeCommits).toBe(1);
|
|
expect(m.byType.find((t) => t.key === "merge")).toBeUndefined();
|
|
});
|
|
|
|
it("counts features and fixes", () => {
|
|
const m = computeProjectMetrics(
|
|
[commit({ type: "feat" }), commit({ type: "feat" }), commit({ type: "fix" })],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
expect(m.features).toBe(2);
|
|
expect(m.fixes).toBe(1);
|
|
});
|
|
|
|
it("ranks types and scopes by volume", () => {
|
|
const m = computeProjectMetrics(
|
|
[
|
|
commit({ type: "feat", scope: "stock" }),
|
|
commit({ type: "feat", scope: "stock" }),
|
|
commit({ type: "fix", scope: "quotes" }),
|
|
],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
expect(m.byType[0]).toEqual({ key: "feat", count: 2 });
|
|
expect(m.byScope[0]).toEqual({ key: "stock", count: 2 });
|
|
});
|
|
|
|
it("omits commits with no scope from the scope breakdown", () => {
|
|
// An empty bar labelled "" tells the reader nothing and takes the place of
|
|
// a real feature area.
|
|
const m = computeProjectMetrics(
|
|
[commit({ scope: "" }), commit({ scope: "stock" })],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
expect(m.byScope).toEqual([{ key: "stock", count: 1 }]);
|
|
});
|
|
|
|
it("counts distinct active days, not commits", () => {
|
|
const m = computeProjectMetrics(
|
|
[
|
|
commit({ authoredAt: "2026-09-01T10:00:00Z" }),
|
|
commit({ authoredAt: "2026-09-01T16:00:00Z" }),
|
|
commit({ authoredAt: "2026-09-03T10:00:00Z" }),
|
|
],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
expect(m.activeDays).toBe(2);
|
|
expect(m.totalCommits).toBe(3);
|
|
});
|
|
|
|
it("spans from the first commit to the last", () => {
|
|
const m = computeProjectMetrics(
|
|
[
|
|
commit({ authoredAt: "2026-09-03T10:00:00Z" }),
|
|
commit({ authoredAt: "2026-05-24T10:00:00Z" }),
|
|
],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
expect(m.firstCommit).toBe("2026-05-24");
|
|
expect(m.lastCommit).toBe("2026-09-03");
|
|
});
|
|
|
|
it("counts workdays across the span, not calendar days", () => {
|
|
const m = computeProjectMetrics(
|
|
[
|
|
commit({ authoredAt: "2026-05-24T10:00:00Z" }),
|
|
commit({ authoredAt: "2026-09-08T10:00:00Z" }),
|
|
],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
// The same window as the Hermes build log.
|
|
expect(m.calendarDays).toBe(108);
|
|
expect(m.workdays).toBe(75);
|
|
});
|
|
|
|
it("builds a per-day activity map", () => {
|
|
const m = computeProjectMetrics(
|
|
[
|
|
commit({ authoredAt: "2026-09-01T10:00:00Z" }),
|
|
commit({ authoredAt: "2026-09-01T16:00:00Z" }),
|
|
commit({ authoredAt: "2026-09-03T10:00:00Z" }),
|
|
],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
expect(m.activity["2026-09-01"]).toBe(2);
|
|
expect(m.activity["2026-09-03"]).toBe(1);
|
|
expect(m.activity["2026-09-02"]).toBeUndefined();
|
|
});
|
|
|
|
it("groups estimated minutes by month", () => {
|
|
const m = computeProjectMetrics(
|
|
[
|
|
commit({ authoredAt: "2026-08-01T10:00:00Z" }),
|
|
commit({ authoredAt: "2026-09-01T10:00:00Z" }),
|
|
],
|
|
DEFAULT_SESSION_CONSTANTS
|
|
);
|
|
const months = m.monthly.map((x) => x.month);
|
|
expect(months).toEqual(["2026-08", "2026-09"]);
|
|
expect(m.monthly.every((x) => x.minutes > 0)).toBe(true);
|
|
});
|
|
|
|
it("records the constants it used", () => {
|
|
// A figure on screen must be explicable by the settings that produced it,
|
|
// not by whatever the settings happen to be when someone later asks.
|
|
const custom = { sessionGapMinutes: 90, preSessionMinutes: 10, postSessionMinutes: 5 };
|
|
const m = computeProjectMetrics([commit({})], custom);
|
|
expect(m.constants).toEqual(custom);
|
|
expect(m.estimatedMinutes).toBe(15);
|
|
});
|
|
}); |