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' }] });
});