feat: add command center chat loop widget

This commit is contained in:
thanhnv
2026-07-08 22:58:02 +09:00
parent 36af576f15
commit a029a51e72
14 changed files with 225 additions and 38 deletions
@@ -5,7 +5,7 @@ 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 } from '../src/telemetry/telemetry.service.js';
import { TelemetryService, buildChatLoopWidget } from '../src/telemetry/telemetry.service.js';
test('readJsonl parses valid lines and skips malformed', () => {
const d = mkdtempSync(join(tmpdir(), 'cp-'));
@@ -53,6 +53,7 @@ test('commandCenter() returns evidence-backed widget contract', () => {
const ids = Object.keys(command.widgets);
assert.deepEqual(ids.sort(), [
'certification',
'chat_loop',
'finops',
'hitl',
'kill_switch',
@@ -71,6 +72,53 @@ test('commandCenter() returns evidence-backed widget contract', () => {
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');