Public: anyone can read this; the crew can change it.

/map loses the sections docs/design/map.md says to cut #2790

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/2770-map-loses-the-sections-docs-design-map-m → main 2 commits fork

  • Merged. mason/2770-map-loses-the-sections-docs-design-map-m was merged into main as c123751.

Review

Sign in to review this change.
10 files changed +53 −195
docs/DESIGN.md +1 −1
@@ -384,7 +384,7 @@ gone, so this list only shrinks.
384384 - **`pages/board.ts`** — the board, the live wall of the crews at work: `.board` `.board-about` `.board-answer` `.board-app` `.board-app-mode` `.board-app-name` `.board-app-nums` `.board-app-week` `.board-asked` `.board-asks` `.board-bar` `.board-brand` `.board-card` `.board-clock` `.board-count` `.board-decision` `.board-decision-main` `.board-declined` `.board-declined-row` `.board-declined-says` `.board-dot` `.board-elapsed` `.board-fill` `.board-forge` `.board-grid` `.board-group` `.board-h` `.board-inferred` `.board-key` `.board-keys` `.board-kind` `.board-landed` `.board-landed-meta` `.board-landed-title` `.board-list` `.board-live` `.board-outcome` `.board-p` `.board-parked-row` `.board-pillar` `.board-pillars` `.board-pillars-h` `.board-quiet` `.board-region` `.board-run` `.board-run-meta` `.board-run-title` `.board-run-who` `.board-scope` `.board-spend` `.board-spend-detail` `.board-state` `.board-sub` `.board-sub-name` `.board-sub-scope` `.board-subs` `.board-thumb` `.board-top` `.board-when` `.board-window` `.board-window-name` `.board-windows` `.spend-table` `.spend-totals`
385385 - **`pages/flow.ts`** — /flow's pipeline bars, one `held-${kind}` per reason a stage holds work: `.flow-bar` `.flow-out` `.held-bar` `.held-done_on_main` `.held-key` `.held-open_pull` `.held-parked_twice` `.held-planner` `.held-red_fix` `.held-run_cap` `.held-running` `.held-setting` `.held-stalled` `.held-startable`
386386 - **`pages/limb.ts`** — acting on a whole limb from its milestone: `.limb-confirm` `.limb-rows` `.limb-why`
387 - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-autonomy` `.map-breath` `.map-canvas` `.map-card` `.map-chart` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-flow` `.map-lane` `.map-lane-caret` `.map-lane-crew` `.map-lane-head` `.map-lane-name` `.map-lane-word` `.map-lanes` `.map-level` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-spend` `.map-spend-fill` `.map-spend-track` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap`
387 - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-lane` `.map-lane-caret` `.map-lane-crew` `.map-lane-head` `.map-lane-name` `.map-lane-word` `.map-lanes` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap`
388388 - **`pages/milestones.ts`** — milestones and one limb opened: `.ms-actions` `.ms-bar` `.ms-count` `.ms-figures` `.ms-head` `.ms-limb` `.ms-limbs` `.ms-more` `.ms-move` `.ms-name` `.ms-planner` `.ms-rows` `.ms-section` `.ms-state` `.ms-top` `.ms-trim` `.ms-weight`
389389 - **`studio/feed_style.ts`** — the home feed's cards, its composer and the apps rail: `.composer` `.composer-bar` `.fcard` `.fcard-acts` `.fcard-form` `.fcard-head` `.fcard-more` `.fcard-reason` `.fcard-reply` `.fcard-text` `.fcard-unblock` `.fcard-who` `.fcard-why` `.feed` `.feed-head` `.home-app` `.home-apps` `.home-apps-verbs` `.home-pick` `.home-rail` `.home-tabs`
390390 - **`studio/style.ts`** — the studio's tell box, app head, word and shelf rows: `.fcard-app` `.shelf-desc` `.shelf-facts` `.shelf-meta` `.studio-ask-actions` `.studio-head` `.studio-tell` `.studio-word`
docs/design/map.md +5 −2
@@ -2,7 +2,7 @@
22
33 Part of #2695. A reading of `src/web/pages/map.ts`, section by section: does a
44 board or an issue's dependency list already show it, and keep or cut. This
5 document changes no code; each **cut** is a follow-up issue, not done here.
5 document changed no code; #2770 made the cuts.
66
77 An issue's dependency list shows one issue's direct edges. A board shows
88 work by state, per app, with a "merged by pillar" line. Neither draws the
@@ -28,4 +28,7 @@ whole graph or follows a chain more than one hop.
2828
2929 Cutting `Autonomy line`, `Critical path` list, `Two weeks` and `Spend bar`
3030 leaves the canvas, counts, pillars, ready and blocked: what only the graph
31 can say.
31 can say. Done in #2770: today's spend against each app's ceiling is read on
32 `/board` (the home's "their ceiling" link goes there), the mode on
33 `settings/autonomy`, and an issue's place in a chain on its dependency list.
34 `GET /api/v1/map` still carries `critical` and each lane's spend for the crew.
src/core/dependencies.ts −43
@@ -621,46 +621,3 @@ export function criticalPath(graph: Graph): Issue[] {
621621 ? best.map((id) => byId.get(id)).filter((i): i is Issue => i !== undefined)
622622 : [];
623623 }
624
625 // ---- level and flow ----
626
627 export interface DayPoint {
628 readonly day: string;
629 /** Open issues at the end of that day: the level. */
630 readonly open: number;
631 /** Issues closed during that day: the flow. */
632 readonly closed: number;
633 }
634
635 /** The last `days` days of open count against closed-per-day, oldest first. */
636 export function levelAndFlow(
637 db: Database,
638 repoId: string,
639 days = 14,
640 now = new Date(),
641 ): DayPoint[] {
642 const out: DayPoint[] = [];
643 for (let back = days - 1; back >= 0; back--) {
644 const end = new Date(now);
645 end.setUTCHours(24, 0, 0, 0);
646 end.setUTCDate(end.getUTCDate() - back);
647 const endIso = end.toISOString();
648 const start = new Date(end);
649 start.setUTCDate(start.getUTCDate() - 1);
650 const startIso = start.toISOString();
651 const open = db
652 .query<{ n: number }, [string, string]>(
653 `SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue'
654 AND created_at < ?2 AND (closed_at IS NULL OR closed_at >= ?2)`,
655 )
656 .get(repoId, endIso)!.n;
657 const closed = db
658 .query<{ n: number }, [string, string, string]>(
659 `SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue'
660 AND closed_at >= ?2 AND closed_at < ?3`,
661 )
662 .get(repoId, startIso, endIso)!.n;
663 out.push({ day: startIso.slice(0, 10), open, closed });
664 }
665 return out;
666 }
src/web/pages/map.ts +3 −127
@@ -3,9 +3,8 @@
33 * a card one level below everything it is blocked by, so the top edge is
44 * what is ready now and the longest open chain runs down the page in the
55 * accent. Issues that wait for nothing and hold nothing up stand apart
6 * below, under a rule. Beside the drawing: the three lists the crew and a
7 * person both read — ready now, blocked on, the critical path — and two
8 * weeks of open count over closed-per-day.
6 * below, under a rule. Beside the drawing: open work by pillar and the two
7 * lists the crew and a person both read — ready now and blocked on.
98 *
109 * One language for every map. A card says its title, who is on it, and one
1110 * word: ready, working, blocked, done, parked, or waiting. Edges are calm
@@ -27,10 +26,8 @@ import {
2726 blockedOn,
2827 criticalPath,
2928 idleFurniture,
30 levelAndFlow,
3129 readyNow,
3230 repoGraph,
33 type DayPoint,
3431 type Edge,
3532 type Graph,
3633 type Ranked,
@@ -67,7 +64,6 @@ import {
6764 type ParkedTwice,
6865 } from "../../crew/parks.ts";
6966 import {
70 OUTSIDERS_NOTE,
7167 proposedWork,
7268 READER_SUGGESTION,
7369 } from "../../crew/direct.ts";
@@ -1042,60 +1038,10 @@ function pillarList(repo: Repo, rows: readonly PillarRow[]): Html {
10421038 </section>`;
10431039 }
10441040
1045 function chart(points: DayPoint[]): Html {
1046 const w = 300;
1047 const h = 84;
1048 const pad = 6;
1049 const maxOpen = Math.max(1, ...points.map((p) => p.open));
1050 const maxClosed = Math.max(1, ...points.map((p) => p.closed));
1051 const step = (w - pad * 2) / Math.max(1, points.length - 1);
1052 const line = points
1053 .map(
1054 (p, i) =>
1055 `${i === 0 ? "M" : "L"}${(pad + i * step).toFixed(1)} ${(
1056 pad +
1057 (h - pad * 2) * (1 - p.open / maxOpen)
1058 ).toFixed(1)}`,
1059 )
1060 .join(" ");
1061 const barW = Math.max(3, step * 0.55);
1062 const bars = points.map((p, i) => {
1063 const bh = (h - pad * 2) * (p.closed / maxClosed);
1064 return html`<rect
1065 class="map-flow"
1066 x="${(pad + i * step - barW / 2).toFixed(1)}"
1067 y="${(h - pad - bh).toFixed(1)}"
1068 width="${barW.toFixed(1)}"
1069 height="${bh.toFixed(1)}"
1070 rx="1.5"
1071 />`;
1072 });
1073 const last = points.at(-1);
1074 const closedTotal = points.reduce((s, p) => s + p.closed, 0);
1075 return html`<section class="map-list">
1076 <h2>Two weeks</h2>
1077 <svg
1078 class="map-chart"
1079 viewBox="0 0 ${w} ${h}"
1080 role="img"
1081 aria-label="Open issues as a line over issues closed per day as bars"
1082 >
1083 ${bars}
1084 <path class="map-level" d="${line}" />
1085 </svg>
1086 <p class="muted small">
1087 <span class="num">${last?.open ?? 0}</span> open now ·
1088 <span class="num">${closedTotal}</span> closed in 14 days
1089 </p>
1090 </section>`;
1091 }
1092
10931041 export const mapCss = /* css */ `
10941042 .map-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 272px; gap: 24px; align-items: start; margin-top: 4px; }
10951043 @media (max-width: 860px) { .map-wrap { grid-template-columns: 1fr; } }
10961044 .map-main { min-width: 0; }
1097 .map-autonomy { flex-basis: 100%; color: var(--muted-fg); text-wrap: pretty; }
1098 .map-autonomy .ico { color: var(--faint); vertical-align: -3px; margin-right: 2px; }
10991045
11001046 /* ---- the canvas ---- */
11011047 .map-canvas { position: relative; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); touch-action: pan-y; user-select: none; -webkit-user-select: none; }
@@ -1174,9 +1120,6 @@ export const mapCss = /* css */ `
11741120 .map-list + .map-list { margin-top: 18px; }
11751121 .map-ask { display: block; }
11761122 .map-pillars ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; font-size: 14px; }
1177 .map-chart { width: 100%; height: auto; display: block; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); }
1178 .map-level { fill: none; stroke: var(--accent); stroke-width: 1.5; }
1179 .map-flow { fill: color-mix(in srgb, var(--fg) 14%, transparent); }
11801123 .map-empty { display: grid; justify-items: center; gap: 8px; padding: 40px 16px; border: 1px dashed var(--border); border-radius: var(--radius); color: var(--muted-fg); text-align: center; }
11811124
11821125 /* ---- lanes ---- */
@@ -1191,11 +1134,6 @@ export const mapCss = /* css */ `
11911134 .map-lane-name .muted { font-weight: 500; }
11921135 .map-lane-word { font-size: 12.5px; color: var(--muted-fg); padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px; line-height: 18px; }
11931136 .map-lane-word[data-autonomy="autonomous"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 34%, transparent); }
1194 .map-spend { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--muted-fg); font-variant-numeric: tabular-nums; }
1195 .map-spend svg { width: 72px; height: 4px; border-radius: 2px; overflow: hidden; }
1196 .map-spend-track { fill: var(--muted); }
1197 .map-spend-fill { fill: var(--muted-fg); }
1198 .map-spend[data-near="1"] .map-spend-fill { fill: var(--attn); }
11991137 .map-lane-crew { display: inline-flex; align-items: center; gap: 4px; }
12001138 .map-lane-crew .avatar { box-shadow: 0 0 0 2px var(--card); }
12011139 .map-lane-head .map-counts { margin: 0 0 0 auto; }
@@ -1363,28 +1301,6 @@ export const mapViewJs = /* js */ `
13631301 })();
13641302 `;
13651303
1366 /** One line on how the repository runs itself, when it does. */
1367 function autonomyLine(ctx: Ctx, repo: Repo): Html | null {
1368 if (repo.autonomy === "off")
1369 return repo.crewOutsiders
1370 ? html`<p class="small map-autonomy">${OUTSIDERS_NOTE}</p>`
1371 : null;
1372 const s = autonomyStatus(ctx.app, repo);
1373 const paused =
1374 s.paused === "ceiling"
1375 ? " · paused at the ceiling until 00:00 UTC"
1376 : s.paused === "no crew"
1377 ? " · paused: no crew"
1378 : s.paused === "nobody"
1379 ? " · paused: nobody to act as"
1380 : "";
1381 const line = `${repo.autonomy === "autonomous" ? "Autonomous" : "Assisted"} · $${s.spentTodayUsd.toFixed(2)} of $${s.ceilingUsd.toFixed(0)} today · ${s.active} of ${s.slots} slots${paused} · ${repo.crewOutsiders ? "anyone signed in directs the crew" : "only writers direct the crew"}`;
1382 return html`<p class="small map-autonomy">
1383 ${icon("play")} ${line} ·
1384 <a href="${repoBase(repo)}/settings/autonomy">settings</a>
1385 </p>`;
1386 }
1387
13881304 const LIVE_KINDS = "run.*,issue.*,pull.*";
13891305
13901306 /** The empty state in the studio's own words, pointing at the app page's
@@ -1423,7 +1339,6 @@ export async function mapPage(ctx: Ctx): Promise<Response> {
14231339 is the least work that still has to happen in order.
14241340 <a href="${repoBase(repo)}/milestones">How far each milestone is</a>.
14251341 </p>
1426 ${autonomyLine(ctx, repo)}
14271342 </div>
14281343 <div
14291344 class="map-wrap"
@@ -1468,22 +1383,6 @@ export async function mapPage(ctx: Ctx): Promise<Response> {
14681383 "Nothing open holds anything up.",
14691384 issueHref,
14701385 )}
1471 ${critical.length > 1
1472 ? html`<section class="map-list">
1473 <h2>Critical path</h2>
1474 <ol>
1475 ${critical.map(
1476 (i) =>
1477 html`<li>
1478 <a href="${issueHref(repoBase(repo), i.number)}"
1479 >${i.title}</a
1480 >
1481 </li>`,
1482 )}
1483 </ol>
1484 </section>`
1485 : null}
1486 ${chart(levelAndFlow(db, repo.id))}
14871386 </aside>
14881387 </div>`;
14891388 return page({
@@ -1730,29 +1629,7 @@ export function globalLanes(app: App, viewer: User | undefined): Lane[] {
17301629 );
17311630 }
17321631
1733 /** "$1.20 of $10", as one string so nothing wraps inside it. */
1734 function spendWords(status: AutonomyStatus): string {
1735 return `$${status.spentTodayUsd.toFixed(2)} of $${status.ceilingUsd.toFixed(0)}`;
1736 }
1737
1738 function spendBar(status: AutonomyStatus): Html {
1739 const share =
1740 status.ceilingUsd > 0 ? status.spentTodayUsd / status.ceilingUsd : 0;
1741 const pct = Math.max(0, Math.min(100, share * 100));
1742 return html`<span
1743 class="map-spend"
1744 ${share >= 0.9 ? html`data-near="1"` : null}
1745 title="Spent today against the day's ceiling"
1746 >
1747 <svg viewBox="0 0 100 4" preserveAspectRatio="none" aria-hidden="true">
1748 <rect class="map-spend-track" width="100" height="4" />
1749 <rect class="map-spend-fill" width="${pct.toFixed(1)}" height="4" />
1750 </svg>
1751 <span>${spendWords(status)}</span>
1752 </span>`;
1753 }
1754
1755 /** A lane: the repository's name, how it runs itself, who is on it, and its map. */
1632 /** A lane: the repository's name, its autonomy mode, who is on it, and its map. */
17561633 export function laneHtml(lane: Lane, width: number): Html {
17571634 const { repo } = lane;
17581635 const id = `lane-${repo.ownerName}-${repo.name}`;
@@ -1771,7 +1648,6 @@ export function laneHtml(lane: Lane, width: number): Html {
17711648 <span class="map-lane-word" data-autonomy="${repo.autonomy}"
17721649 >${repo.autonomy}</span
17731650 >
1774 ${lane.status === undefined ? null : spendBar(lane.status)}
17751651 ${lane.workers.length === 0
17761652 ? null
17771653 : html`<span
src/web/studio/cost_line.ts +1 −1
@@ -22,6 +22,6 @@ export function costLine(db: Database, writable: readonly Repo[]): Html | null {
2222 const noun = `app${writable.length === 1 ? "" : "s"}`;
2323 const sentence = `$${spentUsd.toFixed(2)} spent today across ${writable.length} ${noun} you can write`;
2424 return html`<p class="small muted studio-cost">
25 ${sentence} · ${paused} paused at <a href="/map">their ceiling</a>
25 ${sentence} · ${paused} paused at <a href="/board">their ceiling</a>
2626 </p>`;
2727 }
test/autonomy.test.ts +8 −2
@@ -581,11 +581,17 @@ describe("a repository on its own", () => {
581581 })
582582 ).text();
583583 expect(crew).toContain("reviewer");
584 // Today's spend against the new ceiling is the forge's crew page's; the
585 // map only draws what waits for what.
586 const forgeCrew = await (
587 await fetch(`${servers.url}/settings/crew`, { headers: { cookie } })
588 ).text();
589 expect(forgeCrew).toMatch(/alice\/engine[\s\S]*?assisted/);
590 expect(forgeCrew).toMatch(/\/ \$25 today/);
584591 const map = await (
585592 await fetch(`${servers.url}/alice/engine/map`, { headers: { cookie } })
586593 ).text();
587 expect(map).toContain("Assisted");
588 expect(map).toContain("of $25 today");
594 expect(map).not.toContain("of $25 today");
589595 });
590596
591597 test("the forge's own ceiling holds every repository, whatever each one's says", async () => {
test/dependencies.test.ts +2 −7
@@ -16,7 +16,6 @@ import {
1616 blockedOn,
1717 criticalPath,
1818 dependenciesOf,
19 levelAndFlow,
2019 readyNow,
2120 removeDependency,
2221 repoGraph,
@@ -154,7 +153,7 @@ describe("dependencies", () => {
154153 expect(placed.islandFrom).toBe(4);
155154 });
156155
157 test("closing a blocker frees what waited; the level falls and the flow ticks", () => {
156 test("closing a blocker frees what waited and shortens the critical path", () => {
158157 const closed = setIssueState(app, alice, repo, issue(1), "closed");
159158 expect(closed.ok).toBe(true);
160159 const graph = repoGraph(app.db, repo.id);
@@ -163,10 +162,6 @@ describe("dependencies", () => {
163162 expect(ready).not.toContain(4); // still waits for 2
164163 // 2 → 4 → 5 → 6 is now the longest open chain: 4 still waits for 2.
165164 expect(criticalPath(graph).map((i) => i.number)).toEqual([2, 4, 5, 6]);
166 const points = levelAndFlow(app.db, repo.id, 3);
167 expect(points).toHaveLength(3);
168 expect(points.at(-1)!.open).toBe(6);
169 expect(points.at(-1)!.closed).toBe(1);
170165 const closedNow = getIssue(app.db, repo.id, 1)!;
171166 expect(setIssueState(app, alice, repo, closedNow, "open").ok).toBe(true);
172167 expect(getIssue(app.db, repo.id, 1)?.state).toBe("open");
@@ -293,7 +288,7 @@ describe("dependencies", () => {
293288 await fetch(`${base}/alice/engine/map`, { headers: { cookie } })
294289 ).text();
295290 expect(map).toContain("Ready now");
296 expect(map).toContain("Critical path");
291 expect(map).not.toContain("Critical path");
297292 expect(map).toContain('data-state="ready"');
298293 expect(map).toContain("Extract renderer");
299294 expect(map).toContain("finishing it frees 4");
test/design-map.test.ts +22 −5
@@ -1,7 +1,8 @@
11 /**
2 * docs/design/map.md names every section of the map page, and each one is
3 * still in the page source (#2768). Each entry is a backticked name leading a
4 * bullet; this maps it to the text the page carries for it.
2 * docs/design/map.md names every section of the map page and says keep or
3 * cut (#2768): each one kept is in the page source, each one cut is gone
4 * from it (#2770). Each entry is a backticked name leading a bullet; this
5 * maps it to the text the page carried for it.
56 */
67 import { expect, test } from "bun:test";
78 import { readFileSync } from "node:fs";
@@ -28,8 +29,24 @@ test("the doc names every section it knows, and no other", () => {
2829 expect(named.sort()).toEqual(Object.keys(MARKERS).sort());
2930 });
3031
31 test("every section the doc names is in the page source", () => {
32 for (const name of named) expect(src).toContain(MARKERS[name]!);
32 const verdicts = new Map(
33 [...doc.matchAll(/^- `([^`]+)` —.*\*\*(Keep|Cut)\*\*/gm)].map((m) => [
34 m[1]!,
35 m[2]!,
36 ]),
37 );
38
39 test("every section the doc keeps is in the page source", () => {
40 for (const name of named)
41 if (verdicts.get(name) === "Keep") expect(src).toContain(MARKERS[name]!);
42 });
43
44 test("every section the doc cuts is gone from the page source", () => {
45 const cut = named.filter((name) => verdicts.get(name) === "Cut");
46 expect(cut.sort()).toEqual(
47 ["Autonomy line", "Critical path", "Spend bar", "Two weeks"].sort(),
48 );
49 for (const name of cut) expect(src).not.toContain(MARKERS[name]!);
3350 });
3451
3552 test("every section says keep or cut", () => {
test/map.test.ts +10 −6
@@ -299,7 +299,7 @@ describe("the map, per app and across the forge", () => {
299299 const dormant = await repoOf(alice, "dormant", ["Rewrite the loader"]);
300300 park(alice, dormant.issues[0]!);
301301 // bob/secret: bob's own, which alice cannot see and bob can; assisted,
302 // with no crew, so the spend bar has a ceiling to draw against.
302 // with no crew, so it has a ceiling its lane no longer draws.
303303 const secret = await repoOf(bob, "secret", ["Rotate the keys"]);
304304 const on = await updateRepoSettings(app, bob, secret.repo, {
305305 autonomy: "assisted",
@@ -351,9 +351,13 @@ describe("the map, per app and across the forge", () => {
351351 expect(page).toContain('<i data-state="done"></i>done <b>1</b>');
352352 expect(page).toContain('<i data-state="waiting"></i>waiting <b>1</b>');
353353 expect(page).toContain('<i data-state="parked"></i>parked <b>1</b>');
354 // The lists beside it still read the graph.
354 // The lists beside it still read the graph; the critical path is the
355 // accent on the canvas alone, and spend and flow live on their boards.
355356 expect(page).toContain("Ready now");
356 expect(page).toContain("Critical path");
357 expect(page).toContain("Blocked on");
358 expect(page).not.toContain("Critical path");
359 expect(page).not.toContain("Two weeks");
360 expect(page).not.toContain("map-autonomy");
357361 });
358362
359363 test("the drawing is a live region scoped to its repository, fits, and pans", async () => {
@@ -417,11 +421,11 @@ describe("the map, per app and across the forge", () => {
417421 // A repository with nothing to draw is not a lane.
418422 expect(page).not.toContain('href="/alice/bare/map"');
419423 // The forge's admin reads every repository, so bob's is a lane here,
420 // with today's spend drawn against its ceiling …
424 // saying its mode; today's spend is the board's, not the lane's …
421425 expect(page).toContain('href="/bob/secret/map"');
422426 expect(page).toContain('data-autonomy="assisted"');
423 expect(page).toContain('class="map-spend-fill" width="0.0"');
424 expect(page).toContain("$0.00 of $10");
427 expect(page).not.toContain("map-spend");
428 expect(page).not.toContain("$0.00 of $10");
425429
426430 // … and bob, who is nobody's admin, sees his own and not alice's.
427431 const bobs = await (
test/studio-search.test.ts +1 −1
@@ -455,7 +455,7 @@ describe("the studio home says what the crew cost you today", () => {
455455 const html = await home(aliceCookie);
456456 expect(html).toContain("$8.00 spent today across 2 apps you can write");
457457 expect(html).toContain("1 paused at");
458 expect(html).toMatch(/<a href="\/map">their ceiling<\/a>/);
458 expect(html).toMatch(/<a href="\/board">their ceiling<\/a>/);
459459 });
460460
461461 test("a viewer who can only read sees no figure at all", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs