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.
| Billing: AI Gateway's analytics are read with the token that can see them, and a gateway that priced nothing is said | 1 | /** |
| 2 | * The whole service, on Node, following a repository transfer: a D1 made | |
| 3 | * of node:sqlite with the service's migrations, the other services as | |
| 4 | * fakes that answer by method, and Cloudflare's API as a recorded fetch. | |
| 5 | * | |
| 6 | * Run with `--experimental-transform-types` (see package.json): the | |
| 7 | * service's own modules import without extensions, so a resolve hook adds | |
| 8 | * them. | |
| 9 | */ | |
| 10 | ||
| 11 | import assert from "node:assert/strict"; | |
| 12 | import { readdirSync, readFileSync } from "node:fs"; | |
| 13 | import { registerHooks } from "node:module"; | |
| 14 | import { DatabaseSync } from "node:sqlite"; | |
| 15 | import { beforeEach, test } from "node:test"; | |
| 16 | ||
| 17 | registerHooks({ | |
| 18 | resolve(specifier, context, next) { | |
| 19 | try { | |
| 20 | return next(specifier, context); | |
| 21 | } catch (error) { | |
| 22 | if (specifier.startsWith(".")) return next(`${specifier}.ts`, context); | |
| 23 | throw error; | |
| 24 | } | |
| 25 | }, | |
| 26 | }); | |
| 27 | const { default: worker } = await import("./index.ts"); | |
| 28 | ||
| 29 | // ---- D1, on node:sqlite ------------------------------------------------ | |
| 30 | ||
| 31 | function d1(db: DatabaseSync) { | |
| 32 | const value = (v: unknown) => (v === undefined ? null : typeof v === "boolean" ? Number(v) : v); | |
| 33 | const statement = (sql: string, params: unknown[] = []) => ({ | |
| 34 | sql, | |
| 35 | params, | |
| 36 | bind: (...args: unknown[]) => statement(sql, args.map(value)), | |
| 37 | async first<T>() { | |
| 38 | return (db.prepare(sql).get(...(params as never[])) as T) ?? null; | |
| 39 | }, | |
| 40 | async all<T>() { | |
| 41 | return { results: db.prepare(sql).all(...(params as never[])) as T[], success: true }; | |
| 42 | }, | |
| 43 | async run() { | |
| 44 | const r = db.prepare(sql).run(...(params as never[])); | |
| 45 | return { success: true, meta: { changes: Number(r.changes) } }; | |
| 46 | }, | |
| 47 | }); | |
| 48 | return { | |
| 49 | prepare: (sql: string) => statement(sql), | |
| 50 | async batch(statements: ReturnType<typeof statement>[]) { | |
| 51 | db.exec("BEGIN"); | |
| 52 | try { | |
| 53 | const out = statements.map((s) => ({ results: db.prepare(s.sql).all(...(s.params as never[])) })); | |
| 54 | db.exec("COMMIT"); | |
| 55 | return out; | |
| 56 | } catch (error) { | |
| 57 | db.exec("ROLLBACK"); | |
| 58 | throw error; | |
| 59 | } | |
| 60 | }, | |
| 61 | }; | |
| 62 | } | |
| 63 | ||
| 64 | // ---- The world ------------------------------------------------------------ | |
| 65 | ||
| 66 | const API = "rep_api"; | |
| 67 | const PROJECT = "prj_api"; | |
| 68 | ||
| 69 | type World = ReturnType<typeof world>; | |
| 70 | ||
| 71 | function world() { | |
| 72 | const sqlite = new DatabaseSync(":memory:"); | |
| 73 | const dir = new URL("../migrations/", import.meta.url); | |
| 74 | for (const file of readdirSync(dir).sort()) sqlite.exec(readFileSync(new URL(file, dir), "utf8")); | |
| 75 | const state = { | |
| 76 | sqlite, | |
| 77 | /** Where the repository is now. */ | |
| 78 | path: { namespace: "flagon-io", name: "lab-api" }, | |
| 79 | /** Where the projects service has the project now. */ | |
| 80 | projectWorkspace: "flagon-io", | |
| 81 | /** Each workspace's usage limit state. */ | |
| 82 | limits: new Map<string, string>([["syntaqx", "stopped"]]), | |
| 83 | runnerUp: true, | |
| 84 | /** Builds the runner was asked for: deploy id and token. */ | |
| 85 | builds: [] as { deployId: string; token: string; workspace: string }[], | |
| 86 | /** Cloudflare API calls, as `METHOD path`. */ | |
| 87 | cloudflare: [] as string[], | |
| 88 | kv: new Map<string, { value: string; expiration?: number }>(), | |
| A preview is live only while what it previews is: a branch's preview (one with no pull request) comes down when its branch is deleted, and the sweep takes down any whose branch is already gone. A pull request's preview has always come down when it closes or merges, but a branch's had nothing to take it down, so the previews that preview stacks built for another project's pull request, before projects stopped depending on each other (a9916137), stayed up and showed as a live Preview on projects with no pull requests at all, as flagon-io/automation-lab's v2 did. Moving a project no longer builds such a preview again (rebuildStack is gone with the stacks). A branch's preview from a project's deploy workflow stays while its branch is there, and one whose branches can't be read stays until they can. Checked: the deployments service's typecheck and its 88 tests (new: the sweep takes down a preview of a deleted branch and marks its deployment inactive while keeping a pull request's and a live branch's, and git.deleted takes a branch's preview down at once). | 89 | /** The repository's branches. */ |
| 90 | branches: ["main", "v2"], | |
| Billing: AI Gateway's analytics are read with the token that can see them, and a gateway that priced nothing is said | 91 | }; |
| 92 | const project = () => ({ | |
| 93 | id: PROJECT, | |
| 94 | workspace: state.projectWorkspace, | |
| 95 | slug: "lab-api", | |
| 96 | name: "lab-api", | |
| 97 | primary: true, | |
| 98 | source: { kind: "hosted", repoId: API, repo: { ...state.path }, defaultBranch: "main", rootDir: null }, | |
| 99 | }); | |
| 100 | const ok = (value: unknown) => ({ ok: true, value }); | |
| 101 | const service = (answer: (method: string, args: any) => unknown) => ({ | |
| 102 | async fetch(input: string | Request, init?: RequestInit) { | |
| 103 | const url = typeof input === "string" ? input : input.url; | |
| 104 | const method = new URL(url).pathname.split("/").pop()!; | |
| 105 | const args = init?.body ? JSON.parse(String(init.body)) : {}; | |
| 106 | const answered = await answer(method, args); | |
| 107 | if (answered instanceof Response) return answered; | |
| 108 | return Response.json(answered ?? ok(null)); | |
| 109 | }, | |
| 110 | }); | |
| 111 | const env = { | |
| 112 | DB: d1(sqlite), | |
| 113 | CLOUDFLARE_API_TOKEN: "test", | |
| 114 | CLOUDFLARE_ACCOUNT_ID: "acct", | |
| 115 | DISPATCH_NAMESPACE: "g1t-deployments", | |
| 116 | SITE: "https://g1t.sh", | |
| 117 | DOMAINS: { | |
| 118 | async get(key: string) { | |
| 119 | const found = state.kv.get(key); | |
| 120 | return found ? JSON.parse(found.value) : null; | |
| 121 | }, | |
| 122 | async put(key: string, value: string, options?: { expiration?: number }) { | |
| 123 | state.kv.set(key, { value, expiration: options?.expiration }); | |
| 124 | }, | |
| 125 | async delete(key: string) { | |
| 126 | state.kv.delete(key); | |
| 127 | }, | |
| 128 | }, | |
| 129 | REPOS: service((method) => { | |
| 130 | if (method === "path_by_id") return state.path; | |
| 131 | if (method === "get") return ok({ isPrivate: true }); | |
| A preview is live only while what it previews is: a branch's preview (one with no pull request) comes down when its branch is deleted, and the sweep takes down any whose branch is already gone. A pull request's preview has always come down when it closes or merges, but a branch's had nothing to take it down, so the previews that preview stacks built for another project's pull request, before projects stopped depending on each other (a9916137), stayed up and showed as a live Preview on projects with no pull requests at all, as flagon-io/automation-lab's v2 did. Moving a project no longer builds such a preview again (rebuildStack is gone with the stacks). A branch's preview from a project's deploy workflow stays while its branch is there, and one whose branches can't be read stays until they can. Checked: the deployments service's typecheck and its 88 tests (new: the sweep takes down a preview of a deleted branch and marks its deployment inactive while keeping a pull request's and a live branch's, and git.deleted takes a branch's preview down at once). | 132 | if (method === "branches") return ok(state.branches.map((name) => ({ name, hash: `${name}head` }))); |
| Billing: AI Gateway's analytics are read with the token that can see them, and a gateway that priced nothing is said | 133 | return ok([]); |
| 134 | }), | |
| 135 | PROJECTS: service((method) => { | |
| 136 | if (method === "by_repo") return [project()]; | |
| 137 | return ok(project()); | |
| 138 | }), | |
| 139 | IDENTITY: service((method, args) => { | |
| 140 | if (method === "get_workspace") return { id: `wsp_${args.slug}`, slug: args.slug }; | |
| 141 | if (method === "collaborator_permission") return ok({ role: "write" }); | |
| 142 | return ok(null); | |
| 143 | }), | |
| 144 | BILLING: service((method, args) => { | |
| 145 | if (method === "has_feature") return ok(true); | |
| 146 | if (method === "check_limit") return ok({ state: state.limits.get(args.workspace) ?? "ok" }); | |
| 147 | if (method === "entitlements") return ok({ plan: "paid" }); | |
| 148 | if (method === "reserve") return ok({ id: "res_1", paidBy: "credit" }); | |
| 149 | if (method === "prices") return { prices: [] }; | |
| 150 | return ok(null); | |
| 151 | }), | |
| 152 | WORK: service((method) => { | |
| 153 | if (method === "get_pull") { | |
| 154 | return ok({ pull: { status: "open", headCommit: "pr1head", branch: "v2", author: { username: "syntaqx", kind: "user" } } }); | |
| 155 | } | |
| 156 | return ok(null); | |
| 157 | }), | |
| 158 | ACTIONS: service(() => ({ secrets: {}, variables: {} })), | |
| 159 | RUNNER: service((method, args) => { | |
| 160 | if (!state.runnerUp) return new Response("unavailable", { status: 503 }); | |
| 161 | if (method === "start_deploy") state.builds.push({ deployId: args.deployId, token: args.token, workspace: args.workspace }); | |
| 162 | return ok(true); | |
| 163 | }), | |
| 164 | }; | |
| 165 | return { state, env }; | |
| 166 | } | |
| 167 | ||
| 168 | let w: World; | |
| 169 | const realFetch = globalThis.fetch; | |
| 170 | ||
| 171 | beforeEach(() => { | |
| 172 | w = world(); | |
| 173 | // Cloudflare's API: every call succeeds, and none lists anything. | |
| 174 | globalThis.fetch = (async (input: string | Request, init?: RequestInit) => { | |
| 175 | const url = new URL(typeof input === "string" ? input : input.url); | |
| 176 | w.state.cloudflare.push(`${init?.method ?? "GET"} ${url.pathname}`); | |
| 177 | if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } }); | |
| 178 | return Response.json({ success: true, result: [], result_info: { total_pages: 1 } }); | |
| 179 | }) as typeof fetch; | |
| 180 | // As production was on 2026-10-06: the project's rows still in syntaqx, | |
| 181 | // its apps paused there by syntaqx's limit, minutes before the transfer. | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 182 | // Previews are kept ten years idle, so the sweep never takes one down |
| 183 | // as idle whatever today's date is. | |
| Billing: AI Gateway's analytics are read with the token that can see them, and a gateway that priced nothing is said | 184 | const db = w.state.sqlite; |
| 185 | db.prepare( | |
| 186 | `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at) | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 187 | VALUES (?, ?, 'syntaqx', 'lab-api', 1, 1, 1, 3650, '2026-10-05T00:00:00Z')`, |
| Billing: AI Gateway's analytics are read with the token that can see them, and a gateway that priced nothing is said | 188 | ).run(PROJECT, API); |
| 189 | const app = db.prepare( | |
| 190 | `INSERT INTO apps (script, project_id, workspace, slug, kind, branch, number, commit_sha, deployed_at, created_at, paused_at) | |
| 191 | VALUES (?, ?, 'syntaqx', 'lab-api', ?, ?, ?, ?, '2026-10-05T00:26:29Z', '2026-10-05T00:26:29Z', '2026-10-06T01:20:54Z')`, | |
| 192 | ); | |
| 193 | app.run("lab-api-syntaqx", PROJECT, "production", null, null, "8e500b1"); | |
| 194 | app.run("lab-api-git-v2-syntaqx", PROJECT, "preview", "v2", 1, "d2ca224"); | |
| 195 | const deployment = db.prepare( | |
| 196 | `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status, | |
| 197 | trusted, created_by, created_at, finished_at) | |
| 198 | VALUES (?, ?, 'syntaqx', 'lab-api', ?, 'syntaqx/lab-api', ?, ?, ?, ?, ?, 'ready', 1, 'g1t', '2026-10-05T00:26:00Z', '2026-10-05T00:26:29Z')`, | |
| 199 | ); | |
| 200 | deployment.run("dpl_prod", PROJECT, API, "production", null, null, "8e500b1", "lab-api-syntaqx"); | |
| 201 | deployment.run("dpl_prev", PROJECT, API, "preview", "v2", 1, "d2ca224", "lab-api-git-v2-syntaqx"); | |
| 202 | }); | |
| 203 | ||
| 204 | process.on("exit", () => { | |
| 205 | globalThis.fetch = realFetch; | |
| 206 | }); | |
| 207 | ||
| 208 | const transferred = { | |
| 209 | id: "evt_1", | |
| 210 | type: "repo.transferred", | |
| 211 | source: "repos", | |
| 212 | time: "2026-10-06T01:23:38Z", | |
| 213 | repoId: API, | |
| 214 | actor: "usr_1", | |
| 215 | data: { from: "syntaqx", name: "lab-api", repoId: API, to: "flagon-io" }, | |
| 216 | }; | |
| 217 | ||
| 218 | /** Delivers the event as the queue would; whether it was acked. */ | |
| 219 | async function deliver(event: unknown, attempts = 1): Promise<boolean> { | |
| 220 | let acked = false; | |
| 221 | let retried = false; | |
| 222 | const message = { body: event, attempts, ack: () => (acked = true), retry: () => (retried = true) }; | |
| 223 | await worker.queue({ messages: [message] } as never, w.env as never); | |
| 224 | assert.notEqual(acked, retried); | |
| 225 | return acked; | |
| 226 | } | |
| 227 | ||
| 228 | const rows = (sql: string, ...params: unknown[]) => | |
| 229 | (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row })); | |
| 230 | ||
| 231 | test("a transfer rebuilds every app under the new workspace, paused ones too", async () => { | |
| 232 | assert.equal(await deliver(transferred), true); | |
| 233 | assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]); | |
| 234 | const queued = rows("SELECT script, kind, commit_sha FROM deployments WHERE status = 'queued' ORDER BY script"); | |
| 235 | assert.deepEqual(queued, [ | |
| 236 | { script: "lab-api-flagonio", kind: "production", commit_sha: "8e500b1" }, | |
| 237 | { script: "lab-api-git-v2-flagonio", kind: "preview", commit_sha: "pr1head" }, | |
| 238 | ]); | |
| 239 | assert.equal(w.state.builds.length, 2); | |
| 240 | assert.ok(w.state.builds.every((b) => b.workspace === "flagon-io")); | |
| 241 | }); | |
| 242 | ||
| 243 | test("a second delivery builds nothing more", async () => { | |
| 244 | await deliver(transferred); | |
| 245 | assert.equal(await deliver(transferred, 2), true); | |
| 246 | assert.equal(w.state.builds.length, 2); | |
| 247 | assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2); | |
| 248 | }); | |
| 249 | ||
| 250 | test("a rebuild that could not be queued is not acked, and the next delivery queues it", async () => { | |
| 251 | w.state.runnerUp = false; | |
| 252 | assert.equal(await deliver(transferred), false); | |
| 253 | // The rows moved, but nothing is building. | |
| 254 | assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]); | |
| 255 | assert.equal(rows("SELECT id FROM deployments WHERE status IN ('queued', 'building')").length, 0); | |
| 256 | w.state.runnerUp = true; | |
| 257 | assert.equal(await deliver(transferred, 2), true); | |
| 258 | assert.deepEqual( | |
| 259 | rows("SELECT script FROM deployments WHERE status = 'queued' ORDER BY script").map((r) => r.script), | |
| 260 | ["lab-api-flagonio", "lab-api-git-v2-flagonio"], | |
| 261 | ); | |
| 262 | }); | |
| 263 | ||
| 264 | test("the sweep picks up a move whose deliveries ran out, keyed on the new workspace", async () => { | |
| 265 | w.state.runnerUp = false; | |
| 266 | await deliver(transferred); | |
| 267 | w.state.runnerUp = true; | |
| 268 | // A failed attempt was just made: the sweep waits before trying again. | |
| 269 | await worker.scheduled({} as never, w.env as never); | |
| 270 | assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 0); | |
| 271 | w.state.sqlite.exec("UPDATE deployments SET created_at = '2026-10-06T00:00:00Z' WHERE status = 'failed'"); | |
| 272 | await worker.scheduled({} as never, w.env as never); | |
| 273 | assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2); | |
| 274 | // syntaqx is over its limit, flagon-io is not: nothing of the project's is | |
| 275 | // paused for syntaqx's sake, and nothing new was paused at all. | |
| 276 | assert.ok(!w.state.cloudflare.some((call) => call.startsWith("PUT") && /scripts\/lab-api-flagonio/.test(call))); | |
| 277 | }); | |
| 278 | ||
| 279 | test("a move waits, without refused deployments, while the new workspace is over its limit", async () => { | |
| 280 | w.state.limits.set("flagon-io", "stopped"); | |
| 281 | assert.equal(await deliver(transferred), true); | |
| 282 | assert.equal(rows("SELECT id FROM deployments WHERE created_at > '2026-10-06'").length, 0); | |
| 283 | w.state.limits.delete("flagon-io"); | |
| 284 | await worker.scheduled({} as never, w.env as never); | |
| 285 | assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2); | |
| 286 | }); | |
| 287 | ||
| 288 | test("once the new app is live, the old address redirects to it, paused or not", async () => { | |
| 289 | await deliver(transferred); | |
| 290 | const build = w.state.builds.find((b) => rows("SELECT script FROM deployments WHERE id = ?", b.deployId)[0].script === "lab-api-flagonio")!; | |
| 291 | const finish = await worker.fetch( | |
| 292 | new Request(`https://deployments/jobs/${build.deployId}/finish`, { | |
| 293 | method: "POST", | |
| 294 | body: JSON.stringify({ token: build.token, worker: { mainModule: "index.js", modules: [{ name: "index.js", contentBase64: btoa("export default {}"), contentType: "application/javascript+module" }] }, buildSeconds: 30 }), | |
| 295 | }), | |
| 296 | w.env as never, | |
| 297 | {} as never, | |
| 298 | ); | |
| 299 | assert.deepEqual(await finish.json(), { ok: true, value: true }); | |
| 300 | assert.deepEqual( | |
| 301 | rows("SELECT script, workspace FROM apps WHERE kind = 'production'"), | |
| 302 | [{ script: "lab-api-flagonio", workspace: "flagon-io" }], | |
| 303 | ); | |
| 304 | const [redirect] = rows("SELECT script, target, workspace FROM redirects"); | |
| 305 | assert.deepEqual({ ...redirect }, { script: "lab-api-syntaqx", target: "lab-api-flagonio.g1t.page", workspace: "flagon-io" }); | |
| 306 | // The dispatcher's entry, followed before the old (paused) script runs. | |
| 307 | const entry = w.state.kv.get("lab-api-syntaqx.g1t.page")!; | |
| 308 | assert.deepEqual(JSON.parse(entry.value), { script: "lab-api-syntaqx", redirect: "lab-api-flagonio.g1t.page" }); | |
| 309 | assert.ok(entry.expiration! > Date.now() / 1000 + 89 * 24 * 3600); | |
| 310 | // And the old script itself is the redirect too. | |
| 311 | assert.ok(w.state.cloudflare.some((call) => call === "PUT /client/v4/accounts/acct/workers/dispatch/namespaces/g1t-deployments/scripts/lab-api-syntaqx")); | |
| 312 | // The preview is still to come; the old one stays until it is live. | |
| 313 | assert.equal(rows("SELECT script FROM apps WHERE kind = 'preview'")[0].script, "lab-api-git-v2-syntaqx"); | |
| 314 | }); | |
| 315 | ||
| 316 | test("the sweep never pauses a moved app for its old workspace's limit", async () => { | |
| 317 | // Rows moved (as the event does) but no rebuild yet, and the apps not paused. | |
| 318 | w.state.sqlite.exec("UPDATE settings SET workspace = 'flagon-io'; UPDATE apps SET paused_at = NULL"); | |
| 319 | w.state.runnerUp = false; | |
| 320 | await worker.scheduled({} as never, w.env as never); | |
| 321 | assert.deepEqual(rows("SELECT paused_at FROM apps").map((r) => r.paused_at), [null, null]); | |
| 322 | // When the project's own workspace is over its limit, they are paused. | |
| 323 | w.state.limits.set("flagon-io", "stopped"); | |
| 324 | await worker.scheduled({} as never, w.env as never); | |
| 325 | assert.ok(rows("SELECT paused_at FROM apps").every((r) => r.paused_at)); | |
| 326 | }); | |
| A preview is live only while what it previews is: a branch's preview (one with no pull request) comes down when its branch is deleted, and the sweep takes down any whose branch is already gone. A pull request's preview has always come down when it closes or merges, but a branch's had nothing to take it down, so the previews that preview stacks built for another project's pull request, before projects stopped depending on each other (a9916137), stayed up and showed as a live Preview on projects with no pull requests at all, as flagon-io/automation-lab's v2 did. Moving a project no longer builds such a preview again (rebuildStack is gone with the stacks). A branch's preview from a project's deploy workflow stays while its branch is there, and one whose branches can't be read stays until they can. Checked: the deployments service's typecheck and its 88 tests (new: the sweep takes down a preview of a deleted branch and marks its deployment inactive while keeping a pull request's and a live branch's, and git.deleted takes a branch's preview down at once). | 327 | |
| 328 | test("a branch's preview comes down once its branch is gone, and a pull request's stays", async () => { | |
| 329 | // Branch previews have no pull request: one the deploy workflow built for | |
| 330 | // a branch that is still there, and one a preview stack built (before | |
| 331 | // projects stopped depending on each other) for a branch since deleted. | |
| 332 | const app = w.state.sqlite.prepare( | |
| 333 | `INSERT INTO apps (script, project_id, workspace, slug, kind, branch, number, commit_sha, deployed_at, created_at) | |
| 334 | VALUES (?, ?, 'syntaqx', 'lab-api', 'preview', ?, NULL, 'abc1234', '2026-10-06T22:01:00Z', '2026-10-06T22:01:00Z')`, | |
| 335 | ); | |
| 336 | app.run("lab-api-git-feature-syntaqx", PROJECT, "feature"); | |
| 337 | app.run("lab-api-git-stack-syntaqx", PROJECT, "stack"); | |
| 338 | w.state.sqlite | |
| 339 | .prepare( | |
| 340 | `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status, | |
| 341 | trusted, created_by, created_at, finished_at) | |
| 342 | VALUES ('dpl_stack', ?, 'syntaqx', 'lab-api', ?, 'syntaqx/lab-api', 'preview', 'stack', NULL, 'abc1234', 'lab-api-git-stack-syntaqx', 'ready', 1, 'g1t', '2026-10-06T22:00:00Z', '2026-10-06T22:01:00Z')`, | |
| 343 | ) | |
| 344 | .run(PROJECT, API); | |
| 345 | w.state.branches.push("feature"); | |
| 346 | await worker.scheduled({} as never, w.env as never); | |
| 347 | const previews = () => rows("SELECT script FROM apps WHERE kind = 'preview' ORDER BY script").map((r) => r.script); | |
| 348 | assert.deepEqual(previews(), ["lab-api-git-feature-syntaqx", "lab-api-git-v2-syntaqx"]); | |
| 349 | assert.equal(rows("SELECT status FROM deployments WHERE id = 'dpl_stack'")[0].status, "down"); | |
| 350 | // Deleting the branch takes its preview down at once. | |
| 351 | w.state.branches = w.state.branches.filter((name) => name !== "feature"); | |
| 352 | const deleted = { ...transferred, id: "evt_2", type: "git.deleted", data: { repoId: API, ref: "refs/heads/feature", before: "abc1234" } }; | |
| 353 | assert.equal(await deliver(deleted), true); | |
| 354 | assert.deepEqual(previews(), ["lab-api-git-v2-syntaqx"]); | |
| 355 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.