Public: anyone can read this; the crew can change it.
The map's share card resolves under 100 ms: /og/map/trevato~forge.png and its SVG draw what is open #6160
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6150-the-map-s-share-card-resolves-under-100 → main 1 commit fork 1 thread · 1 unresolved
3 files
changed +72
−7
src/web/og/map.ts +7 −1
| @@ -109,8 +109,14 @@ export function mapInput(title: string, graph: Graph, marks: Marks): CardInput { | ||
| 109 | 109 | const readySet = new Set(ready.map((r) => r.issue.id)); |
| 110 | 110 | const words = new Map(graph.issues.map((i) => [i.id, wordFor(i, readySet, marks)])); |
| 111 | 111 | const open = graph.issues.filter((i) => i.state === "open").length; |
| 112 | // The thumbnail draws what is open: done issues are counted in the caption, not drawn. | |
| 113 | const live = new Set(graph.issues.filter((i) => i.state === "open").map((i) => i.id)); | |
| 112 | 114 | const figure = graphFigure( |
| 113 | graph, | |
| 115 | { | |
| 116 | ...graph, | |
| 117 | issues: graph.issues.filter((i) => live.has(i.id)), | |
| 118 | edges: graph.edges.filter((e) => live.has(e.issueId) && live.has(e.blockedById)), | |
| 119 | }, | |
| 114 | 120 | words, |
| 115 | 121 | path.map((i) => i.id), |
| 116 | 122 | ); |
test/og-designs.test.ts +6 −6
| @@ -93,18 +93,18 @@ describe("the map's thumbnail", () => { | ||
| 93 | 93 | expect(card.lines).toEqual(["5 open issues", "Critical path: 3 issues, #1 → #5", "Next: #6 Issue 6"]); |
| 94 | 94 | const fig = card.figure; |
| 95 | 95 | if (fig?.kind !== "graph") throw new Error("no graph"); |
| 96 | // Only the chains are drawn: 6, on no edge, is counted, not drawn. | |
| 97 | expect(fig.nodes.length).toBe(5); | |
| 96 | // Only the open chains are drawn: 6, on no edge, and 2, done, are counted, not drawn. | |
| 97 | expect(fig.nodes.length).toBe(4); | |
| 98 | 98 | expect(fig.levels).toBe(3); |
| 99 | 99 | expect(fig.nodes.filter((n) => n.critical).length).toBe(3); |
| 100 | 100 | expect(fig.edges.filter((e) => e.critical).length).toBe(2); |
| 101 | expect(fig.edges.filter((e) => e.done).length).toBe(1); | |
| 102 | expect(fig.nodes.map((n) => n.word).toSorted()).toEqual(["blocked", "blocked", "done", "waiting", "working"]); | |
| 101 | expect(fig.edges.filter((e) => e.done).length).toBe(0); | |
| 102 | expect(fig.nodes.map((n) => n.word).toSorted()).toEqual(["blocked", "blocked", "waiting", "working"]); | |
| 103 | 103 | const svg = renderCard(card); |
| 104 | 104 | wellFormed(svg); |
| 105 | // Five cards in the figure, the two critical edges in the accent. | |
| 105 | // Four cards in the figure, the two critical edges in the accent. | |
| 106 | 106 | const figure = svg.slice(svg.indexOf("<g>"), svg.indexOf("</g>")); |
| 107 | expect(figure.match(/<rect /g)?.length).toBe(5); | |
| 107 | expect(figure.match(/<rect /g)?.length).toBe(4); | |
| 108 | 108 | expect(figure.match(new RegExp(`<path [^>]*stroke="${T.accent}"`, "g"))?.length).toBe(2); |
| 109 | 109 | expect(svg).toContain(">MAP<"); |
| 110 | 110 | }); |
test/og-map.test.ts added +59
| @@ -0,0 +1,59 @@ | ||
| 1 | /** The map's share card on 2,000 closed and 300 open issues stays small and quick (#6150). */ | |
| 2 | import { afterAll, beforeAll, expect, setDefaultTimeout, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { createRepo } from "../src/actions/repos.ts"; | |
| 8 | import { addDependency } from "../src/core/dependencies.ts"; | |
| 9 | import { createIssue } from "../src/core/issues.ts"; | |
| 10 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 11 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 12 | ||
| 13 | setDefaultTimeout(60_000); | |
| 14 | ||
| 15 | let dataDir = ""; | |
| 16 | let app: App; | |
| 17 | let servers: Servers; | |
| 18 | ||
| 19 | beforeAll(async () => { | |
| 20 | dataDir = await mkdtemp(join(tmpdir(), "forge-ogmap-")); | |
| 21 | app = createApp({ dataDir, port: 0 }); | |
| 22 | servers = await serve(app); | |
| 23 | const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" }); | |
| 24 | if (!admin.ok) throw new Error(admin.error); | |
| 25 | const made = await createRepo(app, admin.value, { name: "notes", visibility: "public", description: "" }); | |
| 26 | if (!made.ok) throw new Error(String(made.error)); | |
| 27 | const repoId = made.value.id; | |
| 28 | const issues = Array.from({ length: 2300 }, (_, i) => { | |
| 29 | const r = createIssue(app.db, { repoId, authorId: admin.value.id, title: `Issue ${i}`, body: "" }); | |
| 30 | if (!r.ok) throw new Error(r.error); | |
| 31 | return r.value; | |
| 32 | }); | |
| 33 | app.db.transaction(() => { | |
| 34 | // The first 2,000 are chained and closed; the last 300 are chained in tens and open. | |
| 35 | for (let i = 1; i < 2000; i++) addDependency(app.db, issues[i]!, issues[i - 1]!, admin.value.id); | |
| 36 | for (let i = 2000; i < 2300; i++) | |
| 37 | if (i % 10 !== 0) addDependency(app.db, issues[i]!, issues[i - 1]!, admin.value.id); | |
| 38 | for (const w of issues.slice(0, 2000)) | |
| 39 | app.db.run("UPDATE issue SET state = 'closed' WHERE id = ?1", [w.id]); | |
| 40 | })(); | |
| 41 | }); | |
| 42 | ||
| 43 | afterAll(async () => { | |
| 44 | await servers.stop(); | |
| 45 | await rm(dataDir, { recursive: true, force: true }); | |
| 46 | }); | |
| 47 | ||
| 48 | test("the card draws open issues only, names the done count, and resolves fast", async () => { | |
| 49 | await fetch(`${servers.url}/og/map/alice~notes.svg`).then((r) => r.arrayBuffer()); // warm | |
| 50 | const res = await fetch(`${servers.url}/og/map/alice~notes.svg`); | |
| 51 | const svg = await res.text(); | |
| 52 | expect(res.status).toBe(200); | |
| 53 | expect(new TextEncoder().encode(svg).length).toBeLessThan(200_000); | |
| 54 | expect(svg).toContain("2000 done"); | |
| 55 | expect(svg).toContain("300 open issues"); | |
| 56 | const resolve = /resolve;dur=([\d.]+)/.exec(res.headers.get("server-timing") ?? ""); | |
| 57 | expect(resolve).not.toBeNull(); | |
| 58 | expect(Number(resolve![1])).toBeLessThan(100); | |
| 59 | }); |
new above
offline
Wall-clock bound on shared CI can flake. Consider asserting on node count or SVG size, which are deterministic, plus a generous time bound. Add the .png route too.