Public: anyone can read this; the crew can change it.
The preview seed holds a backed bet, a proposed bet and priced issues, so /bets and Next up are seen filled as the owner #8874
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/8866-the-preview-seed-holds-a-backed-bet-a-pr → main 6 commits fork 2 threads · 2 unresolved
4 files
changed +143
−4
forge.json +2 −2
| @@ -3,8 +3,8 @@ | ||
| 3 | 3 | "api.anthropic.com" |
| 4 | 4 | ], |
| 5 | 5 | "app": { |
| 6 | "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/notes", "/feed", "/review", "/search", "/new", "/audit", "/releases", "/apps", "/repos", "/map", "/system", "/talk", "/contribute", "/flow", "/resilience", "/settings", "/preview-admin/sample", "/preview-admin/sample/pulls/3"], | |
| 7 | "looksAsOwner": ["/", "/flow", "/talk", "/preview-admin/sample", "/preview-admin/sample/pulls/3"], | |
| 6 | "looks": ["/", "/forges", "/family", "/front", "/explore", "/board", "/notes", "/feed", "/review", "/new", "/audit", "/releases", "/apps", "/repos", "/map", "/system", "/talk", "/flow", "/resilience", "/settings", "/preview-admin/sample", "/preview-admin/sample/pulls/3", "/preview-admin/sample/next", "/preview-admin/sample/bets"], | |
| 7 | "looksAsOwner": ["/", "/flow", "/talk", "/preview-admin/sample", "/preview-admin/sample/pulls/3", "/preview-admin/sample/next", "/preview-admin/sample/bets"], | |
| 8 | 8 | "run": "bun run src/main.ts", |
| 9 | 9 | "install": "", |
| 10 | 10 | "health": "/healthz", |
src/spawn/preview-seed.ts +111 −1
| @@ -15,13 +15,20 @@ | ||
| 15 | 15 | */ |
| 16 | 16 | |
| 17 | 17 | import type { App } from "../app.ts"; |
| 18 | import type { Issue } from "../core/issues.ts"; | |
| 19 | import type { Repo } from "../core/repos.ts"; | |
| 18 | 20 | import { pinPanel } from "../actions/board-pins.ts"; |
| 19 | 21 | import { writeBoard } from "../actions/boards.ts"; |
| 20 | import { openIssue } from "../actions/issues.ts"; | |
| 22 | import { backBet } from "../actions/bets.ts"; | |
| 23 | import { openIssue, setIssueState } from "../actions/issues.ts"; | |
| 21 | 24 | import { openPull } from "../actions/pulls.ts"; |
| 22 | 25 | import { commitToBranch, createRepo } from "../actions/repos.ts"; |
| 23 | 26 | import { turnValveFor } from "../actions/valves.ts"; |
| 27 | import { insertBet } from "../core/bets.ts"; | |
| 24 | 28 | import { claimRun, createRun, finishRun } from "../core/runs.ts"; |
| 29 | import { noteLimit } from "../core/standing.ts"; | |
| 30 | import { addSubscription } from "../core/subscriptions.ts"; | |
| 31 | import { recordUsage } from "../core/usage.ts"; | |
| 25 | 32 | import { getUserSetting, setUserSetting } from "../core/settings.ts"; |
| 26 | 33 | import { createUser, getUserByName, type User } from "../core/users.ts"; |
| 27 | 34 | import { err, ok, type Result } from "../kernel/result.ts"; |
| @@ -130,6 +137,21 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> { | ||
| 130 | 137 | if (!pull.ok) return err(`the pull: ${String(pull.error)}`); |
| 131 | 138 | if (pull.value.number !== PREVIEW_PULL) return err(`the pull is #${String(pull.value.number)}, not #${String(PREVIEW_PULL)}`); |
| 132 | 139 | |
| 140 | // Issues and pulls share one numbering, so the pull above keeps PREVIEW_PULL | |
| 141 | // and the ones /next lists and the bets hold come after it. | |
| 142 | const listed = [ | |
| 143 | { title: "Next up shows a cut line", body: "So a reviewer sees what fits the week." }, | |
| 144 | { title: "The bets page lists a backed bet", body: "Backed at a share of the week." }, | |
| 145 | { title: "A proposed bet waits for a tap", body: "The owner may back it." }, | |
| 146 | { title: "Issues carry a price", body: "From what past issues cost." }, | |
| 147 | { title: "The week ends on a Sunday", body: "And the list reads as next week below the line." }, | |
| 148 | ].map((input) => openIssue(app, admin, repo.value, input)); | |
| 149 | const open: Issue[] = []; | |
| 150 | for (const i of listed) { | |
| 151 | if (!i.ok) return err(`an issue: ${String(i.error)}`); | |
| 152 | open.push(i.value); | |
| 153 | } | |
| 154 | ||
| 133 | 155 | // A flow week: agent runs that started and spent this week, and a valve |
| 134 | 156 | // turned part way, so /flow has a pipe to draw and one to turn. |
| 135 | 157 | for (const usd of [0.42, 1.18, 0.75]) { |
| @@ -142,6 +164,10 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> { | ||
| 142 | 164 | claimRun(app.db, run.id); |
| 143 | 165 | finishRun(app.db, run.id, { state: "ok", summary: "A seeded run", costUsd: usd }); |
| 144 | 166 | } |
| 167 | const priced = seedPrices(app, admin, repo.value); | |
| 168 | if (!priced.ok) return priced; | |
| 169 | const bets = seedBets(app, admin, repo.value, open); | |
| 170 | if (!bets.ok) return bets; | |
| 145 | 171 | const valve = turnValveFor(app, admin, `${PREVIEW_ADMIN}/${PREVIEW_REPO}`, 60); |
| 146 | 172 | if (!valve.ok) return err(`the valve: ${valve.error}`); |
| 147 | 173 | |
| @@ -158,3 +184,87 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> { | ||
| 158 | 184 | } |
| 159 | 185 | return ok(); |
| 160 | 186 | } |
| 187 | ||
| 188 | /** | |
| 189 | * Finished issues that cost something, so `weekView` can price the open | |
| 190 | * ones, and a weekly capacity it can cut the list by: an inert account | |
| 191 | * that the provider refused at $8 last week and that has spent a little | |
| 192 | * since. Its token is no credential, only the shape one has, and it is | |
| 193 | * granted to no repository or owner, so `pickSubscription` (which joins | |
| 194 | * on grants) never offers it to a run or a conversation. | |
| 195 | */ | |
| 196 | function seedPrices(app: App, admin: User, repo: Repo): Result<void, string> { | |
| 197 | for (const [at, usd] of [1.5, 2, 2.5, 3].entries()) { | |
| 198 | const made = openIssue(app, admin, repo, { | |
| 199 | title: `Done work ${at + 1}`, | |
| 200 | body: "Finished before this preview.", | |
| 201 | }); | |
| 202 | if (!made.ok) return err(`a finished issue: ${String(made.error)}`); | |
| 203 | const run = createRun(app.db, { | |
| 204 | repoId: repo.id, | |
| 205 | kind: "agent", | |
| 206 | issueId: made.value.id, | |
| 207 | actorId: admin.id, | |
| 208 | reason: "seed", | |
| 209 | }); | |
| 210 | claimRun(app.db, run.id); | |
| 211 | finishRun(app.db, run.id, { state: "ok", summary: "A seeded run", costUsd: usd }); | |
| 212 | const closed = setIssueState(app, admin, repo, made.value, "closed", { reason: "done" }); | |
| 213 | if (!closed.ok) return err(`closing a finished issue: ${String(closed.error)}`); | |
| 214 | } | |
| 215 | const added = addSubscription(app.db, app.config.dataDir, { | |
| 216 | name: "preview sample", | |
| 217 | token: "sk-ant-oat01-preview-sample-not-a-credential", | |
| 218 | }); | |
| 219 | if (!added.ok) return err(`the sample account: ${added.error}`); | |
| 220 | const spend = (usd: number, at: Date) => { | |
| 221 | const run = createRun(app.db, { repoId: repo.id, kind: "agent", actorId: admin.id, reason: "seed" }); | |
| 222 | claimRun(app.db, run.id); | |
| 223 | finishRun(app.db, run.id, { state: "ok", summary: "A seeded run" }); | |
| 224 | recordUsage(app.db, { | |
| 225 | runId: run.id, | |
| 226 | repoId: repo.id, | |
| 227 | issueId: null, | |
| 228 | agentId: null, | |
| 229 | subscriptionId: added.value.id, | |
| 230 | tokensIn: 1, | |
| 231 | tokensOut: 1, | |
| 232 | costUsd: usd, | |
| 233 | wallMs: 1000, | |
| 234 | outcome: "ok", | |
| 235 | at: at.toISOString(), | |
| 236 | }); | |
| 237 | }; | |
| 238 | const now = Date.now(); | |
| 239 | const DAY = 86_400_000; | |
| 240 | spend(8, new Date(now - 4 * DAY)); | |
| 241 | noteLimit(app.db, added.value.id, { | |
| 242 | window: "week", | |
| 243 | resetsAt: new Date(now - 3.5 * DAY).toISOString(), | |
| 244 | observedAt: new Date(now - 3.5 * DAY - 3_600_000).toISOString(), | |
| 245 | said: "weekly limit", | |
| 246 | }); | |
| 247 | spend(1, new Date(now - DAY)); | |
| 248 | return ok(); | |
| 249 | } | |
| 250 | ||
| 251 | /** One bet backed at 20% of the week and one proposed, over the open issues. */ | |
| 252 | function seedBets(app: App, admin: User, repo: Repo, open: readonly Issue[]): Result<void, string> { | |
| 253 | const bet = (name: string, from: number) => | |
| 254 | insertBet( | |
| 255 | app.db, | |
| 256 | { | |
| 257 | repoId: repo.id, | |
| 258 | name, | |
| 259 | journeys: "Someone can see it", | |
| 260 | appetite: 20, | |
| 261 | leadId: null, | |
| 262 | issueIds: open.slice(from, from + 2).map((i) => i.id), | |
| 263 | }, | |
| 264 | new Date(), | |
| 265 | ); | |
| 266 | const backed = bet("Show the week", 2); | |
| 267 | bet("Price the list", 4); | |
| 268 | const result = backBet(app, admin, repo, backed.id, 20); | |
| 269 | return result.ok ? ok() : err(`backing a bet: ${result.error.message}`); | |
| 270 | } |
test/forge-looks.test.ts +2
| @@ -54,6 +54,8 @@ const NOT_A_PAGE: Record<string, string> = { | ||
| 54 | 54 | "/events": "a live event stream that never ends", |
| 55 | 55 | "/boards": "the list of boards; /board is pictured, and the slot went to the preview seed's repository as its owner (#8865); covered by test/boards-page.test.ts", |
| 56 | 56 | "/changelog": "the merged changes in words; /releases is pictured, and the slot went to the preview seed's pull as its owner (#8865); covered by test/changelog.test.ts", |
| 57 | "/search": "a search box; its slot went to the preview seed's /next as its owner (#8866)", | |
| 58 | "/contribute": "a static page about how to help; its slot went to the preview seed's /bets as its owner (#8866)", | |
| 57 | 59 | "/migrate": "an import form, nothing to picture until a job runs; the look slot went to /repos", |
| 58 | 60 | }; |
| 59 | 61 |
test/looks-owner.test.ts +28 −1
| @@ -21,6 +21,8 @@ import { readGrant } from "../src/apps/proxy.ts"; | ||
| 21 | 21 | import { AppsRuntime } from "../src/apps/runtime.ts"; |
| 22 | 22 | import { createApp, type App } from "../src/app.ts"; |
| 23 | 23 | import { listPins } from "../src/boards/pins.ts"; |
| 24 | import { listBets } from "../src/core/bets.ts"; | |
| 25 | import { pickSubscription } from "../src/core/subscriptions.ts"; | |
| 24 | 26 | import { looksForEnv } from "../src/core/looks.ts"; |
| 25 | 27 | import { listPulls } from "../src/core/pulls.ts"; |
| 26 | 28 | import { getRepo } from "../src/core/repos.ts"; |
| @@ -103,13 +105,25 @@ describe("the preview seed", () => { | ||
| 103 | 105 | const issues = app.db |
| 104 | 106 | .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue'") |
| 105 | 107 | .get(repo.id); |
| 106 | expect(issues?.n).toBe(2); | |
| 108 | expect(issues?.n).toBe(11); | |
| 107 | 109 | expect(listPulls(app.db, repo.id).length).toBe(1); |
| 108 | 110 | expect(listRuns(app.db, repo.id, { kind: "agent" }).every((r) => r.state === "ok")).toBe(true); |
| 109 | 111 | const pipes = readPipes(app.db, admin.id); |
| 110 | 112 | expect(pipes.usd).toBeGreaterThan(0); |
| 111 | 113 | expect(pipes.valves.find((v) => v.slug === `${PREVIEW_ADMIN}/${PREVIEW_REPO}`)?.level).toBe(60); |
| 112 | 114 | expect(listPins(app.db, admin.id).length).toBe(2); |
| 115 | const bets = listBets(app.db, repo.id).map((b) => [b.name, b.state, b.appetite]); | |
| 116 | expect(bets).toEqual([ | |
| 117 | ["Show the week", "backed", 20], | |
| 118 | ["Price the list", "proposed", 20], | |
| 119 | ]); | |
| 120 | ||
| 121 | // The sample account that prices the week has no grant, so no run or | |
| 122 | // conversation in a preview can be handed its inert token: the admin | |
| 123 | // falls through to the platform's own credential. | |
| 124 | expect(app.db.query("SELECT COUNT(*) AS n FROM subscription WHERE name = 'preview sample'").get()).toEqual({ n: 1 }); | |
| 125 | const picked = pickSubscription(app.db, app.config.dataDir, { repoId: repo.id, ownerId: admin.id, agentId: "" }, new Date()); | |
| 126 | expect(picked.kind).toBe("env"); | |
| 113 | 127 | |
| 114 | 128 | expect(await seedPreview(app, { FORGE_APP_ENV: "preview" })).toEqual(ok({ seeded: false })); |
| 115 | 129 | expect(listPins(app.db, admin.id).length).toBe(2); |
| @@ -167,6 +181,19 @@ describe("a preview hears its parent's admin as the preview admin", () => { | ||
| 167 | 181 | expect((await flow(preview, headers)).status).not.toBe(200); |
outdated
Assert the bet states from the db (one backed at 20, one proposed). Right now "20%" could come from any text on the page. |
||
| 168 | 182 | }, 30_000); |
| 169 | 183 | |
| 184 | test("Next up has a cut line inside the list and /bets a backed bet and a proposed one", async () => { | |
| 185 | const preview = servers[0]!; | |
| 186 | const base = `${preview.url}/${PREVIEW_ADMIN}/${PREVIEW_REPO}`; | |
| 187 | const next = await (await fetch(`${base}/next`, { headers: identity("admin") })).text(); | |
| 188 | expect(next).toContain('role="separator"'); | |
| 189 | expect(next).toContain("below this line reads as next week"); | |
| 190 | expect(next).not.toContain("too little history"); | |
| 191 | const bets = await (await fetch(`${base}/bets`, { headers: identity("admin") })).text(); | |
| 192 | expect(bets).toContain("Show the week"); | |
| 193 | expect(bets).toContain("Price the list"); | |
| 194 | expect(bets).toContain("20%"); | |
| 195 | }); | |
| 196 | ||
| 170 | 197 | test("a mutation it rides is held to the same-origin rule", async () => { |
| 171 | 198 | const preview = servers[0]!; |
| 172 | 199 | const turned = await fetch(`${preview.url}/flow/valve`, { |
new above
offline
A fake subscription lives in the shared table. Show that the account picker and the crew never choose it in a preview, with a test, or say how it is kept out.