From bef6d16f9c99da7a460e755dd51dc5aa0d9a7a2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:36:07 +0900 Subject: [PATCH 1/2] test(kpi): reject malformed direct KPI input bytes --- test/compute-kpi-input-integrity.test.ts | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/compute-kpi-input-integrity.test.ts diff --git a/test/compute-kpi-input-integrity.test.ts b/test/compute-kpi-input-integrity.test.ts new file mode 100644 index 000000000..639729a62 --- /dev/null +++ b/test/compute-kpi-input-integrity.test.ts @@ -0,0 +1,77 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +function runComputeKpi(logPath: string) { + return spawnSync(process.execPath, ["scripts/compute-kpi.mjs", logPath], { + cwd: process.cwd(), + encoding: "utf8", + }); +} + +describe("direct KPI computation input integrity", () => { + it("rejects malformed UTF-8 before computing exchange metrics", () => { + const dir = mkdtempSync(join(tmpdir(), "noema-compute-kpi-")); + try { + const logPath = join(dir, "exchange-30d.ndjson"); + const prefix = Buffer.from( + '{"event":"http_request","route":"/exchange","status_code":200,"latency_ms":123,"note":"', + "utf8", + ); + const suffix = Buffer.from('"}\n', "utf8"); + writeFileSync(logPath, Buffer.concat([prefix, Buffer.from([0xff]), suffix])); + + const result = runComputeKpi(logPath); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("Invalid UTF-8 in KPI log"); + expect(result.stdout).toBe(""); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("preserves valid UTF-8 metrics and ignores non-JSON lines", () => { + const dir = mkdtempSync(join(tmpdir(), "noema-compute-kpi-")); + try { + const logPath = join(dir, "exchange-30d.ndjson"); + writeFileSync(logPath, [ + "not-json", + JSON.stringify({ + event: "http_request", + route: "/exchange", + status_code: 200, + latency_ms: 120, + }), + JSON.stringify({ + event: "http_request", + request: { url: "https://noema.example/exchange?probe=1" }, + status_code: 500, + latency_ms: 200, + }), + JSON.stringify({ + event: "http_request", + route: "/health", + status_code: 200, + latency_ms: 1, + }), + "", + ].join("\n")); + + const result = runComputeKpi(logPath); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(JSON.parse(result.stdout)).toEqual({ + exchange_requests: 2, + exchange_failures: 1, + exchange_failure_rate: 0.5, + exchange_p95_latency_ms: 200, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); From b93db0492bfe60866504a77aff2fea809fae732d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 08:37:24 +0900 Subject: [PATCH 2/2] fix(kpi): decode direct KPI input strictly --- scripts/compute-kpi.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/compute-kpi.mjs b/scripts/compute-kpi.mjs index adf30eefb..e2e64ac77 100755 --- a/scripts/compute-kpi.mjs +++ b/scripts/compute-kpi.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node import fs from "node:fs"; +import { TextDecoder } from "node:util"; const inputPath = process.argv[2] ?? "exchange-30d.ndjson"; @@ -8,7 +9,15 @@ if (!fs.existsSync(inputPath)) { process.exit(1); } -const text = fs.readFileSync(inputPath, "utf8"); +const bytes = fs.readFileSync(inputPath); +let text; +try { + text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); +} catch { + console.error(`Invalid UTF-8 in KPI log: ${inputPath}.`); + process.exit(1); +} + const lines = text.split("\n").filter(Boolean); const latencies = []; let exchanges = 0;