Skip to content
321 linesCodeBlameRaw
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
11import assert from "node:assert/strict";
12import { readdirSync, readFileSync } from "node:fs";
13import { registerHooks } from "node:module";
14import { DatabaseSync } from "node:sqlite";
15import { beforeEach, test } from "node:test";
16
17registerHooks({
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});
27const { default: worker } = await import("./index.ts");
28
29// ---- D1, on node:sqlite ------------------------------------------------
30
31function 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
66const API = "rep_api";
67const PROJECT = "prj_api";
68
69type World = ReturnType<typeof world>;
70
71function 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 }>(),
89 };
90 const project = () => ({
91 id: PROJECT,
92 workspace: state.projectWorkspace,
93 slug: "lab-api",
94 name: "lab-api",
95 primary: true,
96 source: { kind: "hosted", repoId: API, repo: { ...state.path }, defaultBranch: "main", rootDir: null },
97 });
98 const ok = (value: unknown) => ({ ok: true, value });
99 const service = (answer: (method: string, args: any) => unknown) => ({
100 async fetch(input: string | Request, init?: RequestInit) {
101 const url = typeof input === "string" ? input : input.url;
102 const method = new URL(url).pathname.split("/").pop()!;
103 const args = init?.body ? JSON.parse(String(init.body)) : {};
104 const answered = await answer(method, args);
105 if (answered instanceof Response) return answered;
106 return Response.json(answered ?? ok(null));
107 },
108 });
109 const env = {
110 DB: d1(sqlite),
111 CLOUDFLARE_API_TOKEN: "test",
112 CLOUDFLARE_ACCOUNT_ID: "acct",
113 DISPATCH_NAMESPACE: "g1t-deployments",
114 SITE: "https://g1t.sh",
115 DOMAINS: {
116 async get(key: string) {
117 const found = state.kv.get(key);
118 return found ? JSON.parse(found.value) : null;
119 },
120 async put(key: string, value: string, options?: { expiration?: number }) {
121 state.kv.set(key, { value, expiration: options?.expiration });
122 },
123 async delete(key: string) {
124 state.kv.delete(key);
125 },
126 },
127 REPOS: service((method) => {
128 if (method === "path_by_id") return state.path;
129 if (method === "get") return ok({ isPrivate: true });
130 return ok([]);
131 }),
132 PROJECTS: service((method) => {
133 if (method === "by_repo") return [project()];
134 return ok(project());
135 }),
136 IDENTITY: service((method, args) => {
137 if (method === "get_workspace") return { id: `wsp_${args.slug}`, slug: args.slug };
138 if (method === "collaborator_permission") return ok({ role: "write" });
139 return ok(null);
140 }),
141 BILLING: service((method, args) => {
142 if (method === "has_feature") return ok(true);
143 if (method === "check_limit") return ok({ state: state.limits.get(args.workspace) ?? "ok" });
144 if (method === "entitlements") return ok({ plan: "paid" });
145 if (method === "reserve") return ok({ id: "res_1", paidBy: "credit" });
146 if (method === "prices") return { prices: [] };
147 return ok(null);
148 }),
149 WORK: service((method) => {
150 if (method === "get_pull") {
151 return ok({ pull: { status: "open", headCommit: "pr1head", branch: "v2", author: { username: "syntaqx", kind: "user" } } });
152 }
153 return ok(null);
154 }),
155 ACTIONS: service(() => ({ secrets: {}, variables: {} })),
156 RUNNER: service((method, args) => {
157 if (!state.runnerUp) return new Response("unavailable", { status: 503 });
158 if (method === "start_deploy") state.builds.push({ deployId: args.deployId, token: args.token, workspace: args.workspace });
159 return ok(true);
160 }),
161 };
162 return { state, env };
163}
164
165let w: World;
166const realFetch = globalThis.fetch;
167
168beforeEach(() => {
169 w = world();
170 // Cloudflare's API: every call succeeds, and none lists anything.
171 globalThis.fetch = (async (input: string | Request, init?: RequestInit) => {
172 const url = new URL(typeof input === "string" ? input : input.url);
173 w.state.cloudflare.push(`${init?.method ?? "GET"} ${url.pathname}`);
174 if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } });
175 return Response.json({ success: true, result: [], result_info: { total_pages: 1 } });
176 }) as typeof fetch;
177 // As production was on 2026-10-06: the project's rows still in syntaqx,
178 // its apps paused there by syntaqx's limit, minutes before the transfer.
179 const db = w.state.sqlite;
180 db.prepare(
181 `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at)
182 VALUES (?, ?, 'syntaqx', 'lab-api', 1, 1, 1, 7, '2026-10-05T00:00:00Z')`,
183 ).run(PROJECT, API);
184 const app = db.prepare(
185 `INSERT INTO apps (script, project_id, workspace, slug, kind, branch, number, commit_sha, deployed_at, created_at, paused_at)
186 VALUES (?, ?, 'syntaqx', 'lab-api', ?, ?, ?, ?, '2026-10-05T00:26:29Z', '2026-10-05T00:26:29Z', '2026-10-06T01:20:54Z')`,
187 );
188 app.run("lab-api-syntaqx", PROJECT, "production", null, null, "8e500b1");
189 app.run("lab-api-git-v2-syntaqx", PROJECT, "preview", "v2", 1, "d2ca224");
190 const deployment = db.prepare(
191 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
192 trusted, created_by, created_at, finished_at)
193 VALUES (?, ?, 'syntaqx', 'lab-api', ?, 'syntaqx/lab-api', ?, ?, ?, ?, ?, 'ready', 1, 'g1t', '2026-10-05T00:26:00Z', '2026-10-05T00:26:29Z')`,
194 );
195 deployment.run("dpl_prod", PROJECT, API, "production", null, null, "8e500b1", "lab-api-syntaqx");
196 deployment.run("dpl_prev", PROJECT, API, "preview", "v2", 1, "d2ca224", "lab-api-git-v2-syntaqx");
197});
198
199process.on("exit", () => {
200 globalThis.fetch = realFetch;
201});
202
203const transferred = {
204 id: "evt_1",
205 type: "repo.transferred",
206 source: "repos",
207 time: "2026-10-06T01:23:38Z",
208 repoId: API,
209 actor: "usr_1",
210 data: { from: "syntaqx", name: "lab-api", repoId: API, to: "flagon-io" },
211};
212
213/** Delivers the event as the queue would; whether it was acked. */
214async function deliver(event: unknown, attempts = 1): Promise<boolean> {
215 let acked = false;
216 let retried = false;
217 const message = { body: event, attempts, ack: () => (acked = true), retry: () => (retried = true) };
218 await worker.queue({ messages: [message] } as never, w.env as never);
219 assert.notEqual(acked, retried);
220 return acked;
221}
222
223const rows = (sql: string, ...params: unknown[]) =>
224 (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
225
226test("a transfer rebuilds every app under the new workspace, paused ones too", async () => {
227 assert.equal(await deliver(transferred), true);
228 assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]);
229 const queued = rows("SELECT script, kind, commit_sha FROM deployments WHERE status = 'queued' ORDER BY script");
230 assert.deepEqual(queued, [
231 { script: "lab-api-flagonio", kind: "production", commit_sha: "8e500b1" },
232 { script: "lab-api-git-v2-flagonio", kind: "preview", commit_sha: "pr1head" },
233 ]);
234 assert.equal(w.state.builds.length, 2);
235 assert.ok(w.state.builds.every((b) => b.workspace === "flagon-io"));
236});
237
238test("a second delivery builds nothing more", async () => {
239 await deliver(transferred);
240 assert.equal(await deliver(transferred, 2), true);
241 assert.equal(w.state.builds.length, 2);
242 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2);
243});
244
245test("a rebuild that could not be queued is not acked, and the next delivery queues it", async () => {
246 w.state.runnerUp = false;
247 assert.equal(await deliver(transferred), false);
248 // The rows moved, but nothing is building.
249 assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]);
250 assert.equal(rows("SELECT id FROM deployments WHERE status IN ('queued', 'building')").length, 0);
251 w.state.runnerUp = true;
252 assert.equal(await deliver(transferred, 2), true);
253 assert.deepEqual(
254 rows("SELECT script FROM deployments WHERE status = 'queued' ORDER BY script").map((r) => r.script),
255 ["lab-api-flagonio", "lab-api-git-v2-flagonio"],
256 );
257});
258
259test("the sweep picks up a move whose deliveries ran out, keyed on the new workspace", async () => {
260 w.state.runnerUp = false;
261 await deliver(transferred);
262 w.state.runnerUp = true;
263 // A failed attempt was just made: the sweep waits before trying again.
264 await worker.scheduled({} as never, w.env as never);
265 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 0);
266 w.state.sqlite.exec("UPDATE deployments SET created_at = '2026-10-06T00:00:00Z' WHERE status = 'failed'");
267 await worker.scheduled({} as never, w.env as never);
268 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2);
269 // syntaqx is over its limit, flagon-io is not: nothing of the project's is
270 // paused for syntaqx's sake, and nothing new was paused at all.
271 assert.ok(!w.state.cloudflare.some((call) => call.startsWith("PUT") && /scripts\/lab-api-flagonio/.test(call)));
272});
273
274test("a move waits, without refused deployments, while the new workspace is over its limit", async () => {
275 w.state.limits.set("flagon-io", "stopped");
276 assert.equal(await deliver(transferred), true);
277 assert.equal(rows("SELECT id FROM deployments WHERE created_at > '2026-10-06'").length, 0);
278 w.state.limits.delete("flagon-io");
279 await worker.scheduled({} as never, w.env as never);
280 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2);
281});
282
283test("once the new app is live, the old address redirects to it, paused or not", async () => {
284 await deliver(transferred);
285 const build = w.state.builds.find((b) => rows("SELECT script FROM deployments WHERE id = ?", b.deployId)[0].script === "lab-api-flagonio")!;
286 const finish = await worker.fetch(
287 new Request(`https://deployments/jobs/${build.deployId}/finish`, {
288 method: "POST",
289 body: JSON.stringify({ token: build.token, worker: { mainModule: "index.js", modules: [{ name: "index.js", contentBase64: btoa("export default {}"), contentType: "application/javascript+module" }] }, buildSeconds: 30 }),
290 }),
291 w.env as never,
292 {} as never,
293 );
294 assert.deepEqual(await finish.json(), { ok: true, value: true });
295 assert.deepEqual(
296 rows("SELECT script, workspace FROM apps WHERE kind = 'production'"),
297 [{ script: "lab-api-flagonio", workspace: "flagon-io" }],
298 );
299 const [redirect] = rows("SELECT script, target, workspace FROM redirects");
300 assert.deepEqual({ ...redirect }, { script: "lab-api-syntaqx", target: "lab-api-flagonio.g1t.page", workspace: "flagon-io" });
301 // The dispatcher's entry, followed before the old (paused) script runs.
302 const entry = w.state.kv.get("lab-api-syntaqx.g1t.page")!;
303 assert.deepEqual(JSON.parse(entry.value), { script: "lab-api-syntaqx", redirect: "lab-api-flagonio.g1t.page" });
304 assert.ok(entry.expiration! > Date.now() / 1000 + 89 * 24 * 3600);
305 // And the old script itself is the redirect too.
306 assert.ok(w.state.cloudflare.some((call) => call === "PUT /client/v4/accounts/acct/workers/dispatch/namespaces/g1t-deployments/scripts/lab-api-syntaqx"));
307 // The preview is still to come; the old one stays until it is live.
308 assert.equal(rows("SELECT script FROM apps WHERE kind = 'preview'")[0].script, "lab-api-git-v2-syntaqx");
309});
310
311test("the sweep never pauses a moved app for its old workspace's limit", async () => {
312 // Rows moved (as the event does) but no rebuild yet, and the apps not paused.
313 w.state.sqlite.exec("UPDATE settings SET workspace = 'flagon-io'; UPDATE apps SET paused_at = NULL");
314 w.state.runnerUp = false;
315 await worker.scheduled({} as never, w.env as never);
316 assert.deepEqual(rows("SELECT paused_at FROM apps").map((r) => r.paused_at), [null, null]);
317 // When the project's own workspace is over its limit, they are paused.
318 w.state.limits.set("flagon-io", "stopped");
319 await worker.scheduled({} as never, w.env as never);
320 assert.ok(rows("SELECT paused_at FROM apps").every((r) => r.paused_at));
321});