Files
makelore/tests/unit/cloud-channel-conversations.test.tsx
brother7 2e710db0fb
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
feat(agents): 支持个人微信发布与渠道会话
2026-09-13 16:22:36 +08:00

145 lines
9.4 KiB
TypeScript

import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { CloudChannelConversations } from '@/pages/CloudAgents/CloudChannelConversations';
const api = vi.hoisted(() => ({ call: vi.fn(), recovery: vi.fn(), resolvePending: vi.fn(), downloadChannelArtifact: vi.fn() }));
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
const session = { session_id: 'self-session', caller_id: 'self-caller', agent_slug: 'ml-' + 'a'.repeat(32), agent_name: '创作助手',
session_state: 'active', grant_state: 'authorized', access_mode: 'self_only', thread_id: 'private-channel-thread', sequence: 1, created_at: '2026-09-13T01:00:00Z' };
const run = { agent_run_id: 'native-run', status: 'interrupted', interrupt: { approval: { action_requests: [{ name: 'execute', args: { command: 'create slides' } }, { name: 'ls', args: { path: 'outputs' } }] } } };
const output = { name: '创作建议.pptx', path: '/outputs/创作建议.pptx', directory_path: '/outputs', is_dir: false, size: 40960 };
const history = { ...session, run, messages: [{ id: 1, role: 'user', content: '请制作演示文稿' }], queued_requests: [], next_offset: null };
beforeEach(() => {
vi.resetAllMocks();
api.recovery.mockResolvedValue({ recent: null, pending: [] });
api.downloadChannelArtifact.mockResolvedValue({ saved: true });
api.call.mockImplementation(async (operation, input) => {
if (operation === 'channelConversations') return { items: [session], next_offset: null };
if (operation === 'channelConversation') return history;
if (operation === 'channelFiles') return { session_id: session.session_id, thread_id: session.thread_id,
files: input.path === '/' ? [{ name: 'outputs', path: '/outputs', directory_path: '/outputs', is_dir: true, size: 0 }] : [output] };
throw new Error('Unexpected operation ' + operation);
});
});
afterEach(() => { cleanup(); vi.useRealTimers(); });
it('approves the actual action batch through the self-session endpoint and automatically exposes the delivered file', async () => {
const base = api.call.getMockImplementation()!;
let approved = false;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'channelConversationControl') { approved = true; return { session_id: session.session_id, status: 'completed' }; }
if (operation === 'channelConversation') return approved ? { ...history, run: { ...run, status: 'completed', interrupt: undefined }, messages: [{ id: 2, role: 'assistant', content: '演示文稿已完成' }] } : history;
if (operation === 'channelFiles' && !approved) return { files: [] };
return base(operation, input);
});
render(<CloudChannelConversations slug={session.agent_slug} />);
fireEvent.click(await screen.findByRole('button', { name: '允许执行' }));
await screen.findByText('演示文稿已完成');
expect(api.call).toHaveBeenCalledWith('channelConversationControl', expect.objectContaining({ session_id: session.session_id, action: 'resume', run_id: 'native-run',
decision: { decisions: [{ type: 'approve' }, { type: 'approve' }] } }));
fireEvent.click(await screen.findByRole('button', { name: '保存 创作建议.pptx' }));
await screen.findByText('已保存 创作建议.pptx');
expect(api.downloadChannelArtifact).toHaveBeenCalledWith('self-session', '/outputs/创作建议.pptx');
expect(api.call.mock.calls.some(call => ['resume', 'run', 'files', 'submit'].includes(call[0]))).toBe(false);
});
it('retries an uncertain stop with the identical operation ID and keeps other controls disabled', async () => {
const base = api.call.getMockImplementation()!;
let original: unknown;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'channelConversationControl') {
original = input;
api.recovery.mockResolvedValue({ pending: [{ id: 'pending-stop', operation, input }] });
throw new Error('操作超时');
}
return base(operation, input);
});
api.resolvePending.mockImplementation(async () => {
api.recovery.mockResolvedValue({ pending: [] });
return { result: { session_id: session.session_id, status: 'cancelled' } };
});
render(<CloudChannelConversations slug={session.agent_slug} />);
const stop = await screen.findByRole('button', { name: '停止本次任务' });
await waitFor(() => expect(stop).toBeEnabled());
fireEvent.click(stop);
expect(await screen.findByRole('button', { name: '恢复上次操作' })).toBeEnabled();
expect(stop).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: '恢复上次操作' }));
await waitFor(() => expect(api.resolvePending).toHaveBeenCalledWith('pending-stop', false));
expect(api.call.mock.calls.filter(call => call[0] === 'channelConversationControl')).toEqual([['channelConversationControl', original]]);
});
it('allows ignoring a rejected new-session operation before stopping an intake not yet exposed as a Run', async () => {
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => {
if (operation === 'channelConversation') return { ...history, run: null };
if (operation === 'channelConversationControl') {
if (input.action === 'new-session') {
api.recovery.mockResolvedValue({ pending: [{ id: 'rejected-new', operation, input }] });
throw new Error('已有任务正在准备');
}
return { session_id: session.session_id, status: 'cancelled' };
}
return base(operation, input);
});
api.resolvePending.mockImplementation(async () => {
api.recovery.mockResolvedValue({ pending: [] }); return { discarded: true };
});
render(<CloudChannelConversations slug={session.agent_slug} />);
const start = await screen.findByRole('button', { name: '开始新对话' });
await waitFor(() => expect(start).toBeEnabled());
fireEvent.click(start);
fireEvent.click(await screen.findByRole('button', { name: '忽略本机记录' }));
const stop = screen.getByRole('button', { name: '停止本次任务' });
await waitFor(() => expect(stop).toBeEnabled());
fireEvent.click(stop);
await waitFor(() => expect(api.call).toHaveBeenCalledWith('channelConversationControl', expect.objectContaining({ action: 'stop' })));
expect(api.resolvePending).toHaveBeenCalledWith('rejected-new', true);
});
it('recovers a completed new-session receipt only after an explicit click and selects the returned session', async () => {
const pending = { id: 'pending-new', operation: 'channelConversationControl', input: { session_id: session.session_id, operation_id: 'old-operation', action: 'new-session' } };
api.recovery.mockResolvedValueOnce({ pending: [pending] }).mockResolvedValue({ pending: [] });
api.resolvePending.mockResolvedValue({ result: { session_id: 'next-self-session', previous_session_id: session.session_id } });
render(<CloudChannelConversations slug={session.agent_slug} />);
const restore = await screen.findByRole('button', { name: '恢复上次操作' });
expect(api.resolvePending).not.toHaveBeenCalled();
expect(api.call.mock.calls.some(call => call[0] === 'channelConversationControl')).toBe(false);
fireEvent.click(restore);
await waitFor(() => expect(api.call).toHaveBeenCalledWith('channelConversation', { session_id: 'next-self-session' }));
expect(api.resolvePending).toHaveBeenCalledWith('pending-new', false);
});
it('preserves historical self files while hiding controls for a revoked closed session', async () => {
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => operation === 'channelConversation'
? { ...history, session_state: 'closed', grant_state: 'revoked' } : base(operation, input));
render(<CloudChannelConversations slug={session.agent_slug} />);
await screen.findByRole('button', { name: '保存 创作建议.pptx' });
expect(screen.queryByRole('button', { name: '允许执行' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '停止本次任务' })).not.toBeInTheDocument();
});
it('stops history polling when the desktop pane is hidden', async () => {
const view = render(<CloudChannelConversations slug={session.agent_slug} />);
await screen.findByText('请制作演示文稿');
view.rerender(<CloudChannelConversations slug={session.agent_slug} activeView={false} />);
const count = api.call.mock.calls.filter(call => call[0] === 'channelConversation').length;
vi.useFakeTimers();
await act(async () => { await vi.advanceTimersByTimeAsync(20000); });
expect(api.call.mock.calls.filter(call => call[0] === 'channelConversation')).toHaveLength(count);
});
it('reads subsequent native history pages so new results remain visible beyond the first 100 messages', async () => {
const base = api.call.getMockImplementation()!;
api.call.mockImplementation(async (operation, input) => {
if (operation !== 'channelConversation') return base(operation, input);
if (input.offset === 100) return { ...history, messages: [{ id: 101, role: 'assistant', content: '第 101 条消息的交付结果' }], next_offset: null };
return { ...history, next_offset: 100 };
});
render(<CloudChannelConversations slug={session.agent_slug} />);
await screen.findByText('第 101 条消息的交付结果');
expect(screen.getByText('请制作演示文稿')).toBeVisible();
expect(api.call).toHaveBeenCalledWith('channelConversation', { session_id: 'self-session', offset: 100 });
});