feat: integrate pixel teacher presence and resilient classroom preview
This commit is contained in:
@@ -205,6 +205,57 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('continues a failed operation using its history without sending or clearing the current draft', async () => {
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config });
|
||||
projectApi.conversations.mockResolvedValue([conversation]);
|
||||
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
|
||||
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
|
||||
const failed = createLocalConversationSnapshot(project.id, conversation);
|
||||
failed.run = { status: 'error', terminalReason: 'failed' };
|
||||
failed.nodes = [{ kind: 'message', id: 'failed', role: 'assistant', status: 'error', blocks: [] }];
|
||||
conversationApi.snapshot.mockResolvedValue(failed);
|
||||
conversationApi.recover.mockResolvedValue(undefined);
|
||||
conversationApi.submit.mockImplementation(async (input) => ({ accepted: true, conversationId: conversation.id, clientRequestId: input.clientRequestId, runId: 'resume-run', mode: 'prompt' }));
|
||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||
render(<CodingChatPanel />);
|
||||
const composer = within(await screen.findByTestId('coding-message-composer')).getByRole('textbox');
|
||||
fireEvent.change(composer, { target: { value: '这段还没有写完,不要发送' } });
|
||||
fireEvent.click(await screen.findByRole('button', { name: '帮我继续' }));
|
||||
await waitFor(() => expect(conversationApi.submit).toHaveBeenCalledTimes(1));
|
||||
expect(conversationApi.recover).toHaveBeenCalledWith(conversation.id);
|
||||
expect(conversationApi.submit).toHaveBeenCalledWith(expect.objectContaining({ text: expect.stringContaining('已完成的步骤不要重复执行'), attachments: [] }));
|
||||
expect(composer).toHaveValue('这段还没有写完,不要发送');
|
||||
});
|
||||
|
||||
it.each([true, false])('only resumes the matching login account and project (same=%s)', async (same) => {
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config });
|
||||
projectApi.conversations.mockResolvedValue([conversation]);
|
||||
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
|
||||
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
|
||||
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
|
||||
const { useAuthStore } = await import('@/stores/auth');
|
||||
const { codingRecoveryAccount } = await import('@/lib/coding-login-recovery');
|
||||
const user = { username: 'alice', userId: 'a1', tenantId: 't1', deptId: null, authorities: [] };
|
||||
useAuthStore.setState({ user });
|
||||
const workApi = await import('@/lib/coding-work-preview');
|
||||
const ensure = vi.spyOn(workApi, 'ensureWorkPreview').mockResolvedValue({ status: 'ready' });
|
||||
const onLoginRecoveryConsumed = vi.fn();
|
||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||
render(<CodingChatPanel loginRecovery={{ id: 'return-1', projectId: project.id, conversationId: conversation.id, work: true, account: same ? codingRecoveryAccount(user) : 'another-account' }} onLoginRecoveryConsumed={onLoginRecoveryConsumed} />);
|
||||
await screen.findByTestId('coding-message-composer');
|
||||
if (same) {
|
||||
await waitFor(() => expect(ensure).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getByRole('tab', { name: '作品', exact: true })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(onLoginRecoveryConsumed).toHaveBeenCalledTimes(1);
|
||||
} else {
|
||||
expect(screen.getByRole('tab', { name: '操作对话' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(ensure).not.toHaveBeenCalled();
|
||||
}
|
||||
expect(conversationApi.submit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the operation draft when switching work tabs and reopening separate teacher and friend drafts', async () => {
|
||||
localStorage.clear();
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
@@ -247,9 +298,16 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '老师', exact: true }));
|
||||
const teacherInput = await screen.findByRole('textbox', { name: '向老师提问' });
|
||||
expect(workTab).toHaveAttribute('aria-selected', 'true');
|
||||
expect(operationInput).not.toBeVisible();
|
||||
expect(operationInput).toHaveValue('我想先自己试一试');
|
||||
await waitFor(() => expect(teacherInput).toBeEnabled());
|
||||
fireEvent.change(teacherInput, { target: { value: '怎样判断规则清不清楚?' } });
|
||||
fireEvent.click(chatTab);
|
||||
expect(operationInput).toBeVisible();
|
||||
expect(teacherInput).toHaveValue('怎样判断规则清不清楚?');
|
||||
fireEvent.click(screen.getByRole('button', { name: '朋友', exact: true }));
|
||||
expect(chatTab).toHaveAttribute('aria-selected', 'true');
|
||||
const friendInput = await screen.findByRole('textbox', { name: '向朋友提问' });
|
||||
await waitFor(() => expect(friendInput).toBeEnabled());
|
||||
expect(friendInput).toHaveValue('');
|
||||
@@ -259,6 +317,7 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '朋友', exact: true }));
|
||||
expect(screen.getByRole('textbox', { name: '向朋友提问' })).toHaveValue('第一次玩的感受是什么?');
|
||||
fireEvent.click(screen.getByRole('button', { name: '老师', exact: true }));
|
||||
expect(workTab).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByRole('textbox', { name: '向老师提问' })).toHaveValue('怎样判断规则清不清楚?');
|
||||
expect(screen.getByRole('button', { name: '老师', exact: true })).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(screen.getByRole('button', { name: '朋友', exact: true })).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -1297,6 +1356,7 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
await waitFor(() => expect(conversationApi.submit).toHaveBeenCalledOnce());
|
||||
await waitFor(() => expect(screen.getByTestId('coding-file-attachment-input')).toBeEnabled());
|
||||
expect(screen.queryByText(/条消息已被本地 Agent 接收/)).not.toBeInTheDocument();
|
||||
|
||||
act(() => codingConversationStore.getState().applySnapshotEvent({
|
||||
type: 'snapshot',
|
||||
@@ -1321,7 +1381,7 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('shows the 202 acceptance and preserves Enter versus Shift+Enter behavior', async () => {
|
||||
it('keeps preparation feedback and preserves Enter versus Shift+Enter behavior', async () => {
|
||||
const { CodingComposer } = await import('@/pages/Chat/CodingComposer');
|
||||
const onSubmit = vi.fn();
|
||||
render(
|
||||
@@ -1336,7 +1396,6 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
queue={{ items: [] }}
|
||||
error={null}
|
||||
recoverableError={false}
|
||||
acceptedCount={1}
|
||||
attachments={[]}
|
||||
submitting={false}
|
||||
placeholder="Message"
|
||||
@@ -1349,7 +1408,8 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('1 条消息已被本地 Agent 接收。')).toBeInTheDocument();
|
||||
expect(screen.getByText('正在准备本地 Agent,输入框仍可编辑。')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/条消息已被本地 Agent 接收/)).not.toBeInTheDocument();
|
||||
const textbox = screen.getByRole('textbox');
|
||||
fireEvent.keyDown(textbox, { key: 'Enter', code: 'Enter', shiftKey: true });
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user