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

Errors on reports, the sandbox page and the fleet graph are §5's field error, not .error-note #5089

Merged opened this

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

Merged

scout/forge:scout/5070-errors-on-reports-the-sandbox-page-and-t → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/5070-errors-on-reports-the-sandbox-page-and-t was merged into main as df8e641.

Review

Sign in to review this change.
6 files changed +68 −17
src/web/pages/fleet-graph.ts +2 −5
@@ -75,6 +75,7 @@ export function fleetGraphPage(ctx: Ctx): Response {
7575 label: "Repositories",
7676 name: "repo",
7777 id: "fleet-repos",
78 ...(asked.ok ? {} : { error: asked.error }),
7879 value: names,
7980 placeholder:
8081 "owner/name owner/name — empty is everything you can write that spent",
@@ -84,11 +85,7 @@ export function fleetGraphPage(ctx: Ctx): Response {
8485 })}
8586 ${btn("Draw", { type: "submit", variant: "secondary" })}
8687 </form>
87 ${graph === undefined
88 ? html`<p class="error-note" role="alert">
89 ${asked.ok ? "" : asked.error}
90 </p>`
91 : fleetGraphSection(graph)}`,
88 ${graph === undefined ? null : fleetGraphSection(graph)}`,
9289 scriptJs: fleetGraphClientJs,
9390 },
9491 graph === undefined ? 422 : 200,
src/web/pages/reports.ts +20 −8
@@ -60,7 +60,7 @@ function reportForm(
6060 ctx: Ctx,
6161 repo: Repo,
6262 values: FormValues,
63 error?: string,
63 error?: { on?: "title" | "body"; message: string },
6464 status = 200,
6565 ): Response {
6666 const base = repoBase(repo);
@@ -68,6 +68,9 @@ function reportForm(
6868 {
6969 ctx,
7070 title: `Report a problem · ${repo.ownerName}/${repo.name}`,
71 ...(error && !error.on
72 ? { flash: { tone: "danger" as const, message: error.message } }
73 : {}),
7174 crumbs: crumbsFor(repo),
7275 body: html`${repoHeader(ctx, repo, "issues", {
7376 compact: true,
@@ -81,7 +84,6 @@ function reportForm(
8184 nothing acts on it before they do.
8285 </p>
8386 <form class="mt" method="post" action="${base}/report">
84 ${when(error, (m) => html`<p class="error-note">${m}</p>`)}
8587 ${formField({
8688 label: "What is wrong",
8789 name: "title",
@@ -89,6 +91,7 @@ function reportForm(
8991 required: true,
9092 autofocus: true,
9193 hint: `One line, at most ${TITLE_MAX} characters.`,
94 ...(error?.on === "title" ? { error: error.message } : {}),
9295 })}
9396 ${formField({
9497 label: "What happened",
@@ -96,6 +99,7 @@ function reportForm(
9699 type: "textarea",
97100 rows: 8,
98101 value: values.body,
102 ...(error?.on === "body" ? { error: error.message } : {}),
99103 hint: `Plain text, at most ${BODY_MAX} characters. What you did, what you saw, what you expected.`,
100104 })}
101105 <input type="hidden" name="page" value="${values.page}" />
@@ -152,11 +156,17 @@ function reportBody(ctx: Ctx, repo: Repo): Response {
152156 });
153157 }
154158
155 const REFUSED: Record<string, string> = {
156 empty_title: "Say in one line what is wrong.",
157 title_too_long: `That line is too long (${TITLE_MAX} characters at most).`,
158 body_too_long: `That is too long (${BODY_MAX} characters at most).`,
159 queue_full: "Many reports are waiting here already. Try again later.",
159 const REFUSED: Record<string, { on?: "title" | "body"; message: string }> = {
160 empty_title: { on: "title", message: "Say in one line what is wrong." },
161 title_too_long: {
162 on: "title",
163 message: `That line is too long (${TITLE_MAX} characters at most).`,
164 },
165 body_too_long: {
166 on: "body",
167 message: `That is too long (${BODY_MAX} characters at most).`,
168 },
169 queue_full: { message: "Many reports are waiting here already. Try again later." },
160170 };
161171
162172 // POST /:owner/:name/report
@@ -185,7 +195,9 @@ export async function reportSubmit(ctx: Ctx): Promise<Response> {
185195 ctx,
186196 repo,
187197 values,
188 `Too many reports just now. Try again in ${refusal.retryAfter} seconds.`,
198 {

Not blocking: this rate-limit re-render (429 + retry-after, now a flash) is the one refusal with no test of its own. It shares queue_full's path, so it's covered in effect.

199 message: `Too many reports just now. Try again in ${refusal.retryAfter} seconds.`,
200 },
189201 429,
190202 );
191203 response.headers.set("retry-after", String(refusal.retryAfter));
src/web/studio/sandbox.ts +5 −4
@@ -8,6 +8,7 @@
88 import type { Run } from "../../core/runs.ts";
99 import { placementWords } from "../../checks/placement.ts";
1010 import { html, type Html } from "../html.ts";
11 import { badge } from "../ui.ts";
1112
1213 /** Where a check ran, when that was a cluster job; nothing for the default. */
1314 export function placementLine(run: Run): Html | null {
@@ -24,11 +25,11 @@ export function sandboxLine(run: Run): Html | null {
2425 ? "Sandboxed: the shell was held to the policy."
2526 : "Contained: the repository's code ran in a cell of its own, with limits."}
2627 </p>`;
27 return html`<p class="error-note" id="run-sandbox">
28 return html`<p class="small" id="run-sandbox">
29 ${badge(agent ? "Unsandboxed" : "Uncontained", { tone: "danger" })}
2830 ${agent
29 ? "Unsandboxed: the shell ran unrestricted"
30 : "Uncontained: the repository's code ran unrestricted"}${run.sandboxNote ===
31 ""
31 ? "The shell ran unrestricted"
32 : "The repository's code ran unrestricted"}${run.sandboxNote === ""
3233 ? ""
3334 : html` (${run.sandboxNote})`}.
3435 </p>`;
test/crew-sandbox.test.ts +2
@@ -192,6 +192,8 @@ describe("the sandbox on a run's record", () => {
192192 expect(text).toContain("Unsandboxed");
193193 expect(text).toContain("&lt;b&gt;bwrap&lt;/b&gt; is not installed");
194194 expect(text).not.toContain("<b>");
195 expect(text).not.toContain("error-note");
196 expect(text).toContain('data-variant="danger"');
195197
196198 const on = agentRun();
197199 noteRunProgress(app.db, on.id, { sandbox: { enforced: true, note: "" } });
test/fleet-graph.test.ts +2
@@ -349,6 +349,8 @@ describe("the graph", () => {
349349 const body = await res.text();
350350 expect(body).not.toContain("<script>x</script>");
351351 expect(body).toContain("&lt;script&gt;");
352 expect(body).not.toContain("error-note");
353 expect(body).toMatch(/id="fleet-repos"[^>]*aria-invalid="true"[\s\S]*?id="fleet-repos-err"/);
352354 });
353355
354356 test("the API answers the same numbers, and refuses a bad name", async () => {
test/reports.test.ts +37
@@ -547,6 +547,43 @@ describe("reports from outside", () => {
547547 expect(res.status).toBe(429);
548548 expect(((await res.json()) as any).error).toBe("queue_full");
549549 });
550
551 const send = (fields: Record<string, string>) =>
552 fetch(`${base}/alice/game/report`, {
553 method: "POST",
554 headers: { "content-type": "application/x-www-form-urlencoded" },
555 body: new URLSearchParams(fields).toString(),
556 redirect: "manual",
557 });
558
559 test("a title or body refusal sits in its own field", async () => {
560 app.db.run("DELETE FROM report");
561 for (const [fields, id] of [
562 [{ title: "", body: "b" }, "f-title"],
563 [{ title: "t", body: "x".repeat(8_001) }, "f-body"],
564 ] as const) {
565 const res = await send(fields);
566 expect(res.status).toBe(422);
567 const page = await res.text();
568 expect(page).not.toContain("error-note");
569 expect(page).toMatch(new RegExp(`id="${id}"[^>]*aria-invalid="true"[\\s\\S]*?class="error"`));
570 }
571 });
572
573 test("a full queue is the page's flash", async () => {
574 app.db.run("DELETE FROM report");
575 const insert = app.db.query(
576 "INSERT INTO report (id, repo_id, title, body) VALUES (?1, ?2, 't', '')",
577 );
578 for (let i = 0; i < 100; i++) insert.run(`fillform${i}`, game.id);
579 const res = await send({ title: "one more", body: "" });
580 expect(res.status).toBe(429);
581 const page = await res.text();
582 expect(page).not.toContain("error-note");
583 expect(page).toContain('data-tone="danger"');
584 expect(page).toContain("Many reports are waiting here already");
585 app.db.run("DELETE FROM report");
586 });
550587 });
551588
552589 describe("the title of an issue made from a report", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs