Public: anyone can read this; the crew can change it.
The studio suites depend on test order: 7-12 tests fail under every --randomize seed #2069
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/2063-the-studio-suites-depend-on-test-order-7 → main 9 commits fork
4 files
changed +152
−93
test/studio-door.test.ts +17 −19
| @@ -5,7 +5,15 @@ | ||
| 5 | 5 | * (#2038); the rest are test/studio*.test.ts. |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 8 | import { | |
| 9 | afterAll, | |
| 10 | afterEach, | |
| 11 | beforeAll, | |
| 12 | beforeEach, | |
| 13 | describe, | |
| 14 | expect, | |
| 15 | test, | |
| 16 | } from "bun:test"; | |
| 9 | 17 | import { mkdtemp, rm } from "node:fs/promises"; |
| 10 | 18 | import { tmpdir } from "node:os"; |
| 11 | 19 | import { join } from "node:path"; |
| @@ -327,17 +335,6 @@ describe("every open pull's preview on the studio page", () => { | ||
| 327 | 335 | .filter((r) => !isTerminal(r.state)) |
| 328 | 336 | .map((r) => `${r.env}:${r.state}`); |
| 329 | 337 | |
| 330 | /** Wait until the runtime has no deploy open or queued, so the next case | |
| 331 | * does not queue behind this one's (concurrency is 1). */ | |
| 332 | async function deploysIdle(): Promise<void> { | |
| 333 | const deadline = Date.now() + WAIT_MS; | |
| 334 | while (unfinished().length > 0) { | |
| 335 | if (Date.now() > deadline) | |
| 336 | throw new Error(`deploys still open: ${unfinished().join(", ")}`); | |
| 337 | await Bun.sleep(50); | |
| 338 | } | |
| 339 | } | |
| 340 | ||
| 341 | 338 | /** Wait for `env` to serve; fail naming the state it reached. */ |
| 342 | 339 | async function previewServes(record: AppRecord, env: EnvName): Promise<void> { |
| 343 | 340 | const deadline = Date.now() + WAIT_MS; |
| @@ -354,7 +351,9 @@ describe("every open pull's preview on the studio page", () => { | ||
| 354 | 351 | } |
| 355 | 352 | } |
| 356 | 353 | |
| 357 | beforeAll(async () => { | |
| 354 | // A fresh app for each test: one test's open pull would otherwise be the | |
| 355 | // next's preview, and its deploys queue ahead of the next's (concurrency 1). | |
| 356 | beforeEach(async () => { | |
| 358 | 357 | dataDir = await mkdtemp(join(tmpdir(), "forge-studio-previews-")); |
| 359 | 358 | app = createApp({ dataDir, port: 0 }); |
| 360 | 359 | servers = await serve(app); |
| @@ -413,7 +412,7 @@ describe("every open pull's preview on the studio page", () => { | ||
| 413 | 412 | if (!enabled.ok) throw new Error(String(enabled.error)); |
| 414 | 413 | }, 30_000); |
| 415 | 414 | |
| 416 | afterAll(async () => { | |
| 415 | afterEach(async () => { | |
| 417 | 416 | await runtime.stop(); |
| 418 | 417 | servers.stop(); |
| 419 | 418 | app.close(); |
| @@ -451,9 +450,6 @@ describe("every open pull's preview on the studio page", () => { | ||
| 451 | 450 | const after = await (await get("/alice/widget")).text(); |
| 452 | 451 | expect(after).not.toContain("<h2>Previews</h2>"); |
| 453 | 452 | expect(after).not.toContain(`pull-${pull.number}/open`); |
| 454 | // The merge queues production's deploy and the pull's was cut short; | |
| 455 | // leave the single deploy slot free for the next case. | |
| 456 | await deploysIdle(); | |
| 457 | 453 | }, 120_000); |
| 458 | 454 | |
| 459 | 455 | test("a change's page names its preview's data as its own copy and gives the moment it was taken; production's card does not", async () => { |
| @@ -1181,7 +1177,9 @@ describe("the app's live fragment: /:owner/:name/app/state", () => { | ||
| 1181 | 1177 | redirect: "manual", |
| 1182 | 1178 | }); |
| 1183 | 1179 | |
| 1184 | beforeAll(async () => { | |
| 1180 | // A fresh app for each test: one test's pull or preview slot would | |
| 1181 | // otherwise show up in the next's head card and previews. | |
| 1182 | beforeEach(async () => { | |
| 1185 | 1183 | dataDir = await mkdtemp(join(tmpdir(), "forge-studio-state-")); |
| 1186 | 1184 | app = createApp({ dataDir, port: 0 }); |
| 1187 | 1185 | servers = await serve(app); |
| @@ -1225,7 +1223,7 @@ describe("the app's live fragment: /:owner/:name/app/state", () => { | ||
| 1225 | 1223 | record = getAppByRepo(app.db, repo.id)!; |
| 1226 | 1224 | }); |
| 1227 | 1225 | |
| 1228 | afterAll(async () => { | |
| 1226 | afterEach(async () => { | |
| 1229 | 1227 | servers.stop(); |
| 1230 | 1228 | app.close(); |
| 1231 | 1229 | await rm(dataDir, { recursive: true, force: true }); |
test/studio-runs.test.ts +37 −21
| @@ -5,7 +5,16 @@ | ||
| 5 | 5 | * test/studio*.test.ts. |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test"; | |
| 8 | import { | |
| 9 | afterAll, | |
| 10 | afterEach, | |
| 11 | beforeAll, | |
| 12 | beforeEach, | |
| 13 | describe, | |
| 14 | expect, | |
| 15 | spyOn, | |
| 16 | test, | |
| 17 | } from "bun:test"; | |
| 9 | 18 | import { mkdtemp, rm } from "node:fs/promises"; |
| 10 | 19 | import { tmpdir } from "node:os"; |
| 11 | 20 | import { join } from "node:path"; |
| @@ -877,7 +886,9 @@ describe("the app's page tails the deploy that is bringing it alive", () => { | ||
| 877 | 886 | redirect: "manual", |
| 878 | 887 | }); |
| 879 | 888 | |
| 880 | beforeAll(async () => { | |
| 889 | // A fresh app for each test: one test's tell or run would otherwise fill | |
| 890 | // "Now" for the next, and its production slot stand in for the next's. | |
| 891 | beforeEach(async () => { | |
| 881 | 892 | dataDir = await mkdtemp(join(tmpdir(), "forge-studio-deploy-tail-")); |
| 882 | 893 | app = createApp({ dataDir, port: 0 }); |
| 883 | 894 | servers = await serve(app); |
| @@ -918,7 +929,7 @@ describe("the app's page tails the deploy that is bringing it alive", () => { | ||
| 918 | 929 | expect(addToCrew(app, a.value, repo, "scout").ok).toBe(true); |
| 919 | 930 | }); |
| 920 | 931 | |
| 921 | afterAll(async () => { | |
| 932 | afterEach(async () => { | |
| 922 | 933 | servers.stop(); |
| 923 | 934 | app.close(); |
| 924 | 935 | await rm(dataDir, { recursive: true, force: true }); |
| @@ -1425,7 +1436,12 @@ describe('a tell nobody on the crew can take is not "quiet"', () => { | ||
| 1425 | 1436 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); |
| 1426 | 1437 | const enabled = await enableApp(app, alice, repo, { slug: "pad" }); |
| 1427 | 1438 | if (!enabled.ok) throw new Error(String(enabled.error)); |
| 1428 | // No crew at all — the exact state `tell` cannot route out of. | |
| 1439 | // No crew at all — the exact state `tell` cannot route out of. Every | |
| 1440 | // test below reads the one tell told into it here. | |
| 1441 | const told = await post("/alice/pad/tell", { | |
| 1442 | text: "Add a dark mode toggle.", | |
| 1443 | }); | |
| 1444 | if (told.status !== 303) throw new Error(`tell: ${told.status}`); | |
| 1429 | 1445 | }); |
| 1430 | 1446 | |
| 1431 | 1447 | afterAll(async () => { |
| @@ -1435,11 +1451,6 @@ describe('a tell nobody on the crew can take is not "quiet"', () => { | ||
| 1435 | 1451 | }); |
| 1436 | 1452 | |
| 1437 | 1453 | test('a crewless tell renders a "Now" row, not the empty state, and links to where a crew member is added', async () => { |
| 1438 | const told = await post("/alice/pad/tell", { | |
| 1439 | text: "Add a dark mode toggle.", | |
| 1440 | }); | |
| 1441 | expect(told.status).toBe(303); | |
| 1442 | ||
| 1443 | 1454 | const page = await (await get("/alice/pad")).text(); |
| 1444 | 1455 | expect(page).not.toContain("Nothing yet. Tell the crew something."); |
| 1445 | 1456 | expect(page).toContain("Add a dark mode toggle."); |
| @@ -1852,18 +1863,6 @@ describe("a deploy that just landed is news on the front door", () => { | ||
| 1852 | 1863 | deployRun = |
| 1853 | 1864 | finishRun(app.db, deployRun.id, { state: "ok", summary: "deploy" }) ?? |
| 1854 | 1865 | deployRun; |
| 1855 | const servingAt = new Date().toISOString(); | |
| 1856 | putEnv(record, "production", { | |
| 1857 | live: { | |
| 1858 | commitOid: "6c99218", | |
| 1859 | handle: { port: 4321 }, | |
| 1860 | runId: deployRun.id, | |
| 1861 | since: servingAt, | |
| 1862 | servingAt, | |
| 1863 | createdAt: servingAt, | |
| 1864 | manifest: { tty: null }, | |
| 1865 | }, | |
| 1866 | }); | |
| 1867 | 1866 | |
| 1868 | 1867 | // A second app, otherwise identical, that never shipped — the control |
| 1869 | 1868 | // that must keep reading quiet. |
| @@ -1885,6 +1884,23 @@ describe("a deploy that just landed is news on the front door", () => { | ||
| 1885 | 1884 | quietRepo = madeQuiet.value; |
| 1886 | 1885 | }); |
| 1887 | 1886 | |
| 1887 | // Each test starts from the merge's deploy serving just now; a test that | |
| 1888 | // wants another production slot puts its own. | |
| 1889 | beforeEach(() => { | |
| 1890 | const servingAt = new Date().toISOString(); | |
| 1891 | putEnv(record, "production", { | |
| 1892 | live: { | |
| 1893 | commitOid: "6c99218", | |
| 1894 | handle: { port: 4321 }, | |
| 1895 | runId: deployRun.id, | |
| 1896 | since: servingAt, | |
| 1897 | servingAt, | |
| 1898 | createdAt: servingAt, | |
| 1899 | manifest: { tty: null }, | |
| 1900 | }, | |
| 1901 | }); | |
| 1902 | }); | |
| 1903 | ||
| 1888 | 1904 | afterAll(async () => { |
| 1889 | 1905 | servers.stop(); |
| 1890 | 1906 | app.close(); |
test/studio-shipping.test.ts +55 −23
| @@ -5,7 +5,14 @@ | ||
| 5 | 5 | * test/studio*.test.ts. |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 8 | import { | |
| 9 | afterAll, | |
| 10 | beforeAll, | |
| 11 | beforeEach, | |
| 12 | describe, | |
| 13 | expect, | |
| 14 | test, | |
| 15 | } from "bun:test"; | |
| 9 | 16 | import { mkdtemp, rm } from "node:fs/promises"; |
| 10 | 17 | import { tmpdir } from "node:os"; |
| 11 | 18 | import { join } from "node:path"; |
| @@ -1179,6 +1186,12 @@ describe("previewLatency: the median seconds from push to serving", () => { | ||
| 1179 | 1186 | await rm(dataDir, { recursive: true, force: true }); |
| 1180 | 1187 | }); |
| 1181 | 1188 | |
| 1189 | // Each test reads only the deploys it seeds: the window has no upper | |
| 1190 | // bound, so another test's deploys would otherwise land in it. | |
| 1191 | beforeEach(() => { | |
| 1192 | app.db.query("DELETE FROM run WHERE repo_id = ?1").run(repo.id); | |
| 1193 | }); | |
| 1194 | ||
| 1182 | 1195 | /** A deploy run stamped with a known push instant and, unless null, known started/serving instants. */ |
| 1183 | 1196 | const seedDeploy = ( |
| 1184 | 1197 | createdAt: string, |
| @@ -1252,7 +1265,7 @@ describe("previewLatency: the median seconds from push to serving", () => { | ||
| 1252 | 1265 | }); |
| 1253 | 1266 | |
| 1254 | 1267 | test("the split between waiting to be picked up and coming up, seeded beside a total", () => { |
| 1255 | const now = Date.now() + 10_000_000; // clear of the previous test's window | |
| 1268 | const now = Date.now(); | |
| 1256 | 1269 | const since = new Date(now - 60_000).toISOString(); |
| 1257 | 1270 | |
| 1258 | 1271 | // Three deploys with known queue and boot segments: (queue, boot) in seconds. |
| @@ -1293,7 +1306,7 @@ describe("previewLatency: the median seconds from push to serving", () => { | ||
| 1293 | 1306 | }); |
| 1294 | 1307 | |
| 1295 | 1308 | test("a deploy that finished without ever serving is counted apart, never in the median; one still running is counted as neither", () => { |
| 1296 | const now = Date.now() + 20_000_000; // clear of the earlier tests' windows | |
| 1309 | const now = Date.now(); | |
| 1297 | 1310 | const since = new Date(now - 60_000).toISOString(); |
| 1298 | 1311 | |
| 1299 | 1312 | // Two served deploys, 4s and 6s. |
| @@ -1351,7 +1364,7 @@ describe("previewLatency: the median seconds from push to serving", () => { | ||
| 1351 | 1364 | }); |
| 1352 | 1365 | |
| 1353 | 1366 | test("a week with only never-served deploys still reports the failed count, with no median to give", () => { |
| 1354 | const now = Date.now() + 30_000_000; | |
| 1367 | const now = Date.now(); | |
| 1355 | 1368 | const since = new Date(now - 60_000).toISOString(); |
| 1356 | 1369 | |
| 1357 | 1370 | const failed = createRun(app.db, { repoId: repo.id, kind: "deploy" }); |
| @@ -1369,7 +1382,7 @@ describe("previewLatency: the median seconds from push to serving", () => { | ||
| 1369 | 1382 | }); |
| 1370 | 1383 | |
| 1371 | 1384 | test("a deploy the forge's own restart cancelled is never counted as failed, nor in the total it is 'of' (#255, #274)", () => { |
| 1372 | const now = Date.now() + 40_000_000; | |
| 1385 | const now = Date.now(); | |
| 1373 | 1386 | const since = new Date(now - 60_000).toISOString(); |
| 1374 | 1387 | |
| 1375 | 1388 | // A real failure: counted. |
| @@ -1482,12 +1495,40 @@ describe("the studio's own words for how fast a change reaches its preview", () | ||
| 1482 | 1495 | await rm(dataDir, { recursive: true, force: true }); |
| 1483 | 1496 | }); |
| 1484 | 1497 | |
| 1498 | // Each test reads only the deploys it seeds; the agent run from | |
| 1499 | // beforeAll stays, so the page always has history behind it. | |
| 1500 | beforeEach(() => { | |
| 1501 | app.db | |
| 1502 | .query("DELETE FROM run WHERE repo_id = ?1 AND kind = 'deploy'") | |
| 1503 | .run(repo.id); | |
| 1504 | }); | |
| 1505 | ||
| 1485 | 1506 | const get = (path: string) => |
| 1486 | 1507 | fetch(`${base}${path}`, { |
| 1487 | 1508 | headers: { cookie: `${ownerCookie}; forge_mode=studio` }, |
| 1488 | 1509 | redirect: "manual", |
| 1489 | 1510 | }); |
| 1490 | 1511 | |
| 1512 | /** A deploy run stamped with a known push instant and serving instant, and unless null a known started instant. */ | |
| 1513 | const seedDeploy = ( | |
| 1514 | createdAt: string, | |
| 1515 | startedAt: string | null, | |
| 1516 | servingAt: string, | |
| 1517 | ): void => { | |
| 1518 | const deploy = createRun(app.db, { repoId: repo.id, kind: "deploy" }); | |
| 1519 | app.db | |
| 1520 | .query("UPDATE run SET created_at = ?2 WHERE id = ?1") | |
| 1521 | .run(deploy.id, createdAt); | |
| 1522 | if (startedAt !== null) { | |
| 1523 | app.db | |
| 1524 | .query("UPDATE run SET started_at = ?2 WHERE id = ?1") | |
| 1525 | .run(deploy.id, startedAt); | |
| 1526 | } | |
| 1527 | app.db | |
| 1528 | .query("UPDATE run SET serving_at = ?2 WHERE id = ?1") | |
| 1529 | .run(deploy.id, servingAt); | |
| 1530 | }; | |
| 1531 | ||
| 1491 | 1532 | test("fewer than three served deploys this week says so plainly rather than a median of one", async () => { |
| 1492 | 1533 | const before = await (await get("/alice/trickle")).text(); |
| 1493 | 1534 | expect(before).toContain("not enough deploys this week to say"); |
| @@ -1506,24 +1547,6 @@ describe("the studio's own words for how fast a change reaches its preview", () | ||
| 1506 | 1547 | }); |
| 1507 | 1548 | |
| 1508 | 1549 | test("the split reads as the total's explanation, and says when it is drawn from fewer deploys", async () => { |
| 1509 | const seedDeploy = ( | |
| 1510 | createdAt: string, | |
| 1511 | startedAt: string | null, | |
| 1512 | servingAt: string, | |
| 1513 | ): void => { | |
| 1514 | const deploy = createRun(app.db, { repoId: repo.id, kind: "deploy" }); | |
| 1515 | app.db | |
| 1516 | .query("UPDATE run SET created_at = ?2 WHERE id = ?1") | |
| 1517 | .run(deploy.id, createdAt); | |
| 1518 | if (startedAt !== null) { | |
| 1519 | app.db | |
| 1520 | .query("UPDATE run SET started_at = ?2 WHERE id = ?1") | |
| 1521 | .run(deploy.id, startedAt); | |
| 1522 | } | |
| 1523 | app.db | |
| 1524 | .query("UPDATE run SET serving_at = ?2 WHERE id = ?1") | |
| 1525 | .run(deploy.id, servingAt); | |
| 1526 | }; | |
| 1527 | 1550 | // Three deploys with a known queue and boot each. |
| 1528 | 1551 | const now = Date.now(); |
| 1529 | 1552 | const segments: readonly (readonly [number, number])[] = [ |
| @@ -1561,6 +1584,15 @@ describe("the studio's own words for how fast a change reaches its preview", () | ||
| 1561 | 1584 | }); |
| 1562 | 1585 | |
| 1563 | 1586 | test("a deploy that finished without ever serving is counted and worded, and links to the runs list", async () => { |
| 1587 | // Four served deploys first, so the failed one is the fifth. | |
| 1588 | const now = Date.now(); | |
| 1589 | for (let i = 0; i < 4; i++) { | |
| 1590 | seedDeploy( | |
| 1591 | new Date(now + i * 1_000).toISOString(), | |
| 1592 | null, | |
| 1593 | new Date(now + i * 1_000 + 5_000).toISOString(), | |
| 1594 | ); | |
| 1595 | } | |
| 1564 | 1596 | const before = await (await get("/alice/trickle")).text(); |
| 1565 | 1597 | |
| 1566 | 1598 | const failed = createRun(app.db, { |
test/studio.test.ts +43 −30
| @@ -62,6 +62,35 @@ describe("the studio", () => { | ||
| 62 | 62 | redirect: "manual", |
| 63 | 63 | }); |
| 64 | 64 | |
| 65 | /** An app of alice's with scout on its crew. */ | |
| 66 | const makeApp = async (name: string, description = ""): Promise<Repo> => { | |
| 67 | const made = await createRepo(app, alice, { | |
| 68 | name, | |
| 69 | visibility: "private", | |
| 70 | description, | |
| 71 | }); | |
| 72 | if (!made.ok) throw new Error(String(made.error)); | |
| 73 | const seeded = await commitToBranch(app, alice, made.value, { | |
| 74 | message: "Start", | |
| 75 | files: [ | |
| 76 | { | |
| 77 | path: "package.json", | |
| 78 | content: `{"name":"${name}","scripts":{"start":"bun run server.ts"}}\n`, | |
| 79 | }, | |
| 80 | { | |
| 81 | path: "server.ts", | |
| 82 | content: | |
| 83 | "Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response('ok') });\n", | |
| 84 | }, | |
| 85 | ], | |
| 86 | }); | |
| 87 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 88 | const enabled = await enableApp(app, alice, made.value, { slug: name }); | |
| 89 | if (!enabled.ok) throw new Error(String(enabled.error)); | |
| 90 | expect(addToCrew(app, alice, made.value, "scout").ok).toBe(true); | |
| 91 | return made.value; | |
| 92 | }; | |
| 93 | ||
| 65 | 94 | beforeAll(async () => { |
| 66 | 95 | dataDir = await mkdtemp(join(tmpdir(), "forge-studio-")); |
| 67 | 96 | app = createApp({ dataDir, port: 0 }); |
| @@ -87,33 +116,9 @@ describe("the studio", () => { | ||
| 87 | 116 | }) |
| 88 | 117 | ).headers.get("set-cookie") ?? "" |
| 89 | 118 | ).split(";")[0]!; |
| 90 | const made = await createRepo(app, alice, { | |
| 91 | name: "notes", | |
| 92 | visibility: "private", | |
| 93 | description: "a place for notes", | |
| 94 | }); | |
| 95 | if (!made.ok) throw new Error(String(made.error)); | |
| 96 | repo = made.value; | |
| 97 | const seeded = await commitToBranch(app, alice, repo, { | |
| 98 | message: "Start", | |
| 99 | files: [ | |
| 100 | { | |
| 101 | path: "package.json", | |
| 102 | content: '{"name":"notes","scripts":{"start":"bun run server.ts"}}\n', | |
| 103 | }, | |
| 104 | { | |
| 105 | path: "server.ts", | |
| 106 | content: | |
| 107 | "Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response('ok') });\n", | |
| 108 | }, | |
| 109 | ], | |
| 110 | }); | |
| 111 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 112 | const enabled = await enableApp(app, alice, repo, { slug: "notes" }); | |
| 113 | if (!enabled.ok) throw new Error(String(enabled.error)); | |
| 114 | 119 | const scout = await createAgent(app, alice, { username: "scout" }); |
| 115 | 120 | if (!scout.ok) throw new Error(String(scout.error)); |
| 116 | expect(addToCrew(app, alice, repo, "scout").ok).toBe(true); | |
| 121 | repo = await makeApp("notes", "a place for notes"); | |
| 117 | 122 | }); |
| 118 | 123 | |
| 119 | 124 | afterAll(async () => { |
| @@ -278,6 +283,14 @@ describe("the studio", () => { | ||
| 278 | 283 | }); |
| 279 | 284 | |
| 280 | 285 | test("ship lands a change; say tells the crew", async () => { |
| 286 | // Its own app, so the tell it ships is the only one waiting there. | |
| 287 | const repo = await makeApp("shipnotes"); | |
| 288 | const told = await post("/alice/shipnotes/tell", { | |
| 289 | text: "Make the notes page faster.", | |
| 290 | }); | |
| 291 | expect(told.status).toBe(303); | |
| 292 | const tell = listIssues(app.db, repo.id, { kind: "issue", limit: 5 })[0]!; | |
| 293 | expect(tell.assigneeName).toBe("scout"); | |
| 281 | 294 | const branch = await commitToBranch(app, alice, repo, { |
| 282 | 295 | branch: "feature/fast", |
| 283 | 296 | base: "main", |
| @@ -287,13 +300,13 @@ describe("the studio", () => { | ||
| 287 | 300 | if (!branch.ok) throw new Error(JSON.stringify(branch.error)); |
| 288 | 301 | const opened = await openPull(app, alice, repo, { |
| 289 | 302 | title: "Faster", |
| 290 | body: "Closes #1", | |
| 303 | body: `Closes #${tell.number}`, | |
| 291 | 304 | baseBranch: "main", |
| 292 | 305 | headBranch: "feature/fast", |
| 293 | 306 | }); |
| 294 | 307 | if (!opened.ok) throw new Error(String(opened.error)); |
| 295 | 308 | const pull = opened.value; |
| 296 | const said = await post(`/alice/notes/pulls/${pull.number}/say`, { | |
| 309 | const said = await post(`/alice/shipnotes/pulls/${pull.number}/say`, { | |
| 297 | 310 | text: "Keep the images lazy too.", |
| 298 | 311 | }); |
| 299 | 312 | expect(said.status).toBe(303); |
| @@ -310,7 +323,7 @@ describe("the studio", () => { | ||
| 310 | 323 | // A bare POST — a button with no fields, or a client that sent no body |
| 311 | 324 | // at all — ships too: an empty form is not a bad request. |
| 312 | 325 | const shipped = await fetch( |
| 313 | `${base}/alice/notes/pulls/${pull.number}/ship`, | |
| 326 | `${base}/alice/shipnotes/pulls/${pull.number}/ship`, | |
| 314 | 327 | { |
| 315 | 328 | method: "POST", |
| 316 | 329 | headers: { cookie: `${cookie}; forge_mode=studio`, origin: base }, |
| @@ -319,8 +332,8 @@ describe("the studio", () => { | ||
| 319 | 332 | ); |
| 320 | 333 | expect(shipped.status).toBe(303); |
| 321 | 334 | expect(getPull(app.db, repo.id, pull.number)?.mergedAt).not.toBeNull(); |
| 322 | expect(getIssue(app.db, repo.id, 1)?.state).toBe("closed"); | |
| 323 | const page = await (await get("/alice/notes")).text(); | |
| 335 | expect(getIssue(app.db, repo.id, tell.number)?.state).toBe("closed"); | |
| 336 | const page = await (await get("/alice/shipnotes")).text(); | |
| 324 | 337 | expect(page).toContain("shipped"); |
| 325 | 338 | // What was told and then shipped is not still "waiting" in Now. |
| 326 | 339 | expect(page).not.toContain(">waiting<"); |
new above
offline