g1t/services/deployments/src/retries.test.ts
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import { commitMissing, missingCommitMessage, retryDecision, type PastBuild } from "./retries.ts"; | |
| 5 | ||
| 6 | const HOUR = 60 * 60 * 1000; | |
| 7 | const NOW = Date.parse("2026-10-07T12:00:00Z"); | |
| 8 | const GIT = | |
| 9 | "the commit could not be checked out: git -c failed: fatal: reference is not a tree: d2ca224ba0fea430dcc9933cda14d8aa9fa94b4b"; | |
| 10 | ||
| 11 | const build = (hoursAgo: number, over: Partial<PastBuild> = {}): PastBuild => ({ | |
| 12 | status: "failed", | |
| 13 | error: "The build failed: npm run build exited 1.", | |
| 14 | commit_sha: "d2ca224", | |
| 15 | created_at: new Date(NOW - hoursAgo * HOUR).toISOString(), | |
| 16 | ...over, | |
| 17 | }); | |
| 18 | ||
| 19 | test("git's words for a commit that is gone are recognised, and only those", () => { | |
| 20 | assert.equal(commitMissing(GIT), true); | |
| 21 | assert.equal(commitMissing("fatal: remote error: upload-pack: not our ref d2ca224"), true); | |
| 22 | assert.equal(commitMissing(missingCommitMessage({ kind: "preview", number: 1 })), true); | |
| 23 | assert.equal(commitMissing("The build failed: npm run build exited 1."), false); | |
| 24 | assert.equal(commitMissing(null), false); | |
| 25 | }); | |
| 26 | ||
| 27 | test("a commit that is gone says so plainly, naming the pull request", () => { | |
| 28 | assert.equal( | |
| 29 | missingCommitMessage({ kind: "preview", number: 1 }), | |
| 30 | "This pull request's commit no longer exists. Push again, or close pull request #1.", | |
| 31 | ); | |
| 32 | assert.match(missingCommitMessage({ kind: "preview", number: null }), /branch's commit no longer exists/); | |
| 33 | assert.match(missingCommitMessage({ kind: "production", number: null }), /default branch/); | |
| 34 | }); | |
| 35 | ||
| 36 | test("nothing built yet, or the last build went up long ago: build", () => { | |
| 37 | assert.deepEqual(retryDecision([], NOW, HOUR, { backoff: true }), { kind: "build" }); | |
| 38 | assert.deepEqual(retryDecision([build(3, { status: "ready", error: null })], NOW, HOUR, { backoff: true }), { kind: "build" }); | |
| 39 | }); | |
| 40 | ||
| 41 | test("a commit that is gone is never tried again, with or without backoff", () => { | |
| 42 | const history = [build(30, { error: GIT })]; | |
| 43 | assert.deepEqual(retryDecision(history, NOW, HOUR, { backoff: true }), { kind: "stop", reason: "missing_commit" }); | |
| 44 | assert.deepEqual(retryDecision(history, NOW, HOUR), { kind: "stop", reason: "missing_commit" }); | |
| 45 | }); | |
| 46 | ||
| 47 | test("each identical failure doubles the wait", () => { | |
| 48 | // One failure: an hour. | |
| 49 | assert.equal(retryDecision([build(0.5)], NOW, HOUR, { backoff: true }).kind, "wait"); | |
| 50 | assert.equal(retryDecision([build(1)], NOW, HOUR, { backoff: true }).kind, "build"); | |
| 51 | // Two in a row: two hours after the last. | |
| 52 | const two = [build(1.5), build(3)]; | |
| 53 | assert.deepEqual(retryDecision(two, NOW, HOUR, { backoff: true }), { kind: "wait", until: NOW + 0.5 * HOUR }); | |
| 54 | assert.equal(retryDecision([build(2), build(4)], NOW, HOUR, { backoff: true }).kind, "build"); | |
| 55 | }); | |
| 56 | ||
| 57 | test("three identical failures in a row stop it", () => { | |
| 58 | const history = [build(10), build(12), build(14)]; | |
| 59 | assert.deepEqual(retryDecision(history, NOW, HOUR, { backoff: true }), { kind: "stop", reason: "failing" }); | |
| 60 | assert.deepEqual(retryDecision(history, NOW, HOUR), { kind: "stop", reason: "failing" }); | |
| 61 | }); | |
| 62 | ||
| 63 | test("a different error or commit starts the count again", () => { | |
| 64 | const otherError = [build(10), build(12), build(14, { error: "Cloudflare did not take the app: 500" })]; | |
| 65 | assert.equal(retryDecision(otherError, NOW, HOUR, { backoff: true }).kind, "build"); | |
| 66 | const otherCommit = [build(10), build(12, { commit_sha: "8e500b1" }), build(14, { commit_sha: "8e500b1" })]; | |
| 67 | assert.equal(retryDecision(otherCommit, NOW, HOUR, { backoff: true }).kind, "build"); | |
| 68 | // A build that went up in between ends the run. | |
| 69 | const recovered = [build(10), build(12, { status: "ready", error: null }), build(14)]; | |
| 70 | assert.equal(retryDecision(recovered, NOW, HOUR, { backoff: true }).kind, "build"); | |
| 71 | }); | |
| 72 | ||
| 73 | test("a refused or skipped rebuild waits the base delay with backoff, not without", () => { | |
| 74 | const skipped = [build(0.5, { status: "skipped", error: "Over the limit." })]; | |
| 75 | assert.equal(retryDecision(skipped, NOW, HOUR, { backoff: true }).kind, "wait"); | |
| 76 | assert.equal(retryDecision(skipped, NOW, HOUR).kind, "build"); | |
| 77 | }); |