@@ -57,44 +57,42 @@ public void Fields_project_search_enabled_out_of_editor()
5757 }
5858
5959 [ Fact ]
60- public void Starts_on_summary_screen ( )
60+ public void Starts_on_provider_selection_screen ( )
6161 {
6262 using var vm = new SearchConfigEditorViewModel ( _paths ) ;
6363
64- Assert . Equal ( SearchConfigEditorScreen . Summary , vm . CurrentScreen . Value ) ;
64+ Assert . Equal ( SearchConfigEditorScreen . ProviderSelection , vm . CurrentScreen . Value ) ;
6565 Assert . Equal ( "duckduckgo" , vm . CurrentBackendValue ) ;
66- Assert . Equal ( "No additional setup required." , vm . GetSummaryStateText ( ) ) ;
66+ Assert . Null ( vm . CurrentProviderField ) ;
6767 }
6868
6969 [ Fact ]
70- public void Selecting_brave_keeps_single_screen_matrix_active ( )
70+ public void Selecting_brave_moves_to_entry_state ( )
7171 {
7272 using var vm = new SearchConfigEditorViewModel ( _paths ) ;
7373
7474 vm . BeginBackendSelection ( ) ;
7575 vm . SelectBackendForEditing ( "brave" ) ;
7676
77- Assert . Equal ( SearchConfigEditorScreen . Summary , vm . CurrentScreen . Value ) ;
77+ Assert . Equal ( SearchConfigEditorScreen . Entry , vm . CurrentScreen . Value ) ;
7878 Assert . Equal ( "brave" , vm . CurrentBackendValue ) ;
79- Assert . Equal ( "API key required." , vm . GetSummaryStateText ( ) ) ;
8079 Assert . Equal ( "Search.BraveApiKey" , vm . CurrentProviderField ? . Path ) ;
8180 }
8281
8382 [ Fact ]
84- public void Selecting_duckduckgo_has_no_provider_specific_field ( )
83+ public void Selecting_duckduckgo_enters_zero_config_workflow_state ( )
8584 {
8685 using var vm = new SearchConfigEditorViewModel ( _paths ) ;
8786
8887 vm . BeginBackendSelection ( ) ;
8988 vm . SelectBackendForEditing ( "duckduckgo" ) ;
9089
91- Assert . Equal ( SearchConfigEditorScreen . Summary , vm . CurrentScreen . Value ) ;
90+ Assert . Equal ( SearchConfigEditorScreen . Entry , vm . CurrentScreen . Value ) ;
9291 Assert . Null ( vm . CurrentProviderField ) ;
93- Assert . Equal ( "No additional setup required." , vm . GetSummaryStateText ( ) ) ;
9492 }
9593
9694 [ Fact ]
97- public void Selecting_zero_config_provider_keeps_summary_quiet_when_effective_value_is_unchanged ( )
95+ public void Selecting_zero_config_provider_keeps_workflow_clean_when_effective_value_is_unchanged ( )
9896 {
9997 using var vm = new SearchConfigEditorViewModel ( _paths ) ;
10098
@@ -103,7 +101,7 @@ public void Selecting_zero_config_provider_keeps_summary_quiet_when_effective_va
103101 vm . BeginBackendSelection ( ) ;
104102 vm . SelectBackendForEditing ( "duckduckgo" ) ;
105103
106- Assert . Equal ( SearchConfigEditorScreen . Summary , vm . CurrentScreen . Value ) ;
104+ Assert . Equal ( SearchConfigEditorScreen . Entry , vm . CurrentScreen . Value ) ;
107105 Assert . False ( vm . IsDirty ) ;
108106 Assert . Equal ( "duckduckgo" , vm . CurrentBackendValue ) ;
109107 }
@@ -114,12 +112,13 @@ public async Task Brave_probe_failure_opens_override_dialog_before_save()
114112 using var vm = new SearchConfigEditorViewModel ( _paths , new StubHttpClientFactory ( _ =>
115113 new HttpResponseMessage ( HttpStatusCode . Unauthorized ) ) ) ;
116114
117- vm . SetFieldValue ( "Search.Backend" , "brave" ) ;
118- vm . SetFieldValue ( "Search.BraveApiKey" , "bad-key" ) ;
115+ vm . SelectBackendForEditing ( "brave" ) ;
116+ vm . StageFieldValue ( "Search.BraveApiKey" , "bad-key" ) ;
119117
120- await vm . SaveAsync ( TestContext . Current . CancellationToken ) ;
118+ await vm . SubmitCurrentConfigurationAsync ( TestContext . Current . CancellationToken ) ;
121119
122120 Assert . Equal ( SearchConfigEditorDialog . ProbeWarning , vm . ActiveDialog . Value ) ;
121+ Assert . Equal ( SearchConfigEditorScreen . Entry , vm . CurrentScreen . Value ) ;
123122 Assert . Contains ( "authentication failed" , vm . Status . Value . Text , StringComparison . OrdinalIgnoreCase ) ;
124123 }
125124
@@ -175,6 +174,32 @@ public void Blank_secret_without_existing_value_is_still_structurally_invalid()
175174 Assert . False ( vm . HasPersistedSecret ( "Search.BraveApiKey" ) ) ;
176175 }
177176
177+ [ Fact ]
178+ public void Switching_to_duckduckgo_preserves_inactive_searxng_endpoint ( )
179+ {
180+ File . WriteAllText ( _paths . NetclawConfigPath , """
181+ {
182+ "configVersion": 1,
183+ "Search": {
184+ "Backend": "searxng",
185+ "SearXngEndpoint": "https://search.example.com"
186+ }
187+ }
188+ """ ) ;
189+
190+ using var vm = new SearchConfigEditorViewModel ( _paths ) ;
191+
192+ vm . SelectBackendForEditing ( "duckduckgo" ) ;
193+ vm . SaveWithoutProbeOverride ( ) ;
194+
195+ var reloaded = new SearchConfigEditorViewModel ( _paths ) ;
196+ var config = File . ReadAllText ( _paths . NetclawConfigPath ) ;
197+
198+ Assert . Contains ( "\" Backend\" : \" duckduckgo\" " , config , StringComparison . Ordinal ) ;
199+ Assert . Contains ( "\" SearXngEndpoint\" : \" https://search.example.com\" " , config , StringComparison . Ordinal ) ;
200+ Assert . Equal ( "https://search.example.com" , reloaded . FieldValues [ "Search.SearXngEndpoint" ] . Value ) ;
201+ }
202+
178203 [ Fact ]
179204 public async Task Successful_probe_allows_save_without_dialog ( )
180205 {
@@ -184,13 +209,13 @@ public async Task Successful_probe_allows_save_without_dialog()
184209 Content = new StringContent ( "{\" web\" :{\" results\" :[]}}" , Encoding . UTF8 , "application/json" ) ,
185210 } ) ) ;
186211
187- vm . SetFieldValue ( "Search.Backend" , "brave" ) ;
188- vm . SetFieldValue ( "Search.BraveApiKey" , "good-key" ) ;
212+ vm . SelectBackendForEditing ( "brave" ) ;
213+ vm . StageFieldValue ( "Search.BraveApiKey" , "good-key" ) ;
189214
190- await vm . SaveAsync ( TestContext . Current . CancellationToken ) ;
215+ await vm . SubmitCurrentConfigurationAsync ( TestContext . Current . CancellationToken ) ;
191216
192217 Assert . Equal ( SearchConfigEditorDialog . None , vm . ActiveDialog . Value ) ;
193- Assert . Equal ( SearchConfigEditorScreen . Summary , vm . CurrentScreen . Value ) ;
218+ Assert . Equal ( SearchConfigEditorScreen . Saved , vm . CurrentScreen . Value ) ;
194219 Assert . Contains ( "Saved Search settings" , vm . Status . Value . Text , StringComparison . Ordinal ) ;
195220 }
196221
@@ -221,6 +246,21 @@ public void Preserved_state_supports_in_memory_draft_edits()
221246 Assert . Equal ( "https://search.example.com" , vm . FieldValues [ "Search.SearXngEndpoint" ] . Value ) ;
222247 }
223248
249+ [ Fact ]
250+ public void Invalid_endpoint_submission_keeps_typed_draft_without_mutating_accepted_value ( )
251+ {
252+ using var vm = new SearchConfigEditorViewModel ( _paths ) ;
253+
254+ vm . SelectBackendForEditing ( "searxng" ) ;
255+ var field = Assert . IsType < ProjectedConfigField > ( vm . CurrentProviderField ) ;
256+
257+ var result = vm . CommitField ( field . Path , "search.local" ) ;
258+
259+ Assert . False ( result . Success ) ;
260+ Assert . Equal ( "search.local" , vm . FieldValues [ field . Path ] . Value ) ;
261+ Assert . Equal ( "(not configured)" , vm . GetDisplayValue ( field ) ) ;
262+ }
263+
224264 private sealed class StubHttpClientFactory ( Func < HttpRequestMessage , HttpResponseMessage > handler ) : IHttpClientFactory
225265 {
226266 public HttpClient CreateClient ( string name )
0 commit comments