feat: @agor/stores package (3 stores) + 58 BackendAdapter tests
@agor/stores: - theme.svelte.ts, notifications.svelte.ts, health.svelte.ts extracted - Original files replaced with re-exports (zero consumer changes needed) - pnpm workspace + Vite/tsconfig aliases configured BackendAdapter tests (58 new): - backend-adapter.test.ts: 9 tests (lifecycle, singleton, testing seam) - tauri-adapter.test.ts: 28 tests (invoke mapping, command names, params) - electrobun-adapter.test.ts: 21 tests (RPC names, capabilities, stubs) Total: 523 tests passing (was 465, +58)
This commit is contained in:
parent
5e1fd62ed9
commit
f0850f0785
22 changed files with 1389 additions and 25 deletions
91
src/lib/backend/__tests__/backend-adapter.test.ts
Normal file
91
src/lib/backend/__tests__/backend-adapter.test.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Tests for getBackend/setBackend/setBackendForTesting lifecycle
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
getBackend,
|
||||
setBackend,
|
||||
setBackendForTesting,
|
||||
resetBackendForTesting,
|
||||
} from '../backend';
|
||||
import type { BackendAdapter } from '@agor/types';
|
||||
|
||||
beforeEach(() => {
|
||||
resetBackendForTesting();
|
||||
});
|
||||
|
||||
describe('backend singleton', () => {
|
||||
describe('getBackend', () => {
|
||||
it('throws before setBackend is called', () => {
|
||||
expect(() => getBackend()).toThrow(
|
||||
'[backend] Adapter not initialized. Call setBackend() before accessing the backend.'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns adapter after setBackend', () => {
|
||||
const mockAdapter = { capabilities: {} } as unknown as BackendAdapter;
|
||||
setBackend(mockAdapter);
|
||||
expect(getBackend()).toBe(mockAdapter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBackend', () => {
|
||||
it('sets the adapter on first call', () => {
|
||||
const mockAdapter = { capabilities: {} } as unknown as BackendAdapter;
|
||||
setBackend(mockAdapter);
|
||||
expect(getBackend()).toBe(mockAdapter);
|
||||
});
|
||||
|
||||
it('throws on second call', () => {
|
||||
const adapter1 = { capabilities: {} } as unknown as BackendAdapter;
|
||||
const adapter2 = { capabilities: {} } as unknown as BackendAdapter;
|
||||
setBackend(adapter1);
|
||||
expect(() => setBackend(adapter2)).toThrow(
|
||||
'[backend] Adapter already set. setBackend() must be called exactly once.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBackendForTesting', () => {
|
||||
it('works before setBackend', () => {
|
||||
const partial = { getSetting: async () => 'test' };
|
||||
setBackendForTesting(partial);
|
||||
expect(getBackend().getSetting).toBeDefined();
|
||||
});
|
||||
|
||||
it('allows overwrite (no single-set enforcement)', () => {
|
||||
const partial1 = { getSetting: async () => 'v1' };
|
||||
const partial2 = { getSetting: async () => 'v2' };
|
||||
setBackendForTesting(partial1);
|
||||
setBackendForTesting(partial2);
|
||||
// Should use second one
|
||||
expect(getBackend()).toBe(partial2 as unknown as BackendAdapter);
|
||||
});
|
||||
|
||||
it('allows overwrite even after setBackend', () => {
|
||||
const adapter = { capabilities: {} } as unknown as BackendAdapter;
|
||||
setBackend(adapter);
|
||||
const partial = { getSetting: async () => 'test' };
|
||||
// setBackendForTesting bypasses single-set check
|
||||
setBackendForTesting(partial);
|
||||
expect(getBackend()).toBe(partial as unknown as BackendAdapter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetBackendForTesting', () => {
|
||||
it('resets to uninitialized state', () => {
|
||||
const adapter = { capabilities: {} } as unknown as BackendAdapter;
|
||||
setBackend(adapter);
|
||||
resetBackendForTesting();
|
||||
expect(() => getBackend()).toThrow();
|
||||
});
|
||||
|
||||
it('allows setBackend after reset', () => {
|
||||
const adapter1 = { capabilities: {} } as unknown as BackendAdapter;
|
||||
const adapter2 = { capabilities: { supportsPtyMultiplexing: true } } as unknown as BackendAdapter;
|
||||
setBackend(adapter1);
|
||||
resetBackendForTesting();
|
||||
setBackend(adapter2);
|
||||
expect(getBackend()).toBe(adapter2);
|
||||
});
|
||||
});
|
||||
});
|
||||
246
src/lib/backend/__tests__/electrobun-adapter.test.ts
Normal file
246
src/lib/backend/__tests__/electrobun-adapter.test.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
// Tests for ElectrobunAdapter — verifies RPC call names match pty-rpc-schema.ts
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ElectrobunAdapter } from '../ElectrobunAdapter';
|
||||
import type { BackendAdapter } from '@agor/types';
|
||||
|
||||
// Mock RPC handle matching the interface in ElectrobunAdapter
|
||||
function createMockRpc() {
|
||||
const request: Record<string, ReturnType<typeof vi.fn>> = {};
|
||||
const listeners: Array<{ event: string; handler: (payload: unknown) => void }> = [];
|
||||
|
||||
return {
|
||||
request: new Proxy(request, {
|
||||
get: (_target, prop: string) => {
|
||||
if (!request[prop]) request[prop] = vi.fn().mockResolvedValue({ ok: true });
|
||||
return request[prop];
|
||||
},
|
||||
}),
|
||||
addMessageListener: vi.fn((event: string, handler: (payload: unknown) => void) => {
|
||||
listeners.push({ event, handler });
|
||||
}),
|
||||
removeMessageListener: vi.fn(),
|
||||
_listeners: listeners,
|
||||
_calls: request,
|
||||
};
|
||||
}
|
||||
|
||||
let adapter: ElectrobunAdapter;
|
||||
let mockRpc: ReturnType<typeof createMockRpc>;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new ElectrobunAdapter();
|
||||
mockRpc = createMockRpc();
|
||||
adapter.setRpc(mockRpc as any);
|
||||
});
|
||||
|
||||
describe('ElectrobunAdapter', () => {
|
||||
// ── Interface compliance ──────────────────────────────────────────────
|
||||
|
||||
it('implements BackendAdapter interface', () => {
|
||||
const ba: BackendAdapter = adapter;
|
||||
expect(ba.getSetting).toBeTypeOf('function');
|
||||
expect(ba.setSetting).toBeTypeOf('function');
|
||||
expect(ba.startAgent).toBeTypeOf('function');
|
||||
expect(ba.stopAgent).toBeTypeOf('function');
|
||||
expect(ba.createPty).toBeTypeOf('function');
|
||||
expect(ba.btmsgSend).toBeTypeOf('function');
|
||||
expect(ba.bttaskList).toBeTypeOf('function');
|
||||
expect(ba.memoraAvailable).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('has correct capabilities (no OS features)', () => {
|
||||
expect(adapter.capabilities.supportsPtyMultiplexing).toBe(true);
|
||||
expect(adapter.capabilities.supportsNativeMenus).toBe(false);
|
||||
expect(adapter.capabilities.supportsOsKeychain).toBe(false);
|
||||
expect(adapter.capabilities.supportsDesktopNotifications).toBe(false);
|
||||
expect(adapter.capabilities.supportsTelemetry).toBe(false);
|
||||
});
|
||||
|
||||
it('throws when RPC not initialized', () => {
|
||||
const fresh = new ElectrobunAdapter();
|
||||
expect(() => fresh.getSetting('x')).rejects.toThrow('RPC not initialized');
|
||||
});
|
||||
|
||||
// ── Settings (matches pty-rpc-schema "settings.*") ────────────────────
|
||||
|
||||
describe('settings', () => {
|
||||
it('getSetting calls settings.get', async () => {
|
||||
mockRpc.request['settings.get'].mockResolvedValue({ value: 'mocha' });
|
||||
const result = await adapter.getSetting('theme');
|
||||
expect(mockRpc.request['settings.get']).toHaveBeenCalledWith({ key: 'theme' });
|
||||
expect(result).toBe('mocha');
|
||||
});
|
||||
|
||||
it('setSetting calls settings.set', async () => {
|
||||
await adapter.setSetting('theme', 'mocha');
|
||||
expect(mockRpc.request['settings.set']).toHaveBeenCalledWith({ key: 'theme', value: 'mocha' });
|
||||
});
|
||||
|
||||
it('getAllSettings calls settings.getAll', async () => {
|
||||
mockRpc.request['settings.getAll'].mockResolvedValue({
|
||||
settings: { theme: 'mocha', shell: '/bin/bash' },
|
||||
});
|
||||
const result = await adapter.getAllSettings();
|
||||
expect(result).toEqual({ theme: 'mocha', shell: '/bin/bash' });
|
||||
});
|
||||
});
|
||||
|
||||
// ── PTY (matches pty-rpc-schema "pty.*") ──────────────────────────────
|
||||
|
||||
describe('pty', () => {
|
||||
it('createPty calls pty.create', async () => {
|
||||
mockRpc.request['pty.create'].mockResolvedValue({ ok: true });
|
||||
const id = await adapter.createPty({ sessionId: 's1', cols: 80, rows: 24 });
|
||||
expect(mockRpc.request['pty.create']).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: 's1', cols: 80, rows: 24 }),
|
||||
);
|
||||
expect(id).toBe('s1');
|
||||
});
|
||||
|
||||
it('writePty calls pty.write', async () => {
|
||||
await adapter.writePty('s1', 'hello');
|
||||
expect(mockRpc.request['pty.write']).toHaveBeenCalledWith({ sessionId: 's1', data: 'hello' });
|
||||
});
|
||||
|
||||
it('resizePty calls pty.resize', async () => {
|
||||
await adapter.resizePty('s1', 120, 40);
|
||||
expect(mockRpc.request['pty.resize']).toHaveBeenCalledWith({ sessionId: 's1', cols: 120, rows: 40 });
|
||||
});
|
||||
|
||||
it('closePty calls pty.close', async () => {
|
||||
await adapter.closePty('s1');
|
||||
expect(mockRpc.request['pty.close']).toHaveBeenCalledWith({ sessionId: 's1' });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Agent (matches pty-rpc-schema "agent.*") ──────────────────────────
|
||||
|
||||
describe('agent', () => {
|
||||
it('startAgent calls agent.start', async () => {
|
||||
const result = await adapter.startAgent({
|
||||
sessionId: 's1',
|
||||
prompt: 'hello',
|
||||
provider: 'claude',
|
||||
cwd: '/tmp',
|
||||
});
|
||||
expect(mockRpc.request['agent.start']).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionId: 's1', prompt: 'hello', provider: 'claude' }),
|
||||
);
|
||||
expect(result).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('stopAgent calls agent.stop', async () => {
|
||||
const result = await adapter.stopAgent('s1');
|
||||
expect(mockRpc.request['agent.stop']).toHaveBeenCalledWith({ sessionId: 's1' });
|
||||
expect(result).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('sendPrompt calls agent.prompt', async () => {
|
||||
const result = await adapter.sendPrompt('s1', 'do something');
|
||||
expect(mockRpc.request['agent.prompt']).toHaveBeenCalledWith({
|
||||
sessionId: 's1',
|
||||
prompt: 'do something',
|
||||
});
|
||||
expect(result).toEqual({ ok: true });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Files (matches pty-rpc-schema "files.*") ──────────────────────────
|
||||
|
||||
describe('files', () => {
|
||||
it('listDirectory calls files.list', async () => {
|
||||
mockRpc.request['files.list'].mockResolvedValue({
|
||||
entries: [{ name: 'src', type: 'dir', size: 0 }],
|
||||
});
|
||||
const result = await adapter.listDirectory('/project');
|
||||
expect(mockRpc.request['files.list']).toHaveBeenCalledWith({ path: '/project' });
|
||||
expect(result).toEqual([{ name: 'src', path: '/project/src', isDir: true, size: 0 }]);
|
||||
});
|
||||
|
||||
it('readFile calls files.read', async () => {
|
||||
mockRpc.request['files.read'].mockResolvedValue({
|
||||
content: 'hello', encoding: 'utf8', size: 5,
|
||||
});
|
||||
const result = await adapter.readFile('/tmp/file.ts');
|
||||
expect(result).toEqual({ type: 'Text', content: 'hello' });
|
||||
});
|
||||
|
||||
it('writeFile calls files.write', async () => {
|
||||
await adapter.writeFile('/tmp/file.ts', 'content');
|
||||
expect(mockRpc.request['files.write']).toHaveBeenCalledWith({
|
||||
path: '/tmp/file.ts',
|
||||
content: 'content',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Event listeners ───────────────────────────────────────────────────
|
||||
|
||||
describe('events', () => {
|
||||
it('onSidecarMessage subscribes to agent.sidecar', () => {
|
||||
const callback = vi.fn();
|
||||
adapter.onSidecarMessage(callback);
|
||||
expect(mockRpc.addMessageListener).toHaveBeenCalledWith('agent.sidecar', expect.any(Function));
|
||||
});
|
||||
|
||||
it('onPtyData filters by session ID', () => {
|
||||
const callback = vi.fn();
|
||||
adapter.onPtyData('pty-1', callback);
|
||||
expect(mockRpc.addMessageListener).toHaveBeenCalledWith('pty.output', expect.any(Function));
|
||||
|
||||
// Simulate event
|
||||
const handler = mockRpc.addMessageListener.mock.calls[0][1];
|
||||
handler({ sessionId: 'pty-1', data: 'hello' });
|
||||
expect(callback).toHaveBeenCalledWith('hello');
|
||||
|
||||
// Different session — should not trigger
|
||||
callback.mockClear();
|
||||
handler({ sessionId: 'pty-2', data: 'world' });
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unsubscribe removes listener', () => {
|
||||
const unsub = adapter.onSidecarMessage(vi.fn());
|
||||
unsub();
|
||||
expect(mockRpc.removeMessageListener).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Stub methods return safe defaults ─────────────────────────────────
|
||||
|
||||
describe('stub methods', () => {
|
||||
it('btmsg stubs return empty arrays/strings', async () => {
|
||||
expect(await adapter.btmsgGetAgents('g1' as any)).toEqual([]);
|
||||
expect(await adapter.btmsgSend('a1' as any, 'a2' as any, 'hi')).toBe('');
|
||||
expect(await adapter.btmsgGetChannels('g1' as any)).toEqual([]);
|
||||
});
|
||||
|
||||
it('bttask stubs return empty/zero', async () => {
|
||||
expect(await adapter.bttaskList('g1' as any)).toEqual([]);
|
||||
expect(await adapter.bttaskReviewQueueCount('g1' as any)).toBe(0);
|
||||
});
|
||||
|
||||
it('memora stubs return empty/false', async () => {
|
||||
expect(await adapter.memoraAvailable()).toBe(false);
|
||||
expect(await adapter.memoraList()).toEqual({ nodes: [], total: 0 });
|
||||
});
|
||||
|
||||
it('secrets stubs return null/empty', async () => {
|
||||
expect(await adapter.getSecret('x')).toBeNull();
|
||||
expect(await adapter.hasKeyring()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Cleanup ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('destroy', () => {
|
||||
it('removes all registered listeners', async () => {
|
||||
adapter.onSidecarMessage(vi.fn());
|
||||
adapter.onPtyData('p1', vi.fn());
|
||||
await adapter.destroy();
|
||||
// removeMessageListener should have been called for each listener
|
||||
expect(mockRpc.removeMessageListener.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
268
src/lib/backend/__tests__/tauri-adapter.test.ts
Normal file
268
src/lib/backend/__tests__/tauri-adapter.test.ts
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
// Tests for TauriAdapter — verifies correct Tauri command names + param mapping
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockInvoke, mockListen } = vi.hoisted(() => ({
|
||||
mockInvoke: vi.fn(),
|
||||
mockListen: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: mockInvoke }));
|
||||
vi.mock('@tauri-apps/api/event', () => ({ listen: mockListen }));
|
||||
|
||||
import { TauriAdapter } from '../TauriAdapter';
|
||||
import type { BackendAdapter } from '@agor/types';
|
||||
|
||||
let adapter: TauriAdapter;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
adapter = new TauriAdapter();
|
||||
});
|
||||
|
||||
describe('TauriAdapter', () => {
|
||||
it('implements BackendAdapter interface', () => {
|
||||
const ba: BackendAdapter = adapter;
|
||||
expect(ba.getSetting).toBeTypeOf('function');
|
||||
expect(ba.startAgent).toBeTypeOf('function');
|
||||
expect(ba.createPty).toBeTypeOf('function');
|
||||
expect(ba.btmsgSend).toBeTypeOf('function');
|
||||
expect(ba.bttaskList).toBeTypeOf('function');
|
||||
expect(ba.memoraAvailable).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('has correct capabilities', () => {
|
||||
expect(adapter.capabilities.supportsPtyMultiplexing).toBe(true);
|
||||
expect(adapter.capabilities.supportsOsKeychain).toBe(true);
|
||||
expect(adapter.capabilities.supportsDesktopNotifications).toBe(true);
|
||||
expect(adapter.capabilities.supportsTelemetry).toBe(true);
|
||||
});
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('settings', () => {
|
||||
it('getSetting invokes settings_get', async () => {
|
||||
mockInvoke.mockResolvedValue('dark');
|
||||
expect(await adapter.getSetting('theme')).toBe('dark');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('settings_get', { key: 'theme' });
|
||||
});
|
||||
|
||||
it('setSetting invokes settings_set', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.setSetting('theme', 'mocha');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('settings_set', { key: 'theme', value: 'mocha' });
|
||||
});
|
||||
|
||||
it('getAllSettings invokes settings_list and returns map', async () => {
|
||||
mockInvoke.mockResolvedValue([['theme', 'mocha'], ['shell', '/bin/bash']]);
|
||||
expect(await adapter.getAllSettings()).toEqual({ theme: 'mocha', shell: '/bin/bash' });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Agent ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('agent', () => {
|
||||
it('startAgent invokes agent_query with mapped options', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
const result = await adapter.startAgent({ sessionId: 's1', prompt: 'Hello', cwd: '/tmp', provider: 'claude', maxTurns: 10 });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('agent_query', {
|
||||
options: expect.objectContaining({ session_id: 's1', prompt: 'Hello', provider: 'claude', max_turns: 10 }),
|
||||
});
|
||||
expect(result).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('startAgent returns error on failure', async () => {
|
||||
mockInvoke.mockRejectedValue(new Error('Sidecar not running'));
|
||||
const result = await adapter.startAgent({ sessionId: 's2', prompt: 'test' });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toContain('Sidecar not running');
|
||||
});
|
||||
|
||||
it('stopAgent invokes agent_stop', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
expect(await adapter.stopAgent('s1')).toEqual({ ok: true });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('agent_stop', { sessionId: 's1' });
|
||||
});
|
||||
|
||||
it('isAgentReady invokes agent_ready', async () => {
|
||||
mockInvoke.mockResolvedValue(true);
|
||||
expect(await adapter.isAgentReady()).toBe(true);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('agent_ready');
|
||||
});
|
||||
|
||||
it('queryAgent routes local vs remote', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.queryAgent({ session_id: 's1', prompt: 'hi' });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('agent_query', { options: { session_id: 's1', prompt: 'hi' } });
|
||||
|
||||
mockInvoke.mockClear();
|
||||
await adapter.queryAgent({ session_id: 's1', prompt: 'hi', remote_machine_id: 'm1' });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('remote_agent_query', {
|
||||
machineId: 'm1', options: { session_id: 's1', prompt: 'hi' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── PTY ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('pty', () => {
|
||||
it('createPty invokes pty_spawn', async () => {
|
||||
mockInvoke.mockResolvedValue('pty-1');
|
||||
expect(await adapter.createPty({ sessionId: 's1', cols: 80, rows: 24 })).toBe('pty-1');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('pty_spawn', { options: expect.objectContaining({ cols: 80, rows: 24 }) });
|
||||
});
|
||||
|
||||
it('writePty/resizePty/closePty invoke correct commands', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.writePty('p1', 'hi');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('pty_write', { id: 'p1', data: 'hi' });
|
||||
|
||||
await adapter.resizePty('p1', 120, 40);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('pty_resize', { id: 'p1', cols: 120, rows: 40 });
|
||||
|
||||
await adapter.closePty('p1');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('pty_kill', { id: 'p1' });
|
||||
});
|
||||
|
||||
it('spawnPty uses remote_pty_spawn for remote', async () => {
|
||||
mockInvoke.mockResolvedValue('rpty-1');
|
||||
await adapter.spawnPty({ remote_machine_id: 'm1', cols: 80, rows: 24 });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('remote_pty_spawn', { machineId: 'm1', options: { cols: 80, rows: 24 } });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Files ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('files', () => {
|
||||
it('listDirectory invokes list_directory_children and maps fields', async () => {
|
||||
mockInvoke.mockResolvedValue([{ name: 'f.ts', path: '/tmp/f.ts', is_dir: false, size: 100, ext: 'ts' }]);
|
||||
const result = await adapter.listDirectory('/tmp');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('list_directory_children', { path: '/tmp' });
|
||||
expect(result).toEqual([{ name: 'f.ts', path: '/tmp/f.ts', isDir: false, size: 100, ext: 'ts' }]);
|
||||
});
|
||||
|
||||
it('readFile/writeFile invoke correct commands', async () => {
|
||||
mockInvoke.mockResolvedValue({ type: 'Text', content: 'hello' });
|
||||
expect(await adapter.readFile('/tmp/f.ts')).toEqual({ type: 'Text', content: 'hello' });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('read_file_content', { path: '/tmp/f.ts' });
|
||||
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.writeFile('/tmp/f.ts', 'x');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('write_file_content', { path: '/tmp/f.ts', content: 'x' });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Btmsg ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('btmsg', () => {
|
||||
it('btmsgGetAgents invokes btmsg_get_agents', async () => {
|
||||
mockInvoke.mockResolvedValue([]);
|
||||
await adapter.btmsgGetAgents('g1' as any);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('btmsg_get_agents', { groupId: 'g1' });
|
||||
});
|
||||
|
||||
it('btmsgSend invokes btmsg_send with camelCase params', async () => {
|
||||
mockInvoke.mockResolvedValue('msg-1');
|
||||
expect(await adapter.btmsgSend('a1' as any, 'a2' as any, 'hello')).toBe('msg-1');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('btmsg_send', { fromAgent: 'a1', toAgent: 'a2', content: 'hello' });
|
||||
});
|
||||
|
||||
it('btmsgHistory/btmsgMarkRead/btmsgCreateChannel/btmsgRecordHeartbeat use correct commands', async () => {
|
||||
mockInvoke.mockResolvedValue([]);
|
||||
await adapter.btmsgHistory('a1' as any, 'a2' as any, 50);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('btmsg_history', { agentId: 'a1', otherId: 'a2', limit: 50 });
|
||||
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.btmsgMarkRead('r' as any, 's' as any);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('btmsg_mark_read', { readerId: 'r', senderId: 's' });
|
||||
|
||||
mockInvoke.mockResolvedValue('ch-1');
|
||||
expect(await adapter.btmsgCreateChannel('gen', 'g1' as any, 'admin' as any)).toBe('ch-1');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('btmsg_create_channel', { name: 'gen', groupId: 'g1', createdBy: 'admin' });
|
||||
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.btmsgRecordHeartbeat('a1' as any);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('btmsg_record_heartbeat', { agentId: 'a1' });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Bttask ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('bttask', () => {
|
||||
it('bttaskList/bttaskComments invoke correct commands', async () => {
|
||||
mockInvoke.mockResolvedValue([]);
|
||||
await adapter.bttaskList('g1' as any);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('bttask_list', { groupId: 'g1' });
|
||||
|
||||
await adapter.bttaskComments('t1');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('bttask_comments', { taskId: 't1' });
|
||||
});
|
||||
|
||||
it('bttaskCreate invokes bttask_create with all params', async () => {
|
||||
mockInvoke.mockResolvedValue('t1');
|
||||
expect(await adapter.bttaskCreate('Fix', 'Desc', 'high', 'g1' as any, 'admin' as any, 'dev' as any)).toBe('t1');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('bttask_create', {
|
||||
title: 'Fix', description: 'Desc', priority: 'high', groupId: 'g1', createdBy: 'admin', assignedTo: 'dev',
|
||||
});
|
||||
});
|
||||
|
||||
it('bttaskUpdateStatus uses optimistic locking', async () => {
|
||||
mockInvoke.mockResolvedValue(2);
|
||||
expect(await adapter.bttaskUpdateStatus('t1', 'review', 1)).toBe(2);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('bttask_update_status', { taskId: 't1', status: 'review', version: 1 });
|
||||
});
|
||||
|
||||
it('bttaskReviewQueueCount invokes bttask_review_queue_count', async () => {
|
||||
mockInvoke.mockResolvedValue(3);
|
||||
expect(await adapter.bttaskReviewQueueCount('g1' as any)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Memora ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('memora', () => {
|
||||
it('memoraAvailable/memoraList/memoraSearch/memoraGet invoke correct commands', async () => {
|
||||
mockInvoke.mockResolvedValue(true);
|
||||
expect(await adapter.memoraAvailable()).toBe(true);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('memora_available');
|
||||
|
||||
mockInvoke.mockResolvedValue({ nodes: [], total: 0 });
|
||||
await adapter.memoraList();
|
||||
expect(mockInvoke).toHaveBeenCalledWith('memora_list', { tags: null, limit: 50, offset: 0 });
|
||||
|
||||
await adapter.memoraSearch('arch', { tags: ['agor'], limit: 10 });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('memora_search', { query: 'arch', tags: ['agor'], limit: 10 });
|
||||
|
||||
mockInvoke.mockResolvedValue({ id: 5, content: 'test', tags: [] });
|
||||
expect(await adapter.memoraGet(5)).toEqual({ id: 5, content: 'test', tags: [] });
|
||||
expect(mockInvoke).toHaveBeenCalledWith('memora_get', { id: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Secrets + Groups ──────────────────────────────────────────────────
|
||||
|
||||
describe('secrets', () => {
|
||||
it('storeSecret/getSecret invoke correct commands', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.storeSecret('key', 'val');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('secrets_store', { key: 'key', value: 'val' });
|
||||
|
||||
mockInvoke.mockResolvedValue('val');
|
||||
expect(await adapter.getSecret('key')).toBe('val');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('secrets_get', { key: 'key' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('groups', () => {
|
||||
it('loadGroups/saveGroups invoke correct commands', async () => {
|
||||
const groups = { version: 1, groups: [], activeGroupId: 'default' };
|
||||
mockInvoke.mockResolvedValue(groups);
|
||||
expect(await adapter.loadGroups()).toEqual(groups);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('groups_load');
|
||||
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
await adapter.saveGroups(groups as any);
|
||||
expect(mockInvoke).toHaveBeenCalledWith('groups_save', { config: groups });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -18,4 +18,4 @@ export {
|
|||
getAllProjectHealth,
|
||||
getAttentionQueue,
|
||||
getHealthAggregates,
|
||||
} from '@agor/stores/health.svelte';
|
||||
} from '@agor/stores';
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ export {
|
|||
markRead,
|
||||
markAllRead,
|
||||
clearHistory,
|
||||
} from '@agor/stores/notifications.svelte';
|
||||
} from '@agor/stores';
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ export {
|
|||
setTheme,
|
||||
setFlavor,
|
||||
initTheme,
|
||||
} from '@agor/stores/theme.svelte';
|
||||
} from '@agor/stores';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue