Public: anyone can read this; the crew can change it.
The terminal reads its refusals from the modules that own them, not its own copies #5599
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5594-the-terminal-reads-its-refusals-from-the → main 2 commits fork
7 files
changed +85
−8
scripts/shards.ts +3 −2
| @@ -445,7 +445,8 @@ export function sliceFiles( | ||
| 445 | 445 | |
| 446 | 446 | /** |
| 447 | 447 | * Parses `--shard N/M` (or the same shape from env). Empty / undefined means |
| 448 | * no matrix slice — run every named file. | |
| 448 | * no matrix slice — run every named file. Four digits a side: the gate | |
| 449 | * passed 999 files, and a slice per file must still be nameable. | |
| 449 | 450 | */ |
| 450 | 451 | export function parseShardSpec( |
| 451 | 452 | spec: string | undefined, |
| @@ -454,7 +455,7 @@ export function parseShardSpec( | ||
| 454 | 455 | | { ok: true; none: true } |
| 455 | 456 | | { ok: false; error: string } { |
| 456 | 457 | if (spec === undefined || spec.trim() === "") return { ok: true, none: true }; |
| 457 | const m = /^([1-9][0-9]{0,2})\/([1-9][0-9]{0,2})$/.exec(spec.trim()); | |
| 458 | const m = /^([1-9][0-9]{0,3})\/([1-9][0-9]{0,3})$/.exec(spec.trim()); | |
| 458 | 459 | if (!m) |
| 459 | 460 | return { |
| 460 | 461 | ok: false, |
src/actions/origins.ts +4 −1
| @@ -103,6 +103,9 @@ import { | ||
| 103 | 103 | type AccessError, |
| 104 | 104 | } from "./access.ts"; |
| 105 | 105 | |
| 106 | /** The words a connect carries when the account is already connected. */ | |
| 107 | export const ORIGIN_ALREADY_CONNECTED = "that account is already connected"; | |
| 108 | ||
| 106 | 109 | // ---- abuse control ---- |
| 107 | 110 | |
| 108 | 111 | /** |
| @@ -646,7 +649,7 @@ async function completeConnect( | ||
| 646 | 649 | expiresAt: tokens.expiresAt, |
| 647 | 650 | }); |
| 648 | 651 | if (!stored.ok) |
| 649 | return { status: "failed", detail: "that account is already connected" }; | |
| 652 | return { status: "failed", detail: ORIGIN_ALREADY_CONNECTED }; | |
| 650 | 653 | await afterConnect(app, user, stored.value); |
| 651 | 654 | return { status: "done", detail: "", originId: stored.value.id }; |
| 652 | 655 | } |
src/http/api.ts +4 −2
| @@ -74,6 +74,7 @@ import { CAPPED_WORDS } from "../actions/mail.ts"; | ||
| 74 | 74 | import { emailVerified } from "../core/verify.ts"; |
| 75 | 75 | import { readHealth } from "../actions/health.ts"; |
| 76 | 76 | import { isProbe } from "../kernel/probe.ts"; |
| 77 | import { PRIVATE_ADDRESS_REFUSED } from "../kernel/address_words.ts"; | |
| 77 | 78 | import { listChildren } from "../actions/spawn.ts"; |
| 78 | 79 | import { forgeState, PARENT_HEADER } from "../spawn/clients.ts"; |
| 79 | 80 | import { familyView } from "../spawn/family.ts"; |
| @@ -120,6 +121,7 @@ import { | ||
| 120 | 121 | syncOriginNow, |
| 121 | 122 | syncRepoNow, |
| 122 | 123 | unpinRepo, |
| 124 | ORIGIN_ALREADY_CONNECTED, | |
| 123 | 125 | } from "../actions/origins.ts"; |
| 124 | 126 | import { |
| 125 | 127 | createBranch, |
| @@ -6586,11 +6588,11 @@ export function forgeBaseUrl( | ||
| 6586 | 6588 | export function connectMessage(code: string): string { |
| 6587 | 6589 | if (code === "unknown_host") return "that does not look like a forge host"; |
| 6588 | 6590 | if (code === "private_host") |
| 6589 | return "that address is inside a network, not on the internet"; | |
| 6591 | return PRIVATE_ADDRESS_REFUSED; | |
| 6590 | 6592 | if (code === "unreachable") return "that host could not be reached"; |
| 6591 | 6593 | if (code === "bad_token") return "the origin rejected that token"; |
| 6592 | 6594 | if (code === "already_connected") |
| 6593 | return "that account is already connected here"; | |
| 6595 | return `${ORIGIN_ALREADY_CONNECTED} here`; | |
| 6594 | 6596 | if (code === "rate_limited") |
| 6595 | 6597 | return "too many connect attempts; wait a minute"; |
| 6596 | 6598 | return "that origin could not be connected"; |
src/kernel/address_words.ts added +7
| @@ -0,0 +1,7 @@ | ||
| 1 | /** | |
| 2 | * The sentence a refused address carries when it points inside a network | |
| 3 | * rather than at the internet. The terminal and the API both say it, so it | |
| 4 | * is said once here and they import it. | |
| 5 | */ | |
| 6 | export const PRIVATE_ADDRESS_REFUSED = | |
| 7 | "that address is inside a network, not on the internet"; |
src/tui/screens/common.ts +6 −3
| @@ -15,6 +15,9 @@ import { binding, type Binding } from "../app/keys.ts"; | ||
| 15 | 15 | import type { KeyEvent } from "../term/input.ts"; |
| 16 | 16 | import { ARCHIVED_WHY, type RepoCounts } from "../../core/repos.ts"; |
| 17 | 17 | import { SIGNUP_CLOSED_MESSAGE } from "../../actions/users.ts"; |
| 18 | import { ORIGIN_ALREADY_CONNECTED } from "../../actions/origins.ts"; | |
| 19 | import { NEW_ACCOUNT_GUARD_MESSAGE } from "../../core/capacity.ts"; | |
| 20 | import { PRIVATE_ADDRESS_REFUSED } from "../../kernel/address_words.ts"; | |
| 18 | 21 | import type { Session } from "../session.ts"; |
| 19 | 22 | import { G, Screen as Surface, type Rect } from "../term/screen.ts"; |
| 20 | 23 | import { style } from "../term/style.ts"; |
| @@ -826,7 +829,7 @@ const MESSAGES: Record<string, string> = { | ||
| 826 | 829 | unauthorized: "sign in first", |
| 827 | 830 | forbidden: "you do not have permission", |
| 828 | 831 | forbidden_owner: "you cannot create repos for that owner", |
| 829 | refused: "new accounts cannot create repositories yet", | |
| 832 | refused: NEW_ACCOUNT_GUARD_MESSAGE, | |
| 830 | 833 | empty_title: "a title is required", |
| 831 | 834 | empty_body: "write something first", |
| 832 | 835 | title_too_long: "that title is too long (256 characters at most)", |
| @@ -881,10 +884,10 @@ const MESSAGES: Record<string, string> = { | ||
| 881 | 884 | origin_owner_only: "only whoever connected this origin can write through it", |
| 882 | 885 | origin_refused: "the origin refused that; nothing changed here", |
| 883 | 886 | origin_unreachable: "the origin could not be reached; nothing changed", |
| 884 | already_connected: "that account is already connected", | |
| 887 | already_connected: ORIGIN_ALREADY_CONNECTED, | |
| 885 | 888 | already_mirrored: "that repository is already mirrored here", |
| 886 | 889 | unknown_host: "that does not look like a forge", |
| 887 | private_host: "that address is inside a network, not on the internet", | |
| 890 | private_host: PRIVATE_ADDRESS_REFUSED, | |
| 888 | 891 | unreachable: "that instance could not be reached", |
| 889 | 892 | bad_token: "the origin refused that token", |
| 890 | 893 | rate_limited: "too many connect attempts; wait a few minutes", |
test/shards.test.ts +1
| @@ -513,6 +513,7 @@ describe("parseShardSpec", () => { | ||
| 513 | 513 | expect(parseShardSpec("1/3")).toEqual({ ok: true, index: 1, total: 3 }); |
| 514 | 514 | expect(parseShardSpec("3/3")).toEqual({ ok: true, index: 3, total: 3 }); |
| 515 | 515 | expect(parseShardSpec(" 2/4 ")).toEqual({ ok: true, index: 2, total: 4 }); |
| 516 | expect(parseShardSpec("1000/1000")).toEqual({ ok: true, index: 1000, total: 1000 }); | |
| 516 | 517 | for (const bad of ["0/3", "1/0", "4/3", "1", "1-3", "a/b", "1/3/5"]) |
| 517 | 518 | expect(parseShardSpec(bad).ok).toBe(false); |
| 518 | 519 | }); |
test/tui-words.test.ts added +60
| @@ -0,0 +1,60 @@ | ||
| 1 | // The terminal's refusals for a private address, a new account's repository | |
| 2 | // limit and an origin already connected are said by the modules that own | |
| 3 | // them. These tests fail if a copy of one is typed back into a file that | |
| 4 | // should import it. | |
| 5 | import { describe, expect, test } from "bun:test"; | |
| 6 | import { readFileSync } from "node:fs"; | |
| 7 | import { join } from "node:path"; | |
| 8 | import { ORIGIN_ALREADY_CONNECTED } from "../src/actions/origins.ts"; | |
| 9 | import { NEW_ACCOUNT_GUARD_MESSAGE } from "../src/core/capacity.ts"; | |
| 10 | import { PRIVATE_ADDRESS_REFUSED } from "../src/kernel/address_words.ts"; | |
| 11 | ||
| 12 | const root = join(import.meta.dir, ".."); | |
| 13 | const source = (path: string) => readFileSync(join(root, path), "utf8"); | |
| 14 | ||
| 15 | const words: [string, string, string, string[]][] = [ | |
| 16 | [ | |
| 17 | "PRIVATE_ADDRESS_REFUSED", | |
| 18 | PRIVATE_ADDRESS_REFUSED, | |
| 19 | "that address is inside a network, not on the internet", | |
| 20 | ["src/tui/screens/common.ts", "src/http/api.ts"], | |
| 21 | ], | |
| 22 | [ | |
| 23 | "NEW_ACCOUNT_GUARD_MESSAGE", | |
| 24 | NEW_ACCOUNT_GUARD_MESSAGE, | |
| 25 | "new accounts cannot create repositories yet", | |
| 26 | ["src/tui/screens/common.ts"], | |
| 27 | ], | |
| 28 | [ | |
| 29 | "ORIGIN_ALREADY_CONNECTED", | |
| 30 | ORIGIN_ALREADY_CONNECTED, | |
| 31 | "that account is already connected", | |
| 32 | ["src/tui/screens/common.ts", "src/http/api.ts"], | |
| 33 | ], | |
| 34 | ]; | |
| 35 | ||
| 36 | describe("the terminal's refusals are said from one place", () => { | |
| 37 | for (const [name, value, sentence, files] of words) { | |
| 38 | test(`${name} is the sentence, and non-empty`, () => { | |
| 39 | expect(value.length).toBeGreaterThan(0); | |
| 40 | expect(value).toBe(sentence); | |
| 41 | }); | |
| 42 | for (const file of files) { | |
| 43 | test(`${file} imports ${name} rather than typing it`, () => { | |
| 44 | const text = source(file); | |
| 45 | expect(text).toContain(name); | |
| 46 | expect(text).not.toContain(sentence); | |
| 47 | }); | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | test("actions/origins.ts says its sentence once, in the export", () => { | |
| 52 | const text = source("src/actions/origins.ts"); | |
| 53 | expect(text.split(ORIGIN_ALREADY_CONNECTED).length - 1).toBe(1); | |
| 54 | }); | |
| 55 | ||
| 56 | test("core/capacity.ts says its sentence once, in the export", () => { | |
| 57 | const text = source("src/core/capacity.ts"); | |
| 58 | expect(text.split(NEW_ACCOUNT_GUARD_MESSAGE).length - 1).toBe(1); | |
| 59 | }); | |
| 60 | }); |
new above
offline