test(chat-ui): port 13 skipped ChatViewModel tests from v2026.3.11 (#2381)#2389
test(chat-ui): port 13 skipped ChatViewModel tests from v2026.3.11 (#2381)#2389alexey-pelykh merged 1 commit intomainfrom
Conversation
Ports 13 @test functions from upstream commit 061b825 ("macOS: add chat model selector and persist thinking") that were skipped during the fork sync in #2379. Issue #2381 states "9 tests" but the actual v2026.3.8..v2026.3.11 cycle delta is 13 — verified via git log. Ten model-selection tests cover: bootstrap, default patch, provider- qualified disambiguation, provider-qualified slash IDs, stale patch race, send-waits-for-patch, failed-latest replay, failed-latest restore, session-switch late patch, cross-session replay isolation. Three thinking-level tests cover: explicit level persistence, server- provided levels outside menu, stale patch replay. Decision on contextTokens: adapt tests to omit. Fork gutted the field in #2277 — extending types would revert that deliberate decision. Helper infrastructure ported: 14 private helpers (functions, actors, classes). TestChatTransport extended with modelResponses + optional hook params (backward-compatible) and new async query methods. Existing 11 tests preserved verbatim. Local compile blocked by pre-existing symbol references in RemoteClawKit/GatewayChannel.swift (see #2388). Fork CI has no Swift jobs so this state also pre-exists and is orthogonal to this change. Closes #2381. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Ports skipped upstream ChatViewModelTests coverage into the fork to validate model selection + thinking-level persistence behavior in RemoteClawChatViewModel.
Changes:
- Added helper scaffolding to construct histories/sessions/models and drive
RemoteClawChatViewModelin async tests. - Extended the in-file
TestChatTransportto support model listing responses, session patch hooks, and additional introspection helpers. - Added 13 new
@Testcases covering model selection edge-cases and thinking-level selection behavior.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await MainActor.run { vm.load() } | ||
| try await waitUntil("bootstrap") { | ||
| await MainActor.run { | ||
| vm.healthOK && (sessionId == nil || vm.sessionId == sessionId) |
There was a problem hiding this comment.
loadAndWaitBootstrap returns as soon as healthOK becomes true, but RemoteClawChatViewModel.bootstrap() sets healthOK before awaiting fetchSessions()/fetchModels(). The new model-selection tests assert on sessions/modelChoices immediately after this helper, so they can race/flap. Consider waiting for !vm.isLoading (or explicitly for vm.sessions/vm.modelChoices to be populated) in addition to healthOK/sessionId so the helper only returns once bootstrap has fully finished.
| vm.healthOK && (sessionId == nil || vm.sessionId == sessionId) | |
| vm.healthOK && | |
| !vm.isLoading && | |
| (sessionId == nil || vm.sessionId == sessionId) |
| private var continuation: CheckedContinuation<Void, Never>? | ||
|
|
||
| func wait() async { | ||
| await withCheckedContinuation { continuation in | ||
| self.continuation = continuation | ||
| } | ||
| } | ||
|
|
||
| func open() { | ||
| self.continuation?.resume() | ||
| self.continuation = nil |
There was a problem hiding this comment.
AsyncGate.open() is a no-op if it’s called before wait() stores the continuation. Because the model-patch task can be descheduled between appending patchedModels and entering the hook’s await gate.wait(), this can introduce a rare deadlock/flaky timeout in sendWaitsForInFlightModelPatchToFinish. Make AsyncGate resilient to out-of-order calls (e.g., track an isOpen flag so wait() returns immediately if already opened, and/or support multiple waiters).
| private var continuation: CheckedContinuation<Void, Never>? | |
| func wait() async { | |
| await withCheckedContinuation { continuation in | |
| self.continuation = continuation | |
| } | |
| } | |
| func open() { | |
| self.continuation?.resume() | |
| self.continuation = nil | |
| private var isOpen = false | |
| private var continuations: [CheckedContinuation<Void, Never>] = [] | |
| func wait() async { | |
| if self.isOpen { | |
| return | |
| } | |
| await withCheckedContinuation { continuation in | |
| if self.isOpen { | |
| continuation.resume() | |
| } else { | |
| self.continuations.append(continuation) | |
| } | |
| } | |
| } | |
| func open() { | |
| self.isOpen = true | |
| let continuations = self.continuations | |
| self.continuations.removeAll() | |
| for continuation in continuations { | |
| continuation.resume() | |
| } |
Summary
@Testfunctions from upstream commit061b8258bc("macOS: add chat model selector and persist thinking") that were skipped during the fork sync in sync: upstream to v2026.3.11 (235 commits) #2379.git log v2026.3.8..v2026.3.11on upstream. All 13 ported.TestChatTransportextended withmodelResponses+ optionalsetSessionModelHook/setSessionThinkingHookparams (backward-compatible) and 3 new async query methods.Decision on
contextTokensAdapt tests to omit (not extend fork types).
Fork deliberately gutted
contextTokensin #2277 ("gut(session): remove vestigial contextTokens config and display denominator"). Extending fork types would revert that decision. All ported tests drop thecontextTokens:argument fromRemoteClawChatSessionsDefaultsandsessionEntryhelper.Ported tests
10 model-selection tests (
RemoteClawChatViewModel.selectModel/modelSelectionID/modelChoices/patchedModels):bootstrapsModelSelectionFromSessionAndDefaultsselectingDefaultModelPatchesNilAndUpdatesSelectionselectingProviderQualifiedModelDisambiguatesDuplicateModelIDsslashModelIDsStayProviderQualifiedInSelectionAndPatchstaleModelPatchCompletionsDoNotOverwriteNewerSelectionsendWaitsForInFlightModelPatchToFinishfailedLatestModelSelectionDoesNotReplayAfterOlderCompletionFinishesfailedLatestModelSelectionRestoresEarlierSuccessWithoutReplayswitchingSessionsIgnoresLateModelPatchCompletionFromPreviousSessionlateModelCompletionDoesNotReplayCurrentSessionSelectionIntoPreviousSession3 thinking-level tests (
RemoteClawChatViewModel.selectThinkingLevel/initialThinkingLevel/onThinkingLevelChanged):explicitThinkingLevelWinsOverHistoryAndPersistsChangesserverProvidedThinkingLevelsOutsideMenuArePreservedForSendstaleThinkingPatchCompletionReappliesLatestSelectionTest plan
GatewayChannel.swiftcompile break remains).@Test funccount: 24 (11 existing + 13 new).contextTokens→ 0 matches (decision honored).OpenClaw→ 0 matches (rebrand complete).resetSession|compactSession|mainSessionKey→ 0 matches (fork lacks these features; none of the 13 ported tests require them).TestChatTransport).TestChatTransport(historyResponses:)calls still compile.Known limitation
swift testcannot be run locally due to a pre-existing compile break inRemoteClawKit/GatewayChannel.swift— references to symbols that were gutted during the v2026.3.11 sync (ThrowingContinuationSupport,GatewayDeviceAuthPayload,GatewayConnectChallengeSupport). Filed #2388 for that issue. Fork CI has no macOS/Swift jobs so this compile state exists independent of this PR.Once #2388 lands,
swift test --filter ChatViewModelTestswill exercise all 24 tests (11 existing + 13 new).Helper scaffolding note
6 of the 14 ported helpers (
chatTextMessage,sendMessageAndEmitFinal,emitAssistantText,emitToolStart,emitExternalFinal,AsyncCounter) are not referenced by the 13 ported tests — they're used by upstream tests from later sync cycles (optimistic-message, reset/compact triggers) or by upstream's refactored versions of tests the fork preserves verbatim per AC4. Kept as scaffolding to preserve symmetry with upstream for future sync cycles.References
contextTokens: gut(session): remove vestigial contextTokens config and display denominator #2277Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com