189 lines
5.4 KiB
TypeScript
189 lines
5.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const { gatewayRpcMock, hostApiFetchMock, agentsState } = vi.hoisted(() => ({
|
|
gatewayRpcMock: vi.fn(),
|
|
hostApiFetchMock: vi.fn(),
|
|
agentsState: {
|
|
agents: [] as Array<Record<string, unknown>>,
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/stores/gateway', () => ({
|
|
useGatewayStore: {
|
|
getState: () => ({
|
|
rpc: gatewayRpcMock,
|
|
}),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/stores/agents', () => ({
|
|
useAgentsStore: {
|
|
getState: () => agentsState,
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/lib/host-api', () => ({
|
|
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
|
}));
|
|
|
|
describe('chat target routing', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-03-11T12:00:00Z'));
|
|
window.localStorage.clear();
|
|
|
|
agentsState.agents = [
|
|
{
|
|
id: 'main',
|
|
name: 'Main',
|
|
isDefault: true,
|
|
modelDisplay: 'MiniMax',
|
|
inheritedModel: true,
|
|
workspace: '~/.openclaw/workspace',
|
|
agentDir: '~/.openclaw/agents/main/agent',
|
|
mainSessionKey: 'agent:main:main',
|
|
channelTypes: [],
|
|
},
|
|
{
|
|
id: 'research',
|
|
name: 'Research',
|
|
isDefault: false,
|
|
modelDisplay: 'Claude',
|
|
inheritedModel: false,
|
|
workspace: '~/.openclaw/workspace-research',
|
|
agentDir: '~/.openclaw/agents/research/agent',
|
|
mainSessionKey: 'agent:research:desk',
|
|
channelTypes: [],
|
|
},
|
|
];
|
|
|
|
gatewayRpcMock.mockReset();
|
|
gatewayRpcMock.mockImplementation(async (method: string) => {
|
|
if (method === 'chat.history') {
|
|
return { messages: [] };
|
|
}
|
|
if (method === 'chat.send') {
|
|
return { runId: 'run-text' };
|
|
}
|
|
if (method === 'chat.abort') {
|
|
return { ok: true };
|
|
}
|
|
if (method === 'sessions.list') {
|
|
return { sessions: [] };
|
|
}
|
|
throw new Error(`Unexpected gateway RPC: ${method}`);
|
|
});
|
|
|
|
hostApiFetchMock.mockReset();
|
|
hostApiFetchMock.mockResolvedValue({ success: true, result: { runId: 'run-media' } });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('switches to the selected agent main session before sending text', async () => {
|
|
const { useChatStore } = await import('@/stores/chat');
|
|
|
|
useChatStore.setState({
|
|
currentSessionKey: 'agent:main:main',
|
|
currentAgentId: 'main',
|
|
sessions: [{ key: 'agent:main:main' }],
|
|
messages: [{ role: 'assistant', content: 'Existing main history' }],
|
|
sessionLabels: {},
|
|
sessionLastActivity: {},
|
|
sending: false,
|
|
activeRunId: null,
|
|
streamingText: '',
|
|
streamingMessage: null,
|
|
streamingTools: [],
|
|
pendingFinal: false,
|
|
lastUserMessageAt: null,
|
|
pendingToolImages: [],
|
|
error: null,
|
|
loading: false,
|
|
thinkingLevel: null,
|
|
});
|
|
|
|
await useChatStore.getState().sendMessage('Hello direct agent', undefined, 'research');
|
|
|
|
const state = useChatStore.getState();
|
|
expect(state.currentSessionKey).toBe('agent:research:desk');
|
|
expect(state.currentAgentId).toBe('research');
|
|
expect(state.sessions.some((session) => session.key === 'agent:research:desk')).toBe(true);
|
|
expect(state.messages.at(-1)?.content).toBe('Hello direct agent');
|
|
|
|
const historyCall = gatewayRpcMock.mock.calls.find(([method]) => method === 'chat.history');
|
|
expect(historyCall?.[1]).toEqual({ sessionKey: 'agent:research:desk', limit: 200 });
|
|
|
|
const sendCall = gatewayRpcMock.mock.calls.find(([method]) => method === 'chat.send');
|
|
expect(sendCall?.[1]).toMatchObject({
|
|
sessionKey: 'agent:research:desk',
|
|
message: 'Hello direct agent',
|
|
deliver: false,
|
|
});
|
|
expect(typeof (sendCall?.[1] as { idempotencyKey?: unknown })?.idempotencyKey).toBe('string');
|
|
});
|
|
|
|
it('uses the selected agent main session for attachment sends', async () => {
|
|
const { useChatStore } = await import('@/stores/chat');
|
|
|
|
useChatStore.setState({
|
|
currentSessionKey: 'agent:main:main',
|
|
currentAgentId: 'main',
|
|
sessions: [{ key: 'agent:main:main' }],
|
|
messages: [],
|
|
sessionLabels: {},
|
|
sessionLastActivity: {},
|
|
sending: false,
|
|
activeRunId: null,
|
|
streamingText: '',
|
|
streamingMessage: null,
|
|
streamingTools: [],
|
|
pendingFinal: false,
|
|
lastUserMessageAt: null,
|
|
pendingToolImages: [],
|
|
error: null,
|
|
loading: false,
|
|
thinkingLevel: null,
|
|
});
|
|
|
|
await useChatStore.getState().sendMessage(
|
|
'',
|
|
[
|
|
{
|
|
fileName: 'design.png',
|
|
mimeType: 'image/png',
|
|
fileSize: 128,
|
|
stagedPath: '/tmp/design.png',
|
|
preview: 'data:image/png;base64,abc',
|
|
},
|
|
],
|
|
'research',
|
|
);
|
|
|
|
expect(useChatStore.getState().currentSessionKey).toBe('agent:research:desk');
|
|
|
|
expect(hostApiFetchMock).toHaveBeenCalledWith(
|
|
'/api/chat/send-with-media',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: expect.any(String),
|
|
}),
|
|
);
|
|
|
|
const payload = JSON.parse(
|
|
(hostApiFetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
|
) as {
|
|
sessionKey: string;
|
|
message: string;
|
|
media: Array<{ filePath: string }>;
|
|
};
|
|
|
|
expect(payload.sessionKey).toBe('agent:research:desk');
|
|
expect(payload.message).toBe('Process the attached file(s).');
|
|
expect(payload.media[0]?.filePath).toBe('/tmp/design.png');
|
|
});
|
|
});
|