import { describe, it, expect } from 'vitest' import { createNodeRegistry, mapNodeEvent } from './nodes' describe('node registry', () => { it('registers, gets, lists and removes nodes', () => { const reg = createNodeRegistry() expect(reg.list()).toEqual([]) reg.register({ teamId: 't1', url: 'http://127.0.0.1:8080', token: 'zc_a' }) reg.register({ teamId: 't2', url: 'http://127.0.0.1:8081', token: 'zc_b' }) expect(reg.get('t1')).toEqual({ teamId: 't1', url: 'http://127.0.0.1:8080', token: 'zc_a' }) expect(reg.list()).toHaveLength(2) // re-registering the same team replaces, not duplicates reg.register({ teamId: 't1', url: 'http://127.0.0.1:9090', token: 'zc_c' }) expect(reg.list()).toHaveLength(2) expect(reg.get('t1')?.url).toBe('http://127.0.0.1:9090') reg.remove('t1') expect(reg.get('t1')).toBeUndefined() expect(reg.list()).toHaveLength(1) }) it('seeds from an initial list', () => { const reg = createNodeRegistry([{ teamId: 't1', url: 'u', token: 'k' }]) expect(reg.get('t1')?.token).toBe('k') }) }) describe('mapNodeEvent — ZeroClaw /api/events → WsEvent', () => { it('maps agent_start to a thinking activity', () => { const ev = mapNodeEvent('t1', { type: 'agent_start', timestamp: '2026-07-03T11:00:00Z' }) expect(ev).toEqual({ type: 'node:activity', teamId: 't1', kind: 'thinking', label: expect.any(String), ts: '2026-07-03T11:00:00Z', }) }) it('classifies a uno_q_flash tool call as a flash activity', () => { const ev = mapNodeEvent('t1', { type: 'tool_call_start', tool: 'uno_q_flash', timestamp: 'T' }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'flash' }) }) it('classifies a non-flash tool call as a tool activity', () => { const ev = mapNodeEvent('t1', { type: 'tool_call_start', tool: 'sysfs_led', timestamp: 'T' }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'tool', label: expect.stringContaining('sysfs_led') }) }) it('maps a successful flash log message to a flash activity with the address', () => { const ev = mapNodeEvent('t1', { '@timestamp': '2026-07-03T11:34:00Z', message: "Sketch compiled and flashed to the Uno Q MCU sketch partition at 0x80F0000. 'reset run' issued.", }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'flash' }) expect((ev as { label: string }).label).toContain('0x80F0000') expect((ev as { ts: string }).ts).toBe('2026-07-03T11:34:00Z') }) it('maps a compile error / failure to an error activity', () => { const ev = mapNodeEvent('t1', { severity_text: 'ERROR', message: 'Arduino compile error — fix the sketch' }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'error' }) }) it('maps a cloud-exhausted / failover log to a fallback activity (not an error)', () => { const ev = mapNodeEvent('t1', { severity_text: 'ERROR', message: 'Exhausted retries, trying next model_provider/model', }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'fallback' }) expect((ev as { label: string }).label).toMatch(/on-board Qwen/i) }) it('maps a mid-retry ModelProvider failure to a fallback activity', () => { const ev = mapNodeEvent('t1', { message: 'ModelProvider call failed, retrying' }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'fallback' }) }) it('maps agent_end to a response activity', () => { const ev = mapNodeEvent('t1', { type: 'agent_end', timestamp: 'T' }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'response' }) }) it('surfaces a tool_call_result output as the response text (the real answer)', () => { const ev = mapNodeEvent('t1', { message: 'tool_call_result', attributes: { tool: 'i2c_scan', output: 'No I2C devices responded on the bus.', error_reason: null }, event: { action: 'complete', category: 'tool', outcome: 'success' }, }) expect(ev).toMatchObject({ type: 'node:activity', kind: 'response', label: 'No I2C devices responded on the bus.' }) }) it('falls back to a tool ✓ marker when a tool result carries no output', () => { const ev = mapNodeEvent('t1', { message: 'tool_call_result', attributes: { tool: 'matrix_text', output: '' }, event: { outcome: 'success' }, }) expect(ev).toMatchObject({ kind: 'response', label: 'matrix_text ✓' }) }) it('maps a failed tool result to an error activity', () => { const ev = mapNodeEvent('t1', { message: 'tool_call_result', attributes: { tool: 'i2c_scan', output: 'bridge unreachable', error_reason: 'timeout' }, event: { outcome: 'failure' }, }) expect(ev).toMatchObject({ kind: 'error', label: 'bridge unreachable' }) }) it('ignores noisy/internal events (llm_request, plain notes, non-objects)', () => { expect(mapNodeEvent('t1', { type: 'llm_request' })).toBeNull() expect(mapNodeEvent('t1', { message: 'No sandbox backend available, using application-layer security' })).toBeNull() expect(mapNodeEvent('t1', null)).toBeNull() expect(mapNodeEvent('t1', 'nope')).toBeNull() }) })