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

An app note's sentences live in one module, read by the note door, the notes action and the forge toolbar #5637

Merged opened this

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

Merged

mason/forge:mason/5616-an-app-note-s-sentences-live-in-one-modu → main 2 commits fork

  • Merged. mason/5616-an-app-note-s-sentences-live-in-one-modu was merged into main as 38c719b.

Review

Sign in to review this change.
7 files changed +72 −8
src/actions/app_notes.ts +2 −1
@@ -15,6 +15,7 @@ import { plainText } from "../core/reports.ts";
1515 import { workingAgentRuns } from "../core/runs.ts";
1616 import { getUserById, isActive, type User } from "../core/users.ts";
1717 import { unadoptedAmong } from "../crew/direct.ts";
18 import { NOTE_EMPTY } from "../apps/note_words.ts";
1819 import { err, ok, type Result } from "../kernel/result.ts";
1920 import { openIssueThrough, setIssueLabelsThrough, setIssueStateThrough } from "./issues.ts";
2021
@@ -185,7 +186,7 @@ export async function sendNote(
185186 source: string,
186187 ): Promise<Result<NoteView, Refusal>> {
187188 const text = plainText(input.text).trim();
188 if (text === "") return err({ error: "empty", message: "Write a few words first." });
189 if (text === "") return err({ error: "empty", message: NOTE_EMPTY });
189190 if (text.length > TEXT_MAX) return err({ error: "too_long", message: "That is a bit long. Keep it under 2,000 characters." });
190191 if (isBanned(app.db, record.id, sender.key)) return err({ error: "banned", message: "Notes from here are paused." });
191192 if (filtered(text)) return err({ error: "filtered", message: "Please put that in other words." });
src/apps/feedback.ts +2 −1
@@ -19,6 +19,7 @@ import type { AppRecord } from "../core/apps.ts";
1919 import { canAdmin, canRead, getRepoById, type Repo } from "../core/repos.ts";
2020 import { getUserById, isActive, type User } from "../core/users.ts";
2121 import { sha256 } from "../kernel/hash.ts";
22 import { NOTE_FAILED } from "./note_words.ts";
2223
2324 export const REQUEST_MAX = 16 * 1024;
2425 export const VISITOR_COOKIE = "forge_visitor";
@@ -127,7 +128,7 @@ async function readJson(request: Request): Promise<Record<string, unknown> | und
127128 async function send(ask: FeedbackAsk, door: Door): Promise<Response> {
128129 const { request } = ask;
129130 const body = await readJson(request);
130 if (body === undefined) return json(400, { error: "bad_request", message: "That did not go through. Try again." });
131 if (body === undefined) return json(400, { error: "bad_request", message: NOTE_FAILED });
131132 const { sender, fresh } = senderOf(ask, door);
132133 const cookies = fresh === undefined ? {} : { "set-cookie": visitorCookie(fresh, ask.secure) };
133134 let page = "";
src/apps/note_words.ts added +16
@@ -0,0 +1,16 @@
1 /**
2 * What sending a note says back (#5616), from one place: the note door
3 * (`apps/feedback.ts`), the notes action (`actions/app_notes.ts`), the
4 * forge pill's script (`apps/toolbar.ts`), the toolbar's forge page
5 * (`web/studio/toolbar.ts`) and the terminal's report (`tui/screens/report.ts`).
6 * Plain strings, so the web and the terminal say it the same way.
7 */
8
9 /** A note with nothing in it. */
10 export const NOTE_EMPTY = "Write a few words first.";
11
12 /** A note that failed on the way, for a reason the sender cannot fix. */
13 export const NOTE_FAILED = "That did not go through. Try again.";
14
15 /** A note longer than one issue body may be. */
16 export const NOTE_TOO_LONG = "That is too long to send in one go.";
src/apps/toolbar.ts +4 −3
@@ -13,6 +13,7 @@
1313
1414 import { staticResponse } from "../web/asset.ts";
1515 import { sha256 } from "../kernel/hash.ts";
16 import { NOTE_EMPTY, NOTE_FAILED } from "./note_words.ts";
1617
1718 /** The client, as plain source: no `eval`, no inline handlers. */
1819 export const toolbarJs = `(function () {
@@ -153,7 +154,7 @@ export const toolbarJs = `(function () {
153154 tellBox.addEventListener("submit", function (e) {
154155 e.preventDefault();
155156 var text = tellText.value.trim();
156 if (!text) { tellSay.textContent = "Write a few words first."; return; }
157 if (!text) { tellSay.textContent = ${JSON.stringify(NOTE_EMPTY)}; return; }
157158 if (tellGo.disabled) return;
158159 tellGo.disabled = true;
159160 tellGo.setAttribute("aria-busy", "true");
@@ -169,9 +170,9 @@ export const toolbarJs = `(function () {
169170 .then(function (a) {
170171 idle();
171172 if (a.ok) { tellText.value = ""; tellSay.textContent = "Sent. The builders will see it."; refresh(true); }
172 else tellSay.textContent = a.j && a.j.message ? a.j.message : "That did not go through. Try again.";
173 else tellSay.textContent = a.j && a.j.message ? a.j.message : ${JSON.stringify(NOTE_FAILED)};
173174 })
174 .catch(function () { idle(); tellSay.textContent = "That did not go through. Try again."; });
175 .catch(function () { idle(); tellSay.textContent = ${JSON.stringify(NOTE_FAILED)}; });
175176 });
176177 var poll = 0;
177178 function watch(open) {
src/tui/screens/report.ts +2 −1
@@ -7,6 +7,7 @@
77
88 import { openIssueThrough } from "../../actions/issues.ts";
99 import { reportTitle } from "../../actions/page-report.ts";
10 import { NOTE_TOO_LONG } from "../../apps/note_words.ts";
1011 import { MAX_BODY } from "../../core/issues.ts";
1112 import type { Repo } from "../../core/repos.ts";
1213 import type { Ctx, Screen } from "../app/app.ts";
@@ -32,7 +33,7 @@ export function reportCommands(f: Session, ctx: Ctx, parent: () => Screen, repo:
3233 onSubmit: (said) => {
3334 const text = said.trim();
3435 if (text === "") return "Say something first.";
35 if (text.length > MAX_BODY) return "That is too long to send in one go.";
36 if (text.length > MAX_BODY) return NOTE_TOO_LONG;
3637 return (async () => {
3738 const opened = await openIssueThrough(f.app, f.user, repo, { title: reportTitle(text), body: text });
3839 if (opened.ok) ctx.toast(`Issue #${opened.value.number} opened.`, "success");
src/web/studio/toolbar.ts +3 −2
@@ -17,6 +17,7 @@
1717
1818 import { addCommentThrough, openIssueThrough } from "../../actions/issues.ts";
1919 import { reportTitle } from "../../actions/page-report.ts";
20 import { NOTE_TOO_LONG } from "../../apps/note_words.ts";
2021 import { getAppByRepo } from "../../core/apps.ts";
2122 import { getIssue, MAX_BODY } from "../../core/issues.ts";
2223 import { diffBetween, getPull, pullDir, type Pull } from "../../core/pulls.ts";
@@ -275,7 +276,7 @@ export async function toolbarComment(ctx: Ctx): Promise<Response> {
275276 );
276277 if (text === "") return refuse("A comment needs something in it.");
277278 const said = address === "" ? text : `${text}\n\nPage: <${address}>`;
278 if (said.length > MAX_BODY) return refuse("That is too long to send in one go.");
279 if (said.length > MAX_BODY) return refuse(NOTE_TOO_LONG);
279280 const target = pull ? `pull #${pull.number}` : "an issue on this app";
280281
281282 if (field(form, "step") !== "post") {
@@ -315,7 +316,7 @@ export async function toolbarComment(ctx: Ctx): Promise<Response> {
315316 return refuse(
316317 originError(opened.error) ??
317318 (opened.error === "body_too_long"
318 ? "That is too long to send in one go."
319 ? NOTE_TOO_LONG
319320 : "That could not be sent."),
320321 opened.error === "body_too_long" ? 422 : opened.error === "not_found" ? 404 : 403,
321322 );
test/note-words.test.ts added +43
@@ -0,0 +1,43 @@
1 /**
2 * An app note's sentences are said from one place (#5616): the files that
3 * say them import them from `src/apps/note_words.ts`, and a copy typed back
4 * into any of them fails here.
5 */
6 import { describe, expect, test } from "bun:test";
7 import { readFileSync } from "node:fs";
8 import { join } from "node:path";
9 import { NOTE_EMPTY, NOTE_FAILED, NOTE_TOO_LONG } from "../src/apps/note_words.ts";
10 import { toolbarJs } from "../src/apps/toolbar.ts";
11
12 const root = join(import.meta.dir, "..");
13 const speakers = [
14 "src/actions/app_notes.ts",
15 "src/apps/toolbar.ts",
16 "src/apps/feedback.ts",
17 "src/tui/screens/report.ts",
18 "src/web/studio/toolbar.ts",
19 ];
20 const sentences = [NOTE_EMPTY, NOTE_FAILED, NOTE_TOO_LONG];
21
22 describe("an app note's sentences", () => {
23 test("say what they said", () => {
24 expect(sentences).toEqual([
25 "Write a few words first.",
26 "That did not go through. Try again.",
27 "That is too long to send in one go.",
28 ]);
29 });
30
31 for (const file of speakers) {
32 test(`${file} imports them and types none of them out`, () => {
33 const text = readFileSync(join(root, file), "utf8");
34 expect(text).toContain("note_words.ts");
35 for (const sentence of sentences) expect(text).not.toContain(sentence);
36 });
37 }
38
39 test("the pill's script still carries them, as quoted strings", () => {
40 expect(toolbarJs).toContain(JSON.stringify(NOTE_EMPTY));
41 expect(toolbarJs).toContain(JSON.stringify(NOTE_FAILED));
42 });
43 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs