import { describe, expect, it, vi } from 'vitest'; import { A2UISurfaceManager, getValueAtPointer, setValueAtPointer, } from '../../../src/renderer/a2ui/A2UISurfaceManager'; import type { A2UIServerMessage, A2UIComponent } from '../../../src/main/a2ui/types'; describe('A2UISurfaceManager', () => { function createTestComponent(overrides: Partial = {}): A2UIComponent { return { id: 'comp-1', type: 'text', properties: { text: 'Hello' }, ...overrides, }; } describe('createSurface', () => { it('creates a new surface with empty state', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 'surface-1', conversationId: 'conv-1', }); const surface = manager.getSurface('surface-1'); expect(surface).toBeDefined(); expect(surface!.conversationId).toBe('conv-1'); expect(surface!.components.size).toBe(0); expect(surface!.rootIds).toEqual([]); expect(surface!.dataModel).toEqual({}); }); it('preserves metadata including turnIndex on createSurface', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 'surface-1', conversationId: 'conv-1', metadata: { turnIndex: 3 }, }); const surface = manager.getSurface('surface-1'); expect(surface).toBeDefined(); expect(surface!.metadata).toEqual({ turnIndex: 3 }); }); it('notifies listeners on surface creation', () => { const manager = new A2UISurfaceManager(); const listener = vi.fn(); manager.onChange(listener); manager.processMessage({ type: 'createSurface', surfaceId: 'surface-1', conversationId: 'conv-1', }); expect(listener).toHaveBeenCalledWith('surface-1'); }); }); describe('updateComponents', () => { it('adds components to an existing surface', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 'surface-1', conversationId: 'conv-1', }); const component = createTestComponent(); manager.processMessage({ type: 'updateComponents', surfaceId: 'surface-1', components: [component], rootIds: ['comp-1'], }); const surface = manager.getSurface('surface-1'); expect(surface!.components.size).toBe(1); expect(surface!.components.get('comp-1')).toEqual(component); expect(surface!.rootIds).toEqual(['comp-1']); }); it('ignores updateComponents for non-existent surfaces', () => { const manager = new A2UISurfaceManager(); const listener = vi.fn(); manager.onChange(listener); manager.processMessage({ type: 'updateComponents', surfaceId: 'nonexistent', components: [createTestComponent()], rootIds: ['comp-1'], }); expect(listener).not.toHaveBeenCalled(); }); }); describe('updateDataModel', () => { it('sets a value in the data model', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 'surface-1', conversationId: 'conv-1', }); manager.processMessage({ type: 'updateDataModel', surfaceId: 'surface-1', path: '/chartData', value: [{ label: 'A', value: 1 }], }); const dataModel = manager.getDataModel('surface-1'); expect(dataModel).toEqual({ chartData: [{ label: 'A', value: 1 }] }); }); it('ignores updateDataModel for non-existent surfaces', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'updateDataModel', surfaceId: 'nonexistent', path: '/foo', value: 'bar', }); expect(manager.getDataModel('nonexistent')).toEqual({}); }); }); describe('deleteSurface', () => { it('removes a surface', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 'surface-1', conversationId: 'conv-1', }); expect(manager.getSurface('surface-1')).toBeDefined(); manager.processMessage({ type: 'deleteSurface', surfaceId: 'surface-1', }); expect(manager.getSurface('surface-1')).toBeUndefined(); }); }); describe('getSurfaceIds', () => { it('returns surface IDs for a specific conversation', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 's1', conversationId: 'conv-1' }); manager.processMessage({ type: 'createSurface', surfaceId: 's2', conversationId: 'conv-1' }); manager.processMessage({ type: 'createSurface', surfaceId: 's3', conversationId: 'conv-2' }); expect(manager.getSurfaceIds('conv-1')).toEqual(['s1', 's2']); expect(manager.getSurfaceIds('conv-2')).toEqual(['s3']); expect(manager.getSurfaceIds('conv-3')).toEqual([]); }); }); describe('resolveTree', () => { it('resolves a flat component buffer into a nested tree', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 's1', conversationId: 'conv-1' }); manager.processMessage({ type: 'updateComponents', surfaceId: 's1', components: [ { id: 'root', type: 'column', properties: {}, children: ['child-1', 'child-2'] }, { id: 'child-1', type: 'text', properties: { text: 'Hello' } }, { id: 'child-2', type: 'button', properties: { label: 'Click' } }, ], rootIds: ['root'], }); const tree = manager.resolveTree('s1'); expect(tree).toHaveLength(1); expect(tree[0].type).toBe('column'); expect(tree[0].children).toHaveLength(2); expect(tree[0].children[0].type).toBe('text'); expect(tree[0].children[0].properties.text).toBe('Hello'); expect(tree[0].children[1].type).toBe('button'); }); it('resolves data bindings to bound values', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 's1', conversationId: 'conv-1' }); manager.processMessage({ type: 'updateComponents', surfaceId: 's1', components: [ { id: 'chart-1', type: 'chart', properties: { chartType: 'bar' }, dataBinding: '/data' }, ], rootIds: ['chart-1'], }); manager.processMessage({ type: 'updateDataModel', surfaceId: 's1', path: '/data', value: [1, 2, 3], }); const tree = manager.resolveTree('s1'); expect(tree[0].boundValue).toEqual([1, 2, 3]); }); it('returns empty array for non-existent surface', () => { const manager = new A2UISurfaceManager(); expect(manager.resolveTree('nonexistent')).toEqual([]); }); it('filters out unresolvable child references', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 's1', conversationId: 'conv-1' }); manager.processMessage({ type: 'updateComponents', surfaceId: 's1', components: [ { id: 'root', type: 'column', properties: {}, children: ['child-1', 'missing'] }, { id: 'child-1', type: 'text', properties: { text: 'Hello' } }, ], rootIds: ['root'], }); const tree = manager.resolveTree('s1'); expect(tree[0].children).toHaveLength(1); }); }); describe('updateLocalData', () => { it('updates data model and notifies listeners', () => { const manager = new A2UISurfaceManager(); const listener = vi.fn(); manager.processMessage({ type: 'createSurface', surfaceId: 's1', conversationId: 'conv-1' }); manager.onChange(listener); manager.updateLocalData('s1', '/formData/name', 'John'); expect(manager.getDataModel('s1')).toEqual({ formData: { name: 'John' } }); expect(listener).toHaveBeenCalledWith('s1'); }); it('ignores updates for non-existent surfaces', () => { const manager = new A2UISurfaceManager(); const listener = vi.fn(); manager.onChange(listener); manager.updateLocalData('nonexistent', '/foo', 'bar'); expect(listener).not.toHaveBeenCalled(); }); }); describe('clearConversation', () => { it('removes all surfaces for a conversation', () => { const manager = new A2UISurfaceManager(); manager.processMessage({ type: 'createSurface', surfaceId: 's1', conversationId: 'conv-1' }); manager.processMessage({ type: 'createSurface', surfaceId: 's2', conversationId: 'conv-1' }); manager.processMessage({ type: 'createSurface', surfaceId: 's3', conversationId: 'conv-2' }); manager.clearConversation('conv-1'); expect(manager.getSurfaceIds('conv-1')).toEqual([]); expect(manager.getSurfaceIds('conv-2')).toEqual(['s3']); }); }); describe('onChange', () => { it('returns an unsubscribe function', () => { const manager = new A2UISurfaceManager(); const listener = vi.fn(); const unsubscribe = manager.onChange(listener); manager.processMessage({ type: 'createSurface', surfaceId: 's1', conversationId: 'conv-1' }); expect(listener).toHaveBeenCalledTimes(1); unsubscribe(); manager.processMessage({ type: 'createSurface', surfaceId: 's2', conversationId: 'conv-1' }); expect(listener).toHaveBeenCalledTimes(1); }); }); }); describe('JSON Pointer utilities', () => { describe('getValueAtPointer', () => { it('returns the root object for empty or "/" pointer', () => { const obj = { foo: 'bar' }; expect(getValueAtPointer(obj, '')).toBe(obj); expect(getValueAtPointer(obj, '/')).toBe(obj); }); it('gets a top-level value', () => { expect(getValueAtPointer({ name: 'Alice' }, '/name')).toBe('Alice'); }); it('gets a nested value', () => { const obj = { a: { b: { c: 42 } } }; expect(getValueAtPointer(obj, '/a/b/c')).toBe(42); }); it('returns undefined for missing paths', () => { expect(getValueAtPointer({ a: 1 }, '/b')).toBeUndefined(); expect(getValueAtPointer({ a: 1 }, '/a/b/c')).toBeUndefined(); }); it('handles escaped pointer characters', () => { const obj = { 'a/b': { '~c': 'value' } }; expect(getValueAtPointer(obj, '/a~1b/~0c')).toBe('value'); }); }); describe('setValueAtPointer', () => { it('sets a top-level value', () => { const obj: Record = {}; setValueAtPointer(obj, '/name', 'Alice'); expect(obj.name).toBe('Alice'); }); it('creates intermediate objects for nested paths', () => { const obj: Record = {}; setValueAtPointer(obj, '/a/b/c', 42); expect(obj).toEqual({ a: { b: { c: 42 } } }); }); it('does nothing for empty or root pointer', () => { const obj = { foo: 'bar' }; setValueAtPointer(obj, '', 'new'); setValueAtPointer(obj, '/', 'new'); expect(obj).toEqual({ foo: 'bar' }); }); it('handles escaped pointer characters', () => { const obj: Record = {}; setValueAtPointer(obj, '/a~1b', 'value'); expect(obj['a/b']).toBe('value'); }); }); });