146 lines
6.0 KiB
TypeScript
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;
|
|
});
|