singularity-forge/packages/daemon/src/event-formatter.test.ts
2026-04-15 13:38:15 +02:00

402 lines
13 KiB
TypeScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder } from 'discord.js';
import type { SdkAgentEvent } from '@singularity-forge/rpc-client';
import type { PendingBlocker, FormattedEvent } from './types.js';
import type { RpcExtensionUIRequest } from '@singularity-forge/rpc-client';
import {
formatToolStart,
formatToolEnd,
formatMessage,
formatBlocker,
formatCompletion,
formatError,
formatCostUpdate,
formatSessionStarted,
formatTaskTransition,
formatGenericEvent,
formatEvent,
} from './event-formatter.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function embedColor(fe: FormattedEvent): number | null {
return fe.embed?.data.color ?? null;
}
function embedTitle(fe: FormattedEvent): string | undefined {
return fe.embed?.data.title;
}
function embedDescription(fe: FormattedEvent): string | undefined {
return fe.embed?.data.description;
}
// ---------------------------------------------------------------------------
// formatToolStart
// ---------------------------------------------------------------------------
describe('formatToolStart', () => {
it('produces grey embed with tool name', () => {
const result = formatToolStart({ type: 'tool_execution_start', name: 'read_file' });
assert.ok(result.content.includes('read_file'));
assert.equal(embedColor(result), 0x95a5a6); // grey
assert.ok(embedTitle(result)?.includes('read_file'));
});
it('handles missing name gracefully', () => {
const result = formatToolStart({ type: 'tool_execution_start' });
assert.ok(result.content.includes('unknown'));
});
it('includes input in description when present', () => {
const result = formatToolStart({ type: 'tool_execution_start', name: 'bash', input: 'ls -la' });
assert.ok(embedDescription(result)?.includes('ls -la'));
});
});
// ---------------------------------------------------------------------------
// formatToolEnd
// ---------------------------------------------------------------------------
describe('formatToolEnd', () => {
it('shows success icon for normal completion', () => {
const result = formatToolEnd({ type: 'tool_execution_end', name: 'read_file', output: 'done' });
assert.ok(result.content.includes('✅'));
assert.equal(embedColor(result), 0x95a5a6); // grey
});
it('shows error icon and red color for errored tool', () => {
const result = formatToolEnd({ type: 'tool_execution_end', name: 'bash', isError: true });
assert.ok(result.content.includes('❌'));
assert.equal(embedColor(result), 0xe74c3c); // red
});
it('includes duration when present', () => {
const result = formatToolEnd({ type: 'tool_execution_end', name: 'bash', duration: 3500 });
assert.ok(result.embed?.data.footer?.text?.includes('3.5s'));
});
});
// ---------------------------------------------------------------------------
// formatMessage
// ---------------------------------------------------------------------------
describe('formatMessage', () => {
it('extracts text from content blocks', () => {
const result = formatMessage({
type: 'message',
content: [{ type: 'text', text: 'Hello world' }],
});
assert.ok(embedDescription(result)?.includes('Hello world'));
assert.equal(embedColor(result), 0x3498db); // blue
});
it('falls back to message field when content is a string', () => {
const result = formatMessage({ type: 'message', message: 'plain text' });
assert.ok(embedDescription(result)?.includes('plain text'));
});
it('handles empty content blocks', () => {
const result = formatMessage({ type: 'message', content: [] });
assert.ok(result.content.includes('empty message'));
assert.equal(result.embed, undefined);
});
it('handles null content gracefully', () => {
const result = formatMessage({ type: 'message' });
assert.ok(result.content.includes('empty message'));
});
});
// ---------------------------------------------------------------------------
// formatBlocker — select
// ---------------------------------------------------------------------------
describe('formatBlocker', () => {
it('produces ActionRow with numbered buttons for select', () => {
const blocker: PendingBlocker = {
id: 'req-1',
method: 'select',
message: 'Choose an option',
event: {
type: 'extension_ui_request',
id: 'req-1',
method: 'select',
title: 'Choose',
options: ['Option A', 'Option B', 'Option C'],
},
};
const result = formatBlocker(blocker, '12345');
assert.ok(result.content.includes('<@12345>'));
assert.equal(embedColor(result), 0xf1c40f); // yellow
assert.ok(result.components);
assert.ok(result.components!.length > 0);
// Check buttons
const row = result.components![0];
const buttons = row.components;
assert.equal(buttons.length, 3);
});
it('handles empty options array for select', () => {
const blocker: PendingBlocker = {
id: 'req-2',
method: 'select',
message: 'Pick one',
event: {
type: 'extension_ui_request',
id: 'req-2',
method: 'select',
title: 'Pick',
options: [],
},
};
const result = formatBlocker(blocker, '12345');
// No components when no options
assert.equal(result.components, undefined);
// Embed should show 'No options'
const fields = result.embed?.data.fields;
assert.ok(fields?.some((f) => f.value.includes('No options')));
});
it('produces Yes/No buttons for confirm', () => {
const blocker: PendingBlocker = {
id: 'req-3',
method: 'confirm',
message: 'Are you sure?',
event: {
type: 'extension_ui_request',
id: 'req-3',
method: 'confirm',
title: 'Confirm',
message: 'This will delete everything',
},
};
const result = formatBlocker(blocker, '99999');
assert.ok(result.components);
assert.equal(result.components!.length, 1);
const buttons = result.components![0].components;
assert.equal(buttons.length, 2);
});
it('produces text instructions for input method', () => {
const blocker: PendingBlocker = {
id: 'req-4',
method: 'input',
message: 'Enter your name',
event: {
type: 'extension_ui_request',
id: 'req-4',
method: 'input',
title: 'Name',
placeholder: 'John Doe',
},
};
const result = formatBlocker(blocker, '12345');
// No interactive buttons for input — text instructions only
assert.equal(result.components, undefined);
const fields = result.embed?.data.fields;
assert.ok(fields?.some((f) => f.value.includes('Reply in this channel')));
});
it('produces text instructions for editor method', () => {
const blocker: PendingBlocker = {
id: 'req-5',
method: 'editor',
message: 'Edit the config',
event: {
type: 'extension_ui_request',
id: 'req-5',
method: 'editor',
title: 'Config',
prefill: 'key: value',
},
};
const result = formatBlocker(blocker, '12345');
assert.equal(result.components, undefined);
const fields = result.embed?.data.fields;
assert.ok(fields?.some((f) => f.value.includes('Reply in this channel')));
assert.ok(fields?.some((f) => f.value.includes('key: value')));
});
});
// ---------------------------------------------------------------------------
// formatCompletion
// ---------------------------------------------------------------------------
describe('formatCompletion', () => {
it('shows green for completed', () => {
const result = formatCompletion({ type: 'execution_complete', status: 'completed' });
assert.equal(embedColor(result), 0x2ecc71); // green
assert.ok(result.content.includes('🏁'));
});
it('shows red for error status', () => {
const result = formatCompletion({
type: 'execution_complete',
status: 'error',
reason: 'Out of tokens',
});
assert.equal(embedColor(result), 0xe74c3c); // red
assert.ok(embedDescription(result)?.includes('Out of tokens'));
});
it('includes stats when present', () => {
const result = formatCompletion({
type: 'execution_complete',
status: 'completed',
stats: { cost: 0.42, tokens: { total: 10000 } },
});
const fields = result.embed?.data.fields;
assert.ok(fields?.some((f) => f.value.includes('$0.42')));
assert.ok(fields?.some((f) => f.value.includes('10,000')));
});
});
// ---------------------------------------------------------------------------
// formatError
// ---------------------------------------------------------------------------
describe('formatError', () => {
it('includes session ID and error message', () => {
const result = formatError('sess-abc', 'Connection refused');
assert.equal(embedColor(result), 0xe74c3c); // red
assert.ok(embedDescription(result)?.includes('Connection refused'));
assert.ok(result.embed?.data.footer?.text?.includes('sess-abc'));
});
});
// ---------------------------------------------------------------------------
// formatCostUpdate
// ---------------------------------------------------------------------------
describe('formatCostUpdate', () => {
it('formats cumulative cost', () => {
const result = formatCostUpdate({
type: 'cost_update',
cumulativeCost: 1.23,
tokens: { input: 5000, output: 2000 },
});
assert.ok(result.content.includes('$1.23'));
assert.equal(embedColor(result), 0x3498db); // blue
});
it('handles zero cost', () => {
const result = formatCostUpdate({
type: 'cost_update',
cumulativeCost: 0,
tokens: { input: 0, output: 0 },
});
assert.ok(result.content.includes('$0.0000'));
});
});
// ---------------------------------------------------------------------------
// formatSessionStarted
// ---------------------------------------------------------------------------
describe('formatSessionStarted', () => {
it('includes project name', () => {
const result = formatSessionStarted('my-project');
assert.ok(result.content.includes('my-project'));
assert.ok(embedDescription(result)?.includes('my-project'));
assert.equal(embedColor(result), 0x3498db); // blue
});
});
// ---------------------------------------------------------------------------
// formatTaskTransition
// ---------------------------------------------------------------------------
describe('formatTaskTransition', () => {
it('shows complete icon for completed tasks', () => {
const result = formatTaskTransition({
type: 'task_transition',
taskId: 'T01',
sliceId: 'S01',
status: 'complete',
});
assert.ok(result.content.includes('✅'));
assert.equal(embedColor(result), 0x2ecc71); // green
});
it('shows error icon for errored tasks', () => {
const result = formatTaskTransition({
type: 'task_transition',
taskId: 'T02',
status: 'error',
});
assert.ok(result.content.includes('❌'));
assert.equal(embedColor(result), 0xe74c3c); // red
});
});
// ---------------------------------------------------------------------------
// formatGenericEvent
// ---------------------------------------------------------------------------
describe('formatGenericEvent', () => {
it('renders unknown event type as grey embed', () => {
const result = formatGenericEvent({ type: 'some_custom_event', data: 'hello' });
assert.equal(embedColor(result), 0x95a5a6); // grey
assert.ok(embedTitle(result)?.includes('some_custom_event'));
});
it('handles events with no extra fields', () => {
const result = formatGenericEvent({ type: 'bare_event' });
assert.ok(result.content.includes('bare_event'));
});
});
// ---------------------------------------------------------------------------
// formatEvent — dispatch
// ---------------------------------------------------------------------------
describe('formatEvent', () => {
it('dispatches tool_execution_start', () => {
const result = formatEvent({ type: 'tool_execution_start', name: 'read' });
assert.ok(result.content.includes('🔧'));
});
it('dispatches execution_complete', () => {
const result = formatEvent({ type: 'execution_complete', status: 'completed' });
assert.ok(result.content.includes('🏁'));
});
it('falls back to generic for unknown types', () => {
const result = formatEvent({ type: 'totally_unknown' });
assert.ok(result.content.includes('📡'));
});
it('dispatches cost_update', () => {
const result = formatEvent({ type: 'cost_update', cumulativeCost: 0.5 });
assert.ok(result.content.includes('💰'));
});
it('dispatches message types', () => {
for (const type of ['message_start', 'message_end', 'message']) {
const result = formatEvent({ type, message: 'hi' });
assert.ok(result.content.includes('💬'), `Failed for type: ${type}`);
}
});
// Negative: missing type field
it('handles event with missing type gracefully', () => {
const result = formatEvent({} as SdkAgentEvent);
assert.ok(result.content); // should not throw
});
// Negative: null fields
it('handles event with null fields gracefully', () => {
const result = formatEvent({ type: 'tool_execution_start', name: null } as unknown as SdkAgentEvent);
assert.ok(result.content);
});
});