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

Merged opened this

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

Merged

mason/forge:mason/5656-severing-a-fork-and-an-invite-that-is-no → main 1 commit fork 2 threads · 2 unresolved

  • Merged. mason/5656-severing-a-fork-and-an-invite-that-is-no was merged into main as 13aaa04.

Review

Sign in to review this change.
6 files changed +49 −4
src/actions/sever_invite_words.ts added +10
@@ -0,0 +1,10 @@
1 /**

Two concepts in one module (sever and invite). Prefer one module per concept, or put each sentence beside its action.

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 @@
77
88 import type { App as Forge } from "../../app.ts";
99 import { FOLLOW_MODE_REFUSALS, setFollowMode, severForge, takeForgeCode, takeForgeWords } from "../../actions/follow.ts";
10 import { SEVER_CONFIRM } from "../../actions/sever_invite_words.ts";
1011 import { forgeProposalLine, forgeProposals, proposeToParentForge } from "../../actions/upstream.ts";
1112 import { checkUpstream, upstreamState, upstreamWords } from "../../spawn/upstream.ts";
1213 import { parentCredential } from "../../spawn/parent_credential.ts";
@@ -117,7 +118,7 @@ export async function chainScreen(app: Forge, user: User | undefined, ctx: Ctx):
117118 title: "Sever from the parent forge",
118119 label: "type sever to confirm",
119120 onSubmit: (word) => {
120 if (word.trim() !== "sever") return "Type sever to confirm.";
121 if (word.trim() !== "sever") return SEVER_CONFIRM;
121122 return severForge(app, user, repo).then((cut) => {
122123 if (!cut.ok) return ctx.toast(`Not severed: ${cut.error}`, "danger");
123124 ctx.pop();
src/tui/screens/invites.ts +2 −1
@@ -8,6 +8,7 @@
88 */
99
1010 import { inviteLink, list, mint, revoke } from "../../actions/invites.ts";
11 import { INVITE_NOT_OPEN } from "../../actions/sever_invite_words.ts";
1112 import { DEFAULT_DAYS, MAX_DAYS } from "../../core/invites.ts";
1213 import type { Ctx, Screen } from "../app/app.ts";
1314 import { binding } from "../app/keys.ts";
@@ -82,7 +83,7 @@ export function invitesScreen(f: Session, ctx: Ctx): Screen {
8283 } else if (e.key === "x") {
8384 const row = all[state.index];
8485 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;
8687 ctx.invalidate();
8788 } else return false;
8889 return true;
src/web/pages/admin-limits.ts +2 −1
@@ -8,6 +8,7 @@
88 import { changeLimit, listLimits } from "../../actions/limits.ts";
99 import { changeQuotas, readQuotas } from "../../actions/quotas.ts";
1010 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";
1112 import { DEFAULT_DAYS, MAX_DAYS } from "../../core/invites.ts";
1213 import { changeSignup, readSignup } from "../../actions/signup.ts";
1314 import { SIGNUP_MODES } from "../../settings/signup.ts";
@@ -350,6 +351,6 @@ export async function inviteRevoke(ctx: Ctx): Promise<Response> {
350351 "/admin/limits",
351352 done.ok
352353 ? { message: "Invite link revoked." }
353 : { tone: "danger", message: "That link is not open." },
354 : { tone: "danger", message: INVITE_NOT_OPEN },
354355 );
355356 }
src/web/pages/chain.ts +2 −1
@@ -7,6 +7,7 @@
77
88 import { FOLLOW_MODE_REFUSALS, setFollowMode, severForge, takeForgeCode, takeForgeWords } from "../../actions/follow.ts";
99 import { forgeProposals, proposeToParentForge, type ForgeProposal } from "../../actions/upstream.ts";
10 import { SEVER_CONFIRM } from "../../actions/sever_invite_words.ts";
1011 import { canAdmin, canWrite } from "../../core/repos.ts";
1112 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
1213 import type { Ctx } from "../../http/context.ts";
@@ -191,7 +192,7 @@ export async function severSubmit(ctx: Ctx): Promise<Response> {
191192 if (form instanceof Response) return form;
192193 const to = `${repoBase(repo)}/following`;
193194 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 });
195196 const cut = await severForge(ctx.app, ctx.auth.user, repo);
196197 if (!cut.ok)
197198 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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs