From a311146f68564ee1f3aaa55513ff84c9cb77df96 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 22 Sep 2026 19:11:46 -0700 Subject: [PATCH 1/3] fix(setup): treat the final wizard SIGTERM restart as success Signed-off-by: Sebastien Tardif --- .../Pages/WizardPage.xaml.cs | 44 ++++++++++- ...upWizardTerminalCompletionContractTests.cs | 79 +++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs index 5d969c890..f8e0b6fc0 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs +++ b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Navigation; using OpenClaw.Connection; +using OpenClaw.SetupEngine; using OpenClaw.Shared; using OpenClaw.SetupEngine.UI; using Windows.ApplicationModel.DataTransfer; @@ -33,6 +34,10 @@ public sealed partial class WizardPage : Page private int _wizardStepCount; private int _progressPolls; private int _totalProgressPolls; + private int _stepIndex; + private int _totalSteps; + private bool _stepHasOptions; + private readonly WizardFinalStepTracker _finalStepTracker = new WizardFinalStepTracker(); private readonly Dictionary _stepVisits = new(StringComparer.OrdinalIgnoreCase); private readonly List _options = []; private volatile bool _expectedTerminalRestart; @@ -126,6 +131,7 @@ private async Task StartWizardAsync(bool clearTranscript = true) _totalProgressPolls = 0; _lastProgressStepId = ""; _stepVisits.Clear(); + _finalStepTracker.ResetForNewSession(); SetBusy("Connecting to gateway..."); var client = await ConnectClientAsync(); if (generation != _operationGeneration) @@ -284,10 +290,17 @@ private async Task ApplyPayloadAsync(JsonElement payload) if (payload.TryGetProperty("done", out var done) && done.ValueKind == JsonValueKind.True) { var error = payload.TryGetProperty("error", out var err) ? err.ToString() : ""; - if (!string.IsNullOrWhiteSpace(error) && !error.Contains("this.prompt is not a function", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(error)) { - ShowError(error); - return; + var decision = SetupWizardRunner.DecideTerminalWizardError( + payloadIsTerminal: true, + error, + _finalStepTracker.AnsweredFinalStep); + if (!decision.MarksWizardCompleted) + { + ShowError(error); + return; + } } await DisconnectAsync(); @@ -308,11 +321,14 @@ private async Task ApplyPayloadAsync(JsonElement payload) var rawType = step.TryGetProperty("type", out var type) ? type.ToString() : "note"; _stepType = string.IsNullOrWhiteSpace(rawType) ? "note" : rawType.Trim().ToLowerInvariant(); var stepIndex = payload.TryGetProperty("stepIndex", out var indexProperty) && indexProperty.TryGetInt32(out var index) ? index : 0; + _stepIndex = stepIndex; + _totalSteps = payload.TryGetProperty("totalSteps", out var totalProperty) && totalProperty.TryGetInt32(out var total) ? total : 0; _sensitive = step.TryGetProperty("sensitive", out var sensitive) && sensitive.ValueKind == JsonValueKind.True; var title = step.TryGetProperty("title", out var titleProp) ? titleProp.ToString() : ""; var message = WizardPayloadHelpers.ExtractStepMessage(step); var initial = step.TryGetProperty("initialValue", out var initialProp) ? initialProp : default; var hasOptions = StepHasOptions(step); + _stepHasOptions = hasOptions; _stepCategory = WizardStepClassifier.Categorize(_stepType, hasOptions); if (_stepCategory == WizardStepCategory.RequiresAnswer @@ -359,6 +375,7 @@ private async Task ApplyPayloadAsync(JsonElement payload) if (generation != _operationGeneration || _errorState || _client == null) return; + _finalStepTracker.RecordProgressAcknowledgement(); payload = await _client.SendWizardRequestAsync( "wizard.next", WizardNextPayload.Acknowledge(_sessionId, _stepId), @@ -703,6 +720,13 @@ private async Task SendOptionValueAsync(string value) SetBusy("Loading..."); ClearConsoleBanner(); var parameters = new { sessionId = _sessionId, answer = new { stepId = _stepId, value } }; + _finalStepTracker.RecordAnsweredStep( + _stepType, + _stepId, + _currentTitle, + _stepHasOptions, + _stepIndex, + _totalSteps); var payload = await _client.SendWizardRequestAsync("wizard.next", parameters, timeoutMs: TimeoutForCurrentStep()); if (generation != _operationGeneration) return; await ApplyPayloadAsync(payload); @@ -723,6 +747,13 @@ private async Task ExpandMoreOptionsAsync(string moreValue, List= 0, $"Could not find marker after index {current}: {marker}"); + current = next; + } + } + + private static string ExtractMethod(string source, string methodName) + { + var signature = source.IndexOf($"async Task {methodName}(", StringComparison.Ordinal); + Assert.True(signature >= 0, $"Could not find method {methodName}."); + var brace = source.IndexOf('{', signature); + Assert.True(brace >= 0, $"Could not find body for method {methodName}."); + + var depth = 0; + for (var index = brace; index < source.Length; index++) + { + if (source[index] == '{') + depth++; + else if (source[index] == '}') + { + depth--; + if (depth == 0) + return source.Substring(signature, index - signature + 1); + } + } + + throw new InvalidOperationException($"Could not extract method {methodName}."); + } + + private static string WizardPageSource() => + File.ReadAllText( + Path.Combine( + RepositoryRoot(), + "src", + "OpenClaw.SetupEngine.UI", + "Pages", + "WizardPage.xaml.cs")); + private static int CountOccurrences(string source, string value) { var count = 0; From 8c18214d82ebce3fe6073143bceb3202f218226d Mon Sep 17 00:00:00 2001 From: Scott Hanselman Date: Mon, 28 Sep 2026 11:21:20 -0700 Subject: [PATCH 2/3] test(setup): harden terminal completion tracking Clear the final-step marker when the UI sends a skip transition and pin the rejected terminal-error branch in the WinUI source contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bd83f93d-0bd4-41ff-9c95-b0a3b02b1e9b --- .../Pages/WizardPage.xaml.cs | 21 ++++++++++++------- ...upWizardTerminalCompletionContractTests.cs | 14 ++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs index f8e0b6fc0..2de79db3e 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs +++ b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs @@ -870,13 +870,20 @@ private async Task SendCurrentAnswerAsync(bool skip) parameters = new { sessionId = _sessionId, answer = new { stepId = _stepId, value = answerValue } }; } - _finalStepTracker.RecordAnsweredStep( - _stepType, - _stepId, - _currentTitle, - _stepHasOptions, - _stepIndex, - _totalSteps); + if (skip) + { + _finalStepTracker.RecordProgressAcknowledgement(); + } + else + { + _finalStepTracker.RecordAnsweredStep( + _stepType, + _stepId, + _currentTitle, + _stepHasOptions, + _stepIndex, + _totalSteps); + } _expectedTerminalRestart = !skip && _hostAccessPlan.CanControlWslGateway && diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs index a71f7c058..22ea6cabe 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs @@ -48,7 +48,12 @@ public void WinUiDonePayload_UsesDecideTerminalWizardError() apply, StringComparison.Ordinal); Assert.Contains("_finalStepTracker.AnsweredFinalStep", apply, StringComparison.Ordinal); - Assert.Contains("decision.MarksWizardCompleted", apply, StringComparison.Ordinal); + Assert.Contains("if (!decision.MarksWizardCompleted)", apply, StringComparison.Ordinal); + AssertInOrder( + apply, + "if (!decision.MarksWizardCompleted)", + "ShowError(error);", + "return;"); Assert.DoesNotContain("this.prompt is not a function", apply, StringComparison.Ordinal); AssertInOrder( apply, @@ -56,6 +61,13 @@ public void WinUiDonePayload_UsesDecideTerminalWizardError() "SendWizardRequestAsync("); Assert.Contains("_finalStepTracker.ResetForNewSession();", start, StringComparison.Ordinal); AssertRecordsAnswerBeforeNext(sendAnswer); + AssertInOrder( + sendAnswer, + "if (skip)", + "_finalStepTracker.RecordProgressAcknowledgement();", + "else", + "_finalStepTracker.RecordAnsweredStep(", + "SendWizardRequestAsync("); AssertRecordsAnswerBeforeNext(sendOption); AssertRecordsAnswerBeforeNext(expandMore); } From 9d52681d65a79f9eed3bc97101ce3cda98285461 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 7 Oct 2026 10:42:56 -0700 Subject: [PATCH 3/3] test: match wizard contract checks to the rebased page StartWizardAsync is now a synchronous wrapper. The tracker reset lives in StartWizardCoreAsync, so the contract test reads that method. The WSL prompt-error check stays. Native sessions still classify SIGTERM through DecideTerminalWizardError. The contract test covers that distinction instead of requiring the compatibility text to vanish. The four SetupWizardTerminalCompletionContractTests pass. Signed-off-by: Sebastien Tardif --- .../SetupWizardTerminalCompletionContractTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs index 22ea6cabe..fcd968733 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupWizardTerminalCompletionContractTests.cs @@ -37,7 +37,7 @@ public void WinUiDonePayload_UsesDecideTerminalWizardError() { var source = WizardPageSource(); var apply = ExtractMethod(source, "ApplyPayloadAsync"); - var start = ExtractMethod(source, "StartWizardAsync"); + var start = ExtractMethod(source, "StartWizardCoreAsync"); var sendAnswer = ExtractMethod(source, "SendCurrentAnswerAsync"); var sendOption = ExtractMethod(source, "SendOptionValueAsync"); var expandMore = ExtractMethod(source, "ExpandMoreOptionsAsync"); @@ -54,7 +54,10 @@ public void WinUiDonePayload_UsesDecideTerminalWizardError() "if (!decision.MarksWizardCompleted)", "ShowError(error);", "return;"); - Assert.DoesNotContain("this.prompt is not a function", apply, StringComparison.Ordinal); + Assert.Contains( + "(_nativeSession is not null || !error.Contains(\"this.prompt is not a function\", StringComparison.OrdinalIgnoreCase))", + apply, + StringComparison.Ordinal); AssertInOrder( apply, "_finalStepTracker.RecordProgressAcknowledgement();",