diff --git a/packages/pulse/src/container.test.ts b/packages/pulse/src/container.test.ts deleted file mode 100644 index 8a60b28..0000000 --- a/packages/pulse/src/container.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { describe, expect, jest, test } from 'bun:test'; -import { buildContainersFromEvents } from './container.js'; -import type { PulseStore } from './store.js'; - -function makeMockStore( - events: Array<{ id: number; occurredAt: number; kind: string; meta: string }>, -): PulseStore { - return { - appendEvent: jest.fn(() => ({ - id: 1, - occurredAt: Date.now(), - kind: 'mock', - })), - appendEvents: jest.fn(() => []), - getLatest: jest.fn(() => null), - getLatestWhere: jest.fn(() => null), - getRecent: jest.fn(() => []), - queryByKind: jest.fn((kind: string) => - events.filter((e) => e.kind === kind), - ), - getAfter: jest.fn(() => []), - hasEvents: jest.fn(() => false), - putObject: jest.fn(() => 'hash'), - getObject: jest.fn(() => null), - close: jest.fn(), - createObject: jest.fn(() => 1), - getObjectInstance: jest.fn(() => null), - queryObjectsByType: jest.fn(() => []), - archiveEvents: jest.fn(() => 0), - downsampleEvents: jest.fn(() => 0), - }; -} - -describe('buildContainersFromEvents', () => { - test('registers a single container', () => { - const store = makeMockStore([ - { - id: 1, - occurredAt: 1000, - kind: 'container-registered', - meta: JSON.stringify({ - containerId: 'openclaw-neko', - type: 'openclaw', - host: 'neko-vm', - tools: ['exec', 'browser'], - }), - }, - ]); - const containers = buildContainersFromEvents(store); - expect(containers.size).toBe(1); - const c = containers.get('openclaw-neko')!; - expect(c.containerId).toBe('openclaw-neko'); - expect(c.type).toBe('openclaw'); - expect(c.host).toBe('neko-vm'); - expect(c.status).toBe('online'); - expect(c.tools).toEqual(['exec', 'browser']); - expect(c.registeredAt).toBe(1000); - }); - - test('registers multiple containers', () => { - const store = makeMockStore([ - { - id: 1, - occurredAt: 1000, - kind: 'container-registered', - meta: JSON.stringify({ - containerId: 'openclaw-neko', - type: 'openclaw', - host: 'neko-vm', - tools: ['exec'], - }), - }, - { - id: 2, - occurredAt: 2000, - kind: 'container-registered', - meta: JSON.stringify({ - containerId: 'cursor-neko', - type: 'cursor', - host: 'neko-vm', - tools: ['code-edit'], - }), - }, - ]); - const containers = buildContainersFromEvents(store); - expect(containers.size).toBe(2); - expect(containers.get('openclaw-neko')!.type).toBe('openclaw'); - expect(containers.get('cursor-neko')!.type).toBe('cursor'); - }); - - test('status changes (online → busy → offline)', () => { - const store = makeMockStore([ - { - id: 1, - occurredAt: 1000, - kind: 'container-registered', - meta: JSON.stringify({ - containerId: 'openclaw-neko', - type: 'openclaw', - host: 'neko-vm', - tools: [], - }), - }, - { - id: 2, - occurredAt: 2000, - kind: 'container-status-changed', - meta: JSON.stringify({ containerId: 'openclaw-neko', status: 'busy' }), - }, - { - id: 3, - occurredAt: 3000, - kind: 'container-status-changed', - meta: JSON.stringify({ - containerId: 'openclaw-neko', - status: 'offline', - }), - }, - ]); - const containers = buildContainersFromEvents(store); - const c = containers.get('openclaw-neko')!; - expect(c.status).toBe('offline'); - expect(c.updatedAt).toBe(3000); - }); - - test('empty event stream', () => { - const store = makeMockStore([]); - const containers = buildContainersFromEvents(store); - expect(containers.size).toBe(0); - }); -}); diff --git a/packages/pulse/src/council.test.ts b/packages/pulse/src/council.test.ts deleted file mode 100644 index 7994c53..0000000 --- a/packages/pulse/src/council.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { describe, expect, test } from 'bun:test'; -import { runCouncil } from './council.js'; -import type { CouncilMessage, ModeratorFn } from './moderator.js'; -import { createRoundRobinModerator } from './moderators/round-robin.js'; -import type { PersonaState } from './task-events.js'; - -function makePersona(id: string): PersonaState { - return { - personaId: id, - name: id, - container: 'openclaw', - capabilities: [], - registeredAt: Date.now(), - updatedAt: Date.now(), - }; -} - -describe('runCouncil', () => { - test('round-robin with 3 participants', async () => { - const speeches: string[] = []; - const result = await runCouncil({ - moderator: createRoundRobinModerator(), - participants: [makePersona('a'), makePersona('b'), makePersona('c')], - onSpeak: async (id) => { - const msg = `${id} says hello`; - speeches.push(msg); - return msg; - }, - }); - - expect(speeches).toEqual(['a says hello', 'b says hello', 'c says hello']); - expect(result.history).toHaveLength(3); - expect(result.history[0]!.personaId).toBe('a'); - expect(result.history[1]!.personaId).toBe('b'); - expect(result.history[2]!.personaId).toBe('c'); - expect(result.summary).toBe('All participants have spoken'); - // 3 speak rounds + 1 close round = 4 - expect(result.rounds).toBe(4); - }); - - test('maxRounds limits execution', async () => { - // A moderator that always says speak 'a' (never closes) - const infiniteMod: ModeratorFn = async () => ({ - action: 'speak', - personaId: 'a', - }); - - const result = await runCouncil({ - moderator: infiniteMod, - participants: [makePersona('a')], - onSpeak: async () => 'loop', - maxRounds: 3, - }); - - expect(result.history).toHaveLength(3); - expect(result.rounds).toBe(3); - expect(result.summary).toBe('Max rounds exceeded'); - }); - - test('add member mid-council', async () => { - let callCount = 0; - const addingMod: ModeratorFn = async (_participants, _history) => { - callCount++; - if (callCount === 1) { - return { action: 'speak', personaId: 'a' }; - } - if (callCount === 2) { - return { - action: 'add', - persona: makePersona('newcomer'), - }; - } - if (callCount === 3) { - return { action: 'speak', personaId: 'newcomer' }; - } - return { action: 'close', summary: 'done' }; - }; - - const added: string[] = []; - const result = await runCouncil({ - moderator: addingMod, - participants: [makePersona('a')], - onSpeak: async (id) => `${id} speaks`, - onAddMember: async (p) => { - added.push(p.personaId); - }, - }); - - expect(added).toEqual(['newcomer']); - expect(result.history).toHaveLength(2); - expect(result.history[0]!.personaId).toBe('a'); - expect(result.history[1]!.personaId).toBe('newcomer'); - expect(result.summary).toBe('done'); - }); - - test('continues from existing history', async () => { - const existingHistory: CouncilMessage[] = [ - { personaId: 'a', content: 'earlier', timestamp: 1 }, - ]; - - const result = await runCouncil({ - moderator: createRoundRobinModerator(), - participants: [makePersona('a'), makePersona('b')], - history: existingHistory, - onSpeak: async (id) => `${id} continues`, - }); - - // 'a' already spoke, so only 'b' speaks, then close - expect(result.history).toHaveLength(2); - expect(result.history[0]!.personaId).toBe('a'); - expect(result.history[0]!.content).toBe('earlier'); - expect(result.history[1]!.personaId).toBe('b'); - expect(result.rounds).toBe(2); // 1 speak + 1 close - }); - - test('empty history start', async () => { - const result = await runCouncil({ - moderator: createRoundRobinModerator(), - participants: [makePersona('solo')], - onSpeak: async () => 'only one', - }); - - expect(result.history).toHaveLength(1); - expect(result.history[0]!.personaId).toBe('solo'); - }); - - test('spawn sub-council via onSpawn', async () => { - let callCount = 0; - const spawnMod: ModeratorFn = async () => { - callCount++; - if (callCount === 1) { - return { action: 'speak', personaId: 'a' }; - } - if (callCount === 2) { - return { - action: 'spawn', - topicId: 'sub-1', - title: 'Sub task', - participants: [makePersona('a'), makePersona('b')], - }; - } - return { action: 'close', summary: 'parent done' }; - }; - - const spawnCalls: { topicId: string; title: string }[] = []; - const result = await runCouncil({ - moderator: spawnMod, - participants: [makePersona('a'), makePersona('b')], - onSpeak: async (id) => `${id} speaks`, - onSpawn: async (topicId, title, _participants) => { - spawnCalls.push({ topicId, title }); - return { history: [], summary: 'sub-council resolved', rounds: 2 }; - }, - }); - - expect(spawnCalls).toEqual([{ topicId: 'sub-1', title: 'Sub task' }]); - // history: a speaks + sub-council message + (no more speak before close) - expect(result.history).toHaveLength(2); - expect(result.history[1]!.personaId).toBe('sub-council:sub-1'); - expect(result.history[1]!.content).toBe('sub-council resolved'); - expect(result.summary).toBe('parent done'); - }); - - test('nested spawn (sub-council spawns grandchild)', async () => { - let parentCall = 0; - const parentMod: ModeratorFn = async () => { - parentCall++; - if (parentCall === 1) { - return { - action: 'spawn', - topicId: 'child', - title: 'Child topic', - participants: [makePersona('a')], - }; - } - return { action: 'close', summary: 'parent done' }; - }; - - const result = await runCouncil({ - moderator: parentMod, - participants: [makePersona('a')], - onSpeak: async (id) => `${id} speaks`, - onSpawn: async (topicId, _title, participants) => { - // Child council that itself spawns a grandchild - let childCall = 0; - const childMod: ModeratorFn = async () => { - childCall++; - if (childCall === 1) { - return { - action: 'spawn', - topicId: 'grandchild', - title: 'Grandchild topic', - participants, - }; - } - return { action: 'close', summary: `child ${topicId} done` }; - }; - - return runCouncil({ - moderator: childMod, - participants, - onSpeak: async (id) => `${id} in child`, - onSpawn: async (gcTopicId) => { - return { - history: [], - summary: `grandchild ${gcTopicId} resolved`, - rounds: 1, - }; - }, - }); - }, - }); - - expect(result.history).toHaveLength(1); - expect(result.history[0]!.personaId).toBe('sub-council:child'); - expect(result.history[0]!.content).toBe('child child done'); - expect(result.summary).toBe('parent done'); - }); -});