Skip to content

Commit ed83b95

Browse files
committed
refine(config): make search setup a focused workflow
Keep search backend setup on an explicit path from provider selection through validation and save. Preserve inactive backend settings so switching providers does not silently wipe prior configuration.
1 parent 9b2cb61 commit ed83b95

7 files changed

Lines changed: 516 additions & 352 deletions

File tree

src/Netclaw.Cli.Tests/Tui/Config/SearchConfigEditorViewModelTests.cs

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)