fix(pi): retain ownership for uncertain mutations

This commit is contained in:
2026-08-25 23:53:54 +08:00
parent 621ebb1781
commit 019cbb115a
21 changed files with 1113 additions and 67 deletions

View File

@@ -182,7 +182,7 @@ describe('PI-130 feature-complete Coding UI', () => {
expect(await screen.findByText(/请求可能已失效/)).toBeInTheDocument();
});
it('keeps model, thinking, abort, metadata, and fork controls on the selected Conversation', async () => {
it('keeps model, thinking, metadata, and fork controls on the selected idle Conversation', async () => {
const { useProviderStore } = await import('@/stores/providers');
useProviderStore.setState({
accounts: [{
@@ -239,7 +239,7 @@ describe('PI-130 feature-complete Coding UI', () => {
},
},
nodes: [],
run: { status: 'running', runId: 'run-1', mode: 'prompt' },
run: { status: 'idle' },
queue: { items: [] },
context: { usedTokens: 200, contextWindow: 1000, compaction: 'idle' },
pendingInteractions: [],
@@ -270,13 +270,84 @@ describe('PI-130 feature-complete Coding UI', () => {
expect(within(thinkingSelect).getAllByRole('option')).toHaveLength(1);
expect(within(thinkingSelect).getByRole('option', { name: '高思考' })).toBeInTheDocument();
expect(within(thinkingSelect).queryByRole('option', { name: '关闭思考' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '中止' }));
await waitFor(() => expect(interactionApi.abort).toHaveBeenCalledWith('conversation-1'));
fireEvent.click(screen.getByRole('button', { name: '创建分支' }));
await waitFor(() => expect(callbacks.fork).toHaveBeenCalledOnce());
expect(screen.queryByText(/分享|回滚|待办/)).not.toBeInTheDocument();
});
it('blocks overlapping mutations but keeps abort and recover available while confirmation is uncertain', async () => {
interactionApi.abort.mockResolvedValue(undefined);
const recover = vi.fn(async () => undefined);
const fork = vi.fn(async () => undefined);
const { CodingConversationHeader } = await import('@/pages/Chat/CodingConversationHeader');
render(
<CodingConversationHeader
conversation={{
id: 'conversation-uncertain',
agentId: 'agent-1',
title: 'Slow provider',
archivedAt: null,
unread: false,
createdAt: '2026-08-25T00:00:00.000Z',
updatedAt: '2026-08-25T00:00:00.000Z',
model: { accountId: 'account-1', modelId: 'model-a', thinkingLevel: 'medium' },
modelResolution: 'resolved',
}}
snapshot={{
schemaVersion: 1,
conversation: {
id: 'conversation-uncertain',
projectId: 'project-1',
agentId: 'agent-1',
title: 'Slow provider',
model: {
model: { accountId: 'account-1', modelId: 'model-a', thinkingLevel: 'medium' },
modelResolution: 'resolved',
availableThinkingLevels: ['off', 'medium'],
},
},
nodes: [],
run: {
status: 'running',
runId: 'run-uncertain',
mode: 'prompt',
error: {
code: 'CODING_REQUEST_UNCERTAIN',
message: '请求确认延迟,可能仍在执行。',
recoverable: true,
},
},
queue: { items: [] },
context: { usedTokens: 200, contextWindow: 1000, compaction: 'idle' },
pendingInteractions: [],
worker: { status: 'ready', generation: 1 },
cursor: { workerGeneration: 1, seq: 2 },
}}
connectionState="live"
onRename={vi.fn(async () => undefined)}
onArchive={vi.fn(async () => undefined)}
onToggleUnread={vi.fn(async () => undefined)}
onFork={fork}
onRefresh={vi.fn(async () => undefined)}
onRecover={recover}
onOpenInspector={vi.fn()}
/>,
);
expect(screen.getByRole('combobox', { name: '当前对话模型' })).toBeDisabled();
expect(screen.getByRole('combobox', { name: '当前对话思考级别' })).toBeDisabled();
expect(screen.getByRole('button', { name: '整理上下文' })).toBeDisabled();
expect(screen.getByRole('button', { name: '创建分支' })).toBeDisabled();
expect(screen.queryByText('本地编程运行时暂时不可用')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '中止' }));
await waitFor(() => expect(interactionApi.abort).toHaveBeenCalledWith('conversation-uncertain'));
await waitFor(() => expect(screen.getByRole('button', { name: '恢复' })).toBeEnabled());
fireEvent.click(screen.getByRole('button', { name: '恢复' }));
await waitFor(() => expect(recover).toHaveBeenCalledOnce());
expect(fork).not.toHaveBeenCalled();
});
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) => {