Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

130 linesCodeBlameRaw
1// node --test "scripts/deploy/*.test.mjs" (npm run test:deploy)
2//
3// The queries that run on a timer or on every page load, planned by SQLite
4// against each service's migrations as they apply in order: each must be a
5// search of an index, never a scan of the table. D1 is SQLite, so its
6// planner chooses the same way.
7import assert from "node:assert/strict";
8import { readdirSync, readFileSync } from "node:fs";
9import { join } from "node:path";
10import { DatabaseSync } from "node:sqlite";
11import { test } from "node:test";
12
13import { ROOT } from "./stack.mjs";
14
15/** A database with every one of `service`'s migrations applied, in order. */
16function migrated(service) {
17 const db = new DatabaseSync(":memory:");
18 const dir = join(ROOT, "services", service, "migrations");
19 for (const file of readdirSync(dir)
20 .filter((name) => name.endsWith(".sql"))
21 .sort()) {
22 db.exec(readFileSync(join(dir, file), "utf8"));
23 }
24 return db;
25}
26
27/** How SQLite would run `sql`, one line per step. */
28function plan(db, sql, ...params) {
29 return db
30 .prepare(`EXPLAIN QUERY PLAN ${sql}`)
31 .all(...params)
32 .map((row) => row.detail);
33}
34
35function assertSearches(steps, index) {
36 assert.ok(
37 steps.some((step) => step.includes(`INDEX ${index}`)),
38 `expected a search of ${index}, planned:\n ${steps.join("\n ")}`,
39 );
40}
41
42test("webhooks: the hourly purge deletes old deliveries by time", () => {
43 const steps = plan(
44 migrated("webhooks"),
45 "DELETE FROM deliveries WHERE rowid IN (SELECT rowid FROM deliveries WHERE created_at < ? ORDER BY created_at LIMIT ?)",
46 "2026-09-24T00:00:00.000Z",
47 1000,
48 );
49 assertSearches(steps, "deliveries_by_time");
50});
51
52test("billing: this month's users are read from an index, not the ledger", () => {
53 const steps = plan(migrated("billing"), "SELECT DISTINCT workspace FROM ledger WHERE kind = 'usage' AND created_at >= ?", "2026-10-01");
54 assertSearches(steps, "ledger_usage_by_time");
55 assert.ok(
56 steps.some((step) => step.includes("COVERING INDEX")),
57 steps.join("\n"),
58 );
59});
60
61test("events: a repository's events of one type, newest first, page by page", () => {
62 const db = migrated("events");
63 const first = plan(db, "SELECT * FROM events WHERE repo_id = ? AND type IN (?) ORDER BY id DESC LIMIT ?", "rep_1", "git.push", 50);
64 assertSearches(first, "events_repo_type");
65 const next = plan(db, "SELECT * FROM events WHERE repo_id = ? AND type IN (?) AND id < ? ORDER BY id DESC LIMIT ?", "rep_1", "git.push", "evt_9", 200);
66 assertSearches(next, "events_repo_type");
67 // Read in order: no sort of every match.
68 assert.ok(!next.some((step) => step.includes("TEMP B-TREE")), next.join("\n"));
69});
70
71test("actions: a self-hosted runner's poll finds queued jobs by namespace, whatever its case", () => {
72 const steps = plan(
73 migrated("actions"),
74 `SELECT jobs.*, runs.repo AS run_repo FROM jobs JOIN runs ON runs.id = jobs.run_id
75 WHERE jobs.status = 'queued' AND jobs.labels IS NOT NULL AND lower(jobs.namespace) = ?
76 ORDER BY jobs.rowid LIMIT 50`,
77 "acme",
78 );
79 assertSearches(steps, "jobs_self_hosted_lower");
80});
81
82test("chat: a channel's messages and a thread's replies, newest first, page by page", () => {
83 const db = migrated("chat");
84 // The first page passes `~` as the cursor, so both read as a range.
85 const top = plan(
86 db,
87 `SELECT * FROM messages WHERE channel_id = ?1 AND thread_root IS NULL AND id < ?2
88 AND (deleted_at IS NULL OR reply_count > 0) ORDER BY id DESC LIMIT ?3`,
89 "chn_1",
90 "~",
91 51,
92 );
93 assertSearches(top, "messages_by_channel");
94 assert.ok(!top.some((step) => step.includes("TEMP B-TREE")), top.join("\n"));
95 const thread = plan(db, "SELECT * FROM messages WHERE thread_root = ?1 AND channel_id = ?2 AND id < ?3 ORDER BY id DESC LIMIT ?4", "msg_1", "chn_1", "~", 51);
96 assertSearches(thread, "messages_by_thread");
97});
98
99test("chat: the sidebar's unread messages are read from each channel's index, after the last read", () => {
100 const steps = plan(
101 migrated("chat"),
102 `SELECT msg.channel_id, msg.id, msg.author, msg.mentions
103 FROM channel_members m
104 JOIN channels c ON c.id = m.channel_id
105 JOIN messages msg ON msg.channel_id = m.channel_id AND msg.id > COALESCE(m.last_read_id, '')
106 WHERE m.principal = ?1 AND c.workspace_id = ?2 AND c.archived_at IS NULL
107 AND msg.deleted_at IS NULL AND msg.author != ?1
108 LIMIT 5000`,
109 "user:usr_1",
110 "wsp_1",
111 );
112 assertSearches(steps, "channel_members_by_principal");
113 assert.ok(
114 steps.some((step) => step.includes("messages_by_channel (channel_id=? AND id>?)")),
115 steps.join("\n"),
116 );
117});
118
119test("chat: a page's reactions are read by message, and a workspace's emoji by name", () => {
120 const db = migrated("chat");
121 const reactions = plan(
122 db,
123 "SELECT message_id, emoji, principal, created_at FROM reactions WHERE message_id IN (SELECT value FROM json_each(?))",
124 '["msg_1","msg_2"]',
125 );
126 assertSearches(reactions, "sqlite_autoindex_reactions_1");
127 const emoji = plan(db, "SELECT * FROM custom_emoji WHERE workspace_id = ? AND deleted_at IS NULL ORDER BY name", "wsp_1");
128 assertSearches(emoji, "custom_emoji_by_name");
129 assert.ok(!emoji.some((step) => step.includes("TEMP B-TREE")), emoji.join("\n"));
130});