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

Origin, app and report refusals live in one module, read by the API and MCP #5690

Merged opened this

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

Merged

mason/forge:mason/5647-origin-app-and-report-refusals-live-in-o → main 1 commit fork 1 thread · 1 unresolved

  • Merged. mason/5647-origin-app-and-report-refusals-live-in-o was merged into main as d76386a.

Review

Sign in to review this change.
4 files changed +78 −9
src/actions/origin_app_words.ts added +14
@@ -0,0 +1,14 @@
1 /**
2 * The refusals an origin, an app and a report share across every door: the
3 * API's error body and the MCP tool's answer say each from here, so the
4 * wording is one edit away from changing everywhere (#5647).
5 */
6
7 /** An origin (a git host to mirror from) answered nothing usable. */
8 export const ORIGIN_UNREACHABLE = "the origin could not be reached";
9
10 /** Making an app yours when it already is. */
11 export const APP_ALREADY_YOURS = "this app is already yours";
12
13 /** Deciding a report that was already decided. */
14 export const REPORT_ALREADY_DECIDED = "this report was already decided";
src/http/api.ts +11 −6
@@ -16,6 +16,11 @@ import {
1616 } from "../actions/upstream_follow.ts";
1717 import { getFollow } from "../core/upstream_follow.ts";
1818 import { showProvenance } from "../actions/provenance.ts";
19 import {
20 APP_ALREADY_YOURS,
21 ORIGIN_UNREACHABLE,
22 REPORT_ALREADY_DECIDED,
23 } from "../actions/origin_app_words.ts";
1924 import { getProvenance, provenanceFacts } from "../core/provenance.ts";
2025 import { goldenPath } from "../apps/golden_path.ts";
2126 import { upstreamPayload } from "../spawn/upstream.ts";
@@ -772,7 +777,7 @@ function sourceFailure(error: SourceError): Response {
772777 return apiError(
773778 "origin_unreachable",
774779 502,
775 "the origin could not be reached",
780 ORIGIN_UNREACHABLE,
776781 );
777782 }
778783 }
@@ -806,7 +811,7 @@ function originAware(error: string, status = 422): Response {
806811 return apiError(
807812 "origin_unreachable",
808813 502,
809 "the origin could not be reached; nothing was changed",
814 `${ORIGIN_UNREACHABLE}; nothing was changed`,

Same bytes as before. Note that the terminal (src/tui/screens/common.ts:883) says "…; nothing changed" for this same refusal. That's left to the follow-up, because aligning it changes wording.

810815 );
811816 if (error === "origin_refused")
812817 return apiError(
@@ -3350,7 +3355,7 @@ export function apiRoutes(router: Router<Handler>): void {
33503355 );
33513356 if (!accepted.ok)
33523357 return accepted.error === "decided"
3353 ? apiError("decided", 409, "this report was already decided")
3358 ? apiError("decided", 409, REPORT_ALREADY_DECIDED)
33543359 : originAware(accepted.error);
33553360 return json(withLabels(ctx, accepted.value), 201);
33563361 },
@@ -3372,7 +3377,7 @@ export function apiRoutes(router: Router<Handler>): void {
33723377 );
33733378 if (!dismissed.ok)
33743379 return dismissed.error === "decided"
3375 ? apiError("decided", 409, "this report was already decided")
3380 ? apiError("decided", 409, REPORT_ALREADY_DECIDED)
33763381 : respondAccess(dismissed.error, 403);
33773382 return json({ id: ctx.params.id, state: "dismissed" });
33783383 },
@@ -4803,7 +4808,7 @@ export function apiRoutes(router: Router<Handler>): void {
48034808 );
48044809 if (!listed.ok)
48054810 return listed.error === "unreachable"
4806 ? apiError("origin_unreachable", 502, "the origin could not be reached")
4811 ? apiError("origin_unreachable", 502, ORIGIN_UNREACHABLE)
48074812 : respondAccess(listed.error, 404);
48084813 return timePhase(ctx.request, "render", () =>
48094814 json(listed.value.map((row) => remoteRepoJson(ctx, row))),
@@ -5319,7 +5324,7 @@ export function apiRoutes(router: Router<Handler>): void {
53195324 : made.error.kind === "invalid"
53205325 ? apiError("invalid", 422, made.error.message)
53215326 : made.error.kind === "own"
5322 ? apiError("own", 409, "this app is already yours")
5327 ? apiError("own", 409, APP_ALREADY_YOURS)
53235328 : made.error.kind === "unverified"
53245329 ? apiError("unverified", 403, "prove your email first")
53255330 : made.error.kind === "unauthorized"
src/http/mcp.ts +8 −3
@@ -20,6 +20,11 @@
2020 import { listRemoteRepos, listUserOrigins, syncOriginNow } from "../actions/origins.ts";
2121 import { originJson, remoteRepoJson } from "./api.ts";
2222 import { showProvenance } from "../actions/provenance.ts";
23 import {
24 APP_ALREADY_YOURS,
25 ORIGIN_UNREACHABLE,
26 REPORT_ALREADY_DECIDED,
27 } from "../actions/origin_app_words.ts";
2328 import { migrateMessage, migrateRepo, migrationJobFor } from "../actions/migrate.ts";
2429 import type { MirrorJob } from "../origins/jobs.ts";
2530 import { EVENTS_LIMIT_MAX, eventsLimit, eventsPage, validEventsCursor } from "../core/events_page.ts";
@@ -1891,7 +1896,7 @@ const TOOLS: readonly Tool[] = [
18911896 const listed = await listRemoteRepos(ctx.app, user, origin);
18921897 if (!listed.ok)
18931898 return listed.error === "unreachable"
1894 ? bad("origin_unreachable", "the origin could not be reached")
1899 ? bad("origin_unreachable", ORIGIN_UNREACHABLE)
18951900 : bad("not_found", "no such origin");
18961901 return good(listed.value.map((row) => remoteRepoJson(ctx, row)));
18971902 },
@@ -2141,7 +2146,7 @@ const TOOLS: readonly Tool[] = [
21412146 return bad("invalid", "give both `id` and `action`, or neither to list");
21422147 const denied = (error: string): Answer =>
21432148 error === "decided"
2144 ? bad("decided", "this report was already decided")
2149 ? bad("decided", REPORT_ALREADY_DECIDED)
21452150 : error === "not_found"
21462151 ? bad("not_found", "no such report here")
21472152 : bad(
@@ -3055,7 +3060,7 @@ const TOOLS: readonly Tool[] = [
30553060 : made.error.kind === "invalid"
30563061 ? bad("invalid", made.error.message)
30573062 : made.error.kind === "own"
3058 ? bad("own", "this app is already yours")
3063 ? bad("own", APP_ALREADY_YOURS)
30593064 : made.error.kind === "not_member"
30603065 ? bad("not_member", NOT_A_MEMBER)
30613066 : made.error.kind === "unverified"
test/origin-app-words.test.ts added +45
@@ -0,0 +1,45 @@
1 /**
2 * An unreachable origin, an app already yours and a report already decided
3 * are said from one module, src/actions/origin_app_words.ts (#5647). The API
4 * and the MCP import the sentences; a copy typed back into either 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 {
10 APP_ALREADY_YOURS,
11 ORIGIN_UNREACHABLE,
12 REPORT_ALREADY_DECIDED,
13 } from "../src/actions/origin_app_words.ts";
14
15 const root = join(import.meta.dir, "..");
16 const read = (path: string) => readFileSync(join(root, path), "utf8");
17
18 const SENTENCES = {
19 ORIGIN_UNREACHABLE: "the origin could not be reached",
20 APP_ALREADY_YOURS: "this app is already yours",
21 REPORT_ALREADY_DECIDED: "this report was already decided",
22 } as const;
23
24 const DOORS = ["src/http/api.ts", "src/http/mcp.ts"] as const;
25
26 describe("origin, app and report refusals live in one module", () => {
27 test("the module says each sentence, word for word", () => {
28 expect({
29 ORIGIN_UNREACHABLE,
30 APP_ALREADY_YOURS,
31 REPORT_ALREADY_DECIDED,
32 }).toEqual(SENTENCES);
33 });
34
35 for (const door of DOORS) {
36 test(`${door} imports the sentences and types none of them out`, () => {
37 const text = read(door);
38 expect(text).toContain('from "../actions/origin_app_words.ts"');
39 for (const [name, sentence] of Object.entries(SENTENCES)) {
40 expect(text).toContain(name);
41 expect(text).not.toContain(sentence);
42 }
43 });
44 }
45 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs