import { useState } from 'react'; import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { CloudApproval, CloudChat } from '@/pages/CloudAgents/CloudChat'; import { AgentRequirements } from '@/pages/CloudAgents/AgentRequirements'; import { CloudAccessPanel as AccessPanel } from '@/pages/CloudAgents/CloudAccess'; import { useCloudPublication } from '@/pages/CloudAgents/useCloudPublication'; function CloudAccessPanel(props: { slug: string; revision: number; onPublished: () => void }) { const publication = useCloudPublication(props.slug, props.revision, props.onPublished); return ; } const api = vi.hoisted(() => ({ call: vi.fn(), events: vi.fn(), download: vi.fn(), recovery: vi.fn(), remember: vi.fn() })); vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api })); const slug = 'ml-' + 'a'.repeat(32); const trial = { thread_id: 'old-trial', client_thread_id: 'old-client', mode: 'preview', draft_revision: 1, status: 'completed' }; const messages = [ { id: 'q1', role: 'user', content: '为什么会有彩虹?' }, { id: 'a1', role: 'assistant', content: '这是上次回答。[查看观察记录](sandbox:/mnt/data/note.txt)' }, ]; beforeEach(() => { vi.resetAllMocks(); api.recovery.mockResolvedValue({ recent: null, pending: [] }); api.remember.mockResolvedValue({ saved: true }); api.download.mockResolvedValue(undefined); api.events.mockResolvedValue(Object.assign(new EventTarget(), { close: vi.fn() })); api.call.mockImplementation(async (operation: string) => { if (operation === 'threads') return { threads: [trial], next_offset: null }; if (operation === 'history') return { thread_id: trial.thread_id, messages, run: null, next_offset: null }; if (operation === 'attachments') return { attachments: [] }; if (operation === 'files') return { files: [] }; if (operation === 'viewed') return { viewed: true }; throw new Error(operation); }); }); afterEach(cleanup); it('starts private use through the existing publication action and retains its identity on retry', async () => { const base = api.call.getMockImplementation()!; let published = false; let attempts = 0; api.call.mockImplementation(async (operation, input) => { if (operation === 'access') return { published_version: published ? 1 : null, enabled: true, versions: [], grants: [], applications: [] }; if (operation === 'publish') { if (++attempts === 1) throw new Error('连接中断'); published = true; return { version: 1, draft_revision: input.expected_revision }; } if (operation === 'costs') return { items: [], next_offset: null, summary: {} }; if (operation === 'channelBindings') return { items: [] }; return base(operation, input); }); const onPublished = vi.fn(); const view = render(); await waitFor(() => expect(screen.getByRole('button', { name: '确认开始使用' })).toBeEnabled()); expect(screen.queryByLabelText('应用名称')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '确认开始使用' })); await screen.findByText(/连接中断/); view.rerender(); fireEvent.click(screen.getByRole('button', { name: '重试本次发布' })); await screen.findByText('助手已准备好'); const publications = api.call.mock.calls.filter(([operation]) => operation === 'publish'); expect(publications).toHaveLength(2); expect(publications[0][1]).toEqual(publications[1][1]); expect(publications[1][1]).toMatchObject({ expected_revision: 2 }); expect(onPublished).toHaveBeenCalledTimes(1); expect(api.call.mock.calls.some(([operation]) => operation === 'share' || operation === 'createApplication')).toBe(false); }); it('keeps custom requirements intact while toggling and editing answer styles', () => { const onChange = vi.fn(); function Requirements() { const [value, setValue] = useState('我的规则\n\n 保留缩进和解释。'); return { onChange(next); setValue(next); }} />; } render(); fireEvent.click(screen.getByRole('button', { name: '简单易懂' })); fireEvent.click(screen.getByRole('button', { name: '先给我提示' })); expect(screen.getByLabelText('特别要求')).toHaveValue('我的规则\n\n 保留缩进和解释。'); fireEvent.click(screen.getByRole('button', { name: '简单易懂' })); fireEvent.change(screen.getByLabelText('特别要求'), { target: { value: '我的新规则' } }); expect(onChange).toHaveBeenLastCalledWith('我的新规则\n先给我提示和思考问题,让我尝试后再补充答案。'); }); it('waits for a successful save, preserves the old trial and reuses its question without sending', async () => { let finish!: (revision: number) => void; const save = vi.fn(() => new Promise(resolve => { finish = resolve; })); const view = render(); await screen.findByText('这是上次回答。'); fireEvent.click(screen.getByRole('button', { name: '保存并重新试用' })); expect(screen.getByLabelText('消息')).toBeDisabled(); expect(screen.getByText('这是上次回答。')).toBeVisible(); expect(screen.getByRole('button', { name: '新对话' })).toBeDisabled(); await act(async () => { finish(2); }); view.rerender(); expect(screen.getByText('当前试用 · 修订 2')).toBeVisible(); expect(screen.getByLabelText('消息')).toHaveValue('为什么会有彩虹?'); expect(screen.getByText('这是上次回答。')).not.toBeVisible(); fireEvent.click(screen.getByText('对比上一次试用 · 修订 1')); expect(screen.getByText('这是上次回答。')).toBeVisible(); fireEvent.click(screen.getByRole('button', { name: '查看观察记录' })); await waitFor(() => expect(api.download).toHaveBeenCalledWith('old-trial', '/mnt/data/note.txt')); expect(api.call.mock.calls.some(([operation]) => operation === 'preview' || operation === 'submit')).toBe(false); }); it('keeps the old trial and typed question if saving fails and allows a deliberate retry', async () => { const save = vi.fn().mockResolvedValueOnce(undefined).mockResolvedValueOnce(2); render(); await screen.findByText('这是上次回答。'); fireEvent.change(screen.getByLabelText('消息'), { target: { value: '我另外想问的问题' } }); fireEvent.click(screen.getByRole('button', { name: '保存并重新试用' })); await waitFor(() => expect(screen.getByLabelText('消息')).toBeEnabled()); expect(screen.getByText('当前试用 · 修订 1')).toBeVisible(); expect(screen.getByText('这是上次回答。')).toBeVisible(); expect(screen.getByLabelText('消息')).toHaveValue('我另外想问的问题'); fireEvent.click(screen.getByRole('button', { name: '保存并重新试用' })); await screen.findByText('当前试用 · 修订 2'); expect(screen.getByLabelText('消息')).toHaveValue('我另外想问的问题'); expect(save).toHaveBeenCalledTimes(2); }); it.each(['running', 'queued', 'interrupted'])('does not start a replacement trial while the current trial is %s', async status => { const base = api.call.getMockImplementation()!; const run = { agent_run_id: 'run-1', thread_id: trial.thread_id, status, ...(status === 'interrupted' ? { interrupt: { approval: { action_requests: [{ name: 'ls', args: { path: 'outputs' } }] } } } : {}), }; api.call.mockImplementation(async (operation, input) => { if (operation === 'history') return { thread_id: trial.thread_id, messages, run: status === 'queued' ? null : run, next_offset: null, queued_requests: status === 'queued' ? [{ request_id: 'q2', thread_id: trial.thread_id, status: 'queued' }] : [], }; if (operation === 'run') return run; if (operation === 'request') return { request_id: 'q2', thread_id: trial.thread_id, status: 'queued' }; return base(operation, input); }); const save = vi.fn(); render(); await screen.findByText('这是上次回答。'); expect(screen.getByRole('button', { name: '保存并重新试用' })).toBeDisabled(); fireEvent.click(screen.getByRole('button', { name: '保存并重新试用' })); expect(save).not.toHaveBeenCalled(); }); it('shows actual action arguments and retries the same approval decision after a lost response', async () => { const submit = vi.fn().mockRejectedValueOnce(new Error('连接中断')).mockResolvedValueOnce(undefined); const actions = [{ name: 'execute', args: { command: 'create slides' } }, { name: 'ls', args: { path: 'outputs' } }]; render(); expect(screen.getByText('1. 运行命令')).toBeVisible(); expect(screen.getByText('create slides')).toBeVisible(); expect(screen.getByText('outputs')).toBeVisible(); expect(screen.getAllByText('查看技术详情')).toHaveLength(2); fireEvent.click(screen.getByRole('button', { name: '允许执行' })); fireEvent.click(await screen.findByRole('button', { name: '重试本次确认' })); await waitFor(() => expect(submit).toHaveBeenCalledTimes(2)); expect(submit.mock.calls[0][0]).toEqual(submit.mock.calls[1][0]); expect(submit.mock.calls[1][0].decision).toEqual({ decisions: [{ type: 'approve' }, { type: 'approve' }] }); });