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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5616-an-app-note-s-sentences-live-in-one-modu → main 2 commits fork
7 files
changed +72
−8
src/actions/app_notes.ts +2 −1
| @@ -15,6 +15,7 @@ import { plainText } from "../core/reports.ts"; | ||
| 15 | 15 | import { workingAgentRuns } from "../core/runs.ts"; |
| 16 | 16 | import { getUserById, isActive, type User } from "../core/users.ts"; |
| 17 | 17 | import { unadoptedAmong } from "../crew/direct.ts"; |
| 18 | import { NOTE_EMPTY } from "../apps/note_words.ts"; | |
| 18 | 19 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 19 | 20 | import { openIssueThrough, setIssueLabelsThrough, setIssueStateThrough } from "./issues.ts"; |
| 20 | 21 | |
| @@ -185,7 +186,7 @@ export async function sendNote( | ||
| 185 | 186 | source: string, |
| 186 | 187 | ): Promise<Result<NoteView, Refusal>> { |
| 187 | 188 | 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 }); | |
| 189 | 190 | if (text.length > TEXT_MAX) return err({ error: "too_long", message: "That is a bit long. Keep it under 2,000 characters." }); |
| 190 | 191 | if (isBanned(app.db, record.id, sender.key)) return err({ error: "banned", message: "Notes from here are paused." }); |
| 191 | 192 | 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"; | ||
| 19 | 19 | import { canAdmin, canRead, getRepoById, type Repo } from "../core/repos.ts"; |
| 20 | 20 | import { getUserById, isActive, type User } from "../core/users.ts"; |
| 21 | 21 | import { sha256 } from "../kernel/hash.ts"; |
| 22 | import { NOTE_FAILED } from "./note_words.ts"; | |
| 22 | 23 | |
| 23 | 24 | export const REQUEST_MAX = 16 * 1024; |
| 24 | 25 | export const VISITOR_COOKIE = "forge_visitor"; |
| @@ -127,7 +128,7 @@ async function readJson(request: Request): Promise<Record<string, unknown> | und | ||
| 127 | 128 | async function send(ask: FeedbackAsk, door: Door): Promise<Response> { |
| 128 | 129 | const { request } = ask; |
| 129 | 130 | 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 }); | |
| 131 | 132 | const { sender, fresh } = senderOf(ask, door); |
| 132 | 133 | const cookies = fresh === undefined ? {} : { "set-cookie": visitorCookie(fresh, ask.secure) }; |
| 133 | 134 | 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 @@ | ||
| 13 | 13 | |
| 14 | 14 | import { staticResponse } from "../web/asset.ts"; |
| 15 | 15 | import { sha256 } from "../kernel/hash.ts"; |
| 16 | import { NOTE_EMPTY, NOTE_FAILED } from "./note_words.ts"; | |
| 16 | 17 | |
| 17 | 18 | /** The client, as plain source: no `eval`, no inline handlers. */ |
| 18 | 19 | export const toolbarJs = `(function () { |
| @@ -153,7 +154,7 @@ export const toolbarJs = `(function () { | ||
| 153 | 154 | tellBox.addEventListener("submit", function (e) { |
| 154 | 155 | e.preventDefault(); |
| 155 | 156 | 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; } | |
| 157 | 158 | if (tellGo.disabled) return; |
| 158 | 159 | tellGo.disabled = true; |
| 159 | 160 | tellGo.setAttribute("aria-busy", "true"); |
| @@ -169,9 +170,9 @@ export const toolbarJs = `(function () { | ||
| 169 | 170 | .then(function (a) { |
| 170 | 171 | idle(); |
| 171 | 172 | 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)}; | |
| 173 | 174 | }) |
| 174 | .catch(function () { idle(); tellSay.textContent = "That did not go through. Try again."; }); | |
| 175 | .catch(function () { idle(); tellSay.textContent = ${JSON.stringify(NOTE_FAILED)}; }); | |
| 175 | 176 | }); |
| 176 | 177 | var poll = 0; |
| 177 | 178 | function watch(open) { |
src/tui/screens/report.ts +2 −1
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | |
| 8 | 8 | import { openIssueThrough } from "../../actions/issues.ts"; |
| 9 | 9 | import { reportTitle } from "../../actions/page-report.ts"; |
| 10 | import { NOTE_TOO_LONG } from "../../apps/note_words.ts"; | |
| 10 | 11 | import { MAX_BODY } from "../../core/issues.ts"; |
| 11 | 12 | import type { Repo } from "../../core/repos.ts"; |
| 12 | 13 | import type { Ctx, Screen } from "../app/app.ts"; |
| @@ -32,7 +33,7 @@ export function reportCommands(f: Session, ctx: Ctx, parent: () => Screen, repo: | ||
| 32 | 33 | onSubmit: (said) => { |
| 33 | 34 | const text = said.trim(); |
| 34 | 35 | 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; | |
| 36 | 37 | return (async () => { |
| 37 | 38 | const opened = await openIssueThrough(f.app, f.user, repo, { title: reportTitle(text), body: text }); |
| 38 | 39 | if (opened.ok) ctx.toast(`Issue #${opened.value.number} opened.`, "success"); |
src/web/studio/toolbar.ts +3 −2
| @@ -17,6 +17,7 @@ | ||
| 17 | 17 | |
| 18 | 18 | import { addCommentThrough, openIssueThrough } from "../../actions/issues.ts"; |
| 19 | 19 | import { reportTitle } from "../../actions/page-report.ts"; |
| 20 | import { NOTE_TOO_LONG } from "../../apps/note_words.ts"; | |
| 20 | 21 | import { getAppByRepo } from "../../core/apps.ts"; |
| 21 | 22 | import { getIssue, MAX_BODY } from "../../core/issues.ts"; |
| 22 | 23 | import { diffBetween, getPull, pullDir, type Pull } from "../../core/pulls.ts"; |
| @@ -275,7 +276,7 @@ export async function toolbarComment(ctx: Ctx): Promise<Response> { | ||
| 275 | 276 | ); |
| 276 | 277 | if (text === "") return refuse("A comment needs something in it."); |
| 277 | 278 | 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); | |
| 279 | 280 | const target = pull ? `pull #${pull.number}` : "an issue on this app"; |
| 280 | 281 | |
| 281 | 282 | if (field(form, "step") !== "post") { |
| @@ -315,7 +316,7 @@ export async function toolbarComment(ctx: Ctx): Promise<Response> { | ||
| 315 | 316 | return refuse( |
| 316 | 317 | originError(opened.error) ?? |
| 317 | 318 | (opened.error === "body_too_long" |
| 318 | ? "That is too long to send in one go." | |
| 319 | ? NOTE_TOO_LONG | |
| 319 | 320 | : "That could not be sent."), |
| 320 | 321 | opened.error === "body_too_long" ? 422 : opened.error === "not_found" ? 404 : 403, |
| 321 | 322 | ); |
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 | }); |
new above
offline