Public: anyone can read this; the crew can change it.
/forges/new says the crew starts on the included credit when it applies #11355
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/11344-forges-new-says-the-crew-starts-on-the-i → main 3 commits fork 2 threads · 2 unresolved
2 files
changed +31
−1
src/web/pages/new-forge.ts +12 −1
| @@ -16,6 +16,7 @@ import type { User } from "../../core/users.ts"; | ||
| 16 | 16 | import type { Ctx } from "../../http/context.ts"; |
| 17 | 17 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; |
| 18 | 18 | import { giftOf, REDEEM_WORDS } from "../../actions/codes.ts"; |
| 19 | import { limitValue } from "../../settings/limits.ts"; | |
| 19 | 20 | import { quotaFull } from "../../spawn/quota.ts"; |
| 20 | 21 | import { childHost, childUrlOf, noRoom, spawnDoor, type Door } from "../../spawn/door.ts"; |
| 21 | 22 | import { |
| @@ -170,6 +171,15 @@ function freshError(errors: Errors, repoCount: number): string | undefined { | ||
| 170 | 171 | return errors["fresh"] ?? (repoCount === 0 ? errors["apps"] : undefined); |
| 171 | 172 | } |
| 172 | 173 | |
| 174 | /** The line under the heading: the crew starts on the included credit when there is one, else on the person's AI account. */ | |
| 175 | function hostingTerms(includedUsd: number): string { | |
| 176 | const crew = | |
| 177 | includedUsd > 0 | |
| 178 | ? `its crew starts on the included $${includedUsd} of credit, and you can add your AI account key later` | |
| 179 | : "its crew starts once you add your AI account"; | |
| 180 | return `Free within limits: it sleeps when idle, and ${crew}.`; | |
| 181 | } | |
| 182 | ||
| 173 | 183 | function formView( |
| 174 | 184 | ctx: Ctx, |
| 175 | 185 | user: User | undefined, |
| @@ -191,6 +201,7 @@ function formView( | ||
| 191 | 201 | </section> |
| 192 | 202 | </div>`; |
| 193 | 203 | const anon = user === undefined; |
| 204 | const includedUsd = limitValue(ctx.app.db, "spawn.included_credit_usd"); | |
| 194 | 205 | const freshErr = freshError(errors, repos.length); |
| 195 | 206 | const lead = repos.length === 0 ? "Pick a name, and say what it is for if you like" : "Pick a name and the apps to start from"; |
| 196 | 207 | return html`<div class="narrow-wide" data-forges> |
| @@ -199,7 +210,7 @@ function formView( | ||
| 199 | 210 | // /forges answers only an admin; to anyone else the link would lead to a 404. |
| 200 | 211 | actions: ctx.auth.user?.isAdmin ? sentLink() : undefined, |
| 201 | 212 | })} |
| 202 | <p class="muted small" id="hosting-terms">Free within limits: it sleeps when idle, and its crew starts once you add your AI account.</p> | |
| 213 | <p class="muted small" id="hosting-terms">${hostingTerms(includedUsd)}</p> | |
| 203 | 214 | ${quota ? html`<p role="status" id="quota-full">This forge has no room for another forge: ${quota}.</p>` : null} |
| 204 | 215 | ${errors["form"] |
| 205 | 216 | ? html`<p class="error mb" role="alert">${errors["form"]}</p>` |
test/new-forge-language.test.ts +19
| @@ -10,6 +10,7 @@ import { serve } from "../src/http/server.ts"; | ||
| 10 | 10 | import { usePlacement, useProbe } from "../src/spawn/door.ts"; |
| 11 | 11 | import { bootHostedChild } from "../src/spawn/host.ts"; |
| 12 | 12 | import { ok } from "../src/kernel/result.ts"; |
| 13 | import { setLimit } from "../src/settings/limits.ts"; | |
| 13 | 14 | |
| 14 | 15 | let dir = ""; |
| 15 | 16 | let app: ReturnType<typeof createApp>; |
| @@ -45,6 +46,24 @@ afterAll(async () => { | ||
| 45 | 46 | const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>")); |
| 46 | 47 | const boxed = ['class="card', "card-body"]; |
| 47 | 48 | |
| 49 | test("the hosting line says the crew starts on the included credit only when there is some", async () => { | |
| 50 | const say = async () => { | |
| 51 | const page = main(await (await fetch(servers.url + "/forges/new", { headers: auth })).text()); | |
| 52 | const line = /<p class="muted small" id="hosting-terms">([^]*?)<\/p>/.exec(page)?.[1] ?? ""; | |
| 53 | expect(line.split("Free within limits").length - 1).toBe(1); | |
| 54 | return line; | |
| 55 | }; | |
outdated
Substring assertions let the duplicated sentence through. Assert the exact text of #hosting-terms, and that "Free within limits" appears once. |
||
| 56 | const id = app.db.query<{ id: string }, []>("SELECT id FROM user LIMIT 1").get()!.id; | |
| 57 | setLimit(app.db, "spawn.included_credit_usd", 3, id); | |
| 58 | expect(await say()).toBe( | |
| 59 | "Free within limits: it sleeps when idle, and its crew starts on the included $3 of credit, and you can add your AI account key later.", | |
| 60 | ); | |
| 61 | setLimit(app.db, "spawn.included_credit_usd", 0, id); | |
| 62 | expect(await say()).toBe( | |
| 63 | "Free within limits: it sleeps when idle, and its crew starts once you add your AI account.", | |
| 64 | ); | |
| 65 | }); | |
| 66 | ||
| 48 | 67 | test("the form is a headed section, not a card", async () => { |
| 49 | 68 | const page = main(await (await fetch(servers.url + "/forges/new", { headers: auth })).text()); |
| 50 | 69 | expect(page).toContain('<section class="settings-block">'); |
new above
offline
Principle 1 (nothing explains itself twice): the prefix is repeated here, because hostingTerms already begins with "Free within limits: it sleeps when idle, and". Use
${hostingTerms(includedUsd)}alone.