@@ -767,6 +767,7 @@ extension TestChatTransportState {
767767 }
768768}
769769
770+ @Suite ( . serialized)
770771struct ChatViewModelTests {
771772 @Test func `keeps distinct idempotent user turns with identical timestamps and content`() async throws {
772773 let history = historyPayload (
@@ -3432,17 +3433,22 @@ struct ChatViewModelTests {
34323433
34333434 await gate. open ( )
34343435 try await waitUntil ( " history reloaded after compact " ) {
3435- await MainActor . run { vm. messages. first? . content. first? . text == " after compact " }
3436+ await MainActor . run {
3437+ vm. messages. first? . content. first? . text == " after compact " && !vm. isLoading
3438+ }
34363439 }
34373440
34383441 await MainActor . run {
34393442 vm. input = " /compact "
34403443 vm. send ( )
34413444 }
34423445
3443- try await Task . sleep ( for: . milliseconds( 50 ) )
3446+ try await waitUntil ( " compact cooldown rejects immediate retry " ) {
3447+ await MainActor . run {
3448+ vm. errorText == " Please wait before compacting this session again. "
3449+ }
3450+ }
34443451 #expect( await transport. compactSessionKeys ( ) == [ " main " ] )
3445- #expect( await MainActor . run { vm. errorText } == " Please wait before compacting this session again. " )
34463452 }
34473453
34483454 @Test func `compact trigger allows immediate retry after failure`() async throws {
@@ -3615,6 +3621,13 @@ struct ChatViewModelTests {
36153621 modelResponses: [ models] )
36163622
36173623 try await loadAndWaitBootstrap ( vm: vm)
3624+ try await waitUntil ( " model metadata bootstrap " ) {
3625+ await MainActor . run {
3626+ vm. showsModelPicker
3627+ && vm. modelSelectionID == " anthropic/claude-opus-4-6 "
3628+ && vm. defaultModelLabel == " Default: openai/gpt-4.1-mini "
3629+ }
3630+ }
36183631
36193632 #expect( await MainActor . run { vm. showsModelPicker } )
36203633 #expect( await MainActor . run { vm. modelSelectionID } == " anthropic/claude-opus-4-6 " )
@@ -3748,15 +3761,17 @@ struct ChatViewModelTests {
37483761
37493762 try await loadAndWaitBootstrap ( vm: vm)
37503763
3751- await MainActor . run {
3752- vm . selectModel ( " openai/gpt-5.4 " )
3753- vm . selectModel ( " openai/gpt-5.4-pro " )
3764+ await MainActor . run { vm . selectModel ( " openai/gpt-5.4 " ) }
3765+ try await waitUntil ( " older model patch starts " ) {
3766+ await transport . patchedModels ( ) == [ " openai/gpt-5.4 " ]
37543767 }
3768+ await MainActor . run { vm. selectModel ( " openai/gpt-5.4-pro " ) }
37553769
3756- try await waitUntil ( " two model patches complete " ) {
3757- let patched = await transport. patchedModels ( )
3758- return patched == [ " openai/gpt-5.4 " , " openai/gpt-5.4-pro " ]
3770+ try await waitUntil ( " two model patches issued " ) {
3771+ await transport. patchedModels ( ) == [ " openai/gpt-5.4 " , " openai/gpt-5.4-pro " ]
37593772 }
3773+ await sendUserMessage ( vm, text: " after model patches " )
3774+ _ = try await waitForLastSentRunId ( transport)
37603775
37613776 #expect( await MainActor . run { vm. modelSelectionID } == " openai/gpt-5.4-pro " )
37623777 #expect( await MainActor . run { vm. sessions. first ( where: { $0. key == " main " } ) ? . model } == " gpt-5.4-pro " )
@@ -3848,10 +3863,11 @@ struct ChatViewModelTests {
38483863
38493864 try await loadAndWaitBootstrap ( vm: vm)
38503865
3851- await MainActor . run {
3852- vm . selectModel ( " openai/gpt-5.4 " )
3853- vm . selectModel ( " openai/gpt-5.4-pro " )
3866+ await MainActor . run { vm . selectModel ( " openai/gpt-5.4 " ) }
3867+ try await waitUntil ( " older model patch starts " ) {
3868+ await transport . patchedModels ( ) == [ " openai/gpt-5.4 " ]
38543869 }
3870+ await MainActor . run { vm. selectModel ( " openai/gpt-5.4-pro " ) }
38553871
38563872 try await waitUntil ( " older model completion wins after latest failure " ) {
38573873 await MainActor . run {
@@ -3899,10 +3915,11 @@ struct ChatViewModelTests {
38993915
39003916 try await loadAndWaitBootstrap ( vm: vm)
39013917
3902- await MainActor . run {
3903- vm . selectModel ( " openai/gpt-5.4 " )
3904- vm . selectModel ( " openai/gpt-5.4-pro " )
3918+ await MainActor . run { vm . selectModel ( " openai/gpt-5.4 " ) }
3919+ try await waitUntil ( " earlier model patch starts " ) {
3920+ await transport . patchedModels ( ) == [ " openai/gpt-5.4 " ]
39053921 }
3922+ await MainActor . run { vm. selectModel ( " openai/gpt-5.4-pro " ) }
39063923
39073924 try await waitUntil ( " latest failure restores prior successful model " ) {
39083925 await MainActor . run {
@@ -4971,6 +4988,8 @@ struct ChatViewModelTests {
49714988 historyResponses: [
49724989 historyPayload ( sessionKey: " main " , sessionId: " sess-main " ) ,
49734990 historyPayload ( sessionKey: " other " , sessionId: " sess-other " ) ,
4991+ historyPayload ( sessionKey: " main " , sessionId: " sess-main " ) ,
4992+ historyPayload ( sessionKey: " other " , sessionId: " sess-other " ) ,
49744993 ] ,
49754994 sessionsResponses: [ sessions, sessions] ,
49764995 modelResponses: [ models, models] ,
@@ -4983,14 +5002,25 @@ struct ChatViewModelTests {
49835002 try await loadAndWaitBootstrap ( vm: vm, sessionId: " sess-main " )
49845003
49855004 await MainActor . run { vm. selectModel ( " openai/gpt-5.4 " ) }
5005+ try await waitUntil ( " main session model patch starts " ) {
5006+ await transport. patchedModels ( ) == [ " openai/gpt-5.4 " ]
5007+ }
49865008 await MainActor . run { vm. switchSession ( to: " other " ) }
49875009
49885010 try await waitUntil ( " switched sessions " ) {
49895011 await MainActor . run { vm. sessionKey == " other " && vm. sessionId == " sess-other " }
49905012 }
4991- try await waitUntil ( " late model patch finished " ) {
4992- let patched = await transport. patchedModels ( )
4993- return patched == [ " openai/gpt-5.4 " ]
5013+
5014+ await MainActor . run { vm. switchSession ( to: " main " ) }
5015+ try await waitUntil ( " returned to original session " ) {
5016+ await MainActor . run { vm. sessionKey == " main " && vm. sessionId == " sess-main " }
5017+ }
5018+ await sendUserMessage ( vm, text: " after late model patch " )
5019+ _ = try await waitForLastSentRunId ( transport)
5020+
5021+ await MainActor . run { vm. switchSession ( to: " other " ) }
5022+ try await waitUntil ( " reopened other session " ) {
5023+ await MainActor . run { vm. sessionKey == " other " && vm. sessionId == " sess-other " }
49945024 }
49955025
49965026 #expect( await MainActor . run { vm. modelSelectionID } == OpenClawChatViewModel . defaultModelSelectionID)
@@ -5039,6 +5069,9 @@ struct ChatViewModelTests {
50395069 try await loadAndWaitBootstrap ( vm: vm, sessionId: " sess-main " )
50405070
50415071 await MainActor . run { vm. selectModel ( " openai/gpt-5.4 " ) }
5072+ try await waitUntil ( " main session model patch starts " ) {
5073+ await transport. patchedModels ( ) == [ " openai/gpt-5.4 " ]
5074+ }
50425075 await MainActor . run { vm. switchSession ( to: " other " ) }
50435076 try await waitUntil ( " switched to other session " ) {
50445077 await MainActor . run { vm. sessionKey == " other " && vm. sessionId == " sess-other " }
@@ -5399,10 +5432,11 @@ struct ChatViewModelTests {
53995432
54005433 try await loadAndWaitBootstrap ( vm: vm, sessionId: " sess-main " )
54015434
5402- await MainActor . run {
5403- vm . selectThinkingLevel ( " medium " )
5404- vm . selectThinkingLevel ( " high " )
5435+ await MainActor . run { vm . selectThinkingLevel ( " medium " ) }
5436+ try await waitUntil ( " older thinking patch starts " ) {
5437+ await transport . patchedThinkingLevels ( ) == [ " medium " ]
54055438 }
5439+ await MainActor . run { vm. selectThinkingLevel ( " high " ) }
54065440
54075441 try await waitUntil ( " thinking patch replayed latest selection " ) {
54085442 let patched = await transport. patchedThinkingLevels ( )
0 commit comments