Public: anyone can read this; the crew can change it.
The model and people screens read their sentences from the web modules that own them #5634
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5618-the-model-and-people-screens-read-their → main 1 commit fork
5 files
changed +56
−5
src/actions/model_words.ts added +11
| @@ -0,0 +1,11 @@ | ||
| 1 | /** | |
| 2 | * The sentences a model subscription and a spend amount are answered with, | |
| 3 | * said from one place so `/settings/model`, the terminal's model screen and | |
| 4 | * its people screen read the same (`test/model-words.test.ts` keeps them here). | |
| 5 | */ | |
| 6 | ||
| 7 | /** Said once a person's own subscription is added. */ | |
| 8 | export const SUBSCRIPTION_ADDED = "Your subscription is added, for your own repositories."; | |
| 9 | ||
| 10 | /** Said when an amount (a grant, a ceiling, a member's allowance) is refused. */ | |
| 11 | export const AMOUNT_REFUSED = "That amount will not do."; |
src/tui/screens/model.ts +4 −3
| @@ -13,6 +13,7 @@ import { | ||
| 13 | 13 | setAccountCeilings, |
| 14 | 14 | setAccountGrant, |
| 15 | 15 | } from "../../actions/model_grant.ts"; |
| 16 | import { AMOUNT_REFUSED, SUBSCRIPTION_ADDED } from "../../actions/model_words.ts"; | |
| 16 | 17 | import { getAccountCeiling } from "../../core/account_ceiling.ts"; |
| 17 | 18 | import { getUserByName } from "../../core/users.ts"; |
| 18 | 19 | import { budgetLine, ownSubscription } from "../../core/model_grant.ts"; |
| @@ -57,7 +58,7 @@ export function modelScreen(f: Session, ctx: Ctx): Screen { | ||
| 57 | 58 | onSubmit: (apiBase) => { |
| 58 | 59 | const added = addOwnModel(f.app, f.user, { token: token.trim(), apiBase: apiBase.trim() }); |
| 59 | 60 | if (!added.ok) return ADD_MESSAGES[added.error]; |
| 60 | note = "Your subscription is added, for your own repositories."; | |
| 61 | note = SUBSCRIPTION_ADDED; | |
| 61 | 62 | return undefined; |
| 62 | 63 | }, |
| 63 | 64 | }), |
| @@ -85,7 +86,7 @@ export function modelGrantPrompt(f: Session, ctx: Ctx, parent: Screen): Screen { | ||
| 85 | 86 | const usd = text === "none" || text === "0" ? null : Number(text); |
| 86 | 87 | if (usd !== null && !Number.isFinite(usd)) return "Dollars a month, like 20 — or none."; |
| 87 | 88 | const done = setAccountGrant(f.app, f.user, name.trim(), usd); |
| 88 | if (!done.ok) return done.error === "invalid_amount" ? "That amount will not do." : "No such account."; | |
| 89 | if (!done.ok) return done.error === "invalid_amount" ? AMOUNT_REFUSED : "No such account."; | |
| 89 | 90 | ctx.toast(usd === null ? `${name.trim()}: grant taken back.` : `${name.trim()}: $${usd.toFixed(2)} a month.`); |
| 90 | 91 | return undefined; |
| 91 | 92 | }, |
| @@ -122,7 +123,7 @@ export function modelCeilingPrompt(f: Session, ctx: Ctx, parent: Screen): Screen | ||
| 122 | 123 | if (daily === undefined || perRun === undefined || rest.length > 0) |
| 123 | 124 | return "Two amounts, like 20 5."; |
| 124 | 125 | const done = setAccountCeilings(f.app, f.user, name.trim(), daily, perRun); |
| 125 | if (!done.ok) return done.error === "invalid_amount" ? "That amount will not do." : "No such account."; | |
| 126 | if (!done.ok) return done.error === "invalid_amount" ? AMOUNT_REFUSED : "No such account."; | |
| 126 | 127 | ctx.toast(`${name.trim()}: $${done.value.dailyUsd?.toFixed(2)} a day, $${done.value.perRunUsd?.toFixed(2)} a run.`); |
| 127 | 128 | return undefined; |
| 128 | 129 | }, |
src/tui/screens/people.ts +2 −1
| @@ -6,6 +6,7 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | import { setRole } from "../../actions/roles.ts"; |
| 9 | import { AMOUNT_REFUSED } from "../../actions/model_words.ts"; | |
| 9 | 10 | import { listPeople } from "../../core/roles.ts"; |
| 10 | 11 | import type { Ctx, Screen } from "../app/app.ts"; |
| 11 | 12 | import { binding } from "../app/keys.ts"; |
| @@ -45,7 +46,7 @@ export function peopleScreen(f: Session, ctx: Ctx): Screen { | ||
| 45 | 46 | const usd = amount === undefined ? null : Number(amount); |
| 46 | 47 | if (usd !== null && !Number.isFinite(usd)) return "Dollars a month, like 5."; |
| 47 | 48 | const done = setRole(f.app, f.user, name.trim(), role, usd); |
| 48 | if (!done.ok) return done.error === "invalid_amount" ? "That amount will not do." : "No such account, or not one a role is for."; | |
| 49 | if (!done.ok) return done.error === "invalid_amount" ? AMOUNT_REFUSED : "No such account, or not one a role is for."; | |
| 49 | 50 | ctx.toast(`${name.trim()} is a ${role}.`); |
| 50 | 51 | return undefined; |
| 51 | 52 | }, |
src/web/pages/model.ts +2 −1
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | 9 | import { ADD_MESSAGES, addOwnModel, setAccountCeilings, setAccountGrant } from "../../actions/model_grant.ts"; |
| 10 | import { SUBSCRIPTION_ADDED } from "../../actions/model_words.ts"; | |
| 10 | 11 | import { CEILING_MAX_USD, dailyLeft, getAccountCeiling } from "../../core/account_ceiling.ts"; |
| 11 | 12 | import { |
| 12 | 13 | budgetLine, |
| @@ -90,7 +91,7 @@ export async function modelAdd(ctx: Ctx): Promise<Response> { | ||
| 90 | 91 | ctx, |
| 91 | 92 | "/settings/model", |
| 92 | 93 | added.ok |
| 93 | ? { message: "Your subscription is added, for your own repositories." } | |
| 94 | ? { message: SUBSCRIPTION_ADDED } | |
| 94 | 95 | : { tone: "danger", message: ADD_MESSAGES[added.error] }, |
| 95 | 96 | ); |
| 96 | 97 | } |
test/model-words.test.ts added +37
| @@ -0,0 +1,37 @@ | ||
| 1 | /** | |
| 2 | * Adding a model subscription and refusing an amount are said from one | |
| 3 | * place, `src/actions/model_words.ts`, so the web page and the terminal | |
| 4 | * screens read the same. Each file that says them is read as text: a copy | |
| 5 | * of the sentence typed back into it fails here. | |
| 6 | */ | |
| 7 | ||
| 8 | import { describe, expect, test } from "bun:test"; | |
| 9 | import { readFileSync } from "node:fs"; | |
| 10 | import { join } from "node:path"; | |
| 11 | import { AMOUNT_REFUSED, SUBSCRIPTION_ADDED } from "../src/actions/model_words.ts"; | |
| 12 | ||
| 13 | const root = join(import.meta.dir, ".."); | |
| 14 | const source = (path: string): string => readFileSync(join(root, path), "utf8"); | |
| 15 | ||
| 16 | const SAYERS: Record<string, readonly string[]> = { | |
| 17 | "src/web/pages/model.ts": ["SUBSCRIPTION_ADDED"], | |
| 18 | "src/tui/screens/model.ts": ["SUBSCRIPTION_ADDED", "AMOUNT_REFUSED"], | |
| 19 | "src/tui/screens/people.ts": ["AMOUNT_REFUSED"], | |
| 20 | }; | |
| 21 | ||
| 22 | describe("the model and people sentences", () => { | |
| 23 | test("are the words they always were", () => { | |
| 24 | expect(SUBSCRIPTION_ADDED).toBe("Your subscription is added, for your own repositories."); | |
| 25 | expect(AMOUNT_REFUSED).toBe("That amount will not do."); | |
| 26 | }); | |
| 27 | ||
| 28 | for (const [path, names] of Object.entries(SAYERS)) { | |
| 29 | test(`${path} imports them and types no copy`, () => { | |
| 30 | const text = source(path); | |
| 31 | expect(text).toContain('from "../../actions/model_words.ts"'); | |
| 32 | for (const name of names) expect(text).toContain(name); | |
| 33 | expect(text).not.toContain(SUBSCRIPTION_ADDED); | |
| 34 | expect(text).not.toContain(AMOUNT_REFUSED); | |
| 35 | }); | |
| 36 | } | |
| 37 | }); |
new above
offline