fix(coding): isolate async conversation actions

This commit is contained in:
2026-08-24 09:22:31 +08:00
parent 863f005203
commit da92eb1957
4 changed files with 219 additions and 9 deletions

View File

@@ -8,6 +8,15 @@ const interactionApi = vi.hoisted(() => ({
compact: vi.fn(),
model: vi.fn(),
thinking: vi.fn(),
diagnostics: vi.fn(),
}));
const inspectorApi = vi.hoisted(() => ({
changes: vi.fn(),
commands: vi.fn(),
content: vi.fn(),
files: vi.fn(),
find: vi.fn(),
skills: vi.fn(),
}));
vi.mock('@/lib/coding-conversations', async (importOriginal) => ({
@@ -17,6 +26,16 @@ vi.mock('@/lib/coding-conversations', async (importOriginal) => ({
compactCodingConversation: interactionApi.compact,
setCodingConversationModel: interactionApi.model,
setCodingConversationThinking: interactionApi.thinking,
getCodingRuntimeDiagnostics: interactionApi.diagnostics,
}));
vi.mock('@/lib/coding-product-tools', () => ({
getCodingConversationChanges: inspectorApi.changes,
getCodingConversationCommands: inspectorApi.commands,
getCodingFileContent: inspectorApi.content,
getCodingFileStatus: inspectorApi.files,
findCodingFiles: inspectorApi.find,
getCodingSkills: inspectorApi.skills,
}));
describe('PI-130 feature-complete Coding UI', () => {
@@ -217,4 +236,56 @@ describe('PI-130 feature-complete Coding UI', () => {
await waitFor(() => expect(callbacks.fork).toHaveBeenCalledOnce());
expect(screen.queryByText(/分享|回滚|待办/)).not.toBeInTheDocument();
});
it('does not let an old tools load overwrite the newly selected Conversation inspector', async () => {
let resolveOld!: (value: { commands: Array<{ name: string; title: string; description: string; source: 'makelore' }> }) => void;
const oldCommands = new Promise<{ commands: Array<{ name: string; title: string; description: string; source: 'makelore' }> }>((resolve) => {
resolveOld = resolve;
});
inspectorApi.changes.mockResolvedValue({ changes: null });
inspectorApi.files.mockResolvedValue({ files: [] });
inspectorApi.skills.mockResolvedValue({ skills: [] });
inspectorApi.commands.mockImplementation((conversationId: string) => (
conversationId === 'conversation-old'
? oldCommands
: Promise.resolve({ commands: [{ name: 'new-command', title: 'New command', description: 'New', source: 'makelore' as const }] })
));
interactionApi.diagnostics.mockResolvedValue({ revision: { provider: 1, resources: 1 }, workers: [] });
const { CodingWorkspaceInspector } = await import('@/pages/Chat/CodingWorkspaceInspector');
const view = render(
<CodingWorkspaceInspector
key="conversation-old"
open
onOpenChange={vi.fn()}
conversationId="conversation-old"
agentId="agent-old"
snapshot={null}
onUseCommand={vi.fn()}
/>,
);
await waitFor(() => expect(inspectorApi.commands).toHaveBeenCalledWith('conversation-old'));
view.rerender(
<CodingWorkspaceInspector
key="conversation-new"
open
onOpenChange={vi.fn()}
conversationId="conversation-new"
agentId="agent-new"
snapshot={null}
onUseCommand={vi.fn()}
/>,
);
await waitFor(() => expect(inspectorApi.commands).toHaveBeenCalledWith('conversation-new'));
await waitFor(() => expect(screen.getByRole('button', { name: '刷新' })).toBeEnabled());
const commandsTab = screen.getByRole('tab', { name: '命令' });
fireEvent.mouseDown(commandsTab, { button: 0, ctrlKey: false });
await waitFor(() => expect(commandsTab).toHaveAttribute('aria-selected', 'true'));
expect(await screen.findByRole('button', { name: /\/new-command/ })).toBeInTheDocument();
resolveOld({ commands: [{ name: 'old-command', title: 'Old command', description: 'Old', source: 'makelore' }] });
await oldCommands;
await waitFor(() => expect(screen.queryByRole('button', { name: /\/old-command/ })).not.toBeInTheDocument());
expect(screen.getByRole('button', { name: /\/new-command/ })).toBeInTheDocument();
});
});