Public: anyone can read this; the crew can change it.
GET /flow answers 301 to /boards, and the /flow page's own renderer is gone #2846
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2832-get-flow-answers-301-to-boards-and-the-f → main 3 commits fork 3 threads · 3 unresolved
README.md +5 −5
| @@ -887,7 +887,7 @@ code never blocks. If the verdict is on the current head and no run is in | ||
| 887 | 887 | flight, one feedback run is queued to the pull's builder, and a builder is |
| 888 | 888 | assigned when the pull has none. That run takes a builder slot like any |
| 889 | 889 | other. The tick log names each pull it moved, or why it did not, and |
| 890 | `/flow`, the terminal's Flow dialog and the MCP `flow` tool list the pulls | |
| 890 | The flow panel on `/boards`, the terminal's Flow dialog and the MCP `flow` tool list the pulls | |
| 891 | 891 | waiting on a builder and those waiting on a reviewer, oldest first. |
| 892 | 892 | |
| 893 | 893 | **Already done.** A builder that finds its issue already built on the base |
| @@ -921,7 +921,7 @@ gated tree. Red bisects: the front half is gated by itself, a green half | ||
| 921 | 921 | lands, and the red pull is handed back with the failing tests named. Pulls |
| 922 | 922 | that turn green while a candidate is gating wait for the next one. Each |
| 923 | 923 | queue's numbers — how many pulls are queued, how many are gating, and how |
| 924 | many landed in the last hour — show in three places: the /flow page, the | |
| 924 | many landed in the last hour — show in three places: the flow panel on `/boards`, the | |
| 925 | 925 | terminal's Flow command, and the MCP `flow` tool's `queue` field. |
| 926 | 926 | |
| 927 | 927 | An autonomous app's planner is **held** while nothing is landing or the plate |
| @@ -936,7 +936,7 @@ under the first filter that holds it (`crew/held.ts`): `held: running=1 | ||
| 936 | 936 | open_pull=10 stalled=12 run_cap=11` — a run on it, an open pull proposing it, |
| 937 | 937 | the planner's, found done on main and waiting for a person to close it |
| 938 | 938 | (`done_on_main`, never re-planned), stalled, at its run cap, parked twice, covered by a red-main |
| 939 | fix, or a setting's finding. `/flow` draws the same per autonomous app as one | |
| 939 | fix, or a setting's finding. The flow panel on `/boards` draws the same per autonomous app as one | |
| 940 | 940 | bar, and the terminal's Flow command prints it as a line per app |
| 941 | 941 | (`flow/held.ts`, kept 5 s). When nothing is startable while builder slots are free and some of the |
| 942 | 942 | held are stalled, capped or parked twice, the planner is owed one pass with |
| @@ -984,13 +984,13 @@ from before this record say nothing). | ||
| 984 | 984 | mechanical rather than advisory. |
| 985 | 985 | |
| 986 | 986 | `docs/FLOW.md` is where every issue stands: `GET /api/v1/repos/:owner/:name/flow` returns each open issue's stage, since, stalled and unblock, plus what went live in the last 24 h. |
| 987 | Where the whole forge waits is a forge admin's: `/flow` shows each stage from ready to live with what waits, the oldest wait and its capacity, under one sentence naming the bottleneck, and beside each what it let out in the last hour with a 24-hour sparkline, under a line of landings and the median ask→live — every count links to the items it counts. A stalled step is a card on the home (Studio mode) for anyone who can write to the repository: an open pull whose check was never asked for, or is red, past its stage's allowance, with its stage, how long it has been stuck, and one button — Run check or Re-run — that asks for a check at the head and returns to the home. The terminal's **Stalled steps** command (palette) lists the same steps, live, and Enter or `r` does the same re-run; both read `flow/stalled.ts` and act through `flow/unblock.ts`, and neither shows anything to someone who cannot write. A conflicted pull never sits: every tick the crew's open pulls that no longer merge get one bring-up-to-date run each (one in flight per pull); after 2 failed tries in the same conflict, or 24 h since the first, the pull is closed with the conflicting files named and its issue goes back to ready, unassigned, with no strike against the builder. Conflicted pulls are listed oldest first on `/flow`. The terminal's Flow command and the MCP `flow` tool read the same: the sentence, the landings line, and each stage's count out in the last hour and in 24 h. | |
| 987 | Where the whole forge waits is the owner's, on `/boards` (the old `/flow` redirects there, query kept; an agent granted `telemetry:read` reads the same from `/api/v1/telemetry/flow` and the MCP `flow` tool): the flow panel shows each stage from ready to live with what waits, the oldest wait and its capacity, under one sentence naming the bottleneck, and beside each what it let out in the last hour with a 24-hour sparkline, under a line of landings and the median ask→live — every count links to the items it counts. A stalled step is a card on the home (Studio mode) for anyone who can write to the repository: an open pull whose check was never asked for, or is red, past its stage's allowance, with its stage, how long it has been stuck, and one button — Run check or Re-run — that asks for a check at the head and returns to the home. The terminal's **Stalled steps** command (palette) lists the same steps, live, and Enter or `r` does the same re-run; both read `flow/stalled.ts` and act through `flow/unblock.ts`, and neither shows anything to someone who cannot write. A conflicted pull never sits: every tick the crew's open pulls that no longer merge get one bring-up-to-date run each (one in flight per pull); after 2 failed tries in the same conflict, or 24 h since the first, the pull is closed with the conflicting files named and its issue goes back to ready, unassigned, with no strike against the builder. Conflicted pulls are listed oldest first on `/boards`. The terminal's Flow command and the MCP `flow` tool read the same: the sentence, the landings line, and each stage's count out in the last hour and in 24 h. | |
| 988 | 988 | |
| 989 | 989 | `/resilience` is a forge admin's too (a guest is sent to sign in, anyone else gets 404): each protection in `docs/THREATS.md` as one line, green, red, untested or "no test yet", judged by the test file it cites in the last full gate of the forge's own repository (`FORGE_REPO`; other repositories' gates never touch it). The terminal's **Resilience** command (palette, admins) lists the same lines. |
| 990 | 990 | |
| 991 | 991 | The terminal also has an Access command (`:` → Access), a site admin's: it reads the same last-day non-browser requests as Settings → Access and the `access` MCP tool, newest first, and filters them with `s` (since), `u` (user agent) and `p` (path), `c` to clear. |
| 992 | 992 | |
| 993 | `/system` is the machine, for the owner (#1789), laid out like btop (#2721): a meter and a 30-reading block-character history per core, memory and swap meters, load, hwmon temperatures, disk use per mount, disk and network rates (`/proc/diskstats`, `/proc/net/dev`), and a table of every process grouped forge / check (`check:<run>`) / app (`app:<slug>/<env>`) / host with CPU, memory and age, sortable by CPU (`c`) or memory (`m`) or the buttons (shown only with a script; without one the order is by CPU). It reads `/proc` and `/sys` and updates in place over one event stream, `/system/stream` (`?pause` holds it still; no reload, no polling); the 2 s push timer runs only while someone is subscribed, so it costs nothing unwatched. Its history is the `machine` board source. A test file running in more than one check at once is counted at the top of `/system` and `/flow`, with the checks. Command lines are never kept — only `*.test.ts` arguments. | |
| 993 | `/system` is the machine, for the owner (#1789), laid out like btop (#2721): a meter and a 30-reading block-character history per core, memory and swap meters, load, hwmon temperatures, disk use per mount, disk and network rates (`/proc/diskstats`, `/proc/net/dev`), and a table of every process grouped forge / check (`check:<run>`) / app (`app:<slug>/<env>`) / host with CPU, memory and age, sortable by CPU (`c`) or memory (`m`) or the buttons (shown only with a script; without one the order is by CPU). It reads `/proc` and `/sys` and updates in place over one event stream, `/system/stream` (`?pause` holds it still; no reload, no polling); the 2 s push timer runs only while someone is subscribed, so it costs nothing unwatched. Its history is the `machine` board source. A test file running in more than one check at once is counted at the top of `/system` and the `/boards` flow panel, with the checks. Command lines are never kept — only `*.test.ts` arguments. | |
| 994 | 994 | In the terminal, `:` → System shows the same view (the owner's only): per-core CPU with a block-character history, memory, swap, disk and network rates, and each group's top two processes (no sort; the dialog is read-only), and `:` → Telemetry lists the agents and flips `telemetry:read` for the one you press Enter on. |
| 995 | 995 | An agent the owner grants `telemetry:read` (Settings → Telemetry, one switch per agent, revoked the same way) reads the same through `GET /api/v1/telemetry/{system,flow,sources,history}` and the MCP `flow` and `system` tools; private repositories' checks and apps are renamed `check:private-N` and lose their processes and test files, and `flow` (like `system`, an admin agent account needs the grant too: only a person who is an admin reads without it) names only public repositories' conflicted pulls, every read is listed on that page, and an agent without the grant is refused. `/contribute`, `GET /api/v1/contribute` and the MCP `contribute` tool are open to anyone: what the forge is, the bottleneck now, open issues labelled “help wanted” in public repositories, and how to propose (fork, pull, the same gate). The grant is a table, not a token scope, so no token has to be re-issued. |
| 996 | 996 |
forge.json +1 −1
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | "api.anthropic.com" |
| 4 | 4 | ], |
| 5 | 5 | "app": { |
| 6 | "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/boards", "/asked", "/search", "/new", "/audit", "/releases", "/apps", "/migrate", "/map", "/flow", "/system", "/contribute", "/resilience", "/settings"], | |
| 6 | "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/boards", "/asked", "/search", "/new", "/audit", "/releases", "/apps", "/migrate", "/map", "/system", "/contribute", "/resilience", "/settings"], | |
| 7 | 7 | "run": "bun run src/main.ts", |
| 8 | 8 | "install": "", |
| 9 | 9 | "health": "/healthz", |
src/checks/queue.ts +2 −2
| @@ -12,7 +12,7 @@ | ||
| 12 | 12 | * |
| 13 | 13 | * What is queued is not stored: it is the pulls `landIfGreen` admitted, and |
| 14 | 14 | * every sweep admits them again. The queue only orders them and remembers |
| 15 | * how many there are, for /flow. | |
| 15 | * how many there are, for the flow panel on /boards. | |
| 16 | 16 | */ |
| 17 | 17 | |
| 18 | 18 | import type { Pull } from "../core/pulls.ts"; |
| @@ -132,7 +132,7 @@ export function enqueue( | ||
| 132 | 132 | return Promise.all(answers); |
| 133 | 133 | } |
| 134 | 134 | |
| 135 | /** What /flow shows of each repository's line. */ | |
| 135 | /** What the flow panel shows of each repository's line. */ | |
| 136 | 136 | export interface QueueStatus { |
| 137 | 137 | readonly repoId: string; |
| 138 | 138 | readonly queued: number; |
src/http/mcp.ts +1 −1
| @@ -2637,7 +2637,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 2637 | 2637 | { |
| 2638 | 2638 | name: "flow", |
| 2639 | 2639 | description: |
| 2640 | "The forge's pipeline, for a forge admin: ready, building, check queue, checking, review, approved, landed and live, each with how many wait, how many are being worked, when the oldest waiting one entered (oldestWaiting) and what limits it (capacity), and one sentence (says) naming the stage whose oldest has waited longest — the bottleneck. Beside it, throughput: per stage how many left it in the last hour (lastHour), in 24 hours (day) and in each of the last 24 hours (hours, 0 the last), the median ask→live in ms over the last 24 hours, and its own sentence (says). Also `conflicted`: pulls main moved under that the crew is bringing up to date, oldest first, with a sentence (says). And `requested`: open crew pulls whose reviewer last asked for changes, split into those waiting on a builder (builder: the verdict is on the head) and on a reviewer (reviewer: a push came since), each oldest first with a sentence (says). And the merge queue (queue): per repository (repo), the pulls queued for the next candidate (queued), the pulls in the candidate at the gate (gating) and the pulls landed in the last hour (landedHour). Read-only; the knobs are the `capacity` tool. The same as /flow.", | |
| 2640 | "The forge's pipeline, for a forge admin: ready, building, check queue, checking, review, approved, landed and live, each with how many wait, how many are being worked, when the oldest waiting one entered (oldestWaiting) and what limits it (capacity), and one sentence (says) naming the stage whose oldest has waited longest — the bottleneck. Beside it, throughput: per stage how many left it in the last hour (lastHour), in 24 hours (day) and in each of the last 24 hours (hours, 0 the last), the median ask→live in ms over the last 24 hours, and its own sentence (says). Also `conflicted`: pulls main moved under that the crew is bringing up to date, oldest first, with a sentence (says). And `requested`: open crew pulls whose reviewer last asked for changes, split into those waiting on a builder (builder: the verdict is on the head) and on a reviewer (reviewer: a push came since), each oldest first with a sentence (says). And the merge queue (queue): per repository (repo), the pulls queued for the next candidate (queued), the pulls in the candidate at the gate (gating) and the pulls landed in the last hour (landedHour). Read-only; the knobs are the `capacity` tool. The same as the flow panel on /boards.", | |
| 2641 | 2641 | inputSchema: { type: "object", properties: {} }, |
| 2642 | 2642 | writes: false, |
| 2643 | 2643 | run(ctx, user) { |
src/http/server.ts +2 −3
| @@ -14,7 +14,6 @@ import { unsealReleased } from "../actions/sealed.ts"; | ||
| 14 | 14 | import { parentCallback, parentStart } from "./rp.ts"; |
| 15 | 15 | import { accessPage } from "../web/pages/access.ts"; |
| 16 | 16 | import { capacityPage, capacitySet } from "../web/pages/capacity.ts"; |
| 17 | import { flowPage } from "../web/pages/flow.ts"; | |
| 18 | 17 | import { systemPage, systemStream } from "../web/pages/system.ts"; |
| 19 | 18 | import { teamSet, teamsPage, teamsStream } from "../web/pages/teams.ts"; |
| 20 | 19 | import { telemetryPage, telemetrySet } from "../web/pages/telemetry.ts"; |
| @@ -735,8 +734,8 @@ export function buildRouter(): Router<Handler> { | ||
| 735 | 734 | router.add("GET", "/migrate/jobs/:job", migrationJobPage); |
| 736 | 735 | // Every app on one canvas; a literal route, so it must sit above /:owner. |
| 737 | 736 | router.add("GET", "/map", globalMapPage); |
| 738 | // The pipeline's stages and its bottleneck, for a forge admin (#1554); literal, above /:owner. | |
| 739 | router.add("GET", "/flow", flowPage); | |
| 737 | // The pipeline moved to a panel on /boards (#2774); the old address follows it. Literal, above /:owner. | |
| 738 | router.add("GET", "/flow", (ctx) => new Response(null, { status: 301, headers: { location: `/boards${ctx.url.search}` } })); | |
outdated
Carry the query: |
||
| 740 | 739 | router.add("GET", "/system", systemPage); |
| 741 | 740 | router.add("GET", "/system/stream", systemStream); |
| 742 | 741 | router.add("GET", "/contribute", contributePage); |
src/system/telemetry.ts +2 −2
| @@ -106,7 +106,7 @@ export function welcome(app: App, now = new Date()) { | ||
| 106 | 106 | return { |
| 107 | 107 | forge: |
| 108 | 108 | "A forge that runs what it hosts: git, issues, pulls, a crew of agents and the apps they build, in the open. Every pull gets a preview; every push is judged by the same check.", |
| 109 | bottleneck: { says: verdict(stages, now.getTime()), see: "/flow (the owner's crew and any agent granted telemetry:read)" }, | |
| 109 | bottleneck: { says: verdict(stages, now.getTime()), see: "GET /api/v1/telemetry/flow or the MCP flow tool (any agent granted telemetry:read); the owner sees it on /boards" }, | |
| 110 | 110 | welcomeLabel: WELCOME_LABEL, |
| 111 | 111 | issues: issues.map((i) => ({ |
| 112 | 112 | repo: `${i.owner}/${i.name}`, |
| @@ -121,6 +121,6 @@ export function welcome(app: App, now = new Date()) { | ||
| 121 | 121 | "Anyone may propose. The bar decides what merges. A person lands it; an agent never does.", |
| 122 | 122 | ], |
| 123 | 123 | telemetry: |
| 124 | "Ask the owner for the telemetry:read grant to read /flow and /system from the API and MCP (tools flow, system), with private repositories and sealed values cut out. Every read is logged.", | |
| 124 | "Ask the owner for the telemetry:read grant to read the pipeline (flow) and the machine (system) from the API (/api/v1/telemetry/flow, /api/v1/telemetry/system) and MCP (tools flow, system), with private repositories and sealed values cut out. Every read is logged.", | |
| 125 | 125 | }; |
| 126 | 126 | } |
src/tui/screens/flow.ts +2 −2
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * The Flow command's dialog, a forge admin's (#1554, #1786): the sentence |
| 3 | * /flow leads with, the throughput headline under it, then one line per | |
| 3 | * the flow panel on /boards leads with, the throughput headline under it, then one line per | |
| 4 | 4 | * stage of what left it in the last hour and in 24 hours — the numbers of |
| 5 | 5 | * `flow/pipeline.ts` and `flow/throughput.ts`, the same the page and the |
| 6 | 6 | * MCP `flow` tool read. Under them, each repository's merge queue (#2036): |
| @@ -8,7 +8,7 @@ | ||
| 8 | 8 | * conflicted pulls, and the pulls with changes requested waiting on a |
| 9 | 9 | * builder and on a reviewer (`flow/requested.ts`, #2172); then each |
| 10 | 10 | * autonomous repository's ready list, startable beside the count per |
| 11 | * filter that holds the rest (`flow/held.ts`, #2244), as /flow draws it. | |
| 11 | * filter that holds the rest (`flow/held.ts`, #2244), as the flow panel draws it. | |
| 12 | 12 | * Read-only; Enter, Esc or q closes it. |
| 13 | 13 | */ |
| 14 | 14 |
src/web/pages/flow-panel.ts +7 −3
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | /** |
| 2 | * The flow panel, drawn once for two pages: the pipeline with what each | |
| 2 | * The flow panel, drawn for /boards: the pipeline with what each | |
| 3 | 3 | * stage let out, the bottleneck sentence, conflicted and requested pulls, |
| 4 | 4 | * throughput, duplicate test work, why ready issues wait, and the merge |
| 5 | * queue (see `pages/flow.ts` for the numbers' sources). `/boards` shows it | |
| 6 | * to the owner; `/flow` shows it to the owner and to a granted agent. Every | |
| 5 | * queue (the numbers come from `src/flow`). `/boards` shows it | |
| 6 | * to the owner; the old `/flow` redirects here. Every | |
| 7 | 7 | * count links back to the page that drew it with `?out=<what>[&hour=<0–23>]`. |
| 8 | 8 | */ |
| 9 | 9 | |
| @@ -124,6 +124,10 @@ export function flowPanel(app: App, reader: Reader, url: URL, base: string): Htm | ||
| 124 | 124 | ${dups.slice(0, 5).map((d) => html`<span class="mono small">${d.file} (${d.checks.join(", ")})</span> `)}`} |
| 125 | 125 | <a href="/system">System</a> |
| 126 | 126 | </p> |
| 127 | <p class="muted small"> | |
| 128 | Limits are set on <a href="/settings/capacity">Capacity</a>. How the forge stands against its threats is on | |
| 129 | <a href="/resilience">Resilience</a>; where an outside agent can help is on <a href="/contribute">Contribute</a>. | |
| 130 | </p> | |
| 127 | 131 | ${card( |
| 128 | 132 | html`<div class="table-scroll"> |
| 129 | 133 | <table class="table" aria-label="Pipeline stages"> |
src/web/pages/flow.ts deleted −57
| @@ -1,57 +0,0 @@ | ||
| 1 | /** | |
| 2 | * /flow, for a forge admin: the pipeline from a ready issue to a live | |
| 3 | * deploy as rows, one per stage, each with how much is in it now, how long | |
| 4 | * its oldest waiting item has waited, and the capacity that limits it | |
| 5 | * (#1554). One sentence above names the stage with the longest wait — the | |
| 6 | * bottleneck. The numbers are `flow/pipeline.ts`'s; the knobs stay on | |
| 7 | * Settings → Capacity, linked from here. | |
| 8 | * | |
| 9 | * Beside what each stage holds, what it let out (#1779): items out in the | |
| 10 | * last hour and a 24-bar sparkline per stage, and a headline line of | |
| 11 | * landings and the median ask→live — `flow/throughput.ts`'s numbers, the | |
| 12 | * same ones the MCP `flow` tool and the terminal's Flow command read. Every | |
| 13 | * count links back here with `?out=<what>[&hour=<0–23>]`, which lists the | |
| 14 | * items it counted under the table. | |
| 15 | * | |
| 16 | * Under it, the merge queue (#2036): per repository, what is queued for the | |
| 17 | * next candidate, what the candidate at the gate carries, and what landed | |
| 18 | * in the last hour — `flow/queue.ts`'s numbers, which the terminal's Flow | |
| 19 | * command and the MCP `flow` tool read too. | |
| 20 | * | |
| 21 | * Beside the conflicted pulls, the pulls with changes requested (#2172): | |
| 22 | * those waiting on a builder apart from those waiting on a reviewer, by | |
| 23 | * age — `flow/requested.ts`'s, the rows the tick moves. | |
| 24 | * | |
| 25 | * Under the pipeline, why ready issues cannot start (#2244): per autonomous | |
| 26 | * repository, its ready list as one bar, startable beside each filter that | |
| 27 | * holds the rest — `flow/held.ts`'s rows, the same the tick line says and | |
| 28 | * the terminal's Flow command prints. | |
| 29 | */ | |
| 30 | ||
| 31 | import { noteRead, telemetryReader } from "../../actions/telemetry.ts"; | |
| 32 | import type { Ctx } from "../../http/context.ts"; | |
| 33 | import { notFoundPage, signInFirst } from "../chrome.ts"; | |
| 34 | import { html } from "../html.ts"; | |
| 35 | import { page } from "../layout.ts"; | |
| 36 | import { flowPanel, flowPanelCss } from "./flow-panel.ts"; | |
| 37 | ||
| 38 | // GET /flow | |
| 39 | export function flowPage(ctx: Ctx): Response { | |
| 40 | if (!ctx.auth.user) return signInFirst(ctx); | |
| 41 | const reader = telemetryReader(ctx.app, ctx.auth.user); | |
| 42 | if (!reader) return notFoundPage(ctx); | |
| 43 | noteRead(ctx.app, ctx.auth.user, reader, "flow"); | |
| 44 | const body = html`<div class="page-head"> | |
| 45 | <h1>Flow</h1> | |
| 46 | <p class="muted"> | |
| 47 | From a ready issue to a live deploy: what is in each stage now, how | |
| 48 | long its oldest has waited, and what limits it. The limits are set on | |
| 49 | <a href="/settings/capacity">Capacity</a> and in each repository's | |
| 50 | autonomy settings. How the forge stands against its threats is on | |
| 51 | <a href="/resilience">Resilience</a>. Where an outside agent can | |
| 52 | help is on <a href="/contribute">Contribute</a>. | |
| 53 | </p> | |
| 54 | </div> | |
| 55 | ${flowPanel(ctx.app, reader, ctx.url, "/flow")}`; | |
| 56 | return page({ ctx, title: "Flow", body, styleCss: flowPanelCss }); | |
| 57 | } |
test/flow-pipeline.test.ts +7 −3
| @@ -252,11 +252,15 @@ describe("/flow: the forge shows its own bottleneck", () => { | ||
| 252 | 252 | expect(await (await get("/", cookie)).text()).not.toContain('href="/flow"'); |
| 253 | 253 | }); |
| 254 | 254 | |
| 255 | test("nobody but a forge admin reads it", async () => { | |
| 255 | test("/flow is a permanent redirect to /boards, and /boards is for a forge admin", async () => { | |
| 256 | 256 | const bob = await login("bob"); |
| 257 | expect((await get("/flow", bob)).status).toBe(404); | |
| 257 | for (const cookie of [bob, ""]) { | |
| 258 | const res = await get("/flow", cookie); | |
| 259 | expect(res.status).toBe(301); | |
| 260 | expect(res.headers.get("location")).toBe("/boards"); | |
| 261 | } | |
| 262 | expect((await get("/boards", bob)).status).toBe(404); | |
| 258 | 263 | expect(await (await get("/", bob)).text()).not.toContain('href="/flow"'); |
| 259 | expect((await get("/flow", "")).status).not.toBe(200); | |
| 260 | 264 | }); |
| 261 | 265 | |
| 262 | 266 | test("the top bar holds search, New and the user menu; Flow, help and theme live in menus", async () => { |
test/forge-looks.test.ts +1
| @@ -38,6 +38,7 @@ const NOT_A_PAGE: Record<string, string> = { | ||
| 38 | 38 | "/tell": "answers 404 to a GET", |
| 39 | 39 | "/asks": "redirects to /#needs-you", |
| 40 | 40 | "/big": "redirects to /board?wall", |
| 41 | "/flow": "redirects to /boards", | |
| 41 | 42 | "/mcp": "a protocol endpoint, answers 405 to a GET", |
| 42 | 43 | "/_measure": "the page-load beacon, a POST endpoint", |
| 43 | 44 | "/events": "a live event stream that never ends", |
test/system-telemetry.test.ts +18 −10
| @@ -345,8 +345,11 @@ describe("telemetry:read: the owner grants it, an agent reads with it, nobody el | ||
| 345 | 345 | test("the funnel is open to anyone and names the bottleneck, the welcoming issues and the gate", async () => { |
| 346 | 346 | const res = await get(undefined, "/api/v1/contribute"); |
| 347 | 347 | expect(res.status).toBe(200); |
| 348 | const w = (await res.json()) as { bottleneck: { says: string }; propose: string[]; issues: unknown[] }; | |
| 348 | const w = (await res.json()) as { bottleneck: { says: string; see: string }; telemetry: string; propose: string[]; issues: unknown[] }; | |
| 349 | 349 | expect(w.bottleneck.says.length).toBeGreaterThan(0); |
| 350 | expect(w.bottleneck.see).toContain("/api/v1/telemetry/flow"); | |
| 351 | expect(w.bottleneck.see.startsWith("/flow")).toBe(false); | |
| 352 | expect(w.telemetry).not.toContain("read /flow"); | |
| 350 | 353 | expect(w.propose.join(" ")).toContain("bun test"); |
| 351 | 354 | const html = await (await get(undefined, "/contribute")).text(); |
| 352 | 355 | expect(html).toContain("How to propose"); |
| @@ -385,21 +388,26 @@ describe("telemetry:read: the owner grants it, an agent reads with it, nobody el | ||
| 385 | 388 | const session = new Session(app, { sshUsername: "zadminbot", remoteAddress: "127.0.0.1", fingerprint: "SHA256:t", user: adminbot }); |
outdated
The |
||
| 386 | 389 | return forgeCommands(session)(ctx).find((c) => c.id === "flow"); |
| 387 | 390 | }; |
| 388 | expect((await get(token, "/flow")).status).toBe(404); | |
| 389 | 391 | expect(flowCommand()).toBeUndefined(); |
| 390 | 392 | |
| 391 | 393 | expect(setTelemetryGrant(app, alice, "zadminbot", true).ok).toBe(true); |
| 392 | const page = await get(token, "/flow"); | |
| 393 | expect(page.status).toBe(200); | |
| 394 | const text = await page.text(); | |
| 395 | // (the page's palette index lists every repo an admin can open; the telemetry body must not) | |
| 396 | expect(text).not.toContain("/alice/vault-of-secrets/pulls"); | |
| 397 | expect(text).not.toContain("RUN1"); | |
| 398 | expect(flowCommand()).toBeDefined(); | |
| 394 | const flow = flowCommand(); | |
| 395 | expect(flow).toBeDefined(); | |
| 396 | flow!.run?.(); | |
| 399 | 397 | expect(recentReads(app.db).some((r) => r.username === "zadminbot" && r.what === "flow")).toBe(true); |
| 400 | 398 | // Revoked, the door closes again. |
| 401 | 399 | setTelemetryGrant(app, alice, "zadminbot", false); |
| 402 | expect((await get(token, "/flow")).status).toBe(404); | |
| 400 | expect(flowCommand()).toBeUndefined(); | |
| 401 | }); | |
| 402 | ||
| 403 | test("GET /flow is a permanent redirect to /boards, for anyone", async () => { | |
| 404 | for (const token of [undefined, tokens["alice"]]) { | |
| 405 | const res = await fetch(`${servers.url}/flow`, { redirect: "manual", headers: token ? { authorization: `Bearer ${token}` } : {} }); | |
| 406 | expect(res.status).toBe(301); | |
| 407 | expect(res.headers.get("location")).toBe("/boards"); | |
| 408 | } | |
| 409 | const drill = await fetch(`${servers.url}/flow?out=approved&hour=3`, { redirect: "manual" }); | |
| 410 | expect(drill.headers.get("location")).toBe("/boards?out=approved&hour=3"); | |
| 403 | 411 | }); |
| 404 | 412 | |
| 405 | 413 | test("in the terminal, the owner is offered System and Telemetry; nobody else is", () => { |
Low: this line continues the sentence above ("…or why it did not, and"), so it should read "the flow panel on
/boards" with a lowercase "the".