import assert from "node:assert/strict"; import test from "node:test"; import { CHART_VIEW_ENGINE_CACHE_TTL_MS, CHART_VIEW_ENGINE_TIMEOUT_MS, chartViewEngineCacheKey, clearChartViewEngineCache, engineCallFromHttp, engineCallFromThrown, logEngineCallFailure, parseChartViewLayers, readChartViewEngineCache, writeChartViewEngineCache, } from "../src/lib/chart-view-engine.ts"; function captureWarnings(run: () => void): unknown[] { const warnings: unknown[] = []; const original = console.warn; console.warn = (...args: unknown[]) => { warnings.push(args); }; try { run(); } finally { console.warn = original; } return warnings; } const baseKey = { userId: "user-1", date: "1990-04-09", time: "12:00", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, ayanamsa: "raman", nodeMode: "mean", path: "/api/chart", }; test("engine HTTP outcomes classify 429, other errors, and bad JSON separately", () => { assert.equal(engineCallFromHttp({ path: "/api/chart", elapsedMs: 12, httpStatus: 429, payload: { error: "busy" }, }).status, "busy"); assert.equal(engineCallFromHttp({ path: "/api/chart", elapsedMs: 12, httpStatus: 500, payload: { error: "boom" }, }).status, "http_error"); assert.equal(engineCallFromHttp({ path: "/api/chart", elapsedMs: 12, httpStatus: 200, payload: "not-json-object", }).status, "bad_payload"); assert.equal(engineCallFromHttp({ path: "/api/chart", elapsedMs: 12, httpStatus: 200, payload: null, }).status, "bad_payload"); }); test("timeout and abort errors classify as timeout; other throws as http_error", () => { const timeout = engineCallFromThrown("/api/chart", 10_001, Object.assign(new Error("aborted"), { name: "TimeoutError" })); assert.equal(timeout.status, "timeout"); const abort = engineCallFromThrown("/api/chart", 10_001, Object.assign(new Error("aborted"), { name: "AbortError" })); assert.equal(abort.status, "timeout"); const network = engineCallFromThrown("/api/chart", 30, Object.assign(new Error("fetch failed"), { name: "TypeError" })); assert.equal(network.status, "http_error"); assert.equal(network.errorName, "TypeError"); }); test("each non-ok engine result logs path, status, and elapsed ms without birth fields", () => { const cases = [ { status: "busy" as const, httpStatus: 429 }, { status: "http_error" as const, httpStatus: 500 }, { status: "timeout" as const, errorName: "TimeoutError" }, { status: "bad_payload" as const, httpStatus: 200 }, ]; const seen = new Set(); for (const item of cases) { const warnings = captureWarnings(() => { logEngineCallFailure({ path: "/api/chart", elapsedMs: 18, ...item, }); }); assert.equal(warnings.length, 1); const line = JSON.stringify(warnings[0]); assert.match(line, /chart_view_engine_failed/); assert.match(line, new RegExp(item.status)); assert.match(line, /"elapsedMs":18/); assert.doesNotMatch(line, /1990|latitude|longitude|name|birth/); seen.add(item.status); } assert.deepEqual([...seen], ["busy", "http_error", "timeout", "bad_payload"]); }); test("engine cache keys include ayanamsa and node mode and expire", () => { clearChartViewEngineCache(); const raman = chartViewEngineCacheKey(baseKey); const lahiri = chartViewEngineCacheKey({ ...baseKey, ayanamsa: "lahiri" }); const trueNode = chartViewEngineCacheKey({ ...baseKey, nodeMode: "true" }); assert.notEqual(raman, lahiri); assert.notEqual(raman, trueNode); writeChartViewEngineCache(raman, { success: true }, 1_000); assert.deepEqual(readChartViewEngineCache(raman, 1_000), { success: true }); assert.equal(readChartViewEngineCache(raman, 1_000 + CHART_VIEW_ENGINE_CACHE_TTL_MS + 1), null); assert.equal(CHART_VIEW_ENGINE_TIMEOUT_MS, 10_000); }); test("layer query parsing keeps only known layers in stable order", () => { assert.deepEqual(parseChartViewLayers(null), []); assert.deepEqual(parseChartViewLayers("western,varga,nope,qizheng"), ["varga", "western", "qizheng"]); });