Public: anyone can read this; the crew can change it.

A tell's sentences live in one module, read by the tell action, the terminal, runs and the report screen #5701

Merged opened this

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

Merged

mason/forge:mason/5650-a-tell-s-sentences-live-in-one-module-re → main 2 commits fork 1 thread · 1 unresolved

  • Merged. mason/5650-a-tell-s-sentences-live-in-one-module-re was merged into main as a5e6fa0.

Review

Sign in to review this change.
8 files changed +90 −16
src/actions/tell_words.ts added +17
@@ -0,0 +1,17 @@
1 /**
2 * What a tell or a report says when it is empty or does not go (#5650):
3 * one place, so the studio's box, the toolbar's report, the run page and
4 * the terminal read the same. Plain strings, no imports.
5 */
6
7 /** A tell that failed for a reason nobody can be told about. */
8 export const NOT_TOLD_WORDS = "That could not be told.";
9
10 /** A tell from a page whose context could not be read. */
11 export const NOT_TOLD_FROM_WORDS = "That page could not be told from.";
12
13 /** A tell, reply or report sent with nothing in it. */
14 export const SAY_SOMETHING_WORDS = "Say something first.";
15
16 /** A report that did not reach the issue list. */
17 export const NOT_SENT_WORDS = "That could not be sent.";
src/tui/screens/report.ts +3 −2
@@ -7,6 +7,7 @@
77
88 import { openIssueThrough } from "../../actions/issues.ts";
99 import { reportTitle } from "../../actions/page-report.ts";
10 import { NOT_SENT_WORDS, SAY_SOMETHING_WORDS } from "../../actions/tell_words.ts";
1011 import { NOTE_TOO_LONG } from "../../apps/note_words.ts";
1112 import { MAX_BODY } from "../../core/issues.ts";
1213 import type { Repo } from "../../core/repos.ts";
@@ -32,12 +33,12 @@ export function reportCommands(f: Session, ctx: Ctx, parent: () => Screen, repo:
3233 hint: "enter sends",
3334 onSubmit: (said) => {
3435 const text = said.trim();
35 if (text === "") return "Say something first.";
36 if (text === "") return SAY_SOMETHING_WORDS;
3637 if (text.length > MAX_BODY) return NOTE_TOO_LONG;
3738 return (async () => {
3839 const opened = await openIssueThrough(f.app, f.user, repo, { title: reportTitle(text), body: text });
3940 if (opened.ok) ctx.toast(`Issue #${opened.value.number} opened.`, "success");
40 else ctx.toast("That could not be sent.", "danger");
41 else ctx.toast(NOT_SENT_WORDS, "danger");
4142 })();
4243 },
4344 }),
src/tui/screens/tell.ts +6 −5
@@ -10,6 +10,7 @@
1010
1111 import { SENT_WORDS, tell, toldWords, TOO_LONG_WORDS, UNVERIFIED_WORDS, type Told, type TellError } from "../../actions/tell.ts";
1212 import { contextNote, parseContext } from "../../actions/tell_context.ts";
13 import { NOT_TOLD_FROM_WORDS, NOT_TOLD_WORDS, SAY_SOMETHING_WORDS } from "../../actions/tell_words.ts";
1314 import type { Repo } from "../../core/repos.ts";
1415 import { DIRECTION_NOTE, mayDirectCrew } from "../../crew/direct.ts";
1516 import type { Result } from "../../kernel/result.ts";
@@ -30,7 +31,7 @@ export function tellAnswer(told: Result<Told, TellError>): { text: string; tone:
3031 ? UNVERIFIED_WORDS
3132 : told.error === "body_too_long"
3233 ? TOO_LONG_WORDS
33 : "That could not be told.",
34 : NOT_TOLD_WORDS,
3435 };
3536 }
3637
@@ -50,7 +51,7 @@ export function tellCommands(f: Session, ctx: Ctx, parent: () => Screen, repo: R
5051 label: name,
5152 hint: "enter sends",
5253 onSubmit: (said) => {
53 if (said === "") return "Say something first.";
54 if (said === "") return SAY_SOMETHING_WORDS;
5455 return (async () => {
5556 const answer = tellAnswer(await tell(f.app, f.user, repo, said));
5657 ctx.toast(answer.text, answer.tone);
@@ -86,7 +87,7 @@ export function tellCommands(f: Session, ctx: Ctx, parent: () => Screen, repo: R
8687 label: name,
8788 hint: "enter sends",
8889 onSubmit: (said) => {
89 if (said === "") return "Say something first.";
90 if (said === "") return SAY_SOMETHING_WORDS;
9091 return tellFromPage(f, ctx, repo, said, page, number);
9192 },
9293 }),
@@ -112,7 +113,7 @@ export async function tellFromPage(
112113 ): Promise<void> {
113114 const where = parseContext({ page, ...(pull === undefined ? {} : { pull }) });
114115 if (where === null || where === undefined) {
115 ctx.toast("That page could not be told from.", "danger");
116 ctx.toast(NOT_TOLD_FROM_WORDS, "danger");
116117 return;
117118 }
118119 const answer = tellAnswer(await tell(f.app, f.user, repo, `${said}${contextNote(where)}`));
@@ -128,7 +129,7 @@ export function askForSomething(f: Session, ctx: Ctx, parent: () => Screen, repo
128129 label: repo.name,
129130 hint: "enter sends",
130131 onSubmit: (said) => {
131 if (said === "") return "Say something first.";
132 if (said === "") return SAY_SOMETHING_WORDS;
132133 return (async () => {
133134 const told = await tell(f.app, f.user, repo, said, 1, "/");
134135 const answer = told.ok ? { text: SENT_WORDS, tone: "success" as const } : tellAnswer(told);
src/web/client.ts +2 −1
@@ -12,6 +12,7 @@
1212
1313 import { STAGE_BADGE_JS } from "./stage-row.ts";
1414 import { paletteHelpersJs } from "./palette.ts";
15 import { NOT_TOLD_WORDS } from "../actions/tell_words.ts";
1516
1617 /**
1718 * A native `<dialog>` opened as a modal that owns its Escape. Shared by the
@@ -1661,7 +1662,7 @@ export const shellJs = /* js */ `
16611662 if (note) note.textContent = d.ok ? d.flash : d.error;
16621663 if (d.ok) { ta.value = ''; window.setTimeout(function () { box.remove(); }, 2500); }
16631664 }).catch(function () {
1664 if (note) note.textContent = 'That could not be told.';
1665 if (note) note.textContent = '${NOT_TOLD_WORDS}';

Low, not blocking: this drops the constant into a single-quoted JS string. If the sentence ever gets an apostrophe (e.g. "That couldn't be told."), the shell script breaks for everyone. ${JSON.stringify(NOT_TOLD_WORDS)}, with the quotes removed, makes that safe. The guard test's toContain would need the double-quoted form then.

16651666 }).then(function () { sending = false; });
16661667 }
16671668 form.addEventListener('submit', function (e) { e.preventDefault(); send(); });
src/web/pages/runs.ts +2 −1
@@ -11,6 +11,7 @@
1111 * id, so the redirect itself says nothing about what exists.
1212 */
1313
14 import { SAY_SOMETHING_WORDS } from "../../actions/tell_words.ts";
1415 import { readLooks } from "../../checks/looks.ts";
1516 import type { Database } from "bun:sqlite";
1617 import {
@@ -346,7 +347,7 @@ export async function runSteer(ctx: Ctx): Promise<Response> {
346347 tone: "danger",
347348 message:
348349 steered.error === "empty"
349 ? "Say something first."
350 ? SAY_SOMETHING_WORDS
350351 : steered.error === "not_agent"
351352 ? "Only an agent's run can be steered."
352353 : "You need write access to steer a run.",
src/web/studio/actions.ts +7 −6
@@ -13,6 +13,7 @@ import { holdPull, releasePull } from "../../actions/holds.ts";
1313 import { addComment } from "../../actions/issues.ts";
1414 import { parseFrom, SENT_WORDS, tell, toldWords, TOO_LONG_WORDS } from "../../actions/tell.ts";
1515 import { contextNote, parseContext } from "../../actions/tell_context.ts";
16 import { NOT_TOLD_FROM_WORDS, NOT_TOLD_WORDS, SAY_SOMETHING_WORDS } from "../../actions/tell_words.ts";
1617 import { keepThought } from "../../actions/thought.ts";
1718 import { DIRECTION_NOTE } from "../../crew/direct.ts";
1819 import { mergePull } from "../../actions/pulls.ts";
@@ -58,7 +59,7 @@ async function tellFrom(
5859 if (text === "")
5960 return flashOrJson(ctx, back, {
6061 tone: "danger",
61 message: "Say something first.",
62 message: SAY_SOMETHING_WORDS,
6263 });
6364 const told = await tell(ctx.app, ctx.auth.user, repo, text, 1, from);
6465 if (!told.ok && told.error === "body_too_long")
@@ -74,7 +75,7 @@ async function tellFrom(
7475 ? DIRECTION_NOTE
7576 : told.error === "unverified"
7677 ? "Verify your email first."
77 : "That could not be told.",
78 : NOT_TOLD_WORDS,
7879 });
7980 const { routed } = told.value;
8081 // A client's "Ask for something" (named by the form, not guessed from `/`):
@@ -119,9 +120,9 @@ async function tellFromPage(ctx: Ctx, repo: Repo, form: Form): Promise<Response>
119120 if (where?.pull !== undefined) keep.set("pull", String(where.pull));
120121 const back = `${repoBase(repo)}/tell?${keep}`;
121122 if (where === null || where === undefined)
122 return flashTo(ctx, back, { tone: "danger", message: "That page could not be told from." });
123 return flashTo(ctx, back, { tone: "danger", message: NOT_TOLD_FROM_WORDS });
123124 const text = field(form, "text").trim();
124 if (text === "") return flashTo(ctx, back, { tone: "danger", message: "Say something first." });
125 if (text === "") return flashTo(ctx, back, { tone: "danger", message: SAY_SOMETHING_WORDS });
125126 const told = await tell(ctx.app, ctx.auth.user, repo, `${text}${contextNote(where)}`);
126127 if (!told.ok)
127128 return flashTo(ctx, back, {
@@ -133,7 +134,7 @@ async function tellFromPage(ctx: Ctx, repo: Repo, form: Form): Promise<Response>
133134 ? "Verify your email first."
134135 : told.error === "body_too_long"
135136 ? TOO_LONG_WORDS
136 : "That could not be told.",
137 : NOT_TOLD_WORDS,
137138 });
138139 const issue = `${repoBase(repo)}/issues/${told.value.issue.number}`;
139140 return page({
@@ -309,7 +310,7 @@ export async function saySubmit(ctx: Ctx): Promise<Response> {
309310 if (text === "")
310311 return flashTo(ctx, base, {
311312 tone: "danger",
312 message: "Say something first.",
313 message: SAY_SOMETHING_WORDS,
313314 });
314315 const said = addComment(ctx.app, ctx.auth.user, repo, pull, text);
315316 return flashOrJson(ctx, base, {
src/web/studio/toolbar.ts +2 −1
@@ -18,6 +18,7 @@
1818 import { addCommentThrough, openIssueThrough } from "../../actions/issues.ts";
1919 import { COMMENT_EMPTY, COMMENT_NOT_POSTED } from "../../actions/comment_words.ts";
2020 import { reportTitle } from "../../actions/page-report.ts";
21 import { NOT_SENT_WORDS } from "../../actions/tell_words.ts";
2122 import { NOTE_TOO_LONG } from "../../apps/note_words.ts";
2223 import { getAppByRepo } from "../../core/apps.ts";
2324 import { getIssue, MAX_BODY } from "../../core/issues.ts";
@@ -318,7 +319,7 @@ export async function toolbarComment(ctx: Ctx): Promise<Response> {
318319 originError(opened.error) ??
319320 (opened.error === "body_too_long"
320321 ? NOTE_TOO_LONG
321 : "That could not be sent."),
322 : NOT_SENT_WORDS),
322323 opened.error === "body_too_long" ? 422 : opened.error === "not_found" ? 404 : 403,
323324 );
324325 }
test/tell-words.test.ts added +51
@@ -0,0 +1,51 @@
1 /**
2 * A tell's sentences live in one module (#5650): every door that says one
3 * imports it from src/actions/tell_words.ts, so a copy typed back into a
4 * page, a screen or the browser's script fails here before it drifts.
5 */
6 import { describe, expect, test } from "bun:test";
7 import { readFileSync } from "node:fs";
8 import { join } from "node:path";
9 import {
10 NOT_SENT_WORDS,
11 NOT_TOLD_FROM_WORDS,
12 NOT_TOLD_WORDS,
13 SAY_SOMETHING_WORDS,
14 } from "../src/actions/tell_words.ts";
15
16 const SENTENCES = {
17 NOT_TOLD_WORDS: "That could not be told.",
18 NOT_TOLD_FROM_WORDS: "That page could not be told from.",
19 SAY_SOMETHING_WORDS: "Say something first.",
20 NOT_SENT_WORDS: "That could not be sent.",
21 };
22
23 const DOORS = [
24 "src/web/studio/actions.ts",
25 "src/tui/screens/tell.ts",
26 "src/tui/screens/report.ts",
27 "src/web/pages/runs.ts",
28 "src/web/studio/toolbar.ts",
29 "src/web/client.ts",
30 ];
31
32 const read = (path: string): string => readFileSync(join(import.meta.dir, "..", path), "utf8");
33
34 describe("tell words", () => {
35 test("the module says each sentence word for word", () => {
36 expect({ NOT_TOLD_WORDS, NOT_TOLD_FROM_WORDS, SAY_SOMETHING_WORDS, NOT_SENT_WORDS }).toEqual(SENTENCES);
37 });
38
39 for (const door of DOORS) {
40 test(`${door} types none of them out`, () => {
41 const text = read(door);
42 for (const sentence of Object.values(SENTENCES)) expect(text).not.toContain(sentence);
43 expect(text).toContain("actions/tell_words.ts");
44 });
45 }
46
47 test("the browser's script still says the fallback", async () => {
48 const { shellJs } = await import("../src/web/client.ts");
49 expect(shellJs).toContain(`note.textContent = '${NOT_TOLD_WORDS}';`);
50 });
51 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs