Public: anyone can read this; the crew can change it.
Flaky: test/subscriptions.test.ts #2126
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2113-flaky-test-subscriptions-test-ts → main 1 commit fork
2 files
changed +57
−37
scripts/gate.ts −5
| @@ -98,11 +98,6 @@ export const EXCLUDED: readonly Exclusion[] = [ | ||
| 98 | 98 | { file: "test/ssh.test.ts", reason: "not yet admitted", issue: 261 }, |
| 99 | 99 | { file: "test/standards.test.ts", reason: "not yet admitted", issue: 261 }, |
| 100 | 100 | { file: "test/studio-phone.test.ts", reason: "not yet admitted", issue: 261 }, |
| 101 | { | |
| 102 | file: "test/subscriptions.test.ts", | |
| 103 | reason: "not yet admitted", | |
| 104 | issue: 261, | |
| 105 | }, | |
| 106 | 101 | { |
| 107 | 102 | file: "test/tui-code.test.ts", |
| 108 | 103 | reason: |
test/subscriptions.test.ts +57 −32
| @@ -1,5 +1,5 @@ | ||
| 1 | import { getHeadroom } from "../src/core/standing.ts"; | |
| 2 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 1 | import { getHeadroom, setHeadroom } from "../src/core/standing.ts"; | |
| 2 | import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test"; | |
| 3 | 3 | import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs"; |
| 4 | 4 | import { mkdtemp, rm } from "node:fs/promises"; |
| 5 | 5 | import { tmpdir } from "node:os"; |
| @@ -67,11 +67,11 @@ describe("the pool", () => { | ||
| 67 | 67 | let dataDir: string; |
| 68 | 68 | let app: App; |
| 69 | 69 | |
| 70 | beforeAll(async () => { | |
| 70 | beforeEach(async () => { | |
| 71 | 71 | dataDir = await mkdtemp(join(tmpdir(), "forge-subs-")); |
| 72 | 72 | app = createApp({ dataDir, port: 0 }); |
| 73 | 73 | }); |
| 74 | afterAll(async () => { | |
| 74 | afterEach(async () => { | |
| 75 | 75 | app.close(); |
| 76 | 76 | await rm(dataDir, { recursive: true, force: true }); |
| 77 | 77 | }); |
| @@ -147,9 +147,12 @@ describe("the pool", () => { | ||
| 147 | 147 | test("picks the nearest grant whole, an agent's before everyone's, and rotates", () => { |
| 148 | 148 | const work = addSubscription(app.db, dataDir, { name: "work", token: KEY }); |
| 149 | 149 | if (!work.ok) throw new Error(work.error); |
| 150 | const personal = listSubscriptions(app.db).find( | |
| 151 | (s) => s.name === "Personal", | |
| 152 | )!; | |
| 150 | const added = addSubscription(app.db, dataDir, { | |
| 151 | name: "Personal", | |
| 152 | token: OAT, | |
| 153 | }); | |
| 154 | if (!added.ok) throw new Error(added.error); | |
| 155 | const personal = added.value; | |
| 153 | 156 | const scope = { repoId: "repo-1", ownerId: "org-1", agentId: "bot-1" }; |
| 154 | 157 | const now = new Date("2026-09-28T12:00:00Z"); |
| 155 | 158 | |
| @@ -359,14 +362,26 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"Added CREW | ||
| 359 | 362 | throw new Error("did not happen in time"); |
| 360 | 363 | } |
| 361 | 364 | |
| 365 | // Both accounts on the repository's grant, awake, whatever ran before. | |
| 366 | function pool() { | |
| 367 | const named = (name: string, token: string) => { | |
| 368 | const found = listSubscriptions(app.db).find((s) => s.name === name); | |
| 369 | if (found) return found; | |
| 370 | const added = addSubscription(app.db, dataDir, { name, token }); | |
| 371 | if (!added.ok) throw new Error(added.error); | |
| 372 | return added.value; | |
| 373 | }; | |
| 374 | const personal = named("personal", OAT); | |
| 375 | const work = named("work", KEY); | |
| 376 | wakeSubscription(app.db, personal.id); | |
| 377 | wakeSubscription(app.db, work.id); | |
| 378 | setGrants(app.db, repo.id, EVERYONE, [personal.id, work.id]); | |
| 379 | return { personal, work }; | |
| 380 | } | |
| 381 | ||
| 362 | 382 | test("rests the account the provider refused and hands the run to the next", async () => { |
| 363 | const personal = addSubscription(app.db, dataDir, { | |
| 364 | name: "personal", | |
| 365 | token: OAT, | |
| 366 | }); | |
| 367 | const work = addSubscription(app.db, dataDir, { name: "work", token: KEY }); | |
| 368 | if (!personal.ok || !work.ok) throw new Error("pool"); | |
| 369 | setGrants(app.db, repo.id, EVERYONE, [personal.value.id, work.value.id]); | |
| 383 | const { personal, work } = pool(); | |
| 384 | const used = invocations(); | |
| 370 | 385 | |
| 371 | 386 | const issue = openIssue(app, alice, repo, { |
| 372 | 387 | title: "Plant tulips", |
| @@ -379,18 +394,18 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"Added CREW | ||
| 379 | 394 | (r) => r.issueId === issue.value.id && r.state === "ok", |
| 380 | 395 | ), |
| 381 | 396 | ); |
| 382 | expect(invocations()).toBe(2); | |
| 397 | expect(invocations()).toBe(used + 2); | |
| 383 | 398 | // Neither used before: the first by name went first, and was refused. |
| 384 | expect(envOf(1)).toBe(`OAUTH=${OAT}\nKEY=\n`); | |
| 385 | expect(envOf(2)).toBe(`OAUTH=\nKEY=${KEY}\n`); | |
| 399 | expect(envOf(used + 1)).toBe(`OAUTH=${OAT}\nKEY=\n`); | |
| 400 | expect(envOf(used + 2)).toBe(`OAUTH=\nKEY=${KEY}\n`); | |
| 386 | 401 | |
| 387 | const rested = getSubscription(app.db, personal.value.id)!; | |
| 402 | const rested = getSubscription(app.db, personal.id)!; | |
| 388 | 403 | expect(rested.restingUntil).not.toBeNull(); |
| 389 | 404 | expect(new Date(rested.restingUntil!).getTime()).toBeGreaterThan( |
| 390 | 405 | Date.now(), |
| 391 | 406 | ); |
| 392 | 407 | expect(rested.restingSaid).toContain("weekly limit"); |
| 393 | expect(getSubscription(app.db, work.value.id)!.restingUntil).toBeNull(); | |
| 408 | expect(getSubscription(app.db, work.id)!.restingUntil).toBeNull(); | |
| 394 | 409 | |
| 395 | 410 | const runs = listRuns(app.db, repo.id, { kind: "agent" }).filter( |
| 396 | 411 | (r) => r.issueId === issue.value.id, |
| @@ -409,7 +424,7 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"Added CREW | ||
| 409 | 424 | "SELECT subscription_id FROM run WHERE id = ?1", |
| 410 | 425 | ) |
| 411 | 426 | .get(refused.id)!; |
| 412 | expect(first.subscription_id).toBe(personal.value.id); | |
| 427 | expect(first.subscription_id).toBe(personal.id); | |
| 413 | 428 | expect(done.id).not.toBe(refused.id); |
| 414 | 429 | const spent = app.db |
| 415 | 430 | .query< |
| @@ -417,17 +432,14 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"Added CREW | ||
| 417 | 432 | [string] |
| 418 | 433 | >("SELECT subscription_id FROM run WHERE id = ?1") |
| 419 | 434 | .get(done.id)!; |
| 420 | expect(spent.subscription_id).toBe(work.value.id); | |
| 435 | expect(spent.subscription_id).toBe(work.id); | |
| 421 | 436 | }, 60_000); |
| 422 | 437 | |
| 423 | 438 | test("holds a run without a call while every account on the grant rests", async () => { |
| 424 | const work = listSubscriptions(app.db).find((s) => s.name === "work")!; | |
| 425 | restSubscription( | |
| 426 | app.db, | |
| 427 | work.id, | |
| 428 | new Date(Date.now() + 3_600_000).toISOString(), | |
| 429 | "session limit", | |
| 430 | ); | |
| 439 | const { personal, work } = pool(); | |
| 440 | const later = new Date(Date.now() + 3_600_000).toISOString(); | |
| 441 | restSubscription(app.db, personal.id, later, "weekly limit"); | |
| 442 | restSubscription(app.db, work.id, later, "session limit"); | |
| 431 | 443 | const before = invocations(); |
| 432 | 444 | const issue = openIssue(app, alice, repo, { |
| 433 | 445 | title: "Plant roses", |
| @@ -506,7 +518,18 @@ describe("the pages", () => { | ||
| 506 | 518 | const get = (path: string) => |
| 507 | 519 | fetch(`${base}${path}`, { headers: { cookie } }).then((r) => r.text()); |
| 508 | 520 | |
| 521 | // The one account the pages work on, added once whatever ran before. | |
| 522 | const account = () => { | |
| 523 | const found = listSubscriptions(app.db).find((s) => s.name === "work"); | |
| 524 | if (found) return found; | |
| 525 | const added = addSubscription(app.db, dataDir, { name: "work", token: KEY }); | |
| 526 | if (!added.ok) throw new Error(added.error); | |
| 527 | return added.value; | |
| 528 | }; | |
| 529 | ||
| 509 | 530 | test("an admin adds an account and sees its name, never its token", async () => { |
| 531 | // The empty pool is what this test starts from, whatever ran before. | |
| 532 | for (const s of listSubscriptions(app.db)) deleteSubscription(app.db, s.id); | |
| 510 | 533 | let html = await get("/settings/subscriptions"); |
| 511 | 534 | expect(html).toContain("Add one"); |
| 512 | 535 | expect(html).toContain("The crew works on the forge"); |
| @@ -528,7 +551,7 @@ describe("the pages", () => { | ||
| 528 | 551 | }); |
| 529 | 552 | |
| 530 | 553 | test("the crew page grants it to the repository, and a run there picks it", async () => { |
| 531 | const work = listSubscriptions(app.db)[0]!; | |
| 554 | const work = account(); | |
| 532 | 555 | let html = await get(`/ada/hello/settings/crew`); |
| 533 | 556 | expect(html).toContain('class="table grants"'); |
| 534 | 557 | expect(html).toContain(`value="*:${work.id}"`); |
| @@ -563,7 +586,7 @@ describe("the pages", () => { | ||
| 563 | 586 | test("an organization's page carries the table for every repository under it", async () => { |
| 564 | 587 | const org = createOrg(app, ada, "acme"); |
| 565 | 588 | if (!org.ok) throw new Error(String(org.error)); |
| 566 | const work = listSubscriptions(app.db)[0]!; | |
| 589 | const work = account(); | |
| 567 | 590 | let html = await get("/acme"); |
| 568 | 591 | expect(html).toContain("Subscriptions for every repository here"); |
| 569 | 592 | const saved = await post( |
| @@ -579,7 +602,8 @@ describe("the pages", () => { | ||
| 579 | 602 | }); |
| 580 | 603 | |
| 581 | 604 | test("an admin keeps a share of each window for people, and sees each window", async () => { |
| 582 | const work = listSubscriptions(app.db)[0]!; | |
| 605 | const work = account(); | |
| 606 | setHeadroom(app.db, work.id, 0); | |
| 583 | 607 | let html = await get("/settings/subscriptions"); |
| 584 | 608 | expect(html).toContain(`action="/settings/subscriptions/${work.id}/headroom"`); |
| 585 | 609 | expect(html).toContain("5-hour: ~$0.00 used · opens on next use"); |
| @@ -630,8 +654,9 @@ describe("the pages", () => { | ||
| 630 | 654 | redirect: "manual", |
| 631 | 655 | }); |
| 632 | 656 | expect(posted.status).toBe(404); |
| 657 | const work = account(); | |
| 633 | 658 | expect(listSubscriptions(app.db)).toHaveLength(1); |
| 634 | const work = listSubscriptions(app.db)[0]!; | |
| 659 | setHeadroom(app.db, work.id, 25); | |
| 635 | 660 | const headroom = await fetch(`${base}/settings/subscriptions/${work.id}/headroom`, { |
| 636 | 661 | method: "POST", |
| 637 | 662 | headers: { cookie: guest, origin: base }, |
new above
offline