155 lines
9.7 KiB
TypeScript
155 lines
9.7 KiB
TypeScript
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 <AccessPanel slug={props.slug} revision={props.revision} publication={publication} />;
|
||
}
|
||
|
||
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(<CloudAccessPanel slug={slug} revision={2} onPublished={onPublished} />);
|
||
await waitFor(() => expect(screen.getByRole('button', { name: '确认开始使用' })).toBeEnabled());
|
||
expect(screen.queryByLabelText('应用名称')).not.toBeInTheDocument();
|
||
fireEvent.click(screen.getByRole('button', { name: '确认开始使用' }));
|
||
await screen.findByText(/连接中断/);
|
||
view.rerender(<CloudAccessPanel slug={slug} revision={3} onPublished={onPublished} />);
|
||
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 <AgentRequirements value={value} onChange={next => { onChange(next); setValue(next); }} />;
|
||
}
|
||
render(<Requirements />);
|
||
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<number>(resolve => { finish = resolve; }));
|
||
const view = render(<CloudChat slug={slug} previewRevision={1} hasUnsavedChanges onSaveForPreview={save} />);
|
||
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(<CloudChat slug={slug} previewRevision={2} onSaveForPreview={save} />);
|
||
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(<CloudChat slug={slug} previewRevision={1} hasUnsavedChanges onSaveForPreview={save} />);
|
||
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(<CloudChat slug={slug} previewRevision={1} hasUnsavedChanges onSaveForPreview={save} />);
|
||
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(<CloudApproval runId="run-1" interrupt={{ approval: { action_requests: actions } }} submitDecision={submit} />);
|
||
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' }] });
|
||
});
|