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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/8866-the-preview-seed-holds-a-backed-bet-a-pr → main 6 commits fork 2 threads · 2 unresolved

  • Merged. scout/8866-the-preview-seed-holds-a-backed-bet-a-pr was merged into main as 3f6d97a.

Review

Sign in to review this change.
4 files changed +143 −4
forge.json +2 −2
@@ -3,8 +3,8 @@
33 "api.anthropic.com"
44 ],
55 "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"],
88 "run": "bun run src/main.ts",
99 "install": "",
1010 "health": "/healthz",
src/spawn/preview-seed.ts +111 −1
@@ -15,13 +15,20 @@
1515 */
1616
1717 import type { App } from "../app.ts";
18 import type { Issue } from "../core/issues.ts";
19 import type { Repo } from "../core/repos.ts";
1820 import { pinPanel } from "../actions/board-pins.ts";
1921 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";
2124 import { openPull } from "../actions/pulls.ts";
2225 import { commitToBranch, createRepo } from "../actions/repos.ts";
2326 import { turnValveFor } from "../actions/valves.ts";
27 import { insertBet } from "../core/bets.ts";
2428 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";
2532 import { getUserSetting, setUserSetting } from "../core/settings.ts";
2633 import { createUser, getUserByName, type User } from "../core/users.ts";
2734 import { err, ok, type Result } from "../kernel/result.ts";
@@ -130,6 +137,21 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> {
130137 if (!pull.ok) return err(`the pull: ${String(pull.error)}`);
131138 if (pull.value.number !== PREVIEW_PULL) return err(`the pull is #${String(pull.value.number)}, not #${String(PREVIEW_PULL)}`);
132139
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
133155 // A flow week: agent runs that started and spent this week, and a valve
134156 // turned part way, so /flow has a pipe to draw and one to turn.
135157 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>> {
142164 claimRun(app.db, run.id);
143165 finishRun(app.db, run.id, { state: "ok", summary: "A seeded run", costUsd: usd });
144166 }
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;
145171 const valve = turnValveFor(app, admin, `${PREVIEW_ADMIN}/${PREVIEW_REPO}`, 60);
146172 if (!valve.ok) return err(`the valve: ${valve.error}`);
147173
@@ -158,3 +184,87 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> {
158184 }
159185 return ok();
160186 }
outdated

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.

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> = {
5454 "/events": "a live event stream that never ends",
5555 "/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",
5656 "/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)",
5759 "/migrate": "an import form, nothing to picture until a job runs; the look slot went to /repos",
5860 };
5961
test/looks-owner.test.ts +28 −1
@@ -21,6 +21,8 @@ import { readGrant } from "../src/apps/proxy.ts";
2121 import { AppsRuntime } from "../src/apps/runtime.ts";
2222 import { createApp, type App } from "../src/app.ts";
2323 import { listPins } from "../src/boards/pins.ts";
24 import { listBets } from "../src/core/bets.ts";
25 import { pickSubscription } from "../src/core/subscriptions.ts";
2426 import { looksForEnv } from "../src/core/looks.ts";
2527 import { listPulls } from "../src/core/pulls.ts";
2628 import { getRepo } from "../src/core/repos.ts";
@@ -103,13 +105,25 @@ describe("the preview seed", () => {
103105 const issues = app.db
104106 .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue'")
105107 .get(repo.id);
106 expect(issues?.n).toBe(2);
108 expect(issues?.n).toBe(11);
107109 expect(listPulls(app.db, repo.id).length).toBe(1);
108110 expect(listRuns(app.db, repo.id, { kind: "agent" }).every((r) => r.state === "ok")).toBe(true);
109111 const pipes = readPipes(app.db, admin.id);
110112 expect(pipes.usd).toBeGreaterThan(0);
111113 expect(pipes.valves.find((v) => v.slug === `${PREVIEW_ADMIN}/${PREVIEW_REPO}`)?.level).toBe(60);
112114 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");
113127
114128 expect(await seedPreview(app, { FORGE_APP_ENV: "preview" })).toEqual(ok({ seeded: false }));
115129 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", () => {
167181 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.

168182 }, 30_000);
169183
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
170197 test("a mutation it rides is held to the same-origin rule", async () => {
171198 const preview = servers[0]!;
172199 const turned = await fetch(`${preview.url}/flow/valve`, {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs