76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
|
|
import { describe, it, expect, beforeAll } from "vitest";
|
||
|
|
import PocketBase from "pocketbase";
|
||
|
|
|
||
|
|
import { recomputeMetrics } from "@/lib/metrics/store";
|
||
|
|
import { APP_URL, DEMO, adminPb, requireServices, signIn } from "./helpers";
|
||
|
|
|
||
|
|
let admin: PocketBase;
|
||
|
|
let projectId: string;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
await requireServices();
|
||
|
|
admin = await adminPb();
|
||
|
|
const project = await admin
|
||
|
|
.collection("projects")
|
||
|
|
.getFirstListItem(`slug = "${DEMO.projectSlug}"`);
|
||
|
|
projectId = project.id;
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("cached metrics agree with the stored commits", () => {
|
||
|
|
it("counts the same non-merge commits the database holds", async () => {
|
||
|
|
const metrics = await recomputeMetrics(projectId);
|
||
|
|
|
||
|
|
const all = await admin.collection("commits").getFullList({
|
||
|
|
filter: `project = "${projectId}"`,
|
||
|
|
fields: "is_merge",
|
||
|
|
});
|
||
|
|
const merges = all.filter((c) => c.is_merge).length;
|
||
|
|
|
||
|
|
expect(metrics.totalCommits).toBe(all.length - merges);
|
||
|
|
expect(metrics.mergeCommits).toBe(merges);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("stores the constants alongside the figures", async () => {
|
||
|
|
const cached = await admin
|
||
|
|
.collection("project_metrics")
|
||
|
|
.getFirstListItem(`project = "${projectId}"`);
|
||
|
|
const payload = cached.payload as { constants?: Record<string, number> };
|
||
|
|
|
||
|
|
expect(payload.constants).toBeDefined();
|
||
|
|
expect(payload.constants!.sessionGapMinutes).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("never estimates fewer minutes than sessions times padding", async () => {
|
||
|
|
// A sanity floor: every session contributes at least its padding, so a
|
||
|
|
// total below that means clustering silently dropped commits.
|
||
|
|
const cached = await admin
|
||
|
|
.collection("project_metrics")
|
||
|
|
.getFirstListItem(`project = "${projectId}"`);
|
||
|
|
const p = cached.payload as {
|
||
|
|
estimatedMinutes: number;
|
||
|
|
sessionCount: number;
|
||
|
|
constants: { preSessionMinutes: number; postSessionMinutes: number };
|
||
|
|
};
|
||
|
|
|
||
|
|
const floor =
|
||
|
|
p.sessionCount * (p.constants.preSessionMinutes + p.constants.postSessionMinutes);
|
||
|
|
expect(p.estimatedMinutes).toBeGreaterThanOrEqual(floor);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("the dashboard is visible to a client manager", () => {
|
||
|
|
it("renders with the methodology panel", async () => {
|
||
|
|
// Effort figures are shown to clients by explicit decision, so the panel
|
||
|
|
// that explains them must render for them too.
|
||
|
|
const cookie = await signIn(DEMO.managerEmail, DEMO.managerPassword);
|
||
|
|
const response = await fetch(
|
||
|
|
`${APP_URL}/p/${DEMO.clientSlug}/${DEMO.projectSlug}/dashboard`,
|
||
|
|
{ headers: { cookie } }
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(response.status).toBe(200);
|
||
|
|
const html = await response.text();
|
||
|
|
expect(html).toContain("How these numbers are worked out");
|
||
|
|
expect(html.toLowerCase()).toContain("estimated");
|
||
|
|
});
|
||
|
|
});
|