Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

129 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { WorkItem, WorkProject } from "@g1t/contracts";
5
6import {
7 datesText,
8 dayText,
9 healthTone,
10 hillCounts,
11 hillDots,
12 hillY,
13 isLive,
14 percentDone,
15 projectChangeOf,
16 projectGroups,
17 projectPath,
18 projectsPath,
19 sidebarProjects,
20 timeElapsed,
21 timeLeft,
22} from "./projects.ts";
23
24const project = (name: string, state: WorkProject["state"], archivedAt: string | null = null) => ({ name, state, archivedAt });
25const item = (number: number, horizon: WorkItem["horizon"] = "now") => ({ number, horizon }) as WorkItem;
26const form = (fields: Record<string, string>) => {
27 const data = new FormData();
28 for (const [key, value] of Object.entries(fields)) data.set(key, value);
29 return data;
30};
31const today = new Date("2026-10-21T15:00:00Z");
32
33test("a project's pages are under Work", () => {
34 assert.equal(projectsPath("Acme"), "/acme/-/work/projects");
35 assert.equal(projectsPath("acme", "new=1"), "/acme/-/work/projects?new=1");
36 assert.equal(projectPath("acme", "work-overhaul"), "/acme/-/work/projects/work-overhaul");
37 assert.equal(projectPath("acme", "work-overhaul", "items"), "/acme/-/work/projects/work-overhaul/items");
38});
39
40test("the sidebar lists live projects, active first, each by name", () => {
41 const listed = sidebarProjects([
42 project("Zeta", "active"),
43 project("Beta", "paused"),
44 project("Alpha", "active"),
45 project("Gamma", "shaping"),
46 project("Done", "done"),
47 project("Old", "active", "2026-10-01T00:00:00Z"),
48 ]);
49 assert.deepEqual(
50 listed.map((p) => p.name),
51 ["Alpha", "Zeta", "Gamma", "Beta"],
52 );
53 assert.ok(isLive(project("A", "paused")));
54 assert.ok(!isLive(project("A", "dropped")));
55 const { live, finished } = projectGroups([project("B", "done"), project("A", "active"), project("C", "dropped")]);
56 assert.deepEqual(
57 live.map((p) => p.name),
58 ["A"],
59 );
60 assert.deepEqual(
61 finished.map((p) => p.name),
62 ["B", "C"],
63 );
64});
65
66test("dates read as people read them", () => {
67 assert.equal(dayText("2026-12-12", 2026), "Dec 12");
68 assert.equal(dayText("2027-01-04", 2026), "Jan 4, 2027");
69 assert.equal(datesText({ startOn: "2026-10-20", targetOn: "2026-12-12" }, today), "Oct 20 → Dec 12");
70 assert.equal(datesText({ startOn: null, targetOn: "2026-12-12" }, today), "Due Dec 12");
71 assert.equal(datesText({ startOn: null, targetOn: null }, today), null);
72 assert.deepEqual(timeLeft({ targetOn: "2026-10-31", state: "active" }, today), { text: "10 days left", late: false });
73 assert.deepEqual(timeLeft({ targetOn: "2026-12-12", state: "active" }, today), { text: "7 weeks left", late: false });
74 assert.deepEqual(timeLeft({ targetOn: "2026-10-21", state: "active" }, today), { text: "Due today", late: false });
75 assert.deepEqual(timeLeft({ targetOn: "2026-10-18", state: "paused" }, today), { text: "3 days late", late: true });
76 // A finished project has no time left to count.
77 assert.equal(timeLeft({ targetOn: "2026-10-18", state: "done" }, today), null);
78 assert.equal(timeElapsed({ startOn: "2026-10-11", targetOn: "2026-10-31" }, new Date("2026-10-21T00:00:00Z")), 50);
79 assert.equal(timeElapsed({ startOn: null, targetOn: "2026-10-31" }, today), null);
80});
81
82test("progress and health read from the record", () => {
83 assert.equal(percentDone({ openItems: 13, closedItems: 11 }), 45);
84 assert.equal(percentDone({ openItems: 0, closedItems: 0 }), 0);
85 assert.equal(healthTone("at_risk"), "warn");
86 assert.equal(healthTone(null), "neutral");
87});
88
89test("each open work item sits in its stage's quarter of the hill, none on another", () => {
90 const open = [item(1), item(2), item(3), item(4, "next")];
91 const dots = hillDots(open, [
92 { number: 1, hill: "building" },
93 { number: 2, hill: "building" },
94 { number: 3, hill: "landing" },
95 ]);
96 // #4 has no progress: it is still being figured out.
97 assert.deepEqual(
98 dots.map((dot) => [dot.item.number, dot.hill]),
99 [
100 [4, "figuring_out"],
101 [1, "building"],
102 [2, "building"],
103 [3, "landing"],
104 ],
105 );
106 const building = dots.filter((dot) => dot.hill === "building").map((dot) => dot.x);
107 assert.ok(
108 building.every((x) => x > 160 && x < 234),
109 "building is the third quarter, past the top",
110 );
111 assert.notEqual(building[0], building[1]);
112 for (const dot of dots) assert.equal(dot.y, Math.round(hillY(dot.x) * 10) / 10);
113 assert.equal(hillY(160), 16);
114 assert.deepEqual(hillCounts(dots), { figuring_out: 1, decided: 0, building: 2, landing: 1 });
115});
116
117test("a project's form posts only the fields it has", () => {
118 assert.deepEqual(projectChangeOf(form({ health: "at_risk" })), { health: "at_risk" });
119 assert.deepEqual(projectChangeOf(form({ health: "" })), { health: null });
120 assert.deepEqual(projectChangeOf(form({ name: " Work overhaul ", targetOn: "2026-12-12", appetite: "large" })), {
121 name: "Work overhaul",
122 targetOn: "2026-12-12",
123 appetite: "large",
124 });
125 assert.equal(projectChangeOf(form({ name: " " })), "A project needs a name.");
126 assert.equal(projectChangeOf(form({ health: "fine" })), "Choose a health: on track, at risk or off track.");
127 assert.equal(projectChangeOf(form({ startOn: "2026-12-12", targetOn: "2026-10-01" })), "The target date can't be before the start date.");
128 assert.deepEqual(projectChangeOf(form({ brief: "## Problem\n" })), { brief: "## Problem\n" });
129});