godcrm/backend/services/agent-run-dispatcher/__tests__/translator.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

175 lines
6.4 KiB
JavaScript

/**
* ADR-0042 Task 4 — eventTranslator unit tests.
*
* Pure module: no DB, no spawn. Boot guard from backend/test/setup.js still
* runs (vitest setupFiles).
*
* Coverage gates per Task 4 brief — ≥10 cases:
* - output → message_start
* - output with tool_use marker → tool_use (regex hit)
* - output with no marker / non-string content → message_start (regex miss)
* - result → message_stop
* - error → error (with message preserved)
* - info → null (skip)
* - unknown / non-object / null / missing-type → null (defensive)
* - mcp__ tool name (double-underscore namespace) → tool_use
* - stream-json passthrough mode (env-flip)
* - warnLegacyOnce only prints once per process
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
eventTranslator,
warnLegacyOnce,
_resetLegacyWarnForTest,
DEFAULT_BACKSTOP_MS,
} from '../run-stream-handler.mjs';
describe('ADR-0042 eventTranslator — basics', () => {
it('exports DEFAULT_BACKSTOP_MS = 4 hours', () => {
expect(DEFAULT_BACKSTOP_MS).toBe(4 * 60 * 60 * 1000);
});
it('output → message_start (no tool marker)', () => {
const out = eventTranslator({ type: 'output', content: 'hello world', status: 'success', exit: 0 });
expect(out).toEqual({ type: 'message_start' });
});
it('output with tool_use JSON marker → tool_use event', () => {
const text = 'thinking...\n{"type":"tool_use","name":"Bash","input":{"cmd":"ls"}}';
const out = eventTranslator({ type: 'output', content: text });
expect(out).toMatchObject({ type: 'tool_use', name: 'Bash' });
});
it('output with mcp__ tool name preserves double-underscore namespace', () => {
const text = '{"type":"tool_use","name":"mcp__godcrm__update_table_row","input":{}}';
const out = eventTranslator({ type: 'output', content: text });
expect(out).toMatchObject({ type: 'tool_use', name: 'mcp__godcrm__update_table_row' });
});
it('output with non-string content → plain message_start', () => {
const out = eventTranslator({ type: 'output', content: { not: 'a string' } });
expect(out).toEqual({ type: 'message_start' });
});
it('output empty content → plain message_start', () => {
const out = eventTranslator({ type: 'output', content: '' });
expect(out).toEqual({ type: 'message_start' });
});
it('result → message_stop', () => {
const out = eventTranslator({ type: 'result', status: 'success', ticket_id: 1, exit: 0 });
expect(out).toEqual({ type: 'message_stop' });
});
it('error → error event with message preserved', () => {
const out = eventTranslator({ type: 'error', message: 'workspace_missing' });
expect(out).toEqual({ type: 'error', message: 'workspace_missing' });
});
it('error with non-string message → empty string', () => {
const out = eventTranslator({ type: 'error', message: { code: 7 } });
expect(out).toEqual({ type: 'error', message: '' });
});
});
describe('ADR-0042 eventTranslator — null returns', () => {
it('info → null (skipped)', () => {
expect(eventTranslator({ type: 'info', message: 'runner_starting' })).toBeNull();
});
it('unknown event type → null', () => {
expect(eventTranslator({ type: 'wibble' })).toBeNull();
});
it('null input → null', () => {
expect(eventTranslator(null)).toBeNull();
});
it('undefined input → null', () => {
expect(eventTranslator(undefined)).toBeNull();
});
it('non-object input → null', () => {
expect(eventTranslator('result')).toBeNull();
expect(eventTranslator(42)).toBeNull();
});
it('object missing `type` field → null', () => {
expect(eventTranslator({ content: 'orphan' })).toBeNull();
});
it('object with non-string `type` → null', () => {
expect(eventTranslator({ type: 7 })).toBeNull();
});
});
describe('ADR-0042 eventTranslator — tool_use regex edge cases', () => {
it('does NOT match a malformed name (numeric only)', () => {
const text = '{"type":"tool_use","name":"42","input":{}}';
const out = eventTranslator({ type: 'output', content: text });
// 42 fails the [A-Za-z_][\w]* anchor → falls back to message_start
expect(out).toEqual({ type: 'message_start' });
});
it('matches the first tool_use when content has multiple', () => {
const text =
'{"type":"tool_use","name":"Read","input":{}} ' +
'and later {"type":"tool_use","name":"Edit","input":{}}';
const out = eventTranslator({ type: 'output', content: text });
expect(out).toMatchObject({ type: 'tool_use', name: 'Read' });
});
it('content with only the word "tool_use" but no JSON → message_start', () => {
const out = eventTranslator({
type: 'output',
content: 'I considered using a tool_use but decided against it.',
});
// No name match possible → plain message_start.
expect(out).toEqual({ type: 'message_start' });
});
});
describe('ADR-0042 eventTranslator — stream-json passthrough', () => {
// The translator reads STREAM_FORMAT at module load. We can't easily flip
// it mid-test without re-importing the module — vi.resetModules + dynamic
// import is the standard pattern.
it('passes Anthropic stream-json events through unchanged when AGENT_STREAM_FORMAT=stream-json', async () => {
vi.resetModules();
const prev = process.env.AGENT_STREAM_FORMAT;
process.env.AGENT_STREAM_FORMAT = 'stream-json';
try {
const mod = await import('../run-stream-handler.mjs');
const evt = { type: 'message_start', message: { id: 'msg_1' } };
expect(mod.eventTranslator(evt)).toBe(evt);
// info also passes through (not translated).
const info = { type: 'info', message: 'x' };
expect(mod.eventTranslator(info)).toBe(info);
} finally {
if (prev === undefined) delete process.env.AGENT_STREAM_FORMAT;
else process.env.AGENT_STREAM_FORMAT = prev;
vi.resetModules();
}
});
});
describe('ADR-0042 warnLegacyOnce', () => {
let warnSpy;
beforeEach(() => {
_resetLegacyWarnForTest();
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
warnSpy.mockRestore();
_resetLegacyWarnForTest();
});
it('prints exactly once on repeated calls in legacy mode', () => {
warnLegacyOnce();
warnLegacyOnce();
warnLegacyOnce();
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toContain('AGENT_STREAM_FORMAT=legacy');
});
});