Public: anyone can read this; the crew can change it.
A test fails the first-login page on jargon, in its visible text and its labels, for every role that lands there #10756
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10752-a-test-fails-the-first-login-page-on-jar → main 6 commits fork 4 threads · 4 unresolved
7 files
changed +271
−139
docs/MCP.md +2 −2
| @@ -79,7 +79,7 @@ bytes in `tools/list`. After changing a tool run `bun scripts/mcp-table.ts`; | ||
| 79 | 79 | | `confirm_wait` | — | Let an issue go ahead though its blocker closed dropped or split | 269 | |
| 80 | 80 | | `contribute` | — | For a new agent: what this forge is, its bottleneck, help-wanted issues, how to propose | 210 | |
| 81 | 81 | | `converge` | — | Have a fork's crew merge the parent's updates where both changed a file | 200 | |
| 82 | | `crew` | — | List a repository's crew, or add or remove an agent | 313 | | |
| 82 | | `crew` | — | List a repository's crew, or add or remove an agent | 338 | | |
| 83 | 83 | | `data` | — | Read SQLite files you may see: tables, rows, read-only SQL; secrets masked | 367 | |
| 84 | 84 | | `decisions_by` | — | Who decides parked hand-over cards: `me`, `decider`, `decider_tell` | 305 | |
| 85 | 85 | | `detach` | — | Detach a fork from its parent | 210 | |
| @@ -89,7 +89,7 @@ bytes in `tools/list`. After changing a tool run `bun scripts/mcp-table.ts`; | ||
| 89 | 89 | | `egress_grants` | — | Private destinations and grants: list, grant or revoke (site admin) | 289 | |
| 90 | 90 | | `email_verify` | — | Send yourself a new verification link | 126 | |
| 91 | 91 | | `events` | — | Repository events | 259 | |
| 92 | | `explore` | — | The apps, people (and, for an admin, forges) you can see, ranked | 414 | | |
| 92 | | `explore` | — | The apps, people (and, for an admin, forges) you can see, ranked | 547 | | |
| 93 | 93 | | `family` | — | The forge that sent this one and, for a site admin, those it sent, with state, version | 205 | |
| 94 | 94 | | `features` | — | Apps absorbed as forge features: list, read, absorb or un-absorb, change typed settings | 357 | |
| 95 | 95 | | `feed` | — | Your ranked home timeline; `cursor` pages; saved filter applies | 562 | |
src/web/studio/crew.ts +1 −1
| @@ -424,7 +424,7 @@ export function settingsLead(ctx: Ctx, repo: Repo): Html { | ||
| 424 | 424 | export function crewKeyNote(ctx: Ctx): Html | null { |
| 425 | 425 | if (crewHasCredential(ctx.app.db)) return null; |
| 426 | 426 | return html`<p class="small muted" id="crew-key-note" role="status"> |
| 427 | ${icon("alert")} The crew has no model key yet, so it cannot start work. | |
| 427 | ${icon("alert")} The crew cannot start work yet: it needs a subscription. | |
| 428 | 428 | ${ctx.auth.user?.isAdmin |
| 429 | 429 | ? html`<a href="/settings/subscriptions">Add one in Subscriptions</a>.` |
| 430 | 430 | : "Ask this forge's admin to add one."} |
test/first-login-words.test.ts added +54
| @@ -0,0 +1,54 @@ | ||
| 1 | /** | |
| 2 | * The first page of a freshly claimed forge, in plain words (#10752): what | |
outdated
This header describes next-steps-card.test.ts, not this file. Say what this file checks: no jargon on the first page for the owner and the everyday account. |
||
| 3 | * sits in `<main>` for the owner, and for the everyday account the owner | |
| 4 | * makes next, holds none of the words in `helpers/jargon.ts`. | |
| 5 | */ | |
| 6 | ||
| 7 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 8 | import { claimedForge, type ClaimedForge } from "./helpers/claimed-forge.ts"; | |
| 9 | import { jargonIn } from "./helpers/jargon.ts"; | |
| 10 | import { quietLogs } from "./helpers/globals.ts"; | |
| 11 | import { passkeyFields } from "./helpers/passkey.ts"; | |
| 12 | ||
| 13 | quietLogs(); | |
| 14 | ||
| 15 | describe("the first-login page of a spawned forge, in plain words", () => { | |
| 16 | let forge: ClaimedForge; | |
| 17 | let owner = ""; | |
| 18 | const url = (path: string) => `${forge.base}${path}`; | |
| 19 | ||
| 20 | beforeAll(async () => { | |
| 21 | forge = await claimedForge("forge-first-login-words-"); | |
| 22 | owner = forge.owner; | |
| 23 | }, 60_000); | |
| 24 | ||
| 25 | afterAll(() => forge.stop()); | |
| 26 | ||
| 27 | /** The page itself: what sits in `<main>`; the menus around it are their own issue. */ | |
| 28 | const mainOf = (html: string) => /<main\b[\s\S]*?<\/main>/.exec(html)?.[0] ?? ""; | |
| 29 | ||
| 30 | test("the owner's first page has no jargon", async () => { | |
| 31 | const main = mainOf(await (await fetch(url("/"), { headers: { cookie: owner } })).text()); | |
| 32 | expect(main).toContain("Your next steps"); | |
| 33 | expect(jargonIn(main)).toEqual([]); | |
| 34 | }); | |
| 35 | ||
| 36 | test("the everyday account's first page has no jargon", async () => { | |
| 37 | const who = { username: "maria-me", email: "[email protected]" }; | |
| 38 | const res = await forge.post(owner, "/settings/everyday", { | |
outdated
This whole setup duplicates next-steps-card.test.ts. Extract it to test/helpers/ and use it from both. |
||
| 39 | ...who, | |
| 40 | ...(await passkeyFields(forge.base, "/settings/everyday/options", who, { headers: { cookie: owner } })), | |
| 41 | }); | |
| 42 | expect(res.status).toBe(303); | |
| 43 | const daily = (res.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 44 | const main = mainOf(await (await fetch(url("/"), { headers: { cookie: daily } })).text()); | |
| 45 | expect(main).toContain("Your next steps"); | |
| 46 | expect(jargonIn(main)).toEqual([]); | |
| 47 | }); | |
| 48 | ||
| 49 | test("the word list sees the words it is for", () => { | |
| 50 | expect(jargonIn('<p>Add an SSH key</p><a title="Open the palette">x</a>')).toHaveLength(2); | |
| 51 | expect(jargonIn("<p>Your cellar, a gated community</p>")).toEqual([]); | |
| 52 | expect(jargonIn("<p>an ssh-key and a model-key</p>")).toHaveLength(2); | |
| 53 | }); | |
| 54 | }); |
test/helpers/claimed-forge.ts added +151
| @@ -0,0 +1,151 @@ | ||
| 1 | /** | |
| 2 | * A spawned forge whose owner has claimed it: the parent forge, one app, | |
| 3 | * one agent carried over, the child booted and served, the claim done. | |
| 4 | * Shared by the tests about a new forge's first page. | |
| 5 | */ | |
| 6 | ||
| 7 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 8 | import { tmpdir } from "node:os"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import { createRepo } from "../../src/actions/repos.ts"; | |
| 11 | import { childDirOf, spawnForge } from "../../src/actions/spawn.ts"; | |
| 12 | import { createApp, type App } from "../../src/app.ts"; | |
| 13 | import { addCrew, createAgentRow } from "../../src/core/agents.ts"; | |
| 14 | import { createUser } from "../../src/core/users.ts"; | |
| 15 | import { commitFiles } from "../../src/git/write.ts"; | |
| 16 | import { repoDir } from "../../src/git/repos.ts"; | |
| 17 | import { serve } from "../../src/http/server.ts"; | |
| 18 | import { firstBoot } from "../../src/spawn/first-boot.ts"; | |
| 19 | import { hostedPlacement } from "../../src/spawn/placement.ts"; | |
| 20 | import { passkeyFields } from "./passkey.ts"; | |
| 21 | import { seatAs } from "./seat.ts"; | |
| 22 | ||
| 23 | export const PASSWORD = "a-long-next-steps-card-passphrase"; | |
| 24 | const FORGE_JSON = JSON.stringify({ | |
| 25 | app: { run: "bun run index.ts", health: "/" }, | |
| 26 | }); | |
| 27 | ||
| 28 | export type ClaimedForge = { | |
| 29 | /** The child forge's address. */ | |
| 30 | base: string; | |
| 31 | /** The child forge itself, for tests that reach into its data. */ | |
| 32 | child: App; | |
| 33 | /** The owner's session cookie, as `name=value`. */ | |
| 34 | owner: string; | |
| 35 | /** Same-origin form POST to the child. */ | |
| 36 | post: (cookie: string, path: string, form: Record<string, string>) => Promise<Response>; | |
| 37 | /** Stop both forges and remove their data. */ | |
| 38 | stop: () => Promise<void>; | |
| 39 | }; | |
| 40 | ||
| 41 | export async function claimedForge(prefix: string): Promise<ClaimedForge> { | |
| 42 | const dir = await mkdtemp(join(tmpdir(), prefix)); | |
| 43 | const parent = createApp({ dataDir: dir, url: "https://forge.test", port: 0 }); | |
| 44 | const made = await createUser(parent.db, { | |
| 45 | origin: "seed", | |
| 46 | trusted: true, | |
| 47 | username: "alice", | |
| 48 | email: "[email protected]", | |
| 49 | password: PASSWORD, | |
| 50 | isAdmin: true, | |
| 51 | }); | |
| 52 | if (!made.ok) throw new Error("user"); | |
| 53 | const alice = made.value; | |
| 54 | const repo = await createRepo(parent, alice, { name: "bakery" }); | |
| 55 | if (!repo.ok) throw new Error("repo"); | |
| 56 | const c = await commitFiles(repoDir(parent.reposRoot, "alice", "bakery"), { | |
| 57 | branch: "main", | |
| 58 | message: "bakery", | |
| 59 | files: [ | |
| 60 | { path: "forge.json", content: FORGE_JSON }, | |
| 61 | { | |
| 62 | path: "index.ts", | |
| 63 | content: "Bun.serve({fetch(){return new Response('hi')}})\n", | |
| 64 | }, | |
| 65 | ], | |
| 66 | author: { name: "a", email: "[email protected]" }, | |
| 67 | }); | |
| 68 | if (!c.ok) throw new Error("commit"); | |
| 69 | const atlas = await createUser(parent.db, { | |
| 70 | origin: "seed", | |
| 71 | username: "atlas", | |
| 72 | email: "[email protected]", | |
| 73 | agent: true, | |
| 74 | }); | |
| 75 | if (!atlas.ok) throw new Error("agent"); | |
| 76 | createAgentRow(parent.db, { | |
| 77 | userId: atlas.value.id, | |
| 78 | brief: "plan", | |
| 79 | createdBy: alice.id, | |
| 80 | }); | |
| 81 | addCrew(parent.db, repo.value.id, atlas.value.id, alice.id); | |
| 82 | seatAs(parent.db, repo.value.id, atlas.value.id, "planner"); | |
| 83 | const parentServers = await serve(parent); | |
| 84 | const spawned = await spawnForge( | |
| 85 | parent, | |
| 86 | alice, | |
| 87 | { | |
| 88 | name: "maria", | |
| 89 | org: "marias", | |
| 90 | apps: ["alice/bakery"], | |
| 91 | carryBriefs: true, | |
| 92 | carrySubscriptions: false, | |
| 93 | direction: "Run Maria's bakery.", | |
| 94 | }, | |
| 95 | { | |
| 96 | catalog: [ | |
| 97 | { ref: "alice/bakery", readable: true, bytes: 10, forgeJsonBytes: 0 }, | |
| 98 | ], | |
| 99 | briefs: [{ agent: "atlas", text: "plan" }], | |
| 100 | subscriptions: [], | |
| 101 | }, | |
| 102 | { | |
| 103 | placement: hostedPlacement({ host: "forge.test" }), | |
| 104 | request: new Request("https://forge.test/x", { | |
| 105 | method: "POST", | |
| 106 | headers: { origin: "https://forge.test" }, | |
| 107 | }), | |
| 108 | url: new URL("https://forge.test/x"), | |
| 109 | }, | |
| 110 | ); | |
| 111 | if (!spawned.ok) throw new Error(JSON.stringify(spawned.error)); | |
| 112 | const child = createApp({ | |
| 113 | dataDir: childDirOf(parent, spawned.value.bodyId), | |
| 114 | port: 0, | |
| 115 | }); | |
| 116 | await firstBoot(child); | |
| 117 | const servers = await serve(child); | |
| 118 | const link = spawned.value.claimUrl!; | |
| 119 | const token = link.split("/claim#")[1]!; | |
| 120 | const who = { token, username: "maria", email: "[email protected]" }; | |
| 121 | const claimed = await fetch(`${servers.url}/claim`, { | |
| 122 | method: "POST", | |
| 123 | headers: { origin: servers.url }, | |
| 124 | body: new URLSearchParams({ | |
| 125 | ...who, | |
| 126 | ...(await passkeyFields(servers.url, "/claim/options", who)), | |
| 127 | }), | |
| 128 | redirect: "manual", | |
| 129 | }); | |
| 130 | if (claimed.status !== 303) throw new Error(`claim ${claimed.status}`); | |
| 131 | const owner = (claimed.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 132 | return { | |
| 133 | base: servers.url, | |
| 134 | child, | |
| 135 | owner, | |
| 136 | post: (cookie, path, form) => | |
| 137 | fetch(`${servers.url}${path}`, { | |
| 138 | method: "POST", | |
| 139 | headers: { cookie, origin: servers.url }, | |
| 140 | body: new URLSearchParams(form), | |
| 141 | redirect: "manual", | |
| 142 | }), | |
| 143 | stop: async () => { | |
| 144 | servers.stop(); | |
| 145 | child.close(); | |
| 146 | parentServers.stop(); | |
| 147 | parent.close(); | |
| 148 | await rm(dir, { recursive: true, force: true }); | |
| 149 | }, | |
| 150 | }; | |
| 151 | } |
test/helpers/jargon.ts added +46
| @@ -0,0 +1,46 @@ | ||
| 1 | /** | |
| 2 | * Words a zero-technical client should not meet on their first page (#10752). | |
| 3 | * Whole words, case-insensitive. The list is meant to shrink; it has no | |
| 4 | * allow list. A hyphen breaks a word, so "ssh-key" and "model-key" are hits. | |
| 5 | */ | |
| 6 | ||
| 7 | export const JARGON = [ | |
| 8 | "egress", | |
| 9 | "sentinel", | |
| 10 | "shard", | |
| 11 | "cell", | |
| 12 | "gate", | |
| 13 | "rebase", | |
| 14 | "tbw", | |
| 15 | "token", | |
| 16 | "origin", | |
| 17 | "palette", | |
| 18 | "model key", | |
| 19 | "ssh", | |
| 20 | ] as const; | |
| 21 | ||
| 22 | const WORDS = new RegExp( | |
|
||
| 23 | `(?<![A-Za-z0-9_])(?:${JARGON.map((w) => w.replace(/ /g, "[\\s-]+")).join("|")})s?(?![A-Za-z0-9_])`, | |
| 24 | "gi", | |
| 25 | ); | |
| 26 | ||
| 27 | /** What a reader sees or hears of `html`: visible text plus aria-label, title, alt and placeholder values. */ | |
| 28 | export function readableText(html: string): string { | |
| 29 | const parts: string[] = []; | |
| 30 | for (const m of html.matchAll(/\b(?:aria-label|title|alt|placeholder)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi)) | |
| 31 | parts.push(m[1] ?? m[2] ?? ""); | |
| 32 | const body = html | |
| 33 | .replace(/<(script|style|template)\b[\s\S]*?<\/\1>/gi, " ") | |
| 34 | .replace(/<[^>]*>/g, " "); | |
| 35 | parts.push(body); | |
| 36 | return parts.join("\n").replace(/&/g, "&").replace(/'|'/g, "'").replace(/"/g, '"').replace(/\s+/g, " "); | |
| 37 | } | |
| 38 | ||
| 39 | /** Every jargon word found in `html`'s readable text, in order, with a little context. */ | |
| 40 | export function jargonIn(html: string): string[] { | |
| 41 | const text = readableText(html); | |
| 42 | return [...text.matchAll(WORDS)].map((m) => { | |
| 43 | const at = m.index ?? 0; | |
| 44 | return `${m[0]}: …${text.slice(Math.max(0, at - 30), at + m[0].length + 30)}…`; | |
| 45 | }); | |
| 46 | } |
test/next-steps-card.test.ts +10 −129
| @@ -5,54 +5,29 @@ | ||
| 5 | 5 | * second account. |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | import { seatAs } from "./helpers/seat.ts"; | |
| 9 | 8 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; |
| 10 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 11 | import { tmpdir } from "node:os"; | |
| 12 | import { join } from "node:path"; | |
| 13 | import { createRepo } from "../src/actions/repos.ts"; | |
| 14 | import { childDirOf, spawnForge } from "../src/actions/spawn.ts"; | |
| 15 | import { createApp, type App } from "../src/app.ts"; | |
| 16 | import { addCrew, createAgentRow } from "../src/core/agents.ts"; | |
| 17 | 9 | import { createSession } from "../src/core/sessions.ts"; |
| 18 | 10 | import { createUser } from "../src/core/users.ts"; |
| 19 | import { commitFiles } from "../src/git/write.ts"; | |
| 20 | import { repoDir } from "../src/git/repos.ts"; | |
| 21 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 22 | import { firstBoot } from "../src/spawn/first-boot.ts"; | |
| 23 | import { hostedPlacement } from "../src/spawn/placement.ts"; | |
| 11 | import { claimedForge, PASSWORD, type ClaimedForge } from "./helpers/claimed-forge.ts"; | |
| 24 | 12 | import { quietLogs } from "./helpers/globals.ts"; |
| 25 | 13 | import { passkeyFields } from "./helpers/passkey.ts"; |
| 26 | 14 | |
| 27 | 15 | quietLogs(); |
| 28 | 16 | |
| 29 | const PASSWORD = "a-long-next-steps-card-passphrase"; | |
| 30 | const FORGE_JSON = JSON.stringify({ | |
| 31 | app: { run: "bun run index.ts", health: "/" }, | |
| 32 | }); | |
| 33 | 17 | const CARD = "Your next steps"; |
| 34 | 18 | |
| 35 | 19 | describe("the next-steps card of a spawned forge", () => { |
| 36 | let dir = ""; | |
| 37 | let parent: App; | |
| 38 | let parentServers: Servers; | |
| 39 | let child: App; | |
| 40 | let servers: Servers; | |
| 20 | let forge: ClaimedForge; | |
| 41 | 21 | let owner = ""; |
| 42 | 22 | |
| 43 | const url = (path: string) => `${servers.url}${path}`; | |
| 23 | const url = (path: string) => `${forge.base}${path}`; | |
| 44 | 24 | const page = async (cookie: string, path = "/") => |
| 45 | 25 | (await (await fetch(url(path), { headers: { cookie } })).text()).replace( |
| 46 | 26 | /\s+/g, |
| 47 | 27 | " ", |
| 48 | 28 | ); |
| 49 | 29 | const post = (cookie: string, path: string, form: Record<string, string>) => |
| 50 | fetch(url(path), { | |
| 51 | method: "POST", | |
| 52 | headers: { cookie, origin: servers.url }, | |
| 53 | body: new URLSearchParams(form), | |
| 54 | redirect: "manual", | |
| 55 | }); | |
| 30 | forge.post(cookie, path, form); | |
| 56 | 31 | const steps = (html: string) => { |
| 57 | 32 | const card = html.slice(html.indexOf(CARD)); |
| 58 | 33 | return { |
| @@ -63,105 +38,11 @@ describe("the next-steps card of a spawned forge", () => { | ||
| 63 | 38 | }; |
| 64 | 39 | |
| 65 | 40 | beforeAll(async () => { |
| 66 | dir = await mkdtemp(join(tmpdir(), "forge-next-steps-card-")); | |
| 67 | parent = createApp({ dataDir: dir, url: "https://forge.test", port: 0 }); | |
| 68 | const made = await createUser(parent.db, { | |
| 69 | origin: "seed", | |
| 70 | trusted: true, | |
| 71 | username: "alice", | |
| 72 | email: "[email protected]", | |
| 73 | password: PASSWORD, | |
| 74 | isAdmin: true, | |
| 75 | }); | |
| 76 | if (!made.ok) throw new Error("user"); | |
| 77 | const alice = made.value; | |
| 78 | const repo = await createRepo(parent, alice, { name: "bakery" }); | |
| 79 | if (!repo.ok) throw new Error("repo"); | |
| 80 | const c = await commitFiles(repoDir(parent.reposRoot, "alice", "bakery"), { | |
| 81 | branch: "main", | |
| 82 | message: "bakery", | |
| 83 | files: [ | |
| 84 | { path: "forge.json", content: FORGE_JSON }, | |
| 85 | { | |
| 86 | path: "index.ts", | |
| 87 | content: "Bun.serve({fetch(){return new Response('hi')}})\n", | |
| 88 | }, | |
| 89 | ], | |
| 90 | author: { name: "a", email: "[email protected]" }, | |
| 91 | }); | |
| 92 | if (!c.ok) throw new Error("commit"); | |
| 93 | const atlas = await createUser(parent.db, { | |
| 94 | origin: "seed", | |
| 95 | username: "atlas", | |
| 96 | email: "[email protected]", | |
| 97 | agent: true, | |
| 98 | }); | |
| 99 | if (!atlas.ok) throw new Error("agent"); | |
| 100 | createAgentRow(parent.db, { | |
| 101 | userId: atlas.value.id, | |
| 102 | brief: "plan", | |
| 103 | createdBy: alice.id, | |
| 104 | }); | |
| 105 | addCrew(parent.db, repo.value.id, atlas.value.id, alice.id); | |
| 106 | seatAs(parent.db, repo.value.id, atlas.value.id, "planner"); | |
| 107 | parentServers = await serve(parent); | |
| 108 | const spawned = await spawnForge( | |
| 109 | parent, | |
| 110 | alice, | |
| 111 | { | |
| 112 | name: "maria", | |
| 113 | org: "marias", | |
| 114 | apps: ["alice/bakery"], | |
| 115 | carryBriefs: true, | |
| 116 | carrySubscriptions: false, | |
| 117 | direction: "Run Maria's bakery.", | |
| 118 | }, | |
| 119 | { | |
| 120 | catalog: [ | |
| 121 | { ref: "alice/bakery", readable: true, bytes: 10, forgeJsonBytes: 0 }, | |
| 122 | ], | |
| 123 | briefs: [{ agent: "atlas", text: "plan" }], | |
| 124 | subscriptions: [], | |
| 125 | }, | |
| 126 | { | |
| 127 | placement: hostedPlacement({ host: "forge.test" }), | |
| 128 | request: new Request("https://forge.test/x", { | |
| 129 | method: "POST", | |
| 130 | headers: { origin: "https://forge.test" }, | |
| 131 | }), | |
| 132 | url: new URL("https://forge.test/x"), | |
| 133 | }, | |
| 134 | ); | |
| 135 | if (!spawned.ok) throw new Error(JSON.stringify(spawned.error)); | |
| 136 | child = createApp({ | |
| 137 | dataDir: childDirOf(parent, spawned.value.bodyId), | |
| 138 | port: 0, | |
| 139 | }); | |
| 140 | await firstBoot(child); | |
| 141 | servers = await serve(child); | |
| 142 | const link = spawned.value.claimUrl!; | |
| 143 | const token = link.split("/claim#")[1]!; | |
| 144 | const who = { token, username: "maria", email: "[email protected]" }; | |
| 145 | const claimed = await fetch(url("/claim"), { | |
| 146 | method: "POST", | |
| 147 | headers: { origin: servers.url }, | |
| 148 | body: new URLSearchParams({ | |
| 149 | ...who, | |
| 150 | ...(await passkeyFields(servers.url, "/claim/options", who)), | |
| 151 | }), | |
| 152 | redirect: "manual", | |
| 153 | }); | |
| 154 | if (claimed.status !== 303) throw new Error(`claim ${claimed.status}`); | |
| 155 | owner = (claimed.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 41 | forge = await claimedForge("forge-next-steps-card-"); | |
| 42 | owner = forge.owner; | |
| 156 | 43 | }, 60_000); |
| 157 | 44 | |
| 158 | afterAll(async () => { | |
| 159 | servers.stop(); | |
| 160 | child.close(); | |
| 161 | parentServers.stop(); | |
| 162 | parent.close(); | |
| 163 | await rm(dir, { recursive: true, force: true }); | |
| 164 | }); | |
| 45 | afterAll(() => forge.stop()); | |
| 165 | 46 | |
| 166 | 47 | const cardOf = (html: string) => { |
| 167 | 48 | const from = html.indexOf(`id="next-steps"`); |
| @@ -217,7 +98,7 @@ describe("the next-steps card of a spawned forge", () => { | ||
| 217 | 98 | }); |
| 218 | 99 | |
| 219 | 100 | test("a second account never sees the card", async () => { |
| 220 | const other = await createUser(child.db, { | |
| 101 | const other = await createUser(forge.child.db, { | |
| 221 | 102 | origin: "seed", |
| 222 | 103 | trusted: true, |
| 223 | 104 | username: "second", |
| @@ -225,7 +106,7 @@ describe("the next-steps card of a spawned forge", () => { | ||
| 225 | 106 | password: PASSWORD, |
| 226 | 107 | }); |
| 227 | 108 | if (!other.ok) throw new Error("second"); |
| 228 | const session = createSession(child.db, other.value.id); | |
| 109 | const session = createSession(forge.child.db, other.value.id); | |
| 229 | 110 | const home = await page(`forge_session=${session.token}`); |
| 230 | 111 | expect(home).not.toContain(CARD); |
| 231 | 112 | expect(home).not.toContain('data-step="'); |
| @@ -235,7 +116,7 @@ describe("the next-steps card of a spawned forge", () => { | ||
| 235 | 116 | const who = { username: "maria-me", email: "[email protected]" }; |
| 236 | 117 | const res = await post(owner, "/settings/everyday", { |
| 237 | 118 | ...who, |
| 238 | ...(await passkeyFields(servers.url, "/settings/everyday/options", who, { headers: { cookie: owner } })), | |
| 119 | ...(await passkeyFields(forge.base, "/settings/everyday/options", who, { headers: { cookie: owner } })), | |
| 239 | 120 | }); |
| 240 | 121 | expect(res.status).toBe(303); |
| 241 | 122 | const daily = (res.headers.get("set-cookie") ?? "").split(";")[0]!; |
test/spawn-child-apps.test.ts +7 −7
| @@ -169,24 +169,24 @@ describe("a spawned child's apps", () => { | ||
| 169 | 169 | expect(listApps(child.db).length).toBe(before); |
| 170 | 170 | }); |
| 171 | 171 | |
| 172 | test("after claiming, the home shows the app, and says the crew has no model key and where to add one", async () => { | |
| 172 | test("after claiming, the home shows the app, and says the crew cannot start yet and where to add one", async () => { | |
| 173 | 173 | const home = ( |
| 174 | 174 | await (await fetch(`${servers.url}/`, { headers: { cookie } })).text() |
| 175 | 175 | ).replace(/\s+/g, " "); |
| 176 | 176 | expect(home).toContain("bakery"); |
| 177 | 177 | expect(home).not.toContain("No apps yet"); |
| 178 | expect(home).toContain("The crew has no model key yet"); | |
| 178 | expect(home).toContain("The crew cannot start work yet"); | |
| 179 | 179 | expect(home).toContain('href="/settings/subscriptions"'); |
| 180 | 180 | const appPage = await ( |
| 181 | 181 | await fetch(`${servers.url}/marias/bakery`, { headers: { cookie } }) |
| 182 | 182 | ).text(); |
| 183 | expect(appPage).toContain("The crew has no model key yet"); | |
| 183 | expect(appPage).toContain("The crew cannot start work yet"); | |
| 184 | 184 | }); |
| 185 | 185 | |
| 186 | 186 | test("on the app page the key note is an owner fact: not for a stranger or a plain reader", async () => { |
| 187 | 187 | const anonymous = await (await fetch(`${servers.url}/marias/bakery`)).text(); |
| 188 | 188 | expect(anonymous).toContain("bakery"); |
| 189 | expect(anonymous).not.toContain("The crew has no model key yet"); | |
| 189 | expect(anonymous).not.toContain("The crew cannot start work yet"); | |
| 190 | 190 | const reader = await createUser(child.db, { origin: "seed", trusted: true, |
| 191 | 191 | username: "reader", |
| 192 | 192 | email: "[email protected]", |
| @@ -201,14 +201,14 @@ describe("a spawned child's apps", () => { | ||
| 201 | 201 | }) |
| 202 | 202 | ).text(); |
| 203 | 203 | expect(asReader).toContain("bakery"); |
| 204 | expect(asReader).not.toContain("The crew has no model key yet"); | |
| 204 | expect(asReader).not.toContain("The crew cannot start work yet"); | |
| 205 | 205 | // Nor on the home: there the note is for an admin or a writer of an app. |
| 206 | 206 | const readerHome = await ( |
| 207 | 207 | await fetch(`${servers.url}/`, { |
| 208 | 208 | headers: { cookie: `forge_session=${session.token}` }, |
| 209 | 209 | }) |
| 210 | 210 | ).text(); |
| 211 | expect(readerHome).not.toContain("The crew has no model key yet"); | |
| 211 | expect(readerHome).not.toContain("The crew cannot start work yet"); | |
| 212 | 212 | }); |
| 213 | 213 | |
| 214 | 214 | test("first boot registers apps as the founder, an account with no verified email", () => { |
| @@ -262,6 +262,6 @@ describe("a spawned child's apps", () => { | ||
| 262 | 262 | const home = await ( |
| 263 | 263 | await fetch(`${servers.url}/`, { headers: { cookie } }) |
| 264 | 264 | ).text(); |
| 265 | expect(home).not.toContain("The crew has no model key yet"); | |
| 265 | expect(home).not.toContain("The crew cannot start work yet"); | |
| 266 | 266 | }); |
| 267 | 267 | }); |
The wording is plain and under the word budget. Keep it. Show it in a before/after screenshot in the description (box 4).