From 6931f55403f51d0b0db8900c20fba77c10866136 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 15:07:08 +0900 Subject: [PATCH 1/2] test(kpi): require fatal UTF-8 threshold input --- test/check-kpi-input-integrity.test.ts | 75 ++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 test/check-kpi-input-integrity.test.ts diff --git a/test/check-kpi-input-integrity.test.ts b/test/check-kpi-input-integrity.test.ts new file mode 100644 index 000000000..4a9ea487e --- /dev/null +++ b/test/check-kpi-input-integrity.test.ts @@ -0,0 +1,75 @@ +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 runCheckKpi(logPath: string) { + const env = { ...process.env }; + delete env.NOEMA_KPI_REQUIRE_WINDOW_DAYS; + return spawnSync(process.execPath, ["scripts/check-kpi.mjs", logPath, "0.02", "300"], { + cwd: process.cwd(), + encoding: "utf8", + env, + }); +} + +describe("KPI threshold input integrity", () => { + it("rejects malformed UTF-8 before calculating threshold metrics", () => { + const dir = mkdtempSync(join(tmpdir(), "noema-check-kpi-")); + try { + const logPath = join(dir, "exchange-30d.ndjson"); + const prefix = Buffer.from( + '{"event":"http_request","route":"/exchange","status_code":200,"latency_ms":120,"note":"', + "utf8", + ); + const suffix = Buffer.from('"}\n', "utf8"); + writeFileSync(logPath, Buffer.concat([prefix, Buffer.from([0xff]), suffix])); + + const result = runCheckKpi(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 threshold semantics and ignores non-JSON lines", () => { + const dir = mkdtempSync(join(tmpdir(), "noema-check-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: { path: "/health" }, + status_code: 500, + latency_ms: 999, + }), + "", + ].join("\n")); + + const result = runCheckKpi(logPath); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(JSON.parse(result.stdout)).toMatchObject({ + exchange_requests: 1, + exchange_failures: 0, + exchange_failure_rate: 0, + exchange_p95_latency_ms: 120, + pass: true, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); From ee47f520c0983aeaddb792c64a14eada4230276d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 15:09:45 +0900 Subject: [PATCH 2/2] fix(kpi): reject malformed UTF-8 threshold input --- scripts/check-kpi.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/check-kpi.mjs b/scripts/check-kpi.mjs index a9be3b1cc..10ba8c0a8 100755 --- a/scripts/check-kpi.mjs +++ b/scripts/check-kpi.mjs @@ -1,4 +1,6 @@ #!/usr/bin/env node +import { TextDecoder } from "node:util"; + const fs = await import("node:fs/promises"); const { existsSync } = await import("node:fs"); @@ -27,7 +29,15 @@ if (Number.isFinite(requireWindowDays) && requireWindowDays <= 0) { process.exit(1); } -const text = await fs.readFile(inputPath, "utf8"); +const bytes = await fs.readFile(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;