2026-09-19 11:30:37 +00:00
|
|
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
|
|
|
import PocketBase from "pocketbase";
|
|
|
|
|
|
2026-09-19 11:44:34 +00:00
|
|
|
import { APP_URL, DEMO, adminPb, requireServices, signIn } from "./helpers";
|
2026-09-19 11:30:37 +00:00
|
|
|
|
|
|
|
|
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("logWorkAction", () => {
|
|
|
|
|
const created: string[] = [];
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
for (const id of created) {
|
|
|
|
|
await admin.collection("commits").delete(id).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("refuses without a valid owner session", async () => {
|
|
|
|
|
// The action reads the session cookie via next/headers, which is not
|
|
|
|
|
// available outside a request in this harness — it must return a failure
|
|
|
|
|
// rather than silently succeed with no session.
|
|
|
|
|
const { logWorkAction } = await import("@/app/actions/log-work");
|
|
|
|
|
const result = await logWorkAction({
|
|
|
|
|
projectId,
|
|
|
|
|
clientSlug: DEMO.clientSlug,
|
|
|
|
|
projectSlug: DEMO.projectSlug,
|
|
|
|
|
date: "2026-09-19",
|
|
|
|
|
hours: 2,
|
|
|
|
|
message: "Should never be written",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.ok).toBe(false);
|
|
|
|
|
|
|
|
|
|
const rows = await admin.collection("commits").getFullList({
|
|
|
|
|
filter: `project = "${projectId}" && subject = "Should never be written"`,
|
|
|
|
|
});
|
|
|
|
|
expect(rows).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("computeProjectMetrics via recomputeMetrics", () => {
|
|
|
|
|
let commitId: string;
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
if (commitId) await admin.collection("commits").delete(commitId).catch(() => {});
|
2026-09-19 11:44:34 +00:00
|
|
|
const { recomputeMetrics } = await import("@/lib/metrics/store");
|
|
|
|
|
await recomputeMetrics(projectId).catch(() => {});
|
2026-09-19 11:30:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("adds a manual entry's declared minutes to estimatedMinutes", async () => {
|
2026-09-19 11:44:34 +00:00
|
|
|
const { recomputeMetrics } = await import("@/lib/metrics/store");
|
|
|
|
|
const base = await recomputeMetrics(projectId); // fresh baseline, not a possibly-stale cache
|
2026-09-19 11:30:37 +00:00
|
|
|
|
|
|
|
|
const commit = await admin.collection("commits").create({
|
|
|
|
|
project: projectId,
|
|
|
|
|
sha: `manual-test-${Date.now()}`,
|
|
|
|
|
short_sha: "manual",
|
|
|
|
|
message: "Integration test entry",
|
|
|
|
|
subject: "Integration test entry",
|
|
|
|
|
type: "",
|
|
|
|
|
scope: "",
|
|
|
|
|
authored_at: "2026-09-19 12:00:00.000Z",
|
|
|
|
|
is_merge: false,
|
|
|
|
|
additions: 0,
|
|
|
|
|
deletions: 0,
|
|
|
|
|
card: "",
|
2026-09-19 11:44:34 +00:00
|
|
|
link_source: "manual",
|
2026-09-19 11:30:37 +00:00
|
|
|
url: "",
|
|
|
|
|
summary: "Integration test entry",
|
|
|
|
|
summary_version: 0,
|
|
|
|
|
summary_attempts: 0,
|
|
|
|
|
source: "manual",
|
|
|
|
|
manual_minutes: 90,
|
|
|
|
|
});
|
|
|
|
|
commitId = commit.id;
|
|
|
|
|
|
2026-09-19 11:44:34 +00:00
|
|
|
const after = await recomputeMetrics(projectId);
|
|
|
|
|
|
|
|
|
|
expect(after.manualMinutes).toBe(base.manualMinutes + 90);
|
|
|
|
|
expect(after.estimatedMinutes).toBe(base.estimatedMinutes + 90); // flat, not clustered
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("the Log work UI is owner-only", () => {
|
|
|
|
|
it("shows the button to the owner but not to a client manager", async () => {
|
|
|
|
|
const ownerCookie = await signIn(DEMO.ownerEmail, DEMO.ownerPassword);
|
|
|
|
|
const ownerHtml = await fetch(
|
|
|
|
|
`${APP_URL}/p/${DEMO.clientSlug}/${DEMO.projectSlug}/dashboard`,
|
|
|
|
|
{ headers: { cookie: ownerCookie } }
|
|
|
|
|
).then((r) => r.text());
|
|
|
|
|
expect(ownerHtml).toContain("Log work");
|
2026-09-19 11:30:37 +00:00
|
|
|
|
2026-09-19 11:44:34 +00:00
|
|
|
const managerCookie = await signIn(DEMO.managerEmail, DEMO.managerPassword);
|
|
|
|
|
const managerHtml = await fetch(
|
|
|
|
|
`${APP_URL}/p/${DEMO.clientSlug}/${DEMO.projectSlug}/dashboard`,
|
|
|
|
|
{ headers: { cookie: managerCookie } }
|
|
|
|
|
).then((r) => r.text());
|
|
|
|
|
expect(managerHtml).not.toContain("Log work");
|
2026-09-19 11:30:37 +00:00
|
|
|
});
|
|
|
|
|
});
|