import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { TELEMETRY_GAP_BUCKET_LABEL, TELEMETRY_PERCENT_SHOWN_BUCKETS, TELEMETRY_STOP_REASON_LABEL, parseRectificationTelemetrySummary, parseTelemetrySummaryWeeks, } from "../src/lib/admin/rectification-telemetry-summary.ts"; import { RECTIFICATION_TELEMETRY_STOP_REASONS } from "../src/lib/rectification-agentic/v9/telemetry.ts"; import { RANGE_DELIVERY_PERCENT_MIN_GAP } from "../src/lib/rectification-agentic/user-copy.ts"; const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8"); const route = read("../src/app/api/admin/rectification-telemetry/route.ts"); const view = read("../src/components/admin/rectification-telemetry-summary.tsx"); const adminApp = read("../src/components/admin/admin-app.tsx"); const providers = read("../src/lib/admin/providers.ts"); const migration = read("../supabase/migrations/20260926010000_rectification_telemetry.sql"); test("admin telemetry route is a read-only aggregate behind admin permission", () => { assert.match(route, /requirePermission\("admin\.customers\.read"\)/); assert.match(route, /select public\.rectification_telemetry_summary\(\$1::integer\) as summary/); assert.doesNotMatch(route, /export async function (?:POST|PUT|PATCH|DELETE)/); // No per-row read: the route never selects from the table, and the admin role has no table grant. assert.doesNotMatch(route, /from public\.rectification_telemetry\b/); assert.doesNotMatch(migration, /grant [a-z, ]+ on table public\.rectification_telemetry[a-z_]* to admin_runtime/); assert.match(migration, /revoke all on table public\.rectification_telemetry from admin_runtime;/); }); test("admin telemetry page is registered with the same permission and offers no export", () => { assert.match(adminApp, /name: "rectification-telemetry", list: "\/admin\/rectification-telemetry"/); assert.match(providers, /"rectification-telemetry": \{ read: "admin\.customers\.read" \}/); assert.doesNotMatch(view, /download|\.csv|Blob\(|createObjectURL|\/api\/admin\/rectification-telemetry\/rows/i); assert.match(view, /不能逐条查看或导出/); assert.match(view, /保留 180 天/); }); test("summary parser keeps a fixed shape for empty and partial payloads", () => { const empty = parseRectificationTelemetrySummary(null); assert.equal(empty.sessions, 0); assert.equal(empty.retentionDays, 180); assert.deepEqual(empty.weekly, []); assert.equal(empty.medians.rangeWidthMinutesP50, null); const parsed = parseRectificationTelemetrySummary({ weeks: 4, since: "2026-09-07", retention_days: 180, sessions: 3, medians: { range_width_minutes_p50: 12.5, questions_total_p50: "7" }, question_types: { targeted: 2.33, guided: 1, guided_asked_sessions: 2 }, width_distribution: [{ key: "11-20", count: 2 }, { key: "unknown", count: 1 }, { nope: true }], stop_reasons: [{ key: "converged", count: 1 }], precision_gate: { met: 1, not_met: 2, unknown: 0 }, weekly: [{ week: "2026-09-21", sessions: 3, precision_gate_met: 1, precision_gate_known: 3 }], versions: [{ algorithm_version: "a-1", policy_version: null, skill_version: "10.0.30", count: 3 }], }); assert.equal(parsed.weeks, 4); assert.equal(parsed.medians.questionsTotalP50, 7); assert.equal(parsed.questionTypes.guidedAskedSessions, 2); assert.deepEqual(parsed.widthDistribution, [{ key: "11-20", count: 2 }, { key: "unknown", count: 1 }]); assert.equal(parsed.weekly[0]!.precisionGateKnown, 3); assert.equal(parsed.versions[0]!.policyVersion, null); }); test("week selector accepts only the offered windows", () => { assert.equal(parseTelemetrySummaryWeeks("4"), 4); assert.equal(parseTelemetrySummaryWeeks("26"), 26); assert.equal(parseTelemetrySummaryWeeks("52"), 12); assert.equal(parseTelemetrySummaryWeeks(null), 12); }); test("labels cover every stop reason and the percent buckets follow the card rule", () => { for (const reason of RECTIFICATION_TELEMETRY_STOP_REASONS) { assert.ok(TELEMETRY_STOP_REASON_LABEL[reason], reason); assert.match(migration, new RegExp(`'${reason}'`)); } assert.equal(RANGE_DELIVERY_PERCENT_MIN_GAP, 5); assert.deepEqual(TELEMETRY_PERCENT_SHOWN_BUCKETS, ["5-9", "10+"]); assert.match(migration, /\('5-9', 5, 9, 3\), \('10\+', 10, null, 4\)/); for (const key of ["0-2", "3-4", "5-9", "10+", "single"]) assert.ok(TELEMETRY_GAP_BUCKET_LABEL[key], key); });