Files
makelore/tests/unit/cloud-agents-guidance.test.tsx

155 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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