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

Merged opened this

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

Merged

scout/forge:scout/2832-get-flow-answers-301-to-boards-and-the-f → main 3 commits fork 3 threads · 3 unresolved

  • Merged. scout/2832-get-flow-answers-301-to-boards-and-the-f was merged into main as 24f02d1.

Review

Sign in to review this change.
12 files changed +48 −89
README.md +5 −5
@@ -887,7 +887,7 @@ code never blocks. If the verdict is on the current head and no run is in
887887 flight, one feedback run is queued to the pull's builder, and a builder is
888888 assigned when the pull has none. That run takes a builder slot like any
889889 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

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".

891891 waiting on a builder and those waiting on a reviewer, oldest first.
892892
893893 **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
921921 lands, and the red pull is handed back with the failing tests named. Pulls
922922 that turn green while a candidate is gating wait for the next one. Each
923923 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
925925 terminal's Flow command, and the MCP `flow` tool's `queue` field.
926926
927927 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
936936 open_pull=10 stalled=12 run_cap=11` — a run on it, an open pull proposing it,
937937 the planner's, found done on main and waiting for a person to close it
938938 (`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
940940 bar, and the terminal's Flow command prints it as a line per app
941941 (`flow/held.ts`, kept 5 s). When nothing is startable while builder slots are free and some of the
942942 held are stalled, capped or parked twice, the planner is owed one pass with
@@ -984,13 +984,13 @@ from before this record say nothing).
984984 mechanical rather than advisory.
985985
986986 `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.
988988
989989 `/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.
990990
991991 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.
992992
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.
994994 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.
995995 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.
996996
forge.json +1 −1
@@ -3,7 +3,7 @@
33 "api.anthropic.com"
44 ],
55 "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"],
77 "run": "bun run src/main.ts",
88 "install": "",
99 "health": "/healthz",
src/checks/queue.ts +2 −2
@@ -12,7 +12,7 @@
1212 *
1313 * What is queued is not stored: it is the pulls `landIfGreen` admitted, and
1414 * 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.
1616 */
1717
1818 import type { Pull } from "../core/pulls.ts";
@@ -132,7 +132,7 @@ export function enqueue(
132132 return Promise.all(answers);
133133 }
134134
135 /** What /flow shows of each repository's line. */
135 /** What the flow panel shows of each repository's line. */
136136 export interface QueueStatus {
137137 readonly repoId: string;
138138 readonly queued: number;
src/http/mcp.ts +1 −1
@@ -2637,7 +2637,7 @@ const TOOLS: readonly Tool[] = [
26372637 {
26382638 name: "flow",
26392639 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.",
26412641 inputSchema: { type: "object", properties: {} },
26422642 writes: false,
26432643 run(ctx, user) {
src/http/server.ts +2 −3
@@ -14,7 +14,6 @@ import { unsealReleased } from "../actions/sealed.ts";
1414 import { parentCallback, parentStart } from "./rp.ts";
1515 import { accessPage } from "../web/pages/access.ts";
1616 import { capacityPage, capacitySet } from "../web/pages/capacity.ts";
17 import { flowPage } from "../web/pages/flow.ts";
1817 import { systemPage, systemStream } from "../web/pages/system.ts";
1918 import { teamSet, teamsPage, teamsStream } from "../web/pages/teams.ts";
2019 import { telemetryPage, telemetrySet } from "../web/pages/telemetry.ts";
@@ -735,8 +734,8 @@ export function buildRouter(): Router<Handler> {
735734 router.add("GET", "/migrate/jobs/:job", migrationJobPage);
736735 // Every app on one canvas; a literal route, so it must sit above /:owner.
737736 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: (ctx) => new Response(null, { status: 301, headers: { location: "/boards" + ctx.url.search } }). /boards reads the same ?out=/?hour= that /flow did, so existing drill-down links keep working. Add one assertion for it in the redirect test.

740739 router.add("GET", "/system", systemPage);
741740 router.add("GET", "/system/stream", systemStream);
742741 router.add("GET", "/contribute", contributePage);
src/system/telemetry.ts +2 −2
@@ -106,7 +106,7 @@ export function welcome(app: App, now = new Date()) {
106106 return {
107107 forge:
108108 "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" },
110110 welcomeLabel: WELCOME_LABEL,
111111 issues: issues.map((i) => ({
112112 repo: `${i.owner}/${i.name}`,
@@ -121,6 +121,6 @@ export function welcome(app: App, now = new Date()) {
121121 "Anyone may propose. The bar decides what merges. A person lands it; an agent never does.",
122122 ],
123123 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.",
125125 };
126126 }
src/tui/screens/flow.ts +2 −2
@@ -1,6 +1,6 @@
11 /**
22 * 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
44 * stage of what left it in the last hour and in 24 hours — the numbers of
55 * `flow/pipeline.ts` and `flow/throughput.ts`, the same the page and the
66 * MCP `flow` tool read. Under them, each repository's merge queue (#2036):
@@ -8,7 +8,7 @@
88 * conflicted pulls, and the pulls with changes requested waiting on a
99 * builder and on a reviewer (`flow/requested.ts`, #2172); then each
1010 * 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.
1212 * Read-only; Enter, Esc or q closes it.
1313 */
1414
src/web/pages/flow-panel.ts +7 −3
@@ -1,9 +1,9 @@
11 /**
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
33 * stage let out, the bottleneck sentence, conflicted and requested pulls,
44 * 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
77 * count links back to the page that drew it with `?out=<what>[&hour=<0–23>]`.
88 */
99
@@ -124,6 +124,10 @@ export function flowPanel(app: App, reader: Reader, url: URL, base: string): Htm
124124 ${dups.slice(0, 5).map((d) => html`<span class="mono small">${d.file} (${d.checks.join(", ")})</span> `)}`}
125125 <a href="/system">System</a>
126126 </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>
127131 ${card(
128132 html`<div class="table-scroll">
129133 <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", () => {
252252 expect(await (await get("/", cookie)).text()).not.toContain('href="/flow"');
253253 });
254254
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 () => {
256256 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);
258263 expect(await (await get("/", bob)).text()).not.toContain('href="/flow"');
259 expect((await get("/flow", "")).status).not.toBe(200);
260264 });
261265
262266 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> = {
3838 "/tell": "answers 404 to a GET",
3939 "/asks": "redirects to /#needs-you",
4040 "/big": "redirects to /board?wall",
41 "/flow": "redirects to /boards",
4142 "/mcp": "a protocol endpoint, answers 405 to a GET",
4243 "/_measure": "the page-load beacon, a POST endpoint",
4344 "/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
345345 test("the funnel is open to anyone and names the bottleneck, the welcoming issues and the gate", async () => {
346346 const res = await get(undefined, "/api/v1/contribute");
347347 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[] };
349349 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");
350353 expect(w.propose.join(" ")).toContain("bun test");
351354 const html = await (await get(undefined, "/contribute")).text();
352355 expect(html).toContain("How to propose");
@@ -385,21 +388,26 @@ describe("telemetry:read: the owner grants it, an agent reads with it, nobody el
385388 const session = new Session(app, { sshUsername: "zadminbot", remoteAddress: "127.0.0.1", fingerprint: "SHA256:t", user: adminbot });
outdated

The recentReads(...).some(r => r.what === "flow") assertion went away with the page. The terminal's Flow command still records that read, so run it here and assert the read is logged. Otherwise the 'every read is logged' promise has no test.

386389 return forgeCommands(session)(ctx).find((c) => c.id === "flow");
387390 };
388 expect((await get(token, "/flow")).status).toBe(404);
389391 expect(flowCommand()).toBeUndefined();
390392
391393 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?.();
399397 expect(recentReads(app.db).some((r) => r.username === "zadminbot" && r.what === "flow")).toBe(true);
400398 // Revoked, the door closes again.
401399 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");
403411 });
404412
405413 test("in the terminal, the owner is offered System and Telemetry; nobody else is", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs