Files

321 lines
9.8 KiB
TypeScript
Raw Permalink Normal View History

2026-04-29 19:48:14 +00:00
import { assert, assertEquals, assertRejects } from "jsr:@std/assert@^1.0.0";
import { RunnerFileStore } from "../../../uptime.link/ts_api/classes/runner-file-store.ts";
2026-04-30 07:23:22 +00:00
import type { IRunnerMonitorDefinition } from "../../../uptime.link/ts_api/classes/runner-monitor-mapper.ts";
2026-04-29 21:35:21 +00:00
import { RunnerResultIngestor } from "../../../uptime.link/ts_api/classes/runner-result-ingestor.ts";
2026-04-30 07:23:22 +00:00
import { RunnerRuntime } from "../../../uptime.link/ts_api/classes/runner-runtime.ts";
2026-04-29 19:48:14 +00:00
import { UptimeRunner } from "../../../uptimerunner/ts/runner.ts";
2026-04-29 21:35:21 +00:00
import type { IResultSubmitRequest } from "../../../uptimerunner/ts/interfaces.ts";
2026-04-29 19:48:14 +00:00
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[] = [];
2026-04-29 21:35:21 +00:00
const monitors: IRunnerMonitorDefinition[] = [
2026-04-29 19:48:14 +00:00
{
id: "local-http-health",
2026-04-29 21:35:21 +00:00
name: "Local HTTP Health",
intervalMs: 60000,
check: {
type: "http",
url: targetServer.url,
expectedStatusCodes: [200],
expectedBodyIncludes: "healthy",
2026-04-29 19:48:14 +00:00
},
},
{
id: "local-tcp-health",
2026-04-29 21:35:21 +00:00
name: "Local TCP Health",
intervalMs: 60000,
check: {
type: "tcp",
host: "127.0.0.1",
port: tcpServer.port,
},
2026-04-29 19:48:14 +00:00
},
{
id: "manual-assumption",
2026-04-29 21:35:21 +00:00
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",
},
2026-04-29 19:48:14 +00:00
},
];
2026-04-30 07:23:22 +00:00
const runtime = new RunnerRuntime({ monitors, now: () => 1000 });
const coordinator = runtime.coordinator;
const registration = await runtime.registerRunner({
2026-04-29 22:00:50 +00:00
runnerId,
token: runnerToken,
labels: ["scenario:basic"],
2026-04-29 19:48:14 +00:00
});
2026-04-29 22:00:50 +00:00
assertEquals(registration.runner.runnerId, runnerId);
assertEquals(registration.token, runnerToken);
2026-04-30 07:23:22 +00:00
const scheduleResult = await runtime.scheduleDueChecks();
2026-04-29 21:35:21 +00:00
assertEquals(scheduleResult.scheduledChecks.length, 4);
assertEquals(coordinator.getQueueLength(), 4);
2026-04-30 07:23:22 +00:00
assertEquals((await runtime.scheduleDueChecks()).scheduledChecks.length, 0);
const runnerRequestHandler = runtime.createRequestHandler();
2026-04-29 19:48:14 +00:00
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());
}
2026-04-29 21:32:36 +00:00
return await runnerRequestHandler(request);
2026-04-29 19:48:14 +00:00
});
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();
2026-04-29 21:35:21 +00:00
assertEquals(result.checks.length, 4);
assertEquals(result.results.length, 4);
assertEquals(result.results[0].checkId, "monitor-local-http-health");
2026-04-29 19:48:14 +00:00
assertEquals(result.results[0].status, "ok");
2026-04-29 21:35:21 +00:00
assertEquals(result.results[1].checkId, "monitor-local-tcp-health");
2026-04-29 19:48:14 +00:00
assertEquals(result.results[1].status, "ok");
2026-04-29 21:35:21 +00:00
assertEquals(result.results[2].checkId, "monitor-manual-assumption");
2026-04-29 19:48:14 +00:00
assertEquals(result.results[2].status, "ok");
2026-04-29 21:35:21 +00:00
assertEquals(result.results[3].checkId, "monitor-manual-degraded");
assertEquals(result.results[3].status, "not ok");
2026-04-29 19:48:14 +00:00
assertEquals(resultSubmissions.length, 1);
assertEquals(resultSubmissions[0].runnerId, runnerId);
assertEquals(resultSubmissions[0].results[0].status, "ok");
assert(resultSubmissions[0].results[0].responseTime !== undefined);
2026-04-29 21:35:21 +00:00
assertEquals(coordinator.listResults().length, 4);
2026-04-29 19:48:14 +00:00
assertEquals(coordinator.getQueueLength(), 0);
2026-04-29 21:35:21 +00:00
assertMonitorStatusDerivation(coordinator.listResults());
2026-04-30 07:23:22 +00:00
assertEquals(runtime.listMonitorStates().length, 4);
await assertSnapshotPersistence(runtime, targetServer.url);
2026-04-29 19:48:14 +00:00
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");
2026-04-29 22:00:50 +00:00
await assertRunnerTokenLifecycle(
2026-04-30 07:23:22 +00:00
runtime,
2026-04-29 22:00:50 +00:00
coordinatorServer.url,
runner,
);
2026-04-29 19:48:14 +00:00
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<Deno.NetAddr>((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();
},
};
}
2026-04-29 21:35:21 +00:00
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");
}
2026-04-29 22:00:50 +00:00
async function assertRunnerTokenLifecycle(
2026-04-30 07:23:22 +00:00
runtimeArg: RunnerRuntime,
2026-04-29 22:00:50 +00:00
instanceUrlArg: string,
staleRunnerArg: UptimeRunner,
): Promise<void> {
2026-04-30 07:23:22 +00:00
const rotated = await runtimeArg.rotateRunnerToken(runnerId, "rotated-token");
2026-04-29 22:00:50 +00:00
assertEquals(rotated.token, "rotated-token");
2026-04-30 07:23:22 +00:00
assertEquals(runtimeArg.admin.listRunners()[0].tokenPreview, "rotate...oken");
2026-04-29 22:00:50 +00:00
2026-04-30 07:23:22 +00:00
runtimeArg.coordinator.enqueueCheck({
2026-04-29 22:00:50 +00:00
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");
2026-04-30 07:23:22 +00:00
await runtimeArg.disableRunner(runnerId);
assertEquals(runtimeArg.admin.listRunners()[0].enabled, false);
2026-04-29 22:00:50 +00:00
await assertRejects(() => rotatedRunner.runOnce(), Error, "401");
2026-04-30 07:23:22 +00:00
await runtimeArg.enableRunner(runnerId);
await runtimeArg.setRunnerLabels(runnerId, [
"scenario:basic",
"role:external",
]);
2026-04-29 22:00:50 +00:00
assertEquals(
2026-04-30 07:23:22 +00:00
runtimeArg.admin.listRunners()[0].labels.includes("role:external"),
2026-04-29 22:00:50 +00:00
true,
);
}
2026-04-29 19:48:14 +00:00
async function assertSnapshotPersistence(
2026-04-30 07:23:22 +00:00
runtimeArg: RunnerRuntime,
2026-04-29 21:32:36 +00:00
targetUrlArg: string,
2026-04-29 19:48:14 +00:00
): Promise<void> {
const snapshotPath = await Deno.makeTempFile();
2026-04-29 22:00:50 +00:00
const store = RunnerFileStore.fromDenoPath(snapshotPath);
2026-04-29 19:48:14 +00:00
try {
2026-04-30 07:23:22 +00:00
const persistedRuntime = new RunnerRuntime({
coordinator: runtimeArg.coordinator,
fileStore: store,
});
await persistedRuntime.save();
const restoredRuntime = await RunnerRuntime.fromFileStore(store);
assertEquals(restoredRuntime.coordinator.listResults().length, 4);
assertEquals(restoredRuntime.coordinator.listRunners().length, 1);
assertEquals(restoredRuntime.listMonitorStates().length, 4);
restoredRuntime.coordinator.enqueueCheck({
2026-04-29 21:32:36 +00:00
id: "post-restart-http-health",
type: "http",
url: targetUrlArg,
expectedStatusCodes: [200],
expectedBodyIncludes: "healthy",
2026-04-30 07:23:22 +00:00
metadata: {
monitorId: "post-restart-http-health",
},
2026-04-29 21:32:36 +00:00
});
const restoredServer = await startServer(
2026-04-30 07:23:22 +00:00
restoredRuntime.createRequestHandler(),
2026-04-29 21:32:36 +00:00
);
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");
2026-04-30 07:23:22 +00:00
assertEquals(restoredRuntime.coordinator.listResults().length, 5);
assertEquals(restoredRuntime.listMonitorStates().length, 5);
2026-04-29 21:32:36 +00:00
} finally {
await restoredServer.server.shutdown();
}
2026-04-29 19:48:14 +00:00
} 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);
}
}