import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { CloudRecovery } from '@/pages/CloudAgents/CloudRecovery'; import { AppError } from '@/lib/error-model'; import type { CloudPendingOperation } from '../../shared/cloud-agents'; const api = vi.hoisted(() => ({ resolvePending: vi.fn() })); vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api })); const first: CloudPendingOperation = { id: 'create:first', operation: 'createAgent', input: { operation_id: 'original-first', name: '故事搭档' }, created_at: '2026-10-13T03:54:49Z' }; const second: CloudPendingOperation = { id: 'create:second', operation: 'createAgent', input: { operation_id: 'original-second', name: '阅读伙伴' }, created_at: '2026-10-13T03:55:18Z' }; const key: CloudPendingOperation = { id: 'key:original', operation: 'createKey', input: { application_id: 'application', operation_id: 'original-key', name: '应用密钥' }, created_at: '2026-10-13T04:00:00Z' }; const resolved = vi.fn(); beforeEach(() => { vi.resetAllMocks(); }); afterEach(cleanup); async function openRecords() { fireEvent.click(screen.getByRole('button', { name: /^(创建记录|待处理记录)/ })); return await screen.findByRole('dialog'); } function deferred() { let resolve!: (value: T) => void; const promise = new Promise(yes => { resolve = yes; }); return { promise, resolve }; } it('shows only a compact counted creation entry until explicitly opened, without retrying or discarding records', async () => { render(); expect(screen.getByRole('button', { name: /^创建记录/ })).toHaveTextContent('2'); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('complementary', { name: '未确认的云端操作' })).not.toBeInTheDocument(); expect(screen.queryByRole('article', { name: '创建「故事搭档」' })).not.toBeInTheDocument(); expect(screen.queryByText(/有 2 个操作的结果尚未确认/)).not.toBeInTheDocument(); const dialog = await openRecords(); expect(within(dialog).getByRole('article', { name: '创建「故事搭档」' })).toBeVisible(); expect(within(dialog).getByRole('article', { name: '创建「阅读伙伴」' })).toBeVisible(); expect(within(dialog).getAllByRole('button', { name: '再试一次', exact: true })).toHaveLength(2); expect(within(dialog).getAllByRole('button', { name: '不再提醒', exact: true })).toHaveLength(2); expect(api.resolvePending).not.toHaveBeenCalled(); expect(resolved).not.toHaveBeenCalled(); }); it('uses a general records entry for mixed operations and keeps the original message visible only inside the dialog', async () => { const message = { ...second, id: 'message:original', operation: 'submit', input: { query: '我的上次问题' } }; render(); expect(screen.getByRole('button', { name: /^待处理记录/ })).toHaveTextContent('2'); expect(screen.queryByText('我的上次问题')).not.toBeInTheDocument(); const dialog = await openRecords(); expect(within(dialog).getByText('我的上次问题')).toBeVisible(); expect(api.resolvePending).not.toHaveBeenCalled(); }); it('keeps failed records and retries the same record ID after closing and reopening the dialog', async () => { api.resolvePending.mockRejectedValueOnce(new Error('原操作仍未确认')).mockResolvedValueOnce({ result: { slug: 'original-agent' } }); render(); const dialog = await openRecords(); fireEvent.click(within(dialog).getByRole('button', { name: '再试一次' })); await screen.findByRole('alert'); expect(screen.getByRole('alert')).toHaveTextContent('原操作仍未确认'); expect(resolved).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: '关闭记录' })); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); const reopened = await openRecords(); expect(within(reopened).getByRole('article', { name: '创建「故事搭档」' })).toBeVisible(); fireEvent.click(within(reopened).getByRole('button', { name: '再试一次' })); await waitFor(() => expect(resolved).toHaveBeenCalledOnce()); expect(api.resolvePending.mock.calls).toEqual([[first.id, false], [first.id, false]]); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); }); it('removes only the explicitly chosen local reminder and never implicitly retries it', async () => { api.resolvePending.mockResolvedValue({ discarded: true }); const view = render(); const dialog = await openRecords(); fireEvent.click(within(dialog).getAllByRole('button', { name: '不再提醒' })[1]); await waitFor(() => expect(resolved).toHaveBeenCalledOnce()); expect(api.resolvePending.mock.calls).toEqual([[second.id, true]]); view.rerender(); expect(screen.getByRole('article', { name: '创建「故事搭档」' })).toBeVisible(); expect(screen.queryByRole('article', { name: '创建「阅读伙伴」' })).not.toBeInTheDocument(); }); it('serializes double clicks and other actions and prevents dismissal until a request finishes', async () => { const response = deferred<{ result: { slug: string } }>(); api.resolvePending.mockReturnValue(response.promise); render(); const dialog = await openRecords(); await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); const retry = within(dialog).getAllByRole('button', { name: '再试一次' })[0]; act(() => { fireEvent.click(retry); fireEvent.click(retry); }); expect(api.resolvePending).toHaveBeenCalledTimes(1); expect(within(dialog).getByRole('button', { name: '关闭记录' })).toBeDisabled(); for (const discard of within(dialog).getAllByRole('button', { name: '不再提醒' })) expect(discard).toBeDisabled(); fireEvent.keyDown(dialog, { key: 'Escape' }); fireEvent.pointerDown(document.body, { button: 0, pointerType: 'mouse' }); expect(screen.getByRole('dialog')).toBeVisible(); expect(resolved).not.toHaveBeenCalled(); await act(async () => response.resolve({ result: { slug: 'original-agent' } })); expect(resolved).toHaveBeenCalledOnce(); expect(within(dialog).getByRole('button', { name: '关闭记录' })).toBeEnabled(); }); it('keeps a returned key available after modal close and reopen until the result is explicitly closed', async () => { const secret = 'fixture-key-for-unit-only'; api.resolvePending.mockResolvedValue({ result: { secret, id: 'created-key' } }); const view = render(); const dialog = await openRecords(); fireEvent.click(within(dialog).getAllByRole('button', { name: '再试一次' })[0]); await screen.findByText(new RegExp(secret)); view.rerender(); fireEvent.click(screen.getByRole('button', { name: '关闭记录' })); expect(screen.queryByText(new RegExp(secret))).not.toBeInTheDocument(); await openRecords(); expect(screen.getByText(new RegExp(secret))).toBeVisible(); expect(api.resolvePending).toHaveBeenCalledTimes(1); fireEvent.click(screen.getByRole('button', { name: '关闭结果' })); expect(screen.queryByText(new RegExp(secret))).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '关闭记录' })); await openRecords(); expect(screen.queryByText(new RegExp(secret))).not.toBeInTheDocument(); }); it('keeps a result entry available when the final pending record was acknowledged', async () => { api.resolvePending.mockResolvedValue({ result: { secret: 'last-key-fixture' } }); const view = render(); await openRecords(); fireEvent.click(screen.getByRole('button', { name: '再试一次' })); await screen.findByText(/last-key-fixture/); view.rerender(); fireEvent.click(screen.getByRole('button', { name: '关闭记录' })); await openRecords(); expect(screen.getByText(/last-key-fixture/)).toBeVisible(); fireEvent.click(screen.getByRole('button', { name: '关闭结果' })); expect(screen.queryByText(/last-key-fixture/)).not.toBeInTheDocument(); }); it('guides verification requiring fresh input without claiming that verification completed', async () => { const verification = { ...first, id: 'wechat:verification', operation: 'wechatBindVerification', input: { session_key: 'fixture-session' } }; api.resolvePending.mockResolvedValue({ requires_input: true, operation: verification.operation }); render(); await openRecords(); fireEvent.click(screen.getByRole('button', { name: '再试一次' })); expect(await screen.findByText(/还需要验证码/)).toBeVisible(); expect(screen.queryByText('微信连接验证的结果已确认。')).not.toBeInTheDocument(); expect(api.resolvePending).toHaveBeenCalledWith(verification.id, false); }); it('offers only explicit discard for retired operations and explains the retired result without auto actions', async () => { const retired = { ...first, id: 'retired:policy', operation: 'channelPolicy', input: {} }; api.resolvePending.mockResolvedValue({ retired: true, discarded: true, operation: retired.operation }); render(); expect(api.resolvePending).not.toHaveBeenCalled(); await openRecords(); expect(screen.queryByRole('button', { name: '再试一次' })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '丢弃旧记录' })); expect(await screen.findByText(/旧版联系人权限操作已移除/)).toBeVisible(); expect(api.resolvePending).toHaveBeenCalledWith(retired.id, false); }); it('keeps a channel operation visibly pending without claiming a confirmed result', async () => { const operation: CloudPendingOperation = { id: 'unbind:original', operation: 'unbindChannelAccount', input: { channel_account_id: 'wechat-1', operation_id: 'original-unbind', expected_revision: 2 }, created_at: '2026-10-13T04:00:00Z' }; api.resolvePending.mockResolvedValue({ operation: operation.operation, pending: true, result: { status: 'pending' } }); render(); const dialog = await openRecords(); expect(within(dialog).getByRole('article', { name: '解除微信绑定' })).toBeVisible(); fireEvent.click(within(dialog).getByRole('button', { name: '再试一次' })); expect(await within(dialog).findByRole('status')).toHaveTextContent('这次操作还在确认中'); expect(within(dialog).queryByText(/结果已确认/)).not.toBeInTheDocument(); expect(within(dialog).getByRole('article', { name: '解除微信绑定' })).toBeVisible(); expect(resolved).toHaveBeenCalledOnce(); }); it('renders no entry or dialog when there are no pending records or retained results', () => { render(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(api.resolvePending).not.toHaveBeenCalled(); }); it('does not let another action replace an unclosed key result, and unlocks actions after explicit result closure', async () => { const verification = { ...second, operation: 'wechatBindVerification' }; api.resolvePending.mockImplementation(async id => id === key.id ? { result: { secret: 'protected-key-fixture' } } : { requires_input: true, operation: verification.operation }); const view = render(); await openRecords(); fireEvent.click(within(screen.getByRole('article', { name: '创建密钥' })).getByRole('button', { name: '再试一次' })); await screen.findByText(/protected-key-fixture/); view.rerender(); const row = screen.getByRole('article', { name: '微信连接验证' }); const retry = within(row).getByRole('button', { name: '再试一次' }); const discard = within(row).getByRole('button', { name: '不再提醒' }); expect(retry).toBeDisabled(); expect(discard).toBeDisabled(); fireEvent.click(retry); fireEvent.click(discard); expect(api.resolvePending).toHaveBeenCalledTimes(1); expect(screen.getByText(/protected-key-fixture/)).toBeVisible(); fireEvent.click(screen.getByRole('button', { name: '关闭结果' })); expect(retry).toBeEnabled(); fireEvent.click(retry); await screen.findByText(/还需要验证码/); expect(api.resolvePending.mock.calls).toEqual([[key.id, false], [verification.id, false]]); }); it('refreshes pending records after a definite rejection while keeping the error inside the explicit dialog', async () => { api.resolvePending.mockRejectedValue(new AppError('CONFIG', '这条创建已被拒绝', undefined, { commandOutcome: 'definitive_failure' })); const view = render(); await openRecords(); fireEvent.click(screen.getByRole('button', { name: '再试一次' })); expect(await screen.findByRole('alert')).toHaveTextContent('这条创建已被拒绝'); expect(resolved).toHaveBeenCalledOnce(); expect(api.resolvePending).toHaveBeenCalledWith(first.id, false); view.rerender(); fireEvent.click(screen.getByRole('button', { name: '关闭记录' })); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); await openRecords(); expect(screen.queryByRole('article')).not.toBeInTheDocument(); expect(screen.getByRole('alert')).toHaveTextContent('这条创建已被拒绝'); }); it('keeps an explicitly unknown command outcome recoverable without refreshing it as a definite failure', async () => { api.resolvePending.mockRejectedValue(new AppError('NETWORK', '请求结果仍未知', undefined, { commandOutcome: 'unknown' })); render(); await openRecords(); fireEvent.click(screen.getByRole('button', { name: '再试一次' })); expect(await screen.findByRole('alert')).toHaveTextContent('请求结果仍未知'); expect(resolved).not.toHaveBeenCalled(); expect(screen.getByRole('article', { name: '创建「故事搭档」' })).toBeVisible(); fireEvent.click(screen.getByRole('button', { name: '再试一次' })); await waitFor(() => expect(api.resolvePending).toHaveBeenCalledTimes(2)); expect(api.resolvePending.mock.calls).toEqual([[first.id, false], [first.id, false]]); });