ktx/packages/cli/src/demo-progress.test.ts
2026-05-10 23:51:24 +02:00

228 lines
6.8 KiB
TypeScript

import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
import { describe, expect, it } from 'vitest';
import { createPlainProgressEmitter, formatMemoryFlowEventLine } from './demo-progress.js';
function snapshot(events: MemoryFlowEvent[]): MemoryFlowReplayInput {
return {
runId: 'run-1',
connectionId: 'orbit_demo',
adapter: 'live-database',
status: 'running',
sourceDir: null,
syncId: 'sync-1',
errors: [],
events,
plannedWorkUnits: [],
details: { actions: [], provenance: [], transcripts: [] },
};
}
describe('formatMemoryFlowEventLine', () => {
it('formats source_acquired in plain English with adapter and file count', () => {
expect(
formatMemoryFlowEventLine({
type: 'source_acquired',
adapter: 'live-database',
trigger: 'manual_resync',
fileCount: 7,
}),
).toBe('[connect] Connected live-database - 7 database files (manual_resync)');
});
it('formats diff_computed as a comma-separated breakdown', () => {
expect(
formatMemoryFlowEventLine({
type: 'diff_computed',
added: 3,
modified: 1,
deleted: 0,
unchanged: 4,
}),
).toBe('[diff] Tables: +3 new, ~1 changed, =4 unchanged');
});
it('formats diff_computed as "no changes" when every counter is zero', () => {
expect(
formatMemoryFlowEventLine({
type: 'diff_computed',
added: 0,
modified: 0,
deleted: 0,
unchanged: 0,
}),
).toBe('[diff] Tables: no changes');
});
it('formats chunks_planned without removals as a single readable sentence', () => {
expect(
formatMemoryFlowEventLine({
type: 'chunks_planned',
chunkCount: 7,
workUnitCount: 5,
evictionCount: 0,
}),
).toBe('[plan] Grouped 5 tables into 7 business areas');
});
it('formats chunks_planned with removals when evictions are non-zero', () => {
expect(
formatMemoryFlowEventLine({
type: 'chunks_planned',
chunkCount: 7,
workUnitCount: 5,
evictionCount: 2,
}),
).toBe('[plan] Grouped 5 tables into 7 business areas (2 removals)');
});
it('formats work_unit_started in human terms', () => {
expect(
formatMemoryFlowEventLine({
type: 'work_unit_started',
unitKey: 'revenue-policy',
skills: ['sl_expert', 'wiki_writer'],
stepBudget: 40,
}),
).toBe('[analyze] Reviewing "revenue-policy" - budget 40 agent steps');
});
it('suppresses noisy work_unit_step events', () => {
expect(
formatMemoryFlowEventLine({
type: 'work_unit_step',
unitKey: 'revenue-policy',
stepIndex: 3,
stepBudget: 40,
}),
).toBeNull();
});
it('formats candidate_action with friendly target and arrow', () => {
expect(
formatMemoryFlowEventLine({
type: 'candidate_action',
unitKey: 'revenue-policy',
target: 'sl',
action: 'created',
key: 'warehouse.revenue',
}),
).toBe('[draft] revenue-policy -> semantic-layer: created warehouse.revenue');
});
it('formats work_unit_finished with status-aware tag', () => {
expect(
formatMemoryFlowEventLine({
type: 'work_unit_finished',
unitKey: 'revenue-policy',
status: 'success',
}),
).toBe('[done] revenue-policy reviewed');
expect(
formatMemoryFlowEventLine({
type: 'work_unit_finished',
unitKey: 'revenue-policy',
status: 'failed',
reason: 'budget exhausted',
}),
).toBe('[fail] revenue-policy needs attention - budget exhausted');
});
it('formats reconciliation_finished with friendly counter wording', () => {
expect(
formatMemoryFlowEventLine({
type: 'reconciliation_finished',
conflictCount: 0,
fallbackCount: 0,
}),
).toBe('[validate] Reconciled drafts - no conflicts, nothing flagged for review');
expect(
formatMemoryFlowEventLine({
type: 'reconciliation_finished',
conflictCount: 2,
fallbackCount: 1,
}),
).toBe('[validate] Reconciled drafts - 2 conflicts, 1 item flagged for review');
});
it('formats saved with optional shortened commit sha and pluralized memory count', () => {
expect(
formatMemoryFlowEventLine({
type: 'saved',
commitSha: 'abc1234567890', // pragma: allowlist secret
wikiCount: 2,
slCount: 5,
}),
).toBe('[memory] Saved 7 memories (2 wiki, 5 semantic-layer) - commit abc1234');
expect(
formatMemoryFlowEventLine({
type: 'saved',
commitSha: null,
wikiCount: 0,
slCount: 1,
}),
).toBe('[memory] Saved 1 memory (0 wiki, 1 semantic-layer)');
});
it('formats report_created with run id', () => {
expect(
formatMemoryFlowEventLine({
type: 'report_created',
runId: 'run-xyz',
}),
).toBe('[report] Run report ready: run-xyz');
});
});
describe('createPlainProgressEmitter', () => {
it('writes one line per new event and never re-emits prior events', () => {
const written: string[] = [];
const io = {
stdout: { write: (chunk: string) => written.push(chunk), isTTY: false },
stderr: { write: () => undefined },
};
const emit = createPlainProgressEmitter(io);
emit(
snapshot([
{ type: 'source_acquired', adapter: 'live-database', trigger: 'manual_resync', fileCount: 7 },
{ type: 'diff_computed', added: 0, modified: 0, deleted: 0, unchanged: 7 },
]),
);
emit(
snapshot([
{ type: 'source_acquired', adapter: 'live-database', trigger: 'manual_resync', fileCount: 7 },
{ type: 'diff_computed', added: 0, modified: 0, deleted: 0, unchanged: 7 },
{ type: 'work_unit_started', unitKey: 'revenue-policy', skills: ['sl_expert'], stepBudget: 40 },
]),
);
expect(written).toEqual([
'[connect] Connected live-database - 7 database files (manual_resync)\n',
'[diff] Tables: =7 unchanged\n',
'[analyze] Reviewing "revenue-policy" - budget 40 agent steps\n',
]);
});
it('skips suppressed events without advancing visible output', () => {
const written: string[] = [];
const io = {
stdout: { write: (chunk: string) => written.push(chunk), isTTY: false },
stderr: { write: () => undefined },
};
const emit = createPlainProgressEmitter(io);
emit(
snapshot([
{ type: 'work_unit_step', unitKey: 'a', stepIndex: 1, stepBudget: 40 },
{ type: 'work_unit_step', unitKey: 'a', stepIndex: 2, stepBudget: 40 },
{ type: 'work_unit_finished', unitKey: 'a', status: 'success' },
]),
);
expect(written).toEqual(['[done] a reviewed\n']);
});
});