Public: anyone can read this; the crew can change it.
A new app's own instructions promise a check the forge never runs #203
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/192-a-new-app-s-own-instructions-promise-a-c → main 1 commit fork
2 files
changed +46
−3
src/apps/scaffold.ts +10 −3
| @@ -89,11 +89,15 @@ makes "production data" and "preview data" two different things. | ||
| 89 | 89 | |
| 90 | 90 | \`\`\`sh |
| 91 | 91 | PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts # run it here |
| 92 | bun test # the check every pull runs | |
| 92 | bun test # what \`forge.json\`'s app.test runs on every push | |
| 93 | 93 | FORGE_APP_DATA=./data bun run src/tty.ts # the terminal |
| 94 | 94 | FORGE_APP_DATA=./data bun run src/db.ts --check # schema version, integrity |
| 95 | 95 | \`\`\` |
| 96 | 96 | |
| 97 | \`app.test\` in \`forge.json\` runs beside \`app.run\` in every deploy, not before | |
| 98 | it: the preview comes up the moment the app answers, and says \`live\`, or | |
| 99 | \`serving · tests failed\` the moment the suite disagrees. | |
| 100 | ||
| 97 | 101 | ## The shape |
| 98 | 102 | |
| 99 | 103 | One implementation, many doors. \`src/actions.ts\` is every operation as a |
| @@ -178,8 +182,11 @@ preview at its own address, deployed from your branch as you push. | ||
| 178 | 182 | - Work on the branch you were given. You cannot move the default branch; |
| 179 | 183 | a person merges your pull once it is reviewed. |
| 180 | 184 | - Keep state under \`FORGE_APP_DATA\`. Never write elsewhere at runtime. |
| 181 | - Run \`bun test\` before you push. It is the check the pull shows, and every | |
| 182 | behaviour you add gets a test that would fail without it. | |
| 185 | - Run \`bun test\` before you push. \`forge.json\`'s \`app.test\` runs it beside | |
| 186 | the app on every deploy, not before it: the preview comes up the moment | |
| 187 | the app answers and says \`live\`, or \`serving · tests failed\` the moment | |
| 188 | the suite disagrees. Every behaviour you add gets a test that would fail | |
| 189 | without it. | |
| 183 | 190 | - \`src/tty.ts\` is the app's terminal. The exact command that reaches it is |
| 184 | 191 | \`FORGE_APP_SSH\` at runtime; print that verbatim wherever the app tells |
| 185 | 192 | people how to connect. What a person can do on the page they can do here, |
test/apps.test.ts +36
| @@ -74,6 +74,8 @@ async function sh(args: string[], cwd: string) { | ||
| 74 | 74 | } |
| 75 | 75 | |
| 76 | 76 | import { identityKey } from "../src/apps/proxy.ts"; |
| 77 | import { scaffoldFiles } from "../src/apps/scaffold.ts"; | |
| 78 | import { parseCheck } from "../src/apps/manifest.ts"; | |
| 77 | 79 | describe("apps: production, previews, data, gates, crew", () => { |
| 78 | 80 | let dataDir: string; |
| 79 | 81 | let work: string; |
| @@ -1430,3 +1432,37 @@ describe("New app lands on the app's page, not the flash (issue #133)", () => { | ||
| 1430 | 1432 | expect(anonymous.status).toBe(404); |
| 1431 | 1433 | }); |
| 1432 | 1434 | }); |
| 1435 | ||
| 1436 | describe("a new app's own words promise only the check the forge runs (issue #192)", () => { | |
| 1437 | const files = scaffoldFiles({ name: "hello", description: "A place for notes" }); | |
| 1438 | const content = (path: string): string => { | |
| 1439 | const file = files.find((f) => f.path === path); | |
| 1440 | if (file === undefined || typeof file.content !== "string") | |
| 1441 | throw new Error(`scaffold has no text file at ${path}`); | |
| 1442 | return file.content; | |
| 1443 | }; | |
| 1444 | ||
| 1445 | // A sentence that talks about both "check" and "pull" is a sentence | |
| 1446 | // claiming the forge checks the pull — true only when forge.json declares | |
| 1447 | // one (`check`), since an app's own `test` runs beside its deploy instead. | |
| 1448 | const promisesACheck = (prose: string): boolean => | |
| 1449 | prose | |
| 1450 | .split(/(?<=[.!?])\s+/) | |
| 1451 | .some((sentence) => /\bcheck/i.test(sentence) && /\bpull/i.test(sentence)); | |
| 1452 | ||
| 1453 | test("forge.json declares no separate check, and the prose does not claim one", () => { | |
| 1454 | const checked = parseCheck(content("forge.json"), content("package.json")); | |
| 1455 | expect(checked.ok).toBe(true); | |
| 1456 | const declaresCheck = checked.ok && checked.value !== undefined; | |
| 1457 | expect(declaresCheck).toBe(false); | |
| 1458 | expect(promisesACheck(content("README.md"))).toBe(declaresCheck); | |
| 1459 | expect(promisesACheck(content("AGENTS.md"))).toBe(declaresCheck); | |
| 1460 | }); | |
| 1461 | ||
| 1462 | test("the README and AGENTS.md agree on what app.test actually does", () => { | |
| 1463 | for (const path of ["README.md", "AGENTS.md"]) { | |
| 1464 | expect(content(path)).toContain("app.test"); | |
| 1465 | expect(content(path)).toMatch(/beside/); | |
| 1466 | } | |
| 1467 | }); | |
| 1468 | }); |
new above
offline