These tests reference modules (binding.js/broker.js) that no longer exist. Signed-off-by: Xiaonuo <xiaonuo@git.shazhou.work>
This commit is contained in:
@@ -1,170 +0,0 @@
|
||||
import { describe, expect, jest, test } from 'bun:test';
|
||||
import { buildBindingsFromEvents, resolveRole } from './binding.js';
|
||||
import type { Container } from './container.js';
|
||||
import type { PulseStore } from './store.js';
|
||||
import type { PersonaState } from './task-events.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('buildBindingsFromEvents', () => {
|
||||
test('bind persona to container', () => {
|
||||
const store = makeMockStore([
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'persona-bound',
|
||||
meta: JSON.stringify({
|
||||
personaId: 'xiaoju',
|
||||
containerId: 'openclaw-neko',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const bindings = buildBindingsFromEvents(store);
|
||||
expect(bindings.size).toBe(1);
|
||||
const b = bindings.get('xiaoju')!;
|
||||
expect(b.personaId).toBe('xiaoju');
|
||||
expect(b.containerId).toBe('openclaw-neko');
|
||||
expect(b.boundAt).toBe(1000);
|
||||
});
|
||||
|
||||
test('rebind (migration) — new bind overwrites old', () => {
|
||||
const store = makeMockStore([
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'persona-bound',
|
||||
meta: JSON.stringify({
|
||||
personaId: 'xiaoju',
|
||||
containerId: 'openclaw-neko',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
occurredAt: 2000,
|
||||
kind: 'persona-bound',
|
||||
meta: JSON.stringify({
|
||||
personaId: 'xiaoju',
|
||||
containerId: 'hermes-neko',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const bindings = buildBindingsFromEvents(store);
|
||||
expect(bindings.size).toBe(1);
|
||||
expect(bindings.get('xiaoju')!.containerId).toBe('hermes-neko');
|
||||
expect(bindings.get('xiaoju')!.boundAt).toBe(2000);
|
||||
});
|
||||
|
||||
test('unbind removes binding', () => {
|
||||
const store = makeMockStore([
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'persona-bound',
|
||||
meta: JSON.stringify({
|
||||
personaId: 'xiaoju',
|
||||
containerId: 'openclaw-neko',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
occurredAt: 2000,
|
||||
kind: 'persona-unbound',
|
||||
meta: JSON.stringify({
|
||||
personaId: 'xiaoju',
|
||||
containerId: 'openclaw-neko',
|
||||
reason: 'manual',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const bindings = buildBindingsFromEvents(store);
|
||||
expect(bindings.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRole', () => {
|
||||
const personas = new Map<string, PersonaState>([
|
||||
[
|
||||
'xiaoju',
|
||||
{
|
||||
personaId: 'xiaoju',
|
||||
name: '小橘',
|
||||
container: 'openclaw',
|
||||
capabilities: ['coding', 'ops'],
|
||||
registeredAt: 1000,
|
||||
updatedAt: 1000,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const containers = new Map<string, Container>([
|
||||
[
|
||||
'openclaw-neko',
|
||||
{
|
||||
containerId: 'openclaw-neko',
|
||||
type: 'openclaw',
|
||||
host: 'neko-vm',
|
||||
status: 'online',
|
||||
tools: ['exec', 'browser'],
|
||||
registeredAt: 1000,
|
||||
updatedAt: 1000,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
test('resolves role successfully', () => {
|
||||
const bindings = new Map([
|
||||
[
|
||||
'xiaoju',
|
||||
{ personaId: 'xiaoju', containerId: 'openclaw-neko', boundAt: 1000 },
|
||||
],
|
||||
]);
|
||||
const role = resolveRole('xiaoju', personas, bindings, containers);
|
||||
expect(role).not.toBeNull();
|
||||
expect(role!.persona.name).toBe('小橘');
|
||||
expect(role!.container.containerId).toBe('openclaw-neko');
|
||||
});
|
||||
|
||||
test('returns null when persona not bound', () => {
|
||||
const bindings = new Map();
|
||||
const role = resolveRole('xiaoju', personas, bindings, containers);
|
||||
expect(role).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when container does not exist', () => {
|
||||
const bindings = new Map([
|
||||
[
|
||||
'xiaoju',
|
||||
{ personaId: 'xiaoju', containerId: 'nonexistent', boundAt: 1000 },
|
||||
],
|
||||
]);
|
||||
const role = resolveRole('xiaoju', personas, bindings, containers);
|
||||
expect(role).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,413 +0,0 @@
|
||||
import { beforeEach, describe, expect, jest, test } from 'bun:test';
|
||||
import type { LlmClient, LlmResponse } from '../llm-client.js';
|
||||
import type { BrokerEffect } from '../rules/task-rule.js';
|
||||
import type { PulseStore } from '../store.js';
|
||||
import { createBrokerExecutor } from './broker.js';
|
||||
|
||||
function makeMockLlmClient(response: LlmResponse): LlmClient {
|
||||
return {
|
||||
chat: jest.fn(async () => response),
|
||||
};
|
||||
}
|
||||
|
||||
function makeMockRoutineStore(): 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(() => []),
|
||||
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('createBrokerExecutor', () => {
|
||||
let workflowStore: PulseStore;
|
||||
|
||||
beforeEach(() => {
|
||||
workflowStore = makeMockRoutineStore();
|
||||
});
|
||||
|
||||
test('assigns tasks via LLM tool calls', async () => {
|
||||
(
|
||||
workflowStore.queryByKind as ReturnType<typeof jest.fn>
|
||||
).mockImplementation((kind: string) => {
|
||||
if (kind === 'task-created') {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'task-created',
|
||||
meta: JSON.stringify({
|
||||
taskId: 't1',
|
||||
projectId: 'proj-1',
|
||||
title: 'Fix bug',
|
||||
description: 'fix it',
|
||||
type: 'bug',
|
||||
priority: 5,
|
||||
creatorId: 'user-1',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const llmResponse: LlmResponse = {
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call-1',
|
||||
function: {
|
||||
name: 'assign_task',
|
||||
arguments: JSON.stringify({
|
||||
taskId: 't1',
|
||||
assigneeId: 'cursor',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const llmClient = makeMockLlmClient(llmResponse);
|
||||
const executor = createBrokerExecutor({ llmClient, workflowStore });
|
||||
|
||||
const effect: BrokerEffect = { kind: 'broker', taskIds: ['t1'] };
|
||||
await executor(effect);
|
||||
|
||||
const appendCalls = (
|
||||
workflowStore.appendEvent as ReturnType<typeof jest.fn>
|
||||
).mock.calls;
|
||||
|
||||
const routingCalls = appendCalls.filter(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { kind: string }).kind === 'task-routing',
|
||||
);
|
||||
expect(routingCalls).toHaveLength(1);
|
||||
const routingMeta = JSON.parse(routingCalls[0][0].meta);
|
||||
expect(routingMeta.taskId).toBe('t1');
|
||||
expect(routingMeta.brokerSessionId).toBeDefined();
|
||||
|
||||
const assignedCalls = appendCalls.filter(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { kind: string }).kind === 'task-assigned',
|
||||
);
|
||||
expect(assignedCalls).toHaveLength(1);
|
||||
const meta = JSON.parse(assignedCalls[0][0].meta);
|
||||
expect(meta.taskId).toBe('t1');
|
||||
expect(meta.assigneeId).toBe('cursor');
|
||||
expect(meta.assignedBy).toBe('broker');
|
||||
});
|
||||
|
||||
test('writes task-responded on LLM failure', async () => {
|
||||
(
|
||||
workflowStore.queryByKind as ReturnType<typeof jest.fn>
|
||||
).mockImplementation((kind: string) => {
|
||||
if (kind === 'task-created') {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'task-created',
|
||||
meta: JSON.stringify({
|
||||
taskId: 't1',
|
||||
projectId: 'proj-1',
|
||||
title: 'Fix bug',
|
||||
description: 'fix it',
|
||||
type: 'bug',
|
||||
priority: 5,
|
||||
creatorId: 'user-1',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const llmClient: LlmClient = {
|
||||
chat: jest.fn(async () => {
|
||||
throw new Error('LLM unavailable');
|
||||
}),
|
||||
};
|
||||
const executor = createBrokerExecutor({ llmClient, workflowStore });
|
||||
|
||||
const effect: BrokerEffect = { kind: 'broker', taskIds: ['t1'] };
|
||||
await executor(effect);
|
||||
|
||||
const appendCalls = (
|
||||
workflowStore.appendEvent as ReturnType<typeof jest.fn>
|
||||
).mock.calls;
|
||||
|
||||
const routingCalls = appendCalls.filter(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { kind: string }).kind === 'task-routing',
|
||||
);
|
||||
expect(routingCalls).toHaveLength(1);
|
||||
|
||||
const respondedCalls = appendCalls.filter(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { kind: string }).kind === 'task-responded',
|
||||
);
|
||||
expect(respondedCalls).toHaveLength(1);
|
||||
const meta = JSON.parse(respondedCalls[0][0].meta);
|
||||
expect(meta.taskId).toBe('t1');
|
||||
expect(meta.result).toContain('Routing failed');
|
||||
});
|
||||
|
||||
test('skips if no matching tasks found', async () => {
|
||||
(workflowStore.queryByKind as ReturnType<typeof jest.fn>).mockReturnValue(
|
||||
[],
|
||||
);
|
||||
|
||||
const llmClient = makeMockLlmClient({ content: 'ok' });
|
||||
const executor = createBrokerExecutor({ llmClient, workflowStore });
|
||||
|
||||
const effect: BrokerEffect = { kind: 'broker', taskIds: ['nonexistent'] };
|
||||
await executor(effect);
|
||||
|
||||
expect(llmClient.chat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('skips if effect kind is not broker', async () => {
|
||||
const llmClient = makeMockLlmClient({ content: 'ok' });
|
||||
const executor = createBrokerExecutor({ llmClient, workflowStore });
|
||||
|
||||
await executor({ kind: 'other', taskIds: [] } as unknown as BrokerEffect);
|
||||
|
||||
expect(llmClient.chat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles LLM response without tool_calls', async () => {
|
||||
(
|
||||
workflowStore.queryByKind as ReturnType<typeof jest.fn>
|
||||
).mockImplementation((kind: string) => {
|
||||
if (kind === 'task-created') {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'task-created',
|
||||
meta: JSON.stringify({
|
||||
taskId: 't1',
|
||||
projectId: 'proj-1',
|
||||
title: 'Fix',
|
||||
description: 'fix',
|
||||
type: 'bug',
|
||||
priority: 0,
|
||||
creatorId: 'user-1',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const llmClient = makeMockLlmClient({ content: 'ok' });
|
||||
const executor = createBrokerExecutor({ llmClient, workflowStore });
|
||||
|
||||
const effect: BrokerEffect = { kind: 'broker', taskIds: ['t1'] };
|
||||
await executor(effect);
|
||||
|
||||
const appendCalls = (
|
||||
workflowStore.appendEvent as ReturnType<typeof jest.fn>
|
||||
).mock.calls;
|
||||
const assignedCalls = appendCalls.filter(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { kind: string }).kind === 'task-assigned',
|
||||
);
|
||||
expect(assignedCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('uses dynamic personas when getPersonas is provided', async () => {
|
||||
(
|
||||
workflowStore.queryByKind as ReturnType<typeof jest.fn>
|
||||
).mockImplementation((kind: string) => {
|
||||
if (kind === 'task-created') {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'task-created',
|
||||
meta: JSON.stringify({
|
||||
taskId: 't1',
|
||||
projectId: 'proj-1',
|
||||
title: 'Fix bug',
|
||||
description: 'fix it',
|
||||
type: 'bug',
|
||||
priority: 5,
|
||||
creatorId: 'user-1',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const llmResponse: LlmResponse = {
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call-1',
|
||||
function: {
|
||||
name: 'assign_task',
|
||||
arguments: JSON.stringify({
|
||||
taskId: 't1',
|
||||
assigneeId: 'xiaoju',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const llmClient = makeMockLlmClient(llmResponse);
|
||||
const personas = new Map([
|
||||
[
|
||||
'xiaoju',
|
||||
{
|
||||
personaId: 'xiaoju',
|
||||
name: '小橘',
|
||||
container: 'openclaw' as const,
|
||||
capabilities: ['coding', 'ops'],
|
||||
registeredAt: 1000,
|
||||
updatedAt: 1000,
|
||||
},
|
||||
],
|
||||
[
|
||||
'claude',
|
||||
{
|
||||
personaId: 'claude',
|
||||
name: 'Claude',
|
||||
container: 'claude-code' as const,
|
||||
capabilities: ['coding', 'review'],
|
||||
registeredAt: 1000,
|
||||
updatedAt: 1000,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const executor = createBrokerExecutor({
|
||||
llmClient,
|
||||
workflowStore,
|
||||
getPersonas: () => personas,
|
||||
});
|
||||
|
||||
await executor({ kind: 'broker', taskIds: ['t1'] });
|
||||
|
||||
// Verify LLM was called with dynamic prompt containing persona info
|
||||
const chatCall = (llmClient.chat as ReturnType<typeof jest.fn>).mock
|
||||
.calls[0][0];
|
||||
expect(chatCall.messages[0].content).toContain('xiaoju');
|
||||
expect(chatCall.messages[0].content).toContain('claude');
|
||||
expect(chatCall.messages[0].content).toContain('capabilities');
|
||||
|
||||
// Verify tools enum contains dynamic agent ids
|
||||
const assigneeProp =
|
||||
chatCall.tools[0].function.parameters.properties.assigneeId;
|
||||
expect(assigneeProp.enum).toContain('xiaoju');
|
||||
expect(assigneeProp.enum).toContain('claude');
|
||||
expect(assigneeProp.enum).not.toContain('cursor');
|
||||
|
||||
// Verify assignment went through
|
||||
const appendCalls = (
|
||||
workflowStore.appendEvent as ReturnType<typeof jest.fn>
|
||||
).mock.calls;
|
||||
const assignedCalls = appendCalls.filter(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { kind: string }).kind === 'task-assigned',
|
||||
);
|
||||
expect(assignedCalls).toHaveLength(1);
|
||||
const meta = JSON.parse(assignedCalls[0][0].meta);
|
||||
expect(meta.assigneeId).toBe('xiaoju');
|
||||
});
|
||||
|
||||
test('handles multiple task assignments', async () => {
|
||||
(
|
||||
workflowStore.queryByKind as ReturnType<typeof jest.fn>
|
||||
).mockImplementation((kind: string) => {
|
||||
if (kind === 'task-created') {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
occurredAt: 1000,
|
||||
kind: 'task-created',
|
||||
meta: JSON.stringify({
|
||||
taskId: 't1',
|
||||
projectId: 'proj-1',
|
||||
title: 'Task 1',
|
||||
description: 'desc 1',
|
||||
type: 'bug',
|
||||
priority: 5,
|
||||
creatorId: 'user-1',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
occurredAt: 1001,
|
||||
kind: 'task-created',
|
||||
meta: JSON.stringify({
|
||||
taskId: 't2',
|
||||
projectId: 'proj-1',
|
||||
title: 'Task 2',
|
||||
description: 'desc 2',
|
||||
type: 'rfc',
|
||||
priority: 3,
|
||||
creatorId: 'user-1',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const llmResponse: LlmResponse = {
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call-1',
|
||||
function: {
|
||||
name: 'assign_task',
|
||||
arguments: JSON.stringify({
|
||||
taskId: 't1',
|
||||
assigneeId: 'cursor',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'call-2',
|
||||
function: {
|
||||
name: 'assign_task',
|
||||
arguments: JSON.stringify({
|
||||
taskId: 't2',
|
||||
assigneeId: 'cursor',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const llmClient = makeMockLlmClient(llmResponse);
|
||||
const executor = createBrokerExecutor({ llmClient, workflowStore });
|
||||
|
||||
await executor({ kind: 'broker', taskIds: ['t1', 't2'] });
|
||||
|
||||
const appendCalls = (
|
||||
workflowStore.appendEvent as ReturnType<typeof jest.fn>
|
||||
).mock.calls;
|
||||
const assignedCalls = appendCalls.filter(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { kind: string }).kind === 'task-assigned',
|
||||
);
|
||||
expect(assignedCalls).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user