const test = require("node:test"); const assert = require("node:assert/strict"); const { loadConfig } = require("../src/config"); const { createRuntime } = require("../src/runtime"); function testConfig() { return loadConfig({ NODE_ENV: "test", HOST: "127.0.0.1", PORT: "0", DATABASE_PATH: ":memory:", APP_TIMEZONE: "America/Chicago", SHUTDOWN_TIMEOUT_MS: "1000", S3_ENDPOINT: "http://seaweedfs:8333", S3_REGION: "us-east-1", S3_BUCKET: "announcements", S3_ACCESS_KEY_ID: "access-key-that-must-not-leak", S3_SECRET_ACCESS_KEY: "secret-key-that-must-not-leak", S3_FORCE_PATH_STYLE: "true", MQTT_URL: "mqtt://broker:1883", MQTT_PASSWORD: "mqtt-password-that-must-not-leak", HA_WEBHOOK_URL: "http://home-assistant:8123/api/webhook/private-id", }); } const silentLogger = { info() {}, warn() {}, error() {}, }; function testStorage() { return { async start() {}, stop() {}, health() { return { status: "degraded", checkedAt: "2026-08-16T00:00:00.000Z", lastSuccessAt: null, code: "STORAGE_UNAVAILABLE", message: "Object storage is unavailable", }; }, }; } async function responseJson(baseUrl, path, init) { const response = await fetch(`${baseUrl}${path}`, init); return { response, body: await response.json() }; } test("HTTP foundation exposes live, ready, detailed health, and persisted settings", async (t) => { const runtime = createRuntime({ config: testConfig(), logger: silentLogger, storage: testStorage(), }); await runtime.start(); t.after(() => runtime.stop("test cleanup")); const baseUrl = `http://127.0.0.1:${runtime.address.port}`; const live = await responseJson(baseUrl, "/health/live"); assert.equal(live.response.status, 200); assert.deepEqual(live.body, { status: "ok" }); const ready = await responseJson(baseUrl, "/health/ready"); assert.equal(ready.response.status, 200); assert.equal(ready.body.status, "ready"); const status = await responseJson(baseUrl, "/api/health"); assert.equal(status.response.status, 200); assert.equal(status.body.status, "degraded"); assert.equal(status.body.services.database.currentSchemaVersion, 1); assert.equal(status.body.services.storage.status, "degraded"); const statusText = JSON.stringify(status.body); assert.doesNotMatch(statusText, /access-key-that-must-not-leak/); assert.doesNotMatch(statusText, /secret-key-that-must-not-leak/); assert.doesNotMatch(statusText, /mqtt-password-that-must-not-leak/); assert.doesNotMatch(statusText, /private-id/); const statusAlias = await responseJson(baseUrl, "/api/status"); assert.equal(statusAlias.response.status, 200); assert.deepEqual(statusAlias.body, status.body); const initial = await responseJson(baseUrl, "/api/settings"); assert.equal(initial.body.defaultBackgroundDarkened, false); const changed = await responseJson(baseUrl, "/api/settings", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ defaultBackgroundDarkened: true, defaultDurationMs: 9000, }), }); assert.equal(changed.response.status, 200); assert.equal(changed.body.revision, 2); assert.equal(changed.body.defaultBackgroundDarkened, true); const reread = await responseJson(baseUrl, "/api/settings"); assert.equal(reread.body.defaultDurationMs, 9000); }); test("invalid settings receive a structured client error", async (t) => { const runtime = createRuntime({ config: testConfig(), logger: silentLogger, storage: testStorage(), }); await runtime.start(); t.after(() => runtime.stop("test cleanup")); const baseUrl = `http://127.0.0.1:${runtime.address.port}`; const result = await responseJson(baseUrl, "/api/settings", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ defaultBackgroundDarkened: 0.25 }), }); assert.equal(result.response.status, 400); assert.equal(result.body.error.code, "INVALID_SETTINGS"); }); test("liveness stays up while critical readiness failures return 503", async (t) => { const runtime = createRuntime({ config: testConfig(), logger: silentLogger, storage: testStorage(), }); await runtime.start(); t.after(() => runtime.stop("test cleanup")); const baseUrl = `http://127.0.0.1:${runtime.address.port}`; runtime.lifecycle.ready = false; let live = await responseJson(baseUrl, "/health/live"); let ready = await responseJson(baseUrl, "/health/ready"); assert.equal(live.response.status, 200); assert.equal(ready.response.status, 503); assert.equal(ready.body.status, "not_ready"); runtime.lifecycle.ready = true; runtime.migrations.pendingVersions.push(2); ready = await responseJson(baseUrl, "/health/ready"); assert.equal(ready.response.status, 503); assert.equal(ready.body.checks.migrations, "pending"); runtime.migrations.pendingVersions.length = 0; runtime.database.close(); live = await responseJson(baseUrl, "/health/live"); ready = await responseJson(baseUrl, "/health/ready"); assert.equal(live.response.status, 200); assert.equal(ready.response.status, 503); assert.equal(ready.body.checks.database, "error"); }); test("runtime startup and shutdown are idempotent", async () => { const runtime = createRuntime({ config: testConfig(), logger: silentLogger, storage: testStorage(), }); const [firstAddress, secondAddress] = await Promise.all([ runtime.start(), runtime.start(), ]); assert.deepEqual(firstAddress, secondAddress); assert.equal(runtime.lifecycle.ready, true); await Promise.all([runtime.stop("test"), runtime.stop("test")]); assert.equal(runtime.lifecycle.ready, false); assert.equal(runtime.database.open, false); });