import { assert, assertEquals, assertRejects } from "jsr:@std/assert@^1.0.0"; import { RunnerCoordinator } from "../../../uptime.link/ts_api/classes/runner-coordinator.ts"; import { RunnerFileStore } from "../../../uptime.link/ts_api/classes/runner-file-store.ts"; import { createRunnerRequestHandler } from "../../../uptime.link/ts_api/classes/runner-request-handler.ts"; import { RunnerScheduler } from "../../../uptime.link/ts_api/classes/runner-scheduler.ts"; import { UptimeRunner } from "../../../uptimerunner/ts/runner.ts"; import type { IResultSubmitRequest, TCheckJob, } from "../../../uptimerunner/ts/interfaces.ts"; const scenarioName = "uptimerunner-basic"; const runnerId = `scenario-runner-${Date.now().toString(36)}`; const runnerToken = "scenario-token"; const main = async () => { const targetServer = await startServer(() => new Response("healthy")); const tcpServer = startTcpServer(); const resultSubmissions: IResultSubmitRequest[] = []; const checkQueue: TCheckJob[] = [ { id: "local-http-health", type: "http", url: targetServer.url, expectedStatusCodes: [200], expectedBodyIncludes: "healthy", metadata: { scenario: scenarioName, }, }, { id: "local-tcp-health", type: "tcp", host: "127.0.0.1", port: tcpServer.port, }, { id: "manual-assumption", type: "assumption", assumedStatus: "ok", message: "manual assumption is healthy", }, ]; const coordinator = new RunnerCoordinator({ runners: [{ runnerId, token: runnerToken, labels: ["scenario:basic"] }], }); const scheduler = new RunnerScheduler(coordinator, { now: () => 1000 }); const scheduleResult = scheduler.scheduleDueChecks( checkQueue.map((check) => ({ monitorId: `monitor-${check.id}`, check, intervalMs: 60000, })), ); assertEquals(scheduleResult.scheduledChecks.length, 3); assertEquals(coordinator.getQueueLength(), 3); assertEquals( scheduler.scheduleDueChecks(scheduleResult.schedules).scheduledChecks .length, 0, ); const runnerRequestHandler = createRunnerRequestHandler(coordinator); const coordinatorServer = await startServer(async (request) => { if ( request.method === "POST" && new URL(request.url).pathname === "/api/runner/v1/results" ) { resultSubmissions.push(await request.clone().json()); } return await runnerRequestHandler(request); }); try { const runner = new UptimeRunner({ instanceUrl: coordinatorServer.url, runnerId, token: runnerToken, pollIntervalMs: 1000, }); console.log(`[${scenarioName}] Running single runner iteration`); const result = await runner.runOnce(); assertEquals(result.checks.length, 3); assertEquals(result.results.length, 3); assertEquals(result.results[0].checkId, "local-http-health"); assertEquals(result.results[0].status, "ok"); assertEquals(result.results[1].checkId, "local-tcp-health"); assertEquals(result.results[1].status, "ok"); assertEquals(result.results[2].checkId, "manual-assumption"); assertEquals(result.results[2].status, "ok"); assertEquals(resultSubmissions.length, 1); assertEquals(resultSubmissions[0].runnerId, runnerId); assertEquals(resultSubmissions[0].results[0].status, "ok"); assert(resultSubmissions[0].results[0].responseTime !== undefined); assertEquals(coordinator.listResults().length, 3); assertEquals(coordinator.getQueueLength(), 0); await assertSnapshotPersistence(coordinator, targetServer.url); const emptyResult = await runner.runOnce(); assertEquals(emptyResult.checks.length, 0); assertEquals(emptyResult.results.length, 0); assertEquals(resultSubmissions.length, 1); const unauthorizedRunner = new UptimeRunner({ instanceUrl: coordinatorServer.url, runnerId, token: "wrong-token", pollIntervalMs: 1000, }); await assertRejects(() => unauthorizedRunner.runOnce(), Error, "401"); console.log(`[${scenarioName}] Passed`); } finally { await coordinatorServer.server.shutdown(); await targetServer.server.shutdown(); tcpServer.close(); } }; async function startServer( handlerArg: Deno.ServeHandler, ): Promise<{ server: Deno.HttpServer; url: string }> { let resolveListening: (addr: Deno.NetAddr) => void; const listening = new Promise((resolve) => { resolveListening = resolve; }); const server = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen: (addr) => resolveListening(addr), }, handlerArg); const addr = await listening; return { server, url: `http://${addr.hostname}:${addr.port}`, }; } function startTcpServer(): { port: number; close: () => void } { const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 }); const port = (listener.addr as Deno.NetAddr).port; let closed = false; const acceptLoop = async () => { while (!closed) { try { const connection = await listener.accept(); connection.close(); } catch (error) { if (!closed) { throw error; } } } }; acceptLoop(); return { port, close: () => { closed = true; listener.close(); }, }; } async function assertSnapshotPersistence( coordinatorArg: RunnerCoordinator, targetUrlArg: string, ): Promise { const snapshotPath = await Deno.makeTempFile(); const store = new RunnerFileStore({ readTextFile: async () => { try { return await Deno.readTextFile(snapshotPath); } catch (error) { if (error instanceof Deno.errors.NotFound) { return undefined; } throw error; } }, writeTextFile: async (contentArg) => { await Deno.writeTextFile(snapshotPath, contentArg); }, }); try { await store.save(coordinatorArg.getSnapshot()); const snapshot = await store.load(); assert(snapshot); const restoredCoordinator = new RunnerCoordinator({ snapshot }); assertEquals(restoredCoordinator.listResults().length, 3); assertEquals(restoredCoordinator.listRunners().length, 1); restoredCoordinator.enqueueCheck({ id: "post-restart-http-health", type: "http", url: targetUrlArg, expectedStatusCodes: [200], expectedBodyIncludes: "healthy", }); const restoredServer = await startServer( createRunnerRequestHandler(restoredCoordinator), ); try { const restoredRunner = new UptimeRunner({ instanceUrl: restoredServer.url, runnerId, token: runnerToken, }); const restoredResult = await restoredRunner.runOnce(); assertEquals(restoredResult.results.length, 1); assertEquals(restoredResult.results[0].status, "ok"); assertEquals(restoredCoordinator.listResults().length, 4); } finally { await restoredServer.server.shutdown(); } } finally { await Deno.remove(snapshotPath).catch(() => null); } } if (import.meta.main) { try { await main(); } catch (error) { console.error( `[${scenarioName}] Failed: ${ error instanceof Error ? error.stack : String(error) }`, ); Deno.exit(1); } }