Public: anyone can read this; the crew can change it.
Severing a fork and an invite that is not open are said from one module on the web and in the terminal #5717
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5656-severing-a-fork-and-an-invite-that-is-no → main 1 commit fork 2 threads · 2 unresolved
6 files
changed +49
−4
src/actions/sever_invite_words.ts added +10
| @@ -0,0 +1,10 @@ | ||
| 1 | /** | |
| 2 | * The words for severing a fork and for an invite link that is not open, | |
| 3 | * said once so the page, the terminal and every other door read alike. | |
| 4 | */ | |
| 5 | ||
| 6 | /** Severing a fork asks for the word, and says so when it is missing. */ | |
| 7 | export const SEVER_CONFIRM = "Type sever to confirm."; | |
| 8 | ||
| 9 | /** Revoking an invite link that is already used, revoked or expired. */ | |
| 10 | export const INVITE_NOT_OPEN = "That link is not open."; |
src/tui/screens/chain.ts +2 −1
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | |
| 8 | 8 | import type { App as Forge } from "../../app.ts"; |
| 9 | 9 | import { FOLLOW_MODE_REFUSALS, setFollowMode, severForge, takeForgeCode, takeForgeWords } from "../../actions/follow.ts"; |
| 10 | import { SEVER_CONFIRM } from "../../actions/sever_invite_words.ts"; | |
| 10 | 11 | import { forgeProposalLine, forgeProposals, proposeToParentForge } from "../../actions/upstream.ts"; |
| 11 | 12 | import { checkUpstream, upstreamState, upstreamWords } from "../../spawn/upstream.ts"; |
| 12 | 13 | import { parentCredential } from "../../spawn/parent_credential.ts"; |
| @@ -117,7 +118,7 @@ export async function chainScreen(app: Forge, user: User | undefined, ctx: Ctx): | ||
| 117 | 118 | title: "Sever from the parent forge", |
| 118 | 119 | label: "type sever to confirm", |
| 119 | 120 | onSubmit: (word) => { |
| 120 | if (word.trim() !== "sever") return "Type sever to confirm."; | |
| 121 | if (word.trim() !== "sever") return SEVER_CONFIRM; | |
| 121 | 122 | return severForge(app, user, repo).then((cut) => { |
| 122 | 123 | if (!cut.ok) return ctx.toast(`Not severed: ${cut.error}`, "danger"); |
| 123 | 124 | ctx.pop(); |
src/tui/screens/invites.ts +2 −1
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | */ |
| 9 | 9 | |
| 10 | 10 | import { inviteLink, list, mint, revoke } from "../../actions/invites.ts"; |
| 11 | import { INVITE_NOT_OPEN } from "../../actions/sever_invite_words.ts"; | |
| 11 | 12 | import { DEFAULT_DAYS, MAX_DAYS } from "../../core/invites.ts"; |
| 12 | 13 | import type { Ctx, Screen } from "../app/app.ts"; |
| 13 | 14 | import { binding } from "../app/keys.ts"; |
| @@ -82,7 +83,7 @@ export function invitesScreen(f: Session, ctx: Ctx): Screen { | ||
| 82 | 83 | } else if (e.key === "x") { |
| 83 | 84 | const row = all[state.index]; |
| 84 | 85 | if (row === undefined) return false; |
| 85 | note = revoke(f.app, f.user, row.id).ok ? `Revoked ${row.id}.` : "That link is not open."; | |
| 86 | note = revoke(f.app, f.user, row.id).ok ? `Revoked ${row.id}.` : INVITE_NOT_OPEN; | |
| 86 | 87 | ctx.invalidate(); |
| 87 | 88 | } else return false; |
| 88 | 89 | return true; |
src/web/pages/admin-limits.ts +2 −1
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | import { changeLimit, listLimits } from "../../actions/limits.ts"; |
| 9 | 9 | import { changeQuotas, readQuotas } from "../../actions/quotas.ts"; |
| 10 | 10 | import { inviteLink, list as listInviteLinks, mint as mintInviteLink, revoke as revokeInviteLink } from "../../actions/invites.ts"; |
| 11 | import { INVITE_NOT_OPEN } from "../../actions/sever_invite_words.ts"; | |
| 11 | 12 | import { DEFAULT_DAYS, MAX_DAYS } from "../../core/invites.ts"; |
| 12 | 13 | import { changeSignup, readSignup } from "../../actions/signup.ts"; |
| 13 | 14 | import { SIGNUP_MODES } from "../../settings/signup.ts"; |
| @@ -350,6 +351,6 @@ export async function inviteRevoke(ctx: Ctx): Promise<Response> { | ||
| 350 | 351 | "/admin/limits", |
| 351 | 352 | done.ok |
| 352 | 353 | ? { message: "Invite link revoked." } |
| 353 | : { tone: "danger", message: "That link is not open." }, | |
| 354 | : { tone: "danger", message: INVITE_NOT_OPEN }, | |
| 354 | 355 | ); |
| 355 | 356 | } |
src/web/pages/chain.ts +2 −1
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | |
| 8 | 8 | import { FOLLOW_MODE_REFUSALS, setFollowMode, severForge, takeForgeCode, takeForgeWords } from "../../actions/follow.ts"; |
| 9 | 9 | import { forgeProposals, proposeToParentForge, type ForgeProposal } from "../../actions/upstream.ts"; |
| 10 | import { SEVER_CONFIRM } from "../../actions/sever_invite_words.ts"; | |
| 10 | 11 | import { canAdmin, canWrite } from "../../core/repos.ts"; |
| 11 | 12 | import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts"; |
| 12 | 13 | import type { Ctx } from "../../http/context.ts"; |
| @@ -191,7 +192,7 @@ export async function severSubmit(ctx: Ctx): Promise<Response> { | ||
| 191 | 192 | if (form instanceof Response) return form; |
| 192 | 193 | const to = `${repoBase(repo)}/following`; |
| 193 | 194 | if (field(form, "confirm") !== "sever") |
| 194 | return flashOrJson(ctx, to, { tone: "danger", message: "Type sever to confirm." }); | |
| 195 | return flashOrJson(ctx, to, { tone: "danger", message: SEVER_CONFIRM }); | |
| 195 | 196 | const cut = await severForge(ctx.app, ctx.auth.user, repo); |
| 196 | 197 | if (!cut.ok) |
| 197 | 198 | return flashOrJson(ctx, to, { |
test/sever-invite-words.test.ts added +31
| @@ -0,0 +1,31 @@ | ||
| 1 | import { describe, expect, test } from "bun:test"; | |
| 2 | import { readFileSync } from "node:fs"; | |
| 3 | import { join } from "node:path"; | |
| 4 | import { INVITE_NOT_OPEN, SEVER_CONFIRM } from "../src/actions/sever_invite_words.ts"; | |
| 5 | ||
| 6 | // Severing a fork and an invite link that is not open are said from one | |
| 7 | // module; a copy typed back into a page or a screen fails here. | |
| 8 | const root = join(import.meta.dir, ".."); | |
| 9 | const read = (path: string): string => readFileSync(join(root, path), "utf8"); | |
| 10 | ||
| 11 | const SAID_FROM_ONE_PLACE: ReadonlyArray<readonly [string, readonly string[]]> = [ | |
| 12 | [SEVER_CONFIRM, ["src/web/pages/chain.ts", "src/tui/screens/chain.ts"]], | |
| 13 | [INVITE_NOT_OPEN, ["src/web/pages/admin-limits.ts", "src/tui/screens/invites.ts"]], | |
| 14 | ]; | |
| 15 | ||
| 16 | describe("sever and invite words", () => { | |
| 17 | test("the module says each sentence as the doors always did", () => { | |
| 18 | expect(SEVER_CONFIRM).toBe("Type sever to confirm."); | |
| 19 | expect(INVITE_NOT_OPEN).toBe("That link is not open."); | |
| 20 | }); | |
| 21 | ||
| 22 | for (const [sentence, files] of SAID_FROM_ONE_PLACE) { | |
This reads source files as text. Asserting the visible message from the real handler and screen would survive refactors. |
||
| 23 | for (const file of files) { | |
| 24 | test(`${file} imports "${sentence}" rather than typing it`, () => { | |
| 25 | const text = read(file); | |
| 26 | expect(text).not.toContain(sentence); | |
| 27 | expect(text).toContain("actions/sever_invite_words.ts"); | |
| 28 | }); | |
| 29 | } | |
| 30 | } | |
| 31 | }); |
new above
offline
Two concepts in one module (sever and invite). Prefer one module per concept, or put each sentence beside its action.