fix: prevent silent refresh from overwriting newly created conversations in EditorAgentConversation
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Successful in 32s
Project CI / Native shell tests (pull_request) Successful in 2m25s

This commit is contained in:
2026-07-23 13:54:07 +08:00
parent 62bbd3876a
commit 6ab64efb4a
2 changed files with 98 additions and 9 deletions
@@ -264,6 +264,75 @@ describe('useEditorAgentConversation', () => {
expect(result.current.isLoadingMessages).toBe(false);
});
it('does not let a silent refresh replace a newly created conversation', async () => {
const client = createClient();
let resolveRefresh!: (detail: EditorAgentConversationDetail) => void;
let resolveCreate!: (detail: EditorAgentConversationDetail) => void;
const refreshPromise = new Promise<EditorAgentConversationDetail>(
(resolve) => {
resolveRefresh = resolve;
},
);
const createPromise = new Promise<EditorAgentConversationDetail>(
(resolve) => {
resolveCreate = resolve;
},
);
const { result } = renderHook(() =>
useEditorAgentConversation({ projectId: 'project-1', client }),
);
await waitFor(() => {
expect(result.current.activeConversationId).toBe('conversation-1');
});
vi.mocked(client.getConversation).mockReturnValueOnce(refreshPromise);
vi.mocked(client.createConversation).mockReturnValueOnce(createPromise);
let pendingRefresh!: Promise<EditorAgentConversationDetail | null>;
let pendingCreate!: Promise<EditorAgentConversationDetail>;
act(() => {
pendingRefresh = result.current.refreshActiveConversation();
pendingCreate = result.current.createConversation();
});
await act(async () => {
resolveCreate({
conversationId: 'conversation-2',
projectId: 'project-1',
title: '新对话',
messages: [],
createdAt: '2026-07-03T00:01:00.000Z',
updatedAt: '2026-07-03T00:01:00.000Z',
});
await pendingCreate;
});
expect(result.current.activeConversationId).toBe('conversation-2');
await act(async () => {
resolveRefresh({
conversationId: 'conversation-1',
projectId: 'project-1',
title: '旧对话刷新结果',
messages: [
{
id: 10,
role: 'assistant',
text: '旧会话消息',
attachments: [],
toolCall: null,
createdAt: '2026-07-03T00:02:00.000Z',
},
],
createdAt: '2026-07-03T00:00:00.000Z',
updatedAt: '2026-07-03T00:02:00.000Z',
});
await pendingRefresh;
});
expect(result.current.activeConversationId).toBe('conversation-2');
expect(result.current.messages).toEqual([]);
});
it('appends a lazily reconciled tool message delta', async () => {
const client = createClient();
const pendingMessage: EditorAgentMessage = {
@@ -224,22 +224,34 @@ export function useEditorAgentConversation({
);
const loadConversation = useCallback(
async (conversationId: string, options: { showLoading?: boolean } = {}) => {
async (
conversationId: string,
options: {
// `activate` may make conversationId active; `refresh` may apply only
// while that same conversationId is still active.
mode: 'activate' | 'refresh';
showLoading?: boolean;
},
) => {
const requestId = conversationLoadRequestIdRef.current + 1;
conversationLoadRequestIdRef.current = requestId;
const showLoading = options.showLoading ?? true;
const canApplyResult = () =>
conversationLoadRequestIdRef.current === requestId &&
(options.mode === 'activate' ||
activeConversationIdRef.current === conversationId);
if (showLoading) {
setIsLoadingMessages(true);
}
setErrorMessage(null);
try {
const detail = await client.getConversation(conversationId);
if (conversationLoadRequestIdRef.current === requestId) {
if (canApplyResult()) {
applyConversationDetail(detail);
}
return detail;
} catch (error) {
if (conversationLoadRequestIdRef.current === requestId) {
if (canApplyResult()) {
setErrorMessage(
error instanceof Error ? error.message : '读取画布 Agent 会话失败',
);
@@ -326,6 +338,8 @@ export function useEditorAgentConversation({
createConversationRequestIdRef.current === requestId &&
normalizedProjectIdRef.current === requestedProjectId
) {
conversationLoadRequestIdRef.current += 1;
setIsLoadingMessages(false);
applyConversationDetail(detail);
}
return detail;
@@ -354,7 +368,7 @@ export function useEditorAgentConversation({
if (conversationId === activeConversationId) {
return null;
}
return loadConversation(conversationId);
return loadConversation(conversationId, { mode: 'activate' });
},
[activeConversationId, loadConversation],
);
@@ -364,7 +378,10 @@ export function useEditorAgentConversation({
if (!conversationId) {
return null;
}
return loadConversation(conversationId, { showLoading: false });
return loadConversation(conversationId, {
mode: 'refresh',
showLoading: false,
});
}, [loadConversation]);
const requestCanvasRefreshForMessages = useCallback(
@@ -374,9 +391,9 @@ export function useEditorAgentConversation({
const toolCall = message.toolCall;
return Boolean(
toolCall?.externalJobId &&
(toolCall.images.length > 0 ||
(toolCall.videos?.length ?? 0) > 0 ||
(toolCall.audios?.length ?? 0) > 0),
(toolCall.images.length > 0 ||
(toolCall.videos?.length ?? 0) > 0 ||
(toolCall.audios?.length ?? 0) > 0),
);
})
) {
@@ -541,6 +558,7 @@ export function useEditorAgentConversation({
}
const detail = await loadConversation(conversationId, {
mode: 'refresh',
showLoading: false,
});
const updatedMessage = detail.messages.find(
@@ -596,7 +614,9 @@ export function useEditorAgentConversation({
setConversations(remainingConversations);
const nextConversation = remainingConversations[0] ?? null;
if (nextConversation) {
await loadConversation(nextConversation.conversationId);
await loadConversation(nextConversation.conversationId, {
mode: 'activate',
});
return;
}
activeConversationIdRef.current = null;