import { describe, expect, mock, test } from "bun:test"; import { Database } from "bun:sqlite"; const notified: string[] = []; mock.module("../repos", () => ({ loadRepoById: async () => null })); mock.module("./deliver", () => ({ notifyQuietly: async (input: { title: string }) => void notified.push(input.title), })); mock.module("./recipients", () => ({ accountAdmins: async () => ["u"], repoAdmins: async () => ["u"] })); const { ACCOUNT_SCHEMA } = await import("../scoped-db/schema.gen"); const { ACCOUNT_SCHEMA_EXTRA } = await import("../scoped-db/schema-extra"); const { nextRunnerOffline, reportOfflineRunners } = await import("./runners"); const MINUTE = 60_000; const NOW = 1_800_000_000_000; function store() { const db = new Database(":memory:"); for (const statement of [...ACCOUNT_SCHEMA, ...ACCOUNT_SCHEMA_EXTRA]) db.run(statement); const sql = { exec: (query: string, ...params: Array) => { const rows = db.query(query).all(...params) as never[]; return { toArray: () => rows }; }, }; return { db, sql }; } describe("offline runners", () => { test("each outage is reported once, and checking in again clears it", async () => { const { db, sql } = store(); db.query( "INSERT INTO runners (id, repo_id, owner_id, name, labels, token_hash, created_at, last_seen_at) VALUES ('r', NULL, 'org', 'box', '[]', 'h', 0, ?)", ).run(NOW - 2 * MINUTE); expect(nextRunnerOffline(sql, NOW)).toBe(NOW + 8 * MINUTE); await reportOfflineRunners(sql, NOW + 7 * MINUTE); expect(notified).toEqual([]); await reportOfflineRunners(sql, NOW + 11 * MINUTE); await reportOfflineRunners(sql, NOW + 12 * MINUTE); expect(notified).toEqual(["Runner box is offline"]); expect(nextRunnerOffline(sql, NOW + 12 * MINUTE)).toBeNull(); db.query("UPDATE runners SET last_seen_at = ? WHERE id = 'r'").run(NOW + 13 * MINUTE); await reportOfflineRunners(sql, NOW + 14 * MINUTE); expect(nextRunnerOffline(sql, NOW + 14 * MINUTE)).toBe(NOW + 23 * MINUTE); }); });