From a02871400919f9a6bbdaeebd7d99cbe60459475e Mon Sep 17 00:00:00 2001 From: Shangxin Date: Sun, 6 Sep 2026 19:21:58 +0000 Subject: [PATCH] fix(tests): keep RecordingChatStore dispatch contract off ambient context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fake store's Dispatch/SetStateAsync awaited bare inside the gate, so any call made while QueueingSynchronizationContext.RunNext had installed itself captured the queue context. A fire-and-forget dispatch (the RemoveBottomPanelState path) then parked its continuation on a queue no one pumps after the test stops draining it, holding _dispatchGate and deadlocking the test's own Dispatch forever — the ArchiveConversation_ WhenMutationCompletesOffUi hang. Real ChatStore.Dispatch awaits everything with ConfigureAwait(false) so the gate never depends on an external pump; the fake now honors the same contract, making fire-and-forget dispatches self-complete. Verified: previously-hung test 5/5 green, full ChatViewModelTests 348x3 green, full Presentation.Core suite 3316/3316 green. Co-Authored-By: Claude --- .../Chat/ChatViewModelTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs b/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs index 26995e25..f0a85bf4 100644 --- a/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs +++ b/tests/SalmonEgg.Presentation.Core.Tests/Chat/ChatViewModelTests.cs @@ -7564,14 +7564,16 @@ public RecordingChatStore(IState state, IWorkspaceWriter? workspaceWr public async ValueTask Dispatch(ChatAction action) { + // 临界区不捕获环境上下文:与真实 ChatStore.Dispatch 的契约一致, + // 闸的释放绝不依赖外部泵(fire-and-forget 分发也必须能自完成)。 _actions.Enqueue(action); - await _dispatchGate.WaitAsync(); + await _dispatchGate.WaitAsync().ConfigureAwait(false); try { var currentState = LatestState; var updatedState = ChatReducer.Reduce(currentState, action); LatestState = updatedState; - await State.Update(_ => updatedState, CancellationToken.None); + await State.Update(_ => updatedState, CancellationToken.None).ConfigureAwait(false); if (updatedState.Generation > currentState.Generation) { _workspaceWriter?.Enqueue(updatedState, scheduleSave: true); @@ -7579,7 +7581,7 @@ public async ValueTask Dispatch(ChatAction action) if (AfterDispatch is { } afterDispatch) { - await afterDispatch(action); + await afterDispatch(action).ConfigureAwait(false); } } finally @@ -7591,7 +7593,7 @@ public async ValueTask Dispatch(ChatAction action) public async ValueTask SetStateAsync(ChatState state) { LatestState = state; - await State.Update(_ => state, CancellationToken.None); + await State.Update(_ => state, CancellationToken.None).ConfigureAwait(false); } public ValueTask GetCurrentStateAsync()