Public: anyone can read this; the crew can change it.
The forge's own repository is never the target of a new forge's first ask #11427
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11417-the-forge-s-own-repository-is-never-the → main 3 commits fork 3 threads · 3 unresolved
3 files
changed +33
−2
src/actions/first_ask.ts +10 −1
| @@ -11,9 +11,10 @@ import type { User } from "../core/users.ts"; | ||
| 11 | 11 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 12 | 12 | import { newAppFromSentence, NEW_APP_ERRORS } from "./apps.ts"; |
| 13 | 13 | import { updateRepoSettings } from "./repos.ts"; |
| 14 | import { NOT_TOLD_WORDS, SAY_SOMETHING_WORDS } from "./tell_words.ts"; | |
| 14 | import { FORGE_REPO_WORDS, NOT_TOLD_WORDS, SAY_SOMETHING_WORDS } from "./tell_words.ts"; | |
| 15 | 15 | import { FROM_WORDS, TOO_LONG_WORDS, tell, type TellError, type Told } from "./tell.ts"; |
| 16 | 16 | import { parseFrom } from "./tell_from.ts"; |
| 17 | import { deriveAppName } from "../web/pages/apps-name.ts"; | |
| 17 | 18 | import { requireVerified } from "./verify.ts"; |
| 18 | 19 | import { MAX_BODY } from "../core/issues.ts"; |
| 19 | 20 | import { DIRECTION_NOTE } from "../crew/direct.ts"; |
| @@ -33,6 +34,12 @@ export type FirstAskError = { readonly words: string }; | ||
| 33 | 34 | /** The note a person is given when the app was made and filed but autonomy could not be turned on. */ |
| 34 | 35 | export const AUTONOMY_FAILED_WORDS = "The app is made, but the crew could not be set to start on it by itself. Turn on autonomy in its settings."; |
| 35 | 36 | |
| 37 | /** Whether the app this sentence would be named from is the forge's own repository. */ | |
| 38 | function resolvesToForgeRepo(app: App, actor: User, sentence: string): boolean { | |
| 39 | const name = deriveAppName(sentence); | |
| 40 | return name !== undefined && app.config.forgeRepo?.toLowerCase() === `${actor.username}/${name}`.toLowerCase(); | |
| 41 | } | |
| 42 | ||
| 36 | 43 | export async function firstAsk( |
| 37 | 44 | app: App, |
| 38 | 45 | actor: User | undefined, |
| @@ -49,6 +56,8 @@ export async function firstAsk( | ||
| 49 | 56 | if (actor === undefined || actor.kind === "org") return err({ words: DIRECTION_NOTE }); |
| 50 | 57 | if (!requireVerified(app, actor).ok) return err({ words: "Verify your email first." }); |
| 51 | 58 | if (from !== null && typeof parseFrom(from) !== "string") return err({ words: FROM_WORDS }); |
| 59 | // The forge's own repository is never an ask's target: refused before any app exists. | |
| 60 | if (resolvesToForgeRepo(app, actor, text)) return err({ words: FORGE_REPO_WORDS }); | |
| 52 | 61 | // An empty description: the issue the tell files is the one issue of the ask. |
| 53 | 62 | const made = await newAppFromSentence(app, actor, { sentence: text, description: "" }); |
| 54 | 63 | if (!made.ok) return err({ words: NEW_APP_ERRORS[made.code] ?? NOT_TOLD_WORDS }); |
src/actions/tell_words.ts +3
| @@ -15,3 +15,6 @@ export const SAY_SOMETHING_WORDS = "Say something first."; | ||
| 15 | 15 | |
| 16 | 16 | /** A report that did not reach the issue list. */ |
| 17 | 17 | export const NOT_SENT_WORDS = "That could not be sent."; |
| 18 | ||
| 19 | /** A first ask whose app would be the forge's own repository. */ | |
| 20 | export const FORGE_REPO_WORDS = "That name is this forge's own repository. Ask again in other words and it makes a new app."; |
test/first-ask-new-app.test.ts +20 −1
| @@ -7,12 +7,13 @@ | ||
| 7 | 7 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 8 | 8 | import { listIssues, MAX_BODY } from "../src/core/issues.ts"; |
| 9 | 9 | import { TOO_LONG_WORDS } from "../src/actions/tell.ts"; |
| 10 | import { SAY_SOMETHING_WORDS } from "../src/actions/tell_words.ts"; | |
| 10 | import { FORGE_REPO_WORDS, SAY_SOMETHING_WORDS } from "../src/actions/tell_words.ts"; | |
| 11 | 11 | import { listReposOwnedBy } from "../src/core/repos.ts"; |
| 12 | 12 | import { recentThoughts } from "../src/core/thoughts.ts"; |
| 13 | 13 | import { fundingOf, keepFunding } from "../src/spawn/funding.ts"; |
| 14 | 14 | import { crewCanRun } from "../src/web/studio/crew_key.ts"; |
| 15 | 15 | import { getUserByName } from "../src/core/users.ts"; |
| 16 | import { deriveAppName } from "../src/web/pages/apps-name.ts"; | |
| 16 | 17 | import { claimedForge, type ClaimedForge } from "./helpers/claimed-forge.ts"; |
| 17 | 18 | import { quietLogs } from "./helpers/globals.ts"; |
| 18 | 19 | |
| @@ -91,3 +92,21 @@ test("an over-long ask makes no app and says so", async () => { | ||
| 91 | 92 | expect(flashOf(res)).toContain(TOO_LONG_WORDS); |
| 92 | 93 | expect(listReposOwnedBy(db, getUserByName(db, "maria")!.id).length).toBe(before); |
| 93 | 94 | }); |
| 95 | ||
| 96 | test("an ask that would name the forge's own repository is refused and makes no app", async () => { | |
| 97 | const db = forge.child.db; | |
Asserts the opposite of box 1/2: should expect a refusal with the plain words, no repo made, and a receipt. |
||
| 98 | const maria = getUserByName(db, "maria")!; | |
| 99 | const ask = "Quokka census tracker"; | |
| 100 | const before = forge.child.config.forgeRepo; | |
| 101 | forge.child.config.forgeRepo = `maria/${deriveAppName(ask)!}`; | |
| 102 | const count = listReposOwnedBy(db, maria.id).length; | |
| 103 | try { | |
| 104 | const res = await forge.post(forge.owner, "/tell", { app: "new-app", text: ask }); | |
| 105 | expect(res.status).toBe(303); | |
| 106 | expect(flashOf(res)).toContain(FORGE_REPO_WORDS); | |
| 107 | expect(listReposOwnedBy(db, maria.id).length).toBe(count); | |
| 108 | } finally { | |
| 109 | if (before === undefined) delete forge.child.config.forgeRepo; | |
| 110 | else forge.child.config.forgeRepo = before; | |
| 111 | } | |
| 112 | }); |
new above
offline
Runs after the app exists; unreachable now and untested. Refuse before newAppFromSentence, with words from tell_words.ts, and receipt it.