Public: anyone can read this; the crew can change it.

test/prompts.test.ts and test/snapshot-destination.test.ts pass in any order and run in the gate #11100

Merged opened this

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

Merged

mason/forge:mason/11097-test-prompts-test-ts-and-test-snapshot-d → main 4 commits fork 1 thread · 1 unresolved

  • Merged. mason/11097-test-prompts-test-ts-and-test-snapshot-d was merged into main as 26f83a4.

Review

Sign in to review this change.
3 files changed +92 −36
scripts/gate.ts −10
@@ -53,21 +53,11 @@ export const EXCLUDED: readonly Exclusion[] = [
5353 reason: "red alone under --randomize (2026-11-10): the first clean probe after closes the issue: verdict down, not ok",
5454 issue: 11084,
5555 },
56 {
57 file: "test/prompts.test.ts",
58 reason: "red alone under --randomize (2026-11-10): 11 of 23 fail, tests read the prompts and Reels earlier tests made",
59 issue: 11097,
60 },
6156 {
6257 file: "test/rehearsal-real.test.ts",
6358 reason: "green alone (3 of 3, 2026-11-10) but unproven under the gate's load, where on main@accaa65 the second real roll's old forge did not come up",
6459 issue: 11098,
6560 },
66 {
67 file: "test/snapshot-destination.test.ts",
68 reason: "red alone under --randomize (2026-11-10): precedence and follow-the-saved-destination read a destination an earlier test saved",
69 issue: 11097,
70 },
7161 ];
outdated

Remove these four #261 entries (scaffold, ssh-app, ssh-port-taken, ssh): out of scope for #11097, and #11099 on main took them out of the quarantine because #261 is closed.

7262
7363 /**
test/prompts.test.ts +85 −25
@@ -5,7 +5,7 @@
55 * network — the real client is exercised against a fake `fetch`.
66 */
77
8 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { afterEach, beforeEach, describe, expect, test } from "bun:test";
99 import { mkdtemp, rm } from "node:fs/promises";
1010 import { tmpdir } from "node:os";
1111 import { join } from "node:path";
@@ -366,7 +366,7 @@ describe("prompts from Instagram", () => {
366366 let rwToken = "";
367367 let bobToken = "";
368368 let sourceId = "";
369 const instagram = new FakeInstagram();
369 let instagram = new FakeInstagram();
370370 let clock = new Date("2026-09-28T14:59:00Z");
371371 const tick = (iso: string) => {
372372 clock = new Date(iso);
@@ -424,7 +424,47 @@ describe("prompts from Instagram", () => {
424424 const source = () => getPromptSource(app.db, sourceId)!;
425425 const openIssues = () => listIssues(app.db, repo.id, { limit: 50 });
426426
427 beforeAll(async () => {
427 /** Connect the account the way a person would, on the test's own story. */
428 const connect = async (atUtc = "15:00") => {
429 const made = await api(PROMPTS, {
430 method: "POST",
431 body: JSON.stringify({ kind: "instagram", token: TOKEN, at: atUtc }),
432 });
433 expect(made.status).toBe(201);
434 sourceId = ((await made.json()) as { id: string }).id;
435 // The API stamps the real date; the story is told on the test's clock.
436 app.db
437 .query(
438 "UPDATE prompt_source SET token_refreshed_at = ?2, created_at = ?2 WHERE id = ?1",
439 )
440 .run(sourceId, "2026-09-28T14:00:00.000Z");
441 };
442 const reelOne = () => {
443 instagram.reel = reelOf("reel-1", "2026-09-27");
444 instagram.replies = [
445 comment("c2", "first!!", 2, at("2026-09-27T18:01:00+0000"), "quick_one"),
446 comment(
447 "c1",
448 "Add a boss fight at the end of level 3\nIt feels empty right now.",
449 5,
450 at("2026-09-27T18:05:00+0000"),
451 "fan_one",
452 ),
453 comment("c3", "", 99, at("2026-09-27T18:00:00+0000"), "blank"),
454 ];
455 };
456 /** An account connected whose first Reel has been told as #1. */
457 const firstTell = async () => {
458 await connect();
459 reelOne();
460 await tick("2026-09-28T15:00:00Z");
461 expect(source().lastIssueNumber).toBe(1);
462 };
463
464 beforeEach(async () => {
465 instagram = new FakeInstagram();
466 clock = new Date("2026-09-28T14:59:00Z");
467 sourceId = "";
428468 dataDir = await mkdtemp(join(tmpdir(), "forge-prompts-"));
429469 app = createApp({ dataDir, port: 0 });
430470 servers = await serve(app);
@@ -468,7 +508,7 @@ describe("prompts from Instagram", () => {
468508 expect(addToCrew(app, alice, repo, "scout").ok).toBe(true);
469509 });
470510
471 afterAll(async () => {
511 afterEach(async () => {
472512 await routine.stop();
473513 servers.stop();
474514 app.close();
@@ -586,18 +626,8 @@ describe("prompts from Instagram", () => {
586626 });
587627
588628 test("nothing before the hour; once after it; not again the same day", async () => {
589 instagram.reel = reelOf("reel-1", "2026-09-27");
590 instagram.replies = [
591 comment("c2", "first!!", 2, at("2026-09-27T18:01:00+0000"), "quick_one"),
592 comment(
593 "c1",
594 "Add a boss fight at the end of level 3\nIt feels empty right now.",
595 5,
596 at("2026-09-27T18:05:00+0000"),
597 "fan_one",
598 ),
599 comment("c3", "", 99, at("2026-09-27T18:00:00+0000"), "blank"),
600 ];
629 await connect();
630 reelOne();
601631 await tick("2026-09-28T14:59:00Z");
602632 expect(instagram.names()).toEqual([]);
603633 expect(source().lastRanAt).toBeNull();
@@ -645,6 +675,7 @@ describe("prompts from Instagram", () => {
645675 });
646676
647677 test("the same Reel is never told twice", async () => {
678 await firstTell();
648679 instagram.calls.length = 0;
649680 await tick("2026-09-29T15:00:00Z");
650681 // The account is known, so `me` is not asked again; the Reel is known,
@@ -658,6 +689,7 @@ describe("prompts from Instagram", () => {
658689 });
659690
660691 test("a Reel with no comments yet is looked at again tomorrow", async () => {
692 await firstTell();
661693 instagram.calls.length = 0;
662694 instagram.reel = reelOf("reel-2", "2026-09-29");
663695 instagram.replies = [];
@@ -691,6 +723,11 @@ describe("prompts from Instagram", () => {
691723 });
692724
693725 test("no Reel at all is a sentence", async () => {
726 await firstTell();
727 instagram.reel = reelOf("reel-2", "2026-09-29");
728 instagram.replies = [comment("c9", "Heavier jump", 1, at("2026-09-30T09:00:00+0000"), "ana")];
729 await tick("2026-10-01T15:00:00Z");
730 expect(source().lastMediaId).toBe("reel-2");
694731 instagram.reel = undefined;
695732 await tick("2026-10-02T15:00:00Z");
696733 expect(source().lastSaid).toBe("No Reel yet.");
@@ -698,6 +735,7 @@ describe("prompts from Instagram", () => {
698735 });
699736
700737 test("Instagram's refusals are sentences, never throws", async () => {
738 await firstTell();
701739 instagram.failWith = new InstagramError(
702740 190,
703741 "Error validating access token: Session has expired",
@@ -722,10 +760,11 @@ describe("prompts from Instagram", () => {
722760 expect(source().lastSaid).toBe("The run failed: socket hang up.");
723761 instagram.failWith = undefined;
724762 // Nothing above told anybody anything.
725 expect(openIssues()).toHaveLength(2);
763 expect(openIssues()).toHaveLength(1);
726764 });
727765
728766 test("a token older than fifty days is refreshed, and the new one is what runs from then on", async () => {
767 await connect();
729768 const before = source().tokenRefreshedAt;
730769 instagram.calls.length = 0;
731770 instagram.reel = reelOf("reel-3", "2026-11-19");
@@ -736,6 +775,7 @@ describe("prompts from Instagram", () => {
736775 await tick("2026-11-20T15:00:00Z");
737776 expect(instagram.calls).toEqual([
738777 { call: "refresh", token: TOKEN },
778 { call: "me", token: FRESH },
739779 { call: "reel", token: FRESH },
740780 { call: "comments:reel-3", token: FRESH },
741781 ]);
@@ -745,7 +785,7 @@ describe("prompts from Instagram", () => {
745785 ok: true,
746786 value: FRESH,
747787 });
748 expect(source().lastIssueNumber).toBe(3);
788 expect(source().lastIssueNumber).toBe(1);
749789
750790 // The next day: fresh, no refresh.
751791 instagram.calls.length = 0;
@@ -763,7 +803,12 @@ describe("prompts from Instagram", () => {
763803 });
764804
765805 test("Fetch now runs at once, whatever the hour, through the API", async () => {
766 // Already ran today; a new Reel is up.
806 await connect();
807 clock = new Date("2026-11-21T15:00:00Z");
808 // A token pasted the day before: no refresh is due.
809 app.db
810 .query("UPDATE prompt_source SET token_refreshed_at = ?2 WHERE id = ?1")
811 .run(sourceId, "2026-11-20T15:00:00.000Z");
767812 instagram.calls.length = 0;
768813 instagram.reel = reelOf("reel-4", "2026-11-21");
769814 instagram.replies = [
@@ -779,12 +824,12 @@ describe("prompts from Instagram", () => {
779824 account: string;
780825 };
781826 expect(item.lastSaid).toBe(
782 'Told the crew #4: "Co-op please" — @duo, 12 likes.',
827 'Told the crew #1: "Co-op please" — @duo, 12 likes.',
783828 );
784 expect(item.lastIssueNumber).toBe(4);
829 expect(item.lastIssueNumber).toBe(1);
785830 expect(item.account).toBe("gamehandle");
786 expect(instagram.names()).toEqual(["reel", "comments:reel-4"]);
787 expect(getIssue(app.db, repo.id, 4)?.title).toBe("Co-op please");
831 expect(instagram.names()).toEqual(["me", "reel", "comments:reel-4"]);
832 expect(getIssue(app.db, repo.id, 1)?.title).toBe("Co-op please");
788833
789834 const unknown = await api(`${PROMPTS}/nope/run`, { method: "POST" });
790835 expect(unknown.status).toBe(422);
@@ -794,6 +839,7 @@ describe("prompts from Instagram", () => {
794839 });
795840
796841 test("admin only, like the domains: scope, then admin over the repository", async () => {
842 await connect();
797843 expect((await api(PROMPTS, { token: rwToken })).status).toBe(403);
798844 expect((await api(PROMPTS, { token: null })).status).toBe(401);
799845 expect((await api(PROMPTS, { token: bobToken })).status).toBe(404);
@@ -875,12 +921,22 @@ describe("prompts from Instagram", () => {
875921 });
876922
877923 test("the settings page: the card, the steps, the doors", async () => {
924 await connect();
925 clock = new Date("2026-11-21T15:00:00Z");
926 instagram.reel = reelOf("reel-4", "2026-11-21");
927 instagram.replies = [
928 comment("c30", "Co-op please", 12, at("2026-11-21T12:00:00+0000"), "duo"),
929 ];
930 await routine.runNow(sourceId);
931 app.db
932 .query("UPDATE prompt_source SET token_refreshed_at = ?2 WHERE id = ?1")
933 .run(sourceId, "2026-11-20T15:00:00.000Z");
878934 const page = words(await (await web(aliceCookie, SETTINGS)).text());
879935 expect(page).toContain(">Prompts<");
880936 expect(page).toContain("<h2>Instagram</h2>");
881937 expect(page).toContain("@gamehandle");
882938 expect(page).toContain("every day at 15:00 UTC");
883 expect(page).toContain("Told the crew #4");
939 expect(page).toContain("Told the crew #1");
884940 expect(page).toContain("Token refreshed");
885941 expect(page).toContain("refreshes it again around 2027-01-09");
886942 expect(page).toContain("Fetch now");
@@ -911,7 +967,7 @@ describe("prompts from Instagram", () => {
911967 expect(fetched.status).toBe(303);
912968 expect(fetched.headers.get("location")).toBe(SETTINGS);
913969 expect(flashOf(fetched)).toContain(
914 "No new Reel since 2026-11-21; the last one was told as #4.",
970 "No new Reel since 2026-11-21; the last one was told as #1.",
915971 );
916972 expect(instagram.names()).toEqual(["reel"]);
917973
@@ -960,6 +1016,7 @@ describe("prompts from Instagram", () => {
9601016 });
9611017
9621018 test("the API removes, and says so once", async () => {
1019 await connect("09:30");
9631020 const listed = (await (await api(PROMPTS)).json()) as {
9641021 items: { id: string; atUtc: string; account: string }[];
9651022 };
@@ -1010,6 +1067,7 @@ describe("prompts from Instagram", () => {
10101067 });
10111068
10121069 test("a sentence about an error never carries the token, even when Instagram's words quote the request", async () => {
1070 await connect();
10131071 instagram.failWith = new InstagramError(
10141072 400,
10151073 `Bad request to https://graph.instagram.com/v25.0/me/media?fields=id&access_token=${TOKEN}`,
@@ -1031,6 +1089,7 @@ describe("prompts from Instagram", () => {
10311089 test("a run claims its row before any network work: two sweeps over one row tell the crew once", async () => {
10321090 // A second routine over the same database — two forges over one data
10331091 // directory, or a timer started twice — with a slow Instagram.
1092 await connect();
10341093 let release: (() => void) | undefined;
10351094 const slow = new FakeInstagram();
10361095 slow.reel = reelOf("reel-8", "2026-10-11");
@@ -1069,6 +1128,7 @@ describe("prompts from Instagram", () => {
10691128 });
10701129
10711130 test("stop waits for a run in flight, but not forever", async () => {
1131 await connect();
10721132 let hang: (() => void) | undefined;
10731133 const stuck: InstagramClient = {
10741134 me: (t) => instagram.me(t),
test/snapshot-destination.test.ts +7 −1
@@ -5,7 +5,7 @@
55 * for admins and the keys are never shown again.
66 */
77
8 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
99 import { mkdtempSync, rmSync } from "node:fs";
1010 import { tmpdir } from "node:os";
1111 import { join } from "node:path";
@@ -62,6 +62,11 @@ describe("the snapshot destination", () => {
6262 rmSync(root, { recursive: true, force: true });
6363 });
6464
65 // Every test starts with nothing sealed and makes the destination it reads.
66 beforeEach(() => {
67 forgetDestination(app, admin);
68 });
69
6570 const sources = () => ({
6671 db: app.db,
6772 dataDir: root,
@@ -93,6 +98,7 @@ describe("the snapshot destination", () => {
9398 });
9499
95100 test("the service follows a saved or cleared destination without a restart", async () => {
101 expect(saveDestination(app, admin, input).ok).toBe(true);
96102 const svc = startSnapshots(
97103 app,
98104 () => resolveDestination({ ...sources(), env })?.store,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs