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

test/checks.test.ts runs in under 10 s, every test under 1 s, proving what it proves today #10251

Merged opened this

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

Merged

mason/forge:mason/10245-test-checks-test-ts-runs-in-under-10-s-e → main 1 commit fork

  • Merged. mason/10245-test-checks-test-ts-runs-in-under-10-s-e was merged into main as 5632755.

Review

Sign in to review this change.
1 file changed +159 −110
test/checks.test.ts +159 −110
@@ -5,7 +5,7 @@
55 */
66
77 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { existsSync, mkdirSync } from "node:fs";
8 import { existsSync, mkdirSync, writeFileSync } from "node:fs";
99 import { mkdtemp, rm } from "node:fs/promises";
1010 import { tmpdir } from "node:os";
1111 import { join } from "node:path";
@@ -57,6 +57,7 @@ const checkJson = (run: string, extra = ""): string =>
5757
5858 describe("cancelRunsForPull: a landed or closed pull leaves nothing in the lane", () => {
5959 test("queued and running checks and queued deploys of that pull are cancelled, building deploys and other pulls kept", async () => {
60 // Invariant: cancelRunsForPull cancels that pull's queued and running checks and queued deploys, and nothing else.
6061 const dir = await mkdtemp(join(tmpdir(), "forge-cancel-"));
6162 const db = createApp({ dataDir: dir, port: 0 });
6263 try {
@@ -398,7 +399,7 @@ describe("checks", () => {
398399
399400 async function settled(
400401 where: (run: Run) => boolean,
401 timeoutMs = 30_000,
402 timeoutMs = 4_000,
402403 ): Promise<Run> {
403404 const deadline = Date.now() + timeoutMs;
404405 while (Date.now() < deadline) {
@@ -406,11 +407,45 @@ describe("checks", () => {
406407 where,
407408 );
408409 if (run && (run.state === "ok" || run.state === "failed")) return run;
409 await Bun.sleep(50);
410 await Bun.sleep(10);
410411 }
411412 throw new Error("check did not settle");
412413 }
413414
415 /** Waits until `done` holds, polling the database; a check's own pace, not a timer's. */
416 async function until(done: () => boolean, what: string): Promise<void> {
417 const deadline = Date.now() + 4_000;
418 while (!done()) {
419 if (Date.now() > deadline) throw new Error(`never came: ${what}`);
420 await Bun.sleep(10);
421 }
422 }
423
424 /**
425 * A check command that holds its slot until the test lets it go
426 * (`release`), then runs `then`: the test is the clock, so no wait is
427 * guessed at. Never released, it runs until it is cut off.
428 */
429 const held = (name: string, then = "test -f OK.md"): string =>
430 `while [ ! -f '${dataDir}/go-${name}' ]; do sleep 0.01; done; ${then}`;
431 const release = (name: string): void =>
432 writeFileSync(join(dataDir, `go-${name}`), "");
433
434 /** Waits until a fork pull's new head is announced on the bus as `pull.pushed`. */
435 const headAnnounced = (pull: Pull, headOid: string): Promise<void> =>
436 until(
437 () =>
438 listEvents(app.db, { repoId: repo.id, kind: "pull.pushed" }).some(
439 (e) => e.payload["number"] === pull.number && e.payload["headOid"] === headOid,
440 ),
441 "the pull's new head announced",
442 );
443
444 const checksOf = (commitOid: string): Run[] =>
445 listRuns(app.db, repo.id, { kind: "check", limit: 50 }).filter(
446 (r) => r.commitOid === commitOid,
447 );
448
414449 beforeAll(async () => {
415450 dataDir = await mkdtemp(join(tmpdir(), "forge-checks-"));
416451 app = createApp({ dataDir, port: 0 });
@@ -452,16 +487,25 @@ describe("checks", () => {
452487 });
453488
454489 test("a commit that declares no check is not checked", async () => {
490 // Invariant: a push whose commit declares no check queues no run; the next push that declares one is the only run.
455491 const seeded = await commitToBranch(app, alice, repo, {
456492 message: "Start",
457493 files: [{ path: "README.md", content: "# lib\n" }],
458494 });
459495 expect(seeded.ok).toBe(true);
460 await Bun.sleep(300);
461 expect(listRuns(app.db, repo.id, { kind: "check" })).toHaveLength(0);
496 // The push after it declares a check: once that one settles, the
497 // first push's handler (begun earlier, doing no more) has answered too.
498 const declared = await commitToBranch(app, alice, repo, {
499 message: "Declare a passing check",
500 files: [{ path: "forge.json", content: checkJson("true # first") }],
501 });
502 if (!declared.ok) throw new Error(JSON.stringify(declared.error));
503 const run = await settled((r) => r.commitOid === declared.value.commitOid);
504 expect(listRuns(app.db, repo.id, { kind: "check" }).map((r) => r.id)).toEqual([run.id]);
462505 });
463506
464507 test("the default branch is checked on push, with the log kept", async () => {
508 // Invariant: a push to the default branch that declares a check runs it, env main, reason push, its commands in the log.
465509 const declared = await commitToBranch(app, alice, repo, {
466510 message: "Declare a check",
467511 files: [
@@ -480,6 +524,7 @@ describe("checks", () => {
480524 });
481525
482526 test("a check's children hold no cells of their own: the check's cell is the boundary", async () => {
527 // Invariant: a check's command sees FORGE_CELLS=off.
483528 // A forge the tests boot inside a check cell must not wrap its apps in
484529 // nested cells (they never answer in time); it is told so in its env.
485530 const declared = await commitToBranch(app, alice, repo, {
@@ -502,6 +547,7 @@ describe("checks", () => {
502547 });
503548
504549 test("a pull is checked when it opens and each time its branch moves; a failure names the exit", async () => {
550 // Invariant: a branch heading no pull is not checked; opening a pull checks its head with the base's check, and each push to it again.
505551 // A pull is checked by the base's check, not its own.
506552 const gate = await commitToBranch(app, alice, repo, {
507553 message: "The base's check",
@@ -514,13 +560,6 @@ describe("checks", () => {
514560 files: [{ path: "a.txt", content: "a\n" }],
515561 });
516562 if (!branched.ok) throw new Error(JSON.stringify(branched.error));
517 // A branch that heads no pull is nobody's to check yet.
518 await Bun.sleep(300);
519 expect(
520 listRuns(app.db, repo.id, { kind: "check" }).some(
521 (r) => r.commitOid === branched.value.commitOid,
522 ),
523 ).toBe(false);
524563 const opened = await openPull(app, alice, repo, {
525564 title: "Feature",
526565 baseBranch: "main",
@@ -534,6 +573,9 @@ describe("checks", () => {
534573 expect(failed.state).toBe("failed");
535574 expect(failed.env).toBe(`pull-${pull.number}`);
536575 expect(failed.reason).toBe("pull");
576 // A branch that heads no pull is nobody's to check: its push, handled
577 // before the pull opened, left no run beside the pull's own.
578 expect(checksOf(branched.value.commitOid).map((r) => r.id)).toEqual([failed.id]);
537579 expect(failed.summary).toBe("check failed: check exited with code 1");
538580 // `test -f` is not `bun test`: its output is not this shape, so nothing
539581 // is guessed at it.
@@ -584,9 +626,10 @@ describe("checks", () => {
584626 );
585627 expect(passed.state).toBe("ok");
586628 expect(passed.reason).toBe("push");
587 }, 60_000);
629 });
588630
589631 test("a bun test failure in the check step is summarised: how many passed, how many failed, and their names", async () => {
632 // Invariant: a red `bun test` check's summary and detail carry the counts and the failing names.
590633 const gate = await commitToBranch(app, alice, repo, {
591634 message: "The base's check runs bun test",
592635 files: [
@@ -630,9 +673,10 @@ describe("checks", () => {
630673 expect(failed.summary).toBe(
631674 'check failed: 2 of 3 tests — "first failing test" and "second failing test"',
632675 );
633 }, 30_000);
676 });
634677
635678 test("a reader's fork pull is checked at the base's snapshot once a writer approves each head", async () => {
679 // Invariant: a reader's fork pull runs nothing until a writer approves, and each new head waits for its own approval.
636680 const gate = await commitToBranch(app, alice, repo, {
637681 message: "The base's check passes",
638682 files: [{ path: "forge.json", content: checkJson("true") }],
@@ -672,13 +716,10 @@ describe("checks", () => {
672716 files: [{ path: "FORK.md", content: "hi again\n" }],
673717 });
674718 if (!again.ok) throw new Error(JSON.stringify(again.error));
675 // A new head waits for its own approval.
676 await Bun.sleep(300);
677 expect(
678 listRuns(app.db, repo.id, { kind: "check" }).some(
679 (r) => r.issueId === pull.id && r.commitOid === again.value.commitOid,
680 ),
681 ).toBe(false);
719 // The pull follows the fork's branch on its own clock: approve the new
720 // head only once its move was announced and answered (the bus delivers
721 // in step, so the push's handler has met the approval guard by then).
722 await headAnnounced(pull, again.value.commitOid);
682723 const reapproved = await submitReview(app, alice, repo, pull, {
683724 state: "approved",
684725 });
@@ -687,9 +728,14 @@ describe("checks", () => {
687728 (r) => r.issueId === pull.id && r.commitOid === again.value.commitOid,
688729 );
689730 expect(second.state).toBe("ok");
690 }, 60_000);
731 // A new head waits for its own approval: the push, handled before the
732 // approval was given, ran nothing; the one check is the approval's.
733 expect(second.reason).toBe("approved");
734 expect(checksOf(again.value.commitOid).map((r) => r.id)).toEqual([second.id]);
735 });
691736
692737 test("approving a head whose check is already green queues no second check (#4391)", async () => {
738 // Invariant: a second approval of a head already green on the same base queues nothing.
693739 const based = await commitToBranch(app, alice, repo, {
694740 message: "The base's check passes",
695741 files: [{ path: "forge.json", content: checkJson("true") }],
@@ -729,11 +775,24 @@ describe("checks", () => {
729775 body: "Still good.",
730776 });
731777 if (!again.ok) throw new Error(String(again.error));
732 await Bun.sleep(300);
778 // The fork moves on and its new head is approved: once that check
779 // settles, the second approval's handler (begun earlier, doing no
780 // more) has answered too.
781 const next = await commitToBranch(app, bob, fork, {
782 branch: "green-then-approved",
783 message: "After the second look",
784 files: [{ path: "GREEN.md", content: "y\n" }],
785 });
786 if (!next.ok) throw new Error(JSON.stringify(next.error));
787 await headAnnounced(pull, next.value.commitOid);
788 const third = await submitReview(app, alice, repo, pull, { state: "approved" });
789 if (!third.ok) throw new Error(String(third.error));
790 await settled((r) => r.issueId === pull.id && r.commitOid === next.value.commitOid);
733791 expect(mine().map((r) => r.id)).toEqual([first.id]);
734 }, 60_000);
792 });
735793
736794 test("a commit already proven green is not run again; the re-request answers with the earlier verdict (#1795)", async () => {
795 // Invariant: a re-requested check of a commit already green answers with that verdict without running the command.
737796 const made = await commitToBranch(app, alice, repo, {
738797 message: "Proven once",
739798 files: [
@@ -750,14 +809,13 @@ describe("checks", () => {
750809 expect(second.state).toBe("ok");
751810 expect(second.summary).toBe(`already proven green by ${first.id}`);
752811 expect(readRunLog(dataDir, second.id).lines.join("\n")).not.toContain("ran-once");
753 }, 60_000);
812 });
754813
755814 test("a newer commit supersedes a check still waiting for the same environment", async () => {
815 // Invariant: a queued check is cancelled as superseded when a newer commit asks for the same environment.
756816 const slow = await commitToBranch(app, alice, repo, {
757817 message: "A slow check",
758 files: [
759 { path: "forge.json", content: checkJson("sleep 0.5 && test -f OK.md") },
760 ],
818 files: [{ path: "forge.json", content: checkJson(held("supersede")) }],
761819 });
762820 if (!slow.ok) throw new Error(JSON.stringify(slow.error));
763821 // While that one holds the only slot, two more arrive: the first of
@@ -774,26 +832,28 @@ describe("checks", () => {
774832 files: [{ path: "TWO.md", content: "2\n" }],
775833 });
776834 if (!two.ok) throw new Error(JSON.stringify(two.error));
835 await until(() => checksOf(two.value.commitOid).length > 0, "two queued");
777836 const three = await commitToBranch(app, alice, repo, {
778837 message: "Three",
779838 files: [{ path: "THREE.md", content: "3\n" }],
780839 });
781840 if (!three.ok) throw new Error(JSON.stringify(three.error));
841 await until(() => checksOf(three.value.commitOid).length > 0, "three queued");
842 release("supersede");
782843 const last = await settled((r) => r.commitOid === three.value.commitOid);
783844 expect(last.state).toBe("ok");
784 const middle = listRuns(app.db, repo.id, { kind: "check" }).find(
785 (r) => r.commitOid === two.value.commitOid,
786 );
845 const middle = checksOf(two.value.commitOid)[0];
787846 expect(middle?.state).toBe("cancelled");
788847 expect(middle?.summary).toContain("superseded by");
789 }, 60_000);
848 });
790849
791850 test("a new head on a pull stops its running check for the old head, superseded not failed", async () => {
851 // Invariant: a pull's new head cuts off the running check of its old head, which ends cancelled as superseded.
792852 const slow = await commitToBranch(app, alice, repo, {
793853 branch: "supersede",
794 message: "A very slow check",
854 message: "A check that never ends on its own",
795855 files: [
796 { path: "forge.json", content: checkJson("sleep 30") },
856 { path: "forge.json", content: checkJson(held("never")) },
797857 { path: "S.md", content: "s\n" },
798858 ],
799859 });
@@ -813,11 +873,7 @@ describe("checks", () => {
813873 opened.value.id,
814874 );
815875 if (!old) throw new Error("no run");
816 const began = Date.now();
817 while (getRun(app.db, old.id)?.state !== "running") {
818 if (Date.now() - began > 20_000) throw new Error("never started");
819 await Bun.sleep(50);
820 }
876 await until(() => getRun(app.db, old.id)?.state === "running", "old head running");
821877 const next = await commitToBranch(app, alice, repo, {
822878 branch: "supersede",
823879 message: "Fast",
@@ -831,20 +887,20 @@ describe("checks", () => {
831887 const stopped = getRun(app.db, old.id);
832888 expect(stopped?.state).toBe("cancelled");
833889 expect(stopped?.summary).toContain("superseded by");
834 // Well short of the 30 s the old check would have taken.
835 expect(Date.now() - began).toBeLessThan(25_000);
836 }, 60_000);
890 // Stopped, not only marked: the old check, which never ends on its own,
891 // was cut off and wrote its end.
892 await until(
893 () => readRunLog(dataDir, old.id).lines.join("\n").includes("superseded"),
894 "the old head's check cut off",
895 );
896 });
837897
838898 for (const ending of ["merge", "close"] as const)
839899 test(`${ending === "merge" ? "merging" : "closing"} a pull leaves no queued or running run for it`, async () => {
900 // Invariant: a pull merged or closed has its queued checks cancelled with that reason.
840901 const blocker = await commitToBranch(app, alice, repo, {
841902 message: "Hold the lane",
842 files: [
843 {
844 path: "forge.json",
845 content: checkJson(`sleep 0.5 && test -f OK.md # ${ending}`),
846 },
847 ],
903 files: [{ path: "forge.json", content: checkJson(held(`lane-${ending}`)) }],
848904 });
849905 if (!blocker.ok) throw new Error(JSON.stringify(blocker.error));
850906 await checks.request(repo, "lane", blocker.value.commitOid, "push");
@@ -865,12 +921,7 @@ describe("checks", () => {
865921 listRuns(app.db, repo.id, { kind: "check" }).filter(
866922 (r) => r.issueId === pull.id,
867923 );
868 const until = async (done: () => boolean): Promise<void> => {
869 const deadline = Date.now() + 10_000;
870 while (!done() && Date.now() < deadline) await Bun.sleep(25);
871 if (!done()) throw new Error(`never came: still waiting while !done()`);
872 };
873 await until(() => mine().length > 0);
924 await until(() => mine().length > 0, "the pull's check queued");
874925 expect(mine().some((r) => r.state === "queued")).toBe(true);
875926 if (ending === "merge") {
876927 const landed = await mergePull(app, alice, repo, pull, "merge");
@@ -879,7 +930,7 @@ describe("checks", () => {
879930 const closed = setIssueState(app, alice, repo, pull, "closed");
880931 if (!closed.ok) throw new Error(String(closed.error));
881932 }
882 await until(() => mine().every((r) => r.state !== "queued"));
933 await until(() => mine().every((r) => r.state !== "queued"), "the pull's check gone");
883934 expect(mine().filter((r) => r.state === "queued")).toEqual([]);
884935 expect(
885936 mine().some(
@@ -887,7 +938,13 @@ describe("checks", () => {
887938 r.summary === (ending === "merge" ? "pull landed" : "pull closed"),
888939 ),
889940 ).toBe(true);
890 }, 60_000);
941 // The lane is let go, and left empty for the next test.
942 release(`lane-${ending}`);
943 await until(
944 () => checksOf(blocker.value.commitOid).every((r) => r.state !== "queued" && r.state !== "running"),
945 "the lane's holder done",
946 );
947 });
891948
892949 test("a queued gate keeps the last free slot from pull checks", () => {
893950 // One slot free, a gate queued: the pull check passes, the gate starts.
@@ -947,6 +1004,7 @@ describe("checks", () => {
9471004 });
9481005
9491006 test("when a slot frees, the newest of several queued checks runs first", async () => {
1007 // Invariant: of several checks queued behind a held slot, the newest starts first.
9501008 // Three checks, on three environments so none supersedes another. Their
9511009 // commits are written before the slot is taken: written after, on a
9521010 // loaded machine they can outlast the holder, and the oldest then finds
@@ -972,18 +1030,18 @@ describe("checks", () => {
9721030
9731031 // A slow check on its own environment holds the only slot, and the
9741032 // three queue behind it — oldest requested first.
975 const held = await commitToBranch(app, alice, repo, {
1033 const holding = await commitToBranch(app, alice, repo, {
9761034 branch: "order-hold",
9771035 message: "Hold the slot",
9781036 files: [
979 { path: "forge.json", content: checkJson("sleep 0.5 && test -f OK.md") },
1037 { path: "forge.json", content: checkJson(held("order")) },
9801038 ],
9811039 });
982 if (!held.ok) throw new Error(JSON.stringify(held.error));
1040 if (!holding.ok) throw new Error(JSON.stringify(holding.error));
9831041 const holder = await checks.request(
9841042 repo,
9851043 "order-hold",
986 held.value.commitOid,
1044 holding.value.commitOid,
9871045 "push",
9881046 );
9891047 expect(holder).toBeDefined();
@@ -1011,6 +1069,7 @@ describe("checks", () => {
10111069 expect(middleRun?.state).toBe("queued");
10121070 expect(newestRun?.state).toBe("queued");
10131071
1072 release("order");
10141073 await settled((r) => r.id === newestRun!.id);
10151074 await settled((r) => r.id === middleRun!.id);
10161075 await settled((r) => r.id === oldestRun!.id);
@@ -1020,32 +1079,19 @@ describe("checks", () => {
10201079 Date.parse(runs.find((r) => r.id === id)!.startedAt!);
10211080 expect(startedAt(newestRun!.id)).toBeLessThan(startedAt(middleRun!.id));
10221081 expect(startedAt(middleRun!.id)).toBeLessThan(startedAt(oldestRun!.id));
1023 }, 30_000);
1082 });
10241083
10251084 test("a check in flight when the forge stops is queued again, and the next forge runs it", async () => {
1026 const slow = await commitToBranch(app, alice, repo, {
1027 message: "A slower check",
1028 files: [
1029 {
1030 path: "forge.json",
1031 // Hangs the first time it runs, passes the second: no wait to tune.
1032 content: checkJson(
1033 `if [ -f '${dataDir}/ran-once' ]; then test -f OK.md; else touch '${dataDir}/ran-once'; sleep 60; fi`,
1034 ),
1035 },
1036 ],
1037 });
1038 if (!slow.ok) throw new Error(JSON.stringify(slow.error));
1039 const oid = slow.value.commitOid;
1040 const deadline = Date.now() + 20_000;
1041 let cut: Run | undefined;
1042 while (Date.now() < deadline && cut?.state !== "running") {
1043 cut = listRuns(app.db, repo.id, { kind: "check", limit: 50 }).find(
1044 (r) => r.commitOid === oid,
1045 );
1046 await Bun.sleep(50);
1047 }
1048 expect(cut?.state).toBe("running");
1085 // Invariant: stopping the service cancels a running check as queued again, attempt + 1, and the next service runs it to the end.
1086 // Hangs the first time it runs, passes the second: no wait to tune.
1087 const id = await slowCheck(
1088 `if [ -f '${dataDir}/ran-once' ]; then test -f OK.md; else touch '${dataDir}/ran-once'; ${held("never")}; fi`,
1089 "A slower check",
1090 );
1091 // Running is said before the command begins: stop only once it has.
1092 await until(() => existsSync(join(dataDir, "ran-once")), "the command begun");
1093 const oid = getRun(app.db, id)!.commitOid;
1094 const cut = getRun(app.db, id);
10491095
10501096 // This forge stops — a release — with the check under way.
10511097 await checks.stop();
@@ -1060,7 +1106,7 @@ describe("checks", () => {
10601106 checks = startChecks(app, { forgeUrl: servers.url, concurrency: 1 });
10611107 const done = await settled((r) => r.id === again!.id);
10621108 expect(done.state).toBe("ok");
1063 }, 60_000);
1109 });
10641110
10651111 async function slowCheck(command: string, message: string): Promise<string> {
10661112 const made = await commitToBranch(app, alice, repo, {
@@ -1069,27 +1115,24 @@ describe("checks", () => {
10691115 });
10701116 if (!made.ok) throw new Error(JSON.stringify(made.error));
10711117 const oid = made.value.commitOid;
1072 const deadline = Date.now() + 20_000;
1073 let found: Run | undefined;
1074 while (Date.now() < deadline && found?.state !== "running") {
1075 found = listRuns(app.db, repo.id, { kind: "check", limit: 50 }).find(
1076 (r) => r.commitOid === oid,
1077 );
1078 await Bun.sleep(50);
1079 }
1080 expect(found?.state).toBe("running");
1081 return found!.id;
1118 await until(() => checksOf(oid)[0]?.state === "running", `${message} running`);
1119 return checksOf(oid)[0]!.id;
10821120 }
10831121
10841122 test("a roll waits for a running check to finish, within its budget, instead of throwing it away", async () => {
1123 // Invariant: stop with a drain budget lets a running check finish to its verdict, leaving nothing queued.
10851124 await checks.stop();
10861125 checks = startChecks(app, {
10871126 forgeUrl: servers.url,
10881127 concurrency: 1,
10891128 drainMs: 30_000,
10901129 });
1091 const id = await slowCheck("sleep 0.5 && test -f OK.md # drained", "A drained check");
1092 await checks.stop();
1130 const id = await slowCheck(held("drained"), "A drained check");
1131 // Let go only once the stop is under way: a stop that did not wait
1132 // would have cut it off already.
1133 const stopping = checks.stop();
1134 release("drained");
1135 await stopping;
10931136 const run = getRun(app.db, id);
10941137 expect(run?.state).toBe("ok");
10951138 expect(
@@ -1098,16 +1141,17 @@ describe("checks", () => {
10981141 ),
10991142 ).toBe(false);
11001143 checks = startChecks(app, { forgeUrl: servers.url, concurrency: 1 });
1101 }, 60_000);
1144 });
11021145
11031146 test("a person's stop cuts a running check off at once, and queues it again", async () => {
1147 // Invariant: stop without drain cancels a running check at once as queued again, and the next service runs it.
11041148 await checks.stop();
11051149 checks = startChecks(app, {
11061150 forgeUrl: servers.url,
11071151 concurrency: 1,
11081152 drainMs: 30_000,
11091153 });
1110 const id = await slowCheck("sleep 0.5 && test -f OK.md # cut off", "A cut-off check");
1154 const id = await slowCheck(held("cut"), "A cut-off check");
11111155 await checks.stop({ drain: false });
11121156 const run = getRun(app.db, id);
11131157 expect(run?.state).toBe("cancelled");
@@ -1116,26 +1160,27 @@ describe("checks", () => {
11161160 (r) => r.commitOid === run?.commitOid && r.state === "queued",
11171161 );
11181162 expect(again).toBeDefined();
1163 release("cut");
11191164 checks = startChecks(app, { forgeUrl: servers.url, concurrency: 1 });
11201165 const done = await settled((r) => r.id === again?.id);
11211166 expect(done.state).toBe("ok");
1122 }, 60_000);
1167 });
11231168
11241169 test("a check whose pull lands while it runs ends as superseded, not failed", async () => {
1125 const id = await slowCheck("sleep 0.5 && false", "A check overtaken");
1170 // Invariant: a running check whose row is cancelled under it logs superseded with the reason, never failed, even when its command fails.
1171 const id = await slowCheck(held("overtaken", "false"), "A check overtaken");
11261172 cancelRun(app.db, id, "pull landed");
1127 const deadline = Date.now() + 20_000;
1128 let lines = "";
1129 while (Date.now() < deadline && !lines.includes("superseded")) {
1130 await Bun.sleep(100);
1131 lines = readRunLog(dataDir, id).lines.join("\n");
1132 }
1173 release("overtaken");
1174 const log = (): string => readRunLog(dataDir, id).lines.join("\n");
1175 await until(() => log().includes("superseded"), "superseded in the log");
1176 const lines = log();
11331177 expect(lines).toContain("superseded: pull landed");
11341178 expect(lines).not.toContain("failed:");
11351179 expect(getRun(app.db, id)?.state).toBe("cancelled");
1136 }, 60_000);
1180 });
11371181
11381182 test("a check that is declared wrong fails with the reason", async () => {
1183 // Invariant: a malformed check declaration fails the run with the parse reason.
11391184 const wrong = await commitToBranch(app, alice, repo, {
11401185 message: "A wrong check",
11411186 files: [{ path: "forge.json", content: '{"check": "bun test"}\n' }],
@@ -1147,7 +1192,9 @@ describe("checks", () => {
11471192 "check failed: forge.json: check must be an object",
11481193 );
11491194 });
1195
11501196 test("an open pull whose head was never checked is checked when the service boots", async () => {
1197 // Invariant: a booting service checks the head of an open pull that has no check, with reason boot.
11511198 await checks.stop();
11521199 // The pull is checked by the base's check, so the base must ask for it.
11531200 const gate = await commitToBranch(app, alice, repo, {
@@ -1184,7 +1231,7 @@ describe("checks", () => {
11841231 const done = await settled((r) => r.commitOid === opened.value.headOid);
11851232 expect(done.reason).toBe("boot");
11861233 expect(done.state).toBe("ok");
1187 }, 60_000);
1234 });
11881235 });
11891236
11901237 describe("a check its forge died under", () => {
@@ -1335,6 +1382,7 @@ describe("a check its forge died under", () => {
13351382
13361383 describe("startChecks: a booting forge removes only checkouts nobody is running (#1884)", () => {
13371384 test("a live run's checkout survives, a lapsed run's and a stranger's go", async () => {
1385 // Invariant: the boot sweep removes checkouts of lapsed or unknown runs and keeps a live run's.
13381386 const dir = await mkdtemp(join(tmpdir(), "forge-sweep-"));
13391387 const db = createApp({ dataDir: dir, port: 0 });
13401388 try {
@@ -1377,6 +1425,7 @@ describe("startChecks: a booting forge removes only checkouts nobody is running
13771425
13781426 describe("checks: a finished shard is announced as the runner writes it (#2889)", () => {
13791427 test("each shard-done line the check prints puts one run.shard on the bus, and nothing else does", async () => {
1428 // Invariant: each `shard N/M done` line a check prints emits one run.shard event for that run.
13801429 const dir = await mkdtemp(join(tmpdir(), "forge-shards-"));
13811430 const shardApp = createApp({ dataDir: dir, port: 0 });
13821431 const shardServers = await serve(shardApp);
@@ -1412,7 +1461,7 @@ describe("checks: a finished shard is announced as the runner writes it (#2889)"
14121461 while (Date.now() < deadline) {
14131462 run = listRuns(shardApp.db, made.value.id, { kind: "check", limit: 5 })[0];
14141463 if (run && (run.state === "ok" || run.state === "failed")) break;
1415 await Bun.sleep(50);
1464 await Bun.sleep(10);
14161465 }
14171466 expect(run?.state).toBe("ok");
14181467 const shards = listEvents(shardApp.db, { repoId: made.value.id, kind: "run.shard" });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs