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

"Flags are set by a forge admin." lives in one place, read by the flag action, the web page and the terminal #5609

Merged opened this

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

Merged

mason/forge:mason/5593-flags-are-set-by-a-forge-admin-lives-in → main 1 commit fork

  • Merged. mason/5593-flags-are-set-by-a-forge-admin-lives-in was merged into main as 6898f26.

Review

Sign in to review this change.
5 files changed +37 −5
src/actions/flag_winner.ts +2 −2
@@ -7,7 +7,7 @@
77 */
88
99 import type { App } from "../app.ts";
10 import { flagRegistry, mayManageFlags, winnerOffer } from "../core/flags.ts";
10 import { FLAGS_ADMIN_ONLY, flagRegistry, mayManageFlags, winnerOffer } from "../core/flags.ts";
1111 import type { Issue } from "../core/issues.ts";
1212 import { getRepo } from "../core/repos.ts";
1313 import type { User } from "../core/users.ts";
@@ -19,7 +19,7 @@ export function makeWinnerDefault(
1919 name: string,
2020 winner: "on" | "off",
2121 ): { ok: true; issue: Issue } | { ok: false; message: string } {
22 if (!mayManageFlags(actor)) return { ok: false, message: "Flags are set by a forge admin." };
22 if (!mayManageFlags(actor)) return { ok: false, message: FLAGS_ADMIN_ONLY };
2323 const registry = flagRegistry();
2424 const decl = (registry.decls as Record<string, { why: string; issue: number }>)[name];
2525 if (!Object.hasOwn(registry.decls, name) || decl === undefined)
src/core/flags.ts +3
@@ -323,3 +323,6 @@ export function useFlagRegistry(next: Registry | undefined): void {
323323 export function mayManageFlags(user: { isAdmin: boolean; kind: string } | undefined): boolean {
324324 return user !== undefined && user.isAdmin && user.kind !== "agent";
325325 }
326
327 /** What a person who may not manage flags is told, by the action, the page and the terminal alike. */
328 export const FLAGS_ADMIN_ONLY = "Flags are set by a forge admin.";
src/tui/screens/flags.ts +2 −1
@@ -11,6 +11,7 @@
1111
1212 import {
1313 checkInput,
14 FLAGS_ADMIN_ONLY,
1415 flagRegistry,
1516 mayManageFlags,
1617 usesLine,
@@ -111,7 +112,7 @@ export function flagsScreen(f: Session, ctx: Ctx): Screen {
111112 note !== ""
112113 ? note
113114 : !allowed
114 ? "Flags are set by a forge admin."
115 ? FLAGS_ADMIN_ONLY
115116 : names().length === 0
116117 ? "No flag is declared yet."
117118 : usesOf(names()[state.index] ?? names()[0]!);
src/web/pages/flags.ts +2 −2
@@ -6,7 +6,7 @@
66 * Anyone else is told 403, and an agent is never an admin here.
77 */
88
9 import { checkInput, flagRegistry, usesLine, winnerOffer, mayManageFlags, useFlagRegistry, type FlagDecl, type FlagValue } from "../../core/flags.ts";
9 import { checkInput, FLAGS_ADMIN_ONLY, flagRegistry, usesLine, winnerOffer, mayManageFlags, useFlagRegistry, type FlagDecl, type FlagValue } from "../../core/flags.ts";
1010 import { makeWinnerDefault } from "../../actions/flag_winner.ts";
1111 import type { Ctx } from "../../http/context.ts";
1212 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
@@ -31,7 +31,7 @@ function denied(ctx: Ctx): Response | undefined {
3131 ${empty({
3232 icon: "lock",
3333 title: "Admins only",
34 note: "Flags are set by a forge admin.",
34 note: FLAGS_ADMIN_ONLY,
3535 action: btn("Go home", { href: "/", variant: "primary" }),
3636 })}`,
3737 },
test/flags-words.test.ts added +28
@@ -0,0 +1,28 @@
1 /**
2 * "Flags are set by a forge admin." is said from one place, src/core/flags.ts,
3 * so the action, the web page and the terminal cannot drift apart (#5593).
4 * Each file that says it is read as text: a copy typed back fails here.
5 */
6 import { describe, expect, test } from "bun:test";
7 import { readFileSync } from "node:fs";
8 import { join } from "node:path";
9 import { FLAGS_ADMIN_ONLY } from "../src/core/flags.ts";
10
11 const sentences = { FLAGS_ADMIN_ONLY };
12 const readers = ["src/actions/flag_winner.ts", "src/web/pages/flags.ts", "src/tui/screens/flags.ts"];
13
14 describe("the flag words live in src/core/flags.ts", () => {
15 test("each sentence is non-empty", () => {
16 for (const words of Object.values(sentences)) expect(words.trim()).not.toBe("");
17 });
18
19 test("no reader types a sentence out again; each imports it", () => {
20 for (const file of readers) {
21 const source = readFileSync(join(import.meta.dir, "..", file), "utf8");
22 for (const [name, words] of Object.entries(sentences)) {
23 expect(source).not.toContain(words);
24 expect(source).toContain(name);
25 }
26 }
27 });
28 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs