fix: refresh can cancel an in-flight activation
This commit is contained in:
+87
@@ -264,6 +264,93 @@ describe('useEditorAgentConversation', () => {
|
||||
expect(result.current.isLoadingMessages).toBe(false);
|
||||
});
|
||||
|
||||
it('does not let a refresh supersede an in-flight conversation activation', async () => {
|
||||
const client = createClient();
|
||||
let resolveActivation!: (detail: EditorAgentConversationDetail) => void;
|
||||
let resolveRefresh!: (detail: EditorAgentConversationDetail) => void;
|
||||
const activationPromise = new Promise<EditorAgentConversationDetail>(
|
||||
(resolve) => {
|
||||
resolveActivation = resolve;
|
||||
},
|
||||
);
|
||||
const refreshPromise = new Promise<EditorAgentConversationDetail>(
|
||||
(resolve) => {
|
||||
resolveRefresh = resolve;
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(() =>
|
||||
useEditorAgentConversation({ projectId: 'project-1', client }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeConversationId).toBe('conversation-1');
|
||||
});
|
||||
vi.mocked(client.getConversation).mockImplementation((conversationId) => {
|
||||
if (conversationId === 'conversation-2') {
|
||||
return activationPromise;
|
||||
}
|
||||
if (conversationId === 'conversation-1') {
|
||||
return refreshPromise;
|
||||
}
|
||||
throw new Error(`Unexpected conversation: ${conversationId}`);
|
||||
});
|
||||
|
||||
let pendingActivation!: Promise<EditorAgentConversationDetail | null>;
|
||||
let pendingRefresh!: Promise<EditorAgentConversationDetail | null>;
|
||||
act(() => {
|
||||
pendingActivation = result.current.selectConversation('conversation-2');
|
||||
pendingRefresh = result.current.refreshActiveConversation();
|
||||
});
|
||||
|
||||
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-1');
|
||||
expect(result.current.messages).toEqual([]);
|
||||
|
||||
await act(async () => {
|
||||
resolveActivation({
|
||||
conversationId: 'conversation-2',
|
||||
projectId: 'project-1',
|
||||
title: '第二个会话',
|
||||
messages: [
|
||||
{
|
||||
id: 20,
|
||||
role: 'assistant',
|
||||
text: '第二个会话消息',
|
||||
attachments: [],
|
||||
toolCall: null,
|
||||
createdAt: '2026-07-03T00:03:00.000Z',
|
||||
},
|
||||
],
|
||||
createdAt: '2026-07-03T00:03:00.000Z',
|
||||
updatedAt: '2026-07-03T00:03:00.000Z',
|
||||
});
|
||||
await pendingActivation;
|
||||
});
|
||||
|
||||
expect(result.current.activeConversationId).toBe('conversation-2');
|
||||
expect(result.current.messages[0]?.text).toBe('第二个会话消息');
|
||||
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;
|
||||
|
||||
@@ -158,6 +158,7 @@ export function useEditorAgentConversation({
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const normalizedProjectIdRef = useRef(normalizedProjectId);
|
||||
const activeConversationIdRef = useRef<string | null>(null);
|
||||
const pendingActivationConversationIdRef = useRef<string | null>(null);
|
||||
const activeToolCallActionRef = useRef<EditorAgentToolCallActionState>(null);
|
||||
const conversationLoadRequestIdRef = useRef(0);
|
||||
const createConversationRequestIdRef = useRef(0);
|
||||
@@ -234,13 +235,22 @@ export function useEditorAgentConversation({
|
||||
},
|
||||
) => {
|
||||
const requestId = conversationLoadRequestIdRef.current + 1;
|
||||
conversationLoadRequestIdRef.current = requestId;
|
||||
const maySupersedeCurrentLoad =
|
||||
options.mode === 'activate' ||
|
||||
pendingActivationConversationIdRef.current === null;
|
||||
if (maySupersedeCurrentLoad) {
|
||||
conversationLoadRequestIdRef.current = requestId;
|
||||
}
|
||||
if (options.mode === 'activate') {
|
||||
pendingActivationConversationIdRef.current = conversationId;
|
||||
}
|
||||
const showLoading = options.showLoading ?? true;
|
||||
const canApplyResult = () =>
|
||||
conversationLoadRequestIdRef.current === requestId &&
|
||||
(options.mode === 'activate' ||
|
||||
activeConversationIdRef.current === conversationId);
|
||||
if (showLoading) {
|
||||
(pendingActivationConversationIdRef.current === null &&
|
||||
activeConversationIdRef.current === conversationId));
|
||||
if (showLoading && maySupersedeCurrentLoad) {
|
||||
setIsLoadingMessages(true);
|
||||
}
|
||||
setErrorMessage(null);
|
||||
@@ -258,6 +268,13 @@ export function useEditorAgentConversation({
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (
|
||||
options.mode === 'activate' &&
|
||||
conversationLoadRequestIdRef.current === requestId &&
|
||||
pendingActivationConversationIdRef.current === conversationId
|
||||
) {
|
||||
pendingActivationConversationIdRef.current = null;
|
||||
}
|
||||
if (conversationLoadRequestIdRef.current === requestId) {
|
||||
setIsLoadingMessages(false);
|
||||
}
|
||||
@@ -268,6 +285,7 @@ export function useEditorAgentConversation({
|
||||
|
||||
useEffect(() => {
|
||||
conversationLoadRequestIdRef.current += 1;
|
||||
pendingActivationConversationIdRef.current = null;
|
||||
setIsLoadingMessages(false);
|
||||
if (!normalizedProjectId) {
|
||||
setConversations([]);
|
||||
@@ -339,6 +357,7 @@ export function useEditorAgentConversation({
|
||||
normalizedProjectIdRef.current === requestedProjectId
|
||||
) {
|
||||
conversationLoadRequestIdRef.current += 1;
|
||||
pendingActivationConversationIdRef.current = null;
|
||||
setIsLoadingMessages(false);
|
||||
applyConversationDetail(detail);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user