Files
CASAN/packages/casan-control-panel/backend/test/telemetry.test.ts
T

146 lines
6.0 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { readJsonl, readJson, readHead } from '../src/telemetry/telemetry.reader.js';
import { APP_ROOT, PATHS, isStale, metricsAgeSeconds } from '../src/common/app-root.js';
import { TelemetryService, buildChatLoopWidget } from '../src/telemetry/telemetry.service.js';
test('readJsonl parses valid lines and skips malformed', () => {
const d = mkdtempSync(join(tmpdir(), 'cp-'));
const f = join(d, 'x.jsonl');
writeFileSync(f, '{"a":1}\n\nnot-json\n{"a":2}\n');
const rows = readJsonl<{ a: number }>(f);
assert.equal(rows.length, 2);
assert.equal(rows[0].a, 1);
assert.equal(rows[1].a, 2);
});
test('readJsonl/readJson tolerate missing files (no throw)', () => {
assert.deepEqual(readJsonl('/no/such/file.jsonl'), []);
assert.equal(readJson('/no/such/file.json'), null);
assert.equal(readHead('/no/such/head.txt'), null);
});
test('APP_ROOT resolves to the CASAN app root (has a harness marker)', () => {
const marker = existsSync(join(APP_ROOT, 'packages', 'casan-harness')) || existsSync(join(APP_ROOT, '.specify'));
assert.ok(marker, `APP_ROOT=${APP_ROOT} should contain .specify or packages/casan-harness`);
});
test('overview() returns real aggregated shape, never throws on the repo state', () => {
const svc = new TelemetryService();
const o = svc.overview() as any;
assert.ok(o.totals && typeof o.totals.runs === 'number');
assert.ok(o.harness_signals && o.harness_signals['H5-governance']);
assert.ok(o.audit_chain && typeof o.audit_chain.records === 'number');
assert.ok('stale' in o && 'stale_after_s' in o);
// real data: metrics + audit feeds exist in this repo, so counts are non-negative
assert.ok(o.totals.runs >= 0);
assert.ok(o.audit_chain.records >= 0);
});
test('prometheusMetrics() exports aggregate-safe operational metrics only', () => {
const metrics = new TelemetryService().prometheusMetrics();
assert.match(metrics, /^# HELP casan_telemetry_stale/m);
assert.match(metrics, /^casan_runs_total \d+/m);
assert.match(metrics, /^casan_security_blocks_total \d+/m);
assert.match(metrics, /^casan_governance_denials_total \d+/m);
assert.doesNotMatch(metrics, /trace_id|tenant|prompt|evidence/i);
});
test('security()/governance()/cost() return objects with expected keys', () => {
const svc = new TelemetryService();
assert.ok(typeof (svc.security() as any).by_status === 'object');
assert.ok(typeof (svc.governance() as any).by_decision === 'object');
assert.ok('provider_tokens' in (svc.cost() as any));
});
test('commandCenter() returns evidence-backed widget contract', () => {
const svc = new TelemetryService();
const command = svc.commandCenter() as any;
const ids = Object.keys(command.widgets);
assert.deepEqual(ids.sort(), [
'certification',
'chat_loop',
'finops',
'hitl',
'kill_switch',
'maturity',
'security',
'self_improve',
'traceability',
]);
for (const w of Object.values(command.widgets) as any[]) {
assert.ok(w.id && w.title && w.status);
assert.ok(w.envelope && typeof w.envelope.artifact_path === 'string');
assert.ok(['verified', 'present_unverified', 'missing'].includes(w.envelope.status));
assert.equal(typeof w.envelope.verified, 'boolean');
}
assert.ok(Array.isArray(command.briefing));
assert.ok(Array.isArray(command.ticker));
});
test('buildChatLoopWidget summarizes chat replay fixture and evidence', () => {
const d = mkdtempSync(join(tmpdir(), 'cp-chat-'));
const auditPath = join(d, 'chat-turns.jsonl');
writeFileSync(auditPath, '{"seq":1}\n{"seq":2}\n');
const prevBudget = process.env.CASAN_CHAT_TOKEN_BUDGET;
process.env.CASAN_CHAT_TOKEN_BUDGET = '200';
try {
const widget = buildChatLoopWidget(
[
{ seq: 1, timestamp: '2026-07-08T00:00:00Z', chat_id: 'chat1', turn_id: 't1', mode: 'READ_ONLY', decision: 'ANSWERED' },
{
seq: 2,
timestamp: '2026-07-08T00:01:00Z',
chat_id: 'chat1',
turn_id: 't2',
mode: 'OPERATOR',
decision: 'ACTION_COMPLETED',
loop_run: { run_id: 'loop1', decision: 'CERTIFIED' },
record_hash: 'abc123',
},
],
[
{ agent: 'chat.ask-casan', step: 'ask-casan-readonly', total_tokens: 40 },
{ agent: 'chat.operator', step: 'operator:generate_report', total_tokens: 60 },
{ agent: 'planner', step: 'unrelated', total_tokens: 900 },
],
auditPath,
{ ok: true, decision: 'MATCH', records: 2, loop_replayed: 1, diffs: [], audit_path: auditPath },
) as any;
assert.equal(widget.id, 'chat_loop');
assert.equal(widget.status, 'ok');
assert.equal(widget.metrics.turns, 2);
assert.equal(widget.metrics.answered, 1);
assert.equal(widget.metrics.action_completed, 1);
assert.equal(widget.metrics.loop_runs, 1);
assert.equal(widget.metrics.replay_loop_replayed, 1);
assert.equal(widget.metrics.budget_used_pct, 50);
assert.equal(widget.envelope.verified, true);
assert.equal(widget.evidence.latest_turn.loop_run.run_id, 'loop1');
assert.equal(widget.evidence.replay.decision, 'MATCH');
} finally {
if (prevBudget === undefined) delete process.env.CASAN_CHAT_TOKEN_BUDGET;
else process.env.CASAN_CHAT_TOKEN_BUDGET = prevBudget;
}
});
test('freshness helpers behave (age is number|null, isStale is boolean)', () => {
const age = metricsAgeSeconds();
assert.ok(age === null || typeof age === 'number');
assert.equal(typeof isStale(), 'boolean');
});
test('missing metrics feed => stale (fail-loud)', () => {
const prev = process.env.CASAN_DASHBOARD_METRICS;
process.env.CASAN_DASHBOARD_METRICS = '/no/such/metrics.jsonl';
// re-import is not trivial (module caches PATHS); assert the reader-level contract instead:
assert.deepEqual(readJsonl('/no/such/metrics.jsonl'), []);
if (prev === undefined) delete process.env.CASAN_DASHBOARD_METRICS;
else process.env.CASAN_DASHBOARD_METRICS = prev;
void PATHS;
});