diff --git a/docs/spec/SPEC-007-guided-onboarding.md b/docs/spec/SPEC-007-guided-onboarding.md index de987770b..6c527ef96 100644 --- a/docs/spec/SPEC-007-guided-onboarding.md +++ b/docs/spec/SPEC-007-guided-onboarding.md @@ -41,7 +41,7 @@ existing-install re-entry paths. ### Step 5: Final Validation -- run config and health validation +- automatically run config and health validation when the step is reached - show summary with remediation guidance on failure - output next-step commands (`netclaw chat`, `netclaw config`) diff --git a/src/Netclaw.Cli.Tests/Tui/InitWizardPageTests.cs b/src/Netclaw.Cli.Tests/Tui/InitWizardPageTests.cs index 14c5949ab..5becd4a16 100644 --- a/src/Netclaw.Cli.Tests/Tui/InitWizardPageTests.cs +++ b/src/Netclaw.Cli.Tests/Tui/InitWizardPageTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Netclaw.Cli.Provider; using Netclaw.Cli.Tui; +using Netclaw.Cli.Tui.Wizard; using Netclaw.Cli.Tui.Wizard.Steps; using Netclaw.Configuration; using Netclaw.Providers; @@ -197,6 +198,34 @@ public async Task TeamPosture_DefaultFeatures_AllEnabledInWrittenConfig() } } + [Fact] + public async Task EnteringHealthCheckStep_StartsValidationWithoutSecondEnter() + { + var vm = CreateViewModel(); + try + { + AdvanceToStep(vm, WizardStepIds.SecurityPosture); + var postureStep = Assert.IsType(vm.Orchestrator.CurrentStep); + postureStep.SelectedPosture = DeploymentPosture.Personal; + + vm.GoNext(); + + Assert.Equal(WizardStepIds.HealthCheck, vm.Orchestrator.CurrentStep?.StepId); + var completion = vm.HealthCheckStep.HealthCheckCompletion; + Assert.NotNull(completion); + + await completion!.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); + + Assert.True(vm.HealthCheckStep.IsComplete.Value); + Assert.False(vm.HealthCheckStep.IsRunning.Value); + Assert.Contains(vm.HealthCheckStep.Results, r => r.Label == "Configuration written" && r.Passed == true); + } + finally + { + vm.Dispose(); + } + } + // ── Helpers ────────────────────────────────────────────────────────────── /// @@ -230,8 +259,7 @@ private static void AdvanceToStep(InitWizardViewModel vm, string stepId) _ => new InitWizardPage(), _ => { - capturedVm = new InitWizardViewModel( - _paths, _registry, _fakeProbe, _fakeSlackProbe, _fakeDiscordProbe); + capturedVm = CreateViewModel(); return capturedVm; }); }); @@ -243,4 +271,7 @@ private static void AdvanceToStep(InitWizardViewModel vm, string stepId) return (terminal, app, capturedVm!); } + + private InitWizardViewModel CreateViewModel() + => new(_paths, _registry, _fakeProbe, _fakeSlackProbe, _fakeDiscordProbe); } diff --git a/src/Netclaw.Cli/Tui/InitWizardViewModel.cs b/src/Netclaw.Cli/Tui/InitWizardViewModel.cs index 6a8781e43..b0e25220e 100644 --- a/src/Netclaw.Cli/Tui/InitWizardViewModel.cs +++ b/src/Netclaw.Cli/Tui/InitWizardViewModel.cs @@ -158,17 +158,25 @@ public void GoNext() { if (_orchestrator.CurrentStep is HealthCheckStepViewModel healthStep) { - if (!healthStep.IsRunning.Value && !healthStep.IsComplete.Value) - healthStep.StartWithOrchestrator(_orchestrator); + StartHealthCheckIfReady(healthStep); return; } - _orchestrator.GoNext(); + var advanced = _orchestrator.GoNext(); _context.StatusMessage.Value = ""; + if (advanced && _orchestrator.CurrentStep is HealthCheckStepViewModel enteredHealthStep) + StartHealthCheckIfReady(enteredHealthStep); + OnStepContentChanged?.Invoke(); RequestRedraw(); } + private void StartHealthCheckIfReady(HealthCheckStepViewModel healthStep) + { + if (!healthStep.IsRunning.Value && !healthStep.IsComplete.Value) + healthStep.StartWithOrchestrator(_orchestrator); + } + /// /// Go back in the wizard. Returns false if at the beginning (caller should quit). /// diff --git a/src/Netclaw.Cli/Tui/Wizard/Steps/HealthCheckStepView.cs b/src/Netclaw.Cli/Tui/Wizard/Steps/HealthCheckStepView.cs index 58466df75..b0bffe0a3 100644 --- a/src/Netclaw.Cli/Tui/Wizard/Steps/HealthCheckStepView.cs +++ b/src/Netclaw.Cli/Tui/Wizard/Steps/HealthCheckStepView.cs @@ -4,6 +4,7 @@ // // ----------------------------------------------------------------------- using R3; +using Netclaw.Cli.Tui; using Termina.Input; using Termina.Layout; using Termina.Reactive; @@ -29,17 +30,24 @@ public ILayoutNode BuildContent(IWizardStepViewModel stepVm, StepViewCallbacks c foreach (var item in items) { - var (icon, color) = item.Passed switch + if (item.Passed is null) + { + lines.Add(SpinnerViews.Labeled(item.Label, Color.Yellow)); + continue; + } + + var (icon, color) = item.Passed.Value switch { true => ("\u2713", Color.Green), false => ("\u2717", Color.Red), - null => ("\u25cf", Color.Yellow) }; lines.Add(new TextNode($" {icon} {item.Label}").WithForeground(color)); } if (lines.Count == 0) - lines.Add(new TextNode(" Press Enter to run health checks...").WithForeground(Color.BrightBlack)); + lines.Add(vm.IsRunning.Value + ? SpinnerViews.Labeled("Starting health checks...", Color.Yellow) + : new TextNode(" Health checks start automatically...").WithForeground(Color.BrightBlack)); // Post-flight summary: once the checks finish, nudge toward the bootstrap-vs-config // split so the operator knows where ongoing settings live (simplify-netclaw-init §6). diff --git a/tests/smoke/screenshots/config-search-brave-entry.approved.png b/tests/smoke/screenshots/config-search-brave-entry.approved.png index 83871b9de..9b2262995 100644 Binary files a/tests/smoke/screenshots/config-search-brave-entry.approved.png and b/tests/smoke/screenshots/config-search-brave-entry.approved.png differ diff --git a/tests/smoke/screenshots/config-search-saved.approved.png b/tests/smoke/screenshots/config-search-saved.approved.png index b54a63b05..d3d568c63 100644 Binary files a/tests/smoke/screenshots/config-search-saved.approved.png and b/tests/smoke/screenshots/config-search-saved.approved.png differ diff --git a/tests/smoke/screenshots/config-search-selection.approved.png b/tests/smoke/screenshots/config-search-selection.approved.png index eb534c21c..e9b6454b8 100644 Binary files a/tests/smoke/screenshots/config-search-selection.approved.png and b/tests/smoke/screenshots/config-search-selection.approved.png differ diff --git a/tests/smoke/tapes/init-wizard.tape b/tests/smoke/tapes/init-wizard.tape index 60eb7cde0..d17f1e35c 100644 --- a/tests/smoke/tapes/init-wizard.tape +++ b/tests/smoke/tapes/init-wizard.tape @@ -71,10 +71,8 @@ Wait+Screen@10s /Who will interact with this Netclaw instance/ Enter # ─── Step 4 of 4: Health Check ────────────────────────────────────── -Wait+Screen@10s /Press Enter to run health checks/ -Enter - -# Health checks contact ollama, write config, validate, and start the daemon. +# Health checks start automatically when the final step opens. No extra Enter. +# They contact ollama, write config, validate, and start the daemon. # On a clean bootstrap, once validation passes the wizard launches chat # automatically — there is no Enter gate / post-flight summary to confirm. # Anchor on a health item that renders during the run (it stays on screen