import { assert, assertEquals, assertRejects } from "jsr:@std/assert@^1.0.0"; import { RunnerAdmin } from "../../../uptime.link/ts_api/classes/runner-admin.ts"; import { RunnerCoordinator } from "../../../uptime.link/ts_api/classes/runner-coordinator.ts"; import { RunnerFileStore } from "../../../uptime.link/ts_api/classes/runner-file-store.ts"; import { createRunnerSchedulesFromMonitors, type IRunnerMonitorDefinition, } from "../../../uptime.link/ts_api/classes/runner-monitor-mapper.ts"; import { createRunnerRequestHandler } from "../../../uptime.link/ts_api/classes/runner-request-handler.ts"; import { RunnerResultIngestor } from "../../../uptime.link/ts_api/classes/runner-result-ingestor.ts"; import { RunnerScheduler } from "../../../uptime.link/ts_api/classes/runner-scheduler.ts"; import { UptimeRunner } from "../../../uptimerunner/ts/runner.ts"; import type { IResultSubmitRequest } 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 monitors: IRunnerMonitorDefinition[] = [ { id: "local-http-health", name: "Local HTTP Health", intervalMs: 60000, check: { type: "http", url: targetServer.url, expectedStatusCodes: [200], expectedBodyIncludes: "healthy", }, }, { id: "local-tcp-health", name: "Local TCP Health", intervalMs: 60000, check: { type: "tcp", host: "127.0.0.1", port: tcpServer.port, }, }, { id: "manual-assumption", name: "Manual Assumption", intervalMs: 60000, check: { type: "assumption", assumedStatus: "ok", message: "manual assumption is healthy", }, }, { id: "manual-degraded", name: "Manual Degraded", intervalMs: 60000, check: { type: "assumption", assumedStatus: "not ok", message: "manual assumption is degraded", }, }, ]; const coordinator = new RunnerCoordinator(); const admin = new RunnerAdmin(coordinator); const registration = admin.registerRunner({ runnerId, token: runnerToken, labels: ["scenario:basic"], }); assertEquals(registration.runner.runnerId, runnerId); assertEquals(registration.token, runnerToken); const scheduler = new RunnerScheduler(coordinator, { now: () => 1000 }); const scheduleResult = scheduler.scheduleDueChecks( createRunnerSchedulesFromMonitors(monitors), ); assertEquals(scheduleResult.scheduledChecks.length, 4); assertEquals(coordinator.getQueueLength(), 4); 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, 4); assertEquals(result.results.length, 4); assertEquals(result.results[0].checkId, "monitor-local-http-health"); assertEquals(result.results[0].status, "ok"); assertEquals(result.results[1].checkId, "monitor-local-tcp-health"); assertEquals(result.results[1].status, "ok"); assertEquals(result.results[2].checkId, "monitor-manual-assumption"); assertEquals(result.results[2].status, "ok"); assertEquals(result.results[3].checkId, "monitor-manual-degraded"); assertEquals(result.results[3].status, "not 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, 4); assertEquals(coordinator.getQueueLength(), 0); assertMonitorStatusDerivation(coordinator.listResults()); 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"); await assertRunnerTokenLifecycle( admin, coordinator, coordinatorServer.url, runner, ); 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(); }, }; } function assertMonitorStatusDerivation( results: IResultSubmitRequest["results"], ): void { const ingestor = new RunnerResultIngestor(); const ingestion = ingestor.ingest(results); assertEquals(ingestion.monitorStates.length, 4); assertEquals(ingestion.ignoredResults.length, 0); assertEquals( ingestor.getMonitorState("local-http-health")?.status, "operational", ); assertEquals( ingestor.getMonitorState("local-tcp-health")?.status, "operational", ); assertEquals( ingestor.getMonitorState("manual-assumption")?.status, "operational", ); assertEquals(ingestor.getMonitorState("manual-degraded")?.status, "degraded"); } async function assertRunnerTokenLifecycle( adminArg: RunnerAdmin, coordinatorArg: RunnerCoordinator, instanceUrlArg: string, staleRunnerArg: UptimeRunner, ): Promise { const rotated = adminArg.rotateRunnerToken(runnerId, "rotated-token"); assertEquals(rotated.token, "rotated-token"); assertEquals(adminArg.listRunners()[0].tokenPreview, "rotate...oken"); coordinatorArg.enqueueCheck({ id: "post-rotate-assumption", type: "assumption", assumedStatus: "ok", metadata: { monitorId: "post-rotate", }, }); await assertRejects(() => staleRunnerArg.runOnce(), Error, "401"); const rotatedRunner = new UptimeRunner({ instanceUrl: instanceUrlArg, runnerId, token: rotated.token, }); const rotatedResult = await rotatedRunner.runOnce(); assertEquals(rotatedResult.results.length, 1); assertEquals(rotatedResult.results[0].status, "ok"); adminArg.disableRunner(runnerId); assertEquals(adminArg.listRunners()[0].enabled, false); await assertRejects(() => rotatedRunner.runOnce(), Error, "401"); adminArg.enableRunner(runnerId); adminArg.setRunnerLabels(runnerId, ["scenario:basic", "role:external"]); assertEquals( adminArg.listRunners()[0].labels.includes("role:external"), true, ); } async function assertSnapshotPersistence( coordinatorArg: RunnerCoordinator, targetUrlArg: string, ): Promise { const snapshotPath = await Deno.makeTempFile(); const store = RunnerFileStore.fromDenoPath(snapshotPath); try { await store.save(coordinatorArg.getSnapshot()); const snapshot = await store.load(); assert(snapshot); const restoredCoordinator = new RunnerCoordinator({ snapshot }); assertEquals(restoredCoordinator.listResults().length, 4); 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, 5); } 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); } }