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

Merged opened this

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

Merged

mason/forge:mason/5618-the-model-and-people-screens-read-their → main 1 commit fork

  • Merged. mason/5618-the-model-and-people-screens-read-their was merged into main as 117716b.

Review

Sign in to review this change.
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 {
1313 setAccountCeilings,
1414 setAccountGrant,
1515 } from "../../actions/model_grant.ts";
16 import { AMOUNT_REFUSED, SUBSCRIPTION_ADDED } from "../../actions/model_words.ts";
1617 import { getAccountCeiling } from "../../core/account_ceiling.ts";
1718 import { getUserByName } from "../../core/users.ts";
1819 import { budgetLine, ownSubscription } from "../../core/model_grant.ts";
@@ -57,7 +58,7 @@ export function modelScreen(f: Session, ctx: Ctx): Screen {
5758 onSubmit: (apiBase) => {
5859 const added = addOwnModel(f.app, f.user, { token: token.trim(), apiBase: apiBase.trim() });
5960 if (!added.ok) return ADD_MESSAGES[added.error];
60 note = "Your subscription is added, for your own repositories.";
61 note = SUBSCRIPTION_ADDED;
6162 return undefined;
6263 },
6364 }),
@@ -85,7 +86,7 @@ export function modelGrantPrompt(f: Session, ctx: Ctx, parent: Screen): Screen {
8586 const usd = text === "none" || text === "0" ? null : Number(text);
8687 if (usd !== null && !Number.isFinite(usd)) return "Dollars a month, like 20 — or none.";
8788 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.";
8990 ctx.toast(usd === null ? `${name.trim()}: grant taken back.` : `${name.trim()}: $${usd.toFixed(2)} a month.`);
9091 return undefined;
9192 },
@@ -122,7 +123,7 @@ export function modelCeilingPrompt(f: Session, ctx: Ctx, parent: Screen): Screen
122123 if (daily === undefined || perRun === undefined || rest.length > 0)
123124 return "Two amounts, like 20 5.";
124125 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.";
126127 ctx.toast(`${name.trim()}: $${done.value.dailyUsd?.toFixed(2)} a day, $${done.value.perRunUsd?.toFixed(2)} a run.`);
127128 return undefined;
128129 },
src/tui/screens/people.ts +2 −1
@@ -6,6 +6,7 @@
66 */
77
88 import { setRole } from "../../actions/roles.ts";
9 import { AMOUNT_REFUSED } from "../../actions/model_words.ts";
910 import { listPeople } from "../../core/roles.ts";
1011 import type { Ctx, Screen } from "../app/app.ts";
1112 import { binding } from "../app/keys.ts";
@@ -45,7 +46,7 @@ export function peopleScreen(f: Session, ctx: Ctx): Screen {
4546 const usd = amount === undefined ? null : Number(amount);
4647 if (usd !== null && !Number.isFinite(usd)) return "Dollars a month, like 5.";
4748 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.";
4950 ctx.toast(`${name.trim()} is a ${role}.`);
5051 return undefined;
5152 },
src/web/pages/model.ts +2 −1
@@ -7,6 +7,7 @@
77 */
88
99 import { ADD_MESSAGES, addOwnModel, setAccountCeilings, setAccountGrant } from "../../actions/model_grant.ts";
10 import { SUBSCRIPTION_ADDED } from "../../actions/model_words.ts";
1011 import { CEILING_MAX_USD, dailyLeft, getAccountCeiling } from "../../core/account_ceiling.ts";
1112 import {
1213 budgetLine,
@@ -90,7 +91,7 @@ export async function modelAdd(ctx: Ctx): Promise<Response> {
9091 ctx,
9192 "/settings/model",
9293 added.ok
93 ? { message: "Your subscription is added, for your own repositories." }
94 ? { message: SUBSCRIPTION_ADDED }
9495 : { tone: "danger", message: ADD_MESSAGES[added.error] },
9596 );
9697 }
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs