Skip to content

Commit 6cb7bd2

Browse files
steipeteranjeshj
andcommitted
fix(setup): extend timeouts for slow wizard steps
Co-authored-by: Ranjesh Jaganathan <ranjeshj@microsoft.com>
1 parent 38fd2cf commit 6cb7bd2

5 files changed

Lines changed: 193 additions & 15 deletions

File tree

src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ private async Task ApplyPayloadAsync(JsonElement payload)
306306
payload = await _client.SendWizardRequestAsync(
307307
"wizard.next",
308308
WizardNextPayload.Acknowledge(_sessionId, _stepId),
309-
timeoutMs: WizardTimeouts.ForStep(title, message));
309+
timeoutMs: WizardTimeouts.ForStep(title, message, _stepId));
310310

311311
if (generation != _operationGeneration || _errorState || _client == null)
312312
return;
@@ -786,7 +786,19 @@ private void UpdateContinueState()
786786
ErrorText.Visibility = Visibility.Collapsed;
787787
}
788788

789-
private int TimeoutForCurrentStep() => WizardTimeouts.ForStep(_currentTitle, _currentMessage);
789+
private int TimeoutForCurrentStep()
790+
{
791+
IReadOnlyCollection<WizardOptionValue>? selectedOptions = null;
792+
if (WizardSelection.RequiresSelection(_stepType))
793+
{
794+
var selectedValues = GetSelectedOptionValues().ToHashSet(StringComparer.Ordinal);
795+
selectedOptions = _options
796+
.Where(option => selectedValues.Contains(option.Value))
797+
.ToArray();
798+
}
799+
800+
return WizardTimeouts.ForStep(_currentTitle, _currentMessage, _stepId, selectedOptions);
801+
}
790802

791803
private void ResetInputs()
792804
{

src/OpenClaw.SetupEngine/SetupWizardRunner.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ async Task<JsonElement> SendWizardNextAsync(object parameters, int timeoutMs)
231231
}
232232
: WizardNextPayload.Acknowledge(sessionId, parsed.StepId);
233233

234-
payload = await SendWizardNextAsync(parameters, TimeoutFor(parsed));
234+
payload = await SendWizardNextAsync(parameters, TimeoutFor(parsed, answerResult.Answer));
235235
}
236236
}
237237
catch (OperationCanceledException)
@@ -478,7 +478,14 @@ private static bool TryGetConfiguredAnswer(WizardPayload step, Dictionary<string
478478
return false;
479479
}
480480

481-
private static int TimeoutFor(WizardPayload step) => WizardTimeouts.ForStep(step.Title, step.Message);
481+
private static int TimeoutFor(WizardPayload step, string? answer = null)
482+
=> WizardTimeouts.ForGatewayStep(
483+
step.Title,
484+
step.Message,
485+
step.StepId,
486+
step.StepType,
487+
step.Options,
488+
answer);
482489

483490
private static bool IsRestartLikeWizardDisconnect(Exception ex)
484491
{

src/OpenClaw.SetupEngine/WizardTimeouts.cs

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ public static class WizardTimeouts
66
/// <summary>Default per-step wizard request timeout.</summary>
77
public const int DefaultTimeoutMs = 30_000;
88

9-
/// <summary>Extended timeout for steps that wait on external auth.</summary>
10-
public const int AuthTimeoutMs = 300_000;
9+
/// <summary>Extended timeout for steps that wait on auth or external setup work.</summary>
10+
public const int SlowStepTimeoutMs = 300_000;
1111

1212
/// <summary>Polling delay for gateway progress/status wizard steps.</summary>
1313
public static readonly TimeSpan ProgressPollDelay = TimeSpan.FromSeconds(1);
@@ -24,16 +24,82 @@ public static class WizardTimeouts
2424
"browser", "authenticate", "verification",
2525
};
2626

27-
/// <summary>Auth-style steps get <see cref="AuthTimeoutMs"/>; others get <see cref="DefaultTimeoutMs"/>.</summary>
28-
public static int ForStep(string? title, string? message)
27+
private static readonly string[] s_slowSetupHints =
2928
{
30-
var text = $"{title} {message}";
31-
foreach (var hint in s_authHints)
29+
"plugin", "install", "download", "teams",
30+
};
31+
32+
/// <summary>
33+
/// Auth and external setup steps get <see cref="SlowStepTimeoutMs"/>; ordinary
34+
/// questions get <see cref="DefaultTimeoutMs"/>. Only selected option metadata
35+
/// is considered so an unselected slow integration does not extend every choice.
36+
/// </summary>
37+
public static int ForStep(
38+
string? title,
39+
string? message,
40+
string? stepId = null,
41+
IReadOnlyCollection<WizardOptionValue>? selectedOptions = null)
42+
{
43+
var promptText = JoinText(title, message);
44+
if (HasAnyHint(promptText, s_authHints))
45+
return SlowStepTimeoutMs;
46+
47+
// With a choice step, only the submitted option proves which operation
48+
// will run. This keeps "skip" and unrelated options on the short path.
49+
var slowText = selectedOptions is null
50+
? JoinText(title, message, stepId)
51+
: JoinOptionText(selectedOptions);
52+
if (HasAnyHint(slowText, s_slowSetupHints))
53+
return SlowStepTimeoutMs;
54+
55+
return DefaultTimeoutMs;
56+
}
57+
58+
internal static int ForGatewayStep(
59+
string? title,
60+
string? message,
61+
string? stepId,
62+
string? stepType,
63+
IReadOnlyList<WizardOptionValue> options,
64+
string? answer = null)
65+
{
66+
var category = WizardStepClassifier.Categorize(stepType, options.Count > 0);
67+
IReadOnlyCollection<WizardOptionValue>? selectedOptions =
68+
category == WizardStepCategory.RequiresAnswer && options.Count > 0 ? [] : null;
69+
70+
if (selectedOptions is not null && !string.IsNullOrWhiteSpace(answer))
3271
{
33-
if (text.Contains(hint, StringComparison.OrdinalIgnoreCase))
34-
return AuthTimeoutMs;
72+
if (string.Equals(stepType, "multiselect", StringComparison.OrdinalIgnoreCase)
73+
&& WizardAnswerBuilder.TryResolveOptions(options, answer, out var multiselectOptions))
74+
{
75+
selectedOptions = multiselectOptions;
76+
}
77+
else if (!string.Equals(stepType, "multiselect", StringComparison.OrdinalIgnoreCase)
78+
&& WizardAnswerBuilder.TryFindOption(options, answer, out var selectedOption))
79+
{
80+
selectedOptions = [selectedOption];
81+
}
3582
}
3683

37-
return DefaultTimeoutMs;
84+
return ForStep(title, message, stepId, selectedOptions);
3885
}
86+
87+
private static string JoinOptionText(IEnumerable<WizardOptionValue> options)
88+
{
89+
var parts = new List<string?>();
90+
foreach (var option in options)
91+
{
92+
parts.Add(option.Value);
93+
parts.Add(option.Label);
94+
parts.Add(option.Hint);
95+
}
96+
97+
return JoinText(parts.ToArray());
98+
}
99+
100+
private static string JoinText(params string?[] parts) =>
101+
string.Join(' ', parts.Where(part => !string.IsNullOrWhiteSpace(part)));
102+
103+
private static bool HasAnyHint(string text, IEnumerable<string> hints) =>
104+
hints.Any(hint => text.Contains(hint, StringComparison.OrdinalIgnoreCase));
39105
}

tests/OpenClaw.SetupEngine.Tests/WizardTimeoutsTests.cs

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Text.Json;
2+
13
namespace OpenClaw.SetupEngine.Tests;
24

35
public class WizardTimeoutsTests
@@ -10,17 +12,45 @@ public class WizardTimeoutsTests
1012
[InlineData("Enter the verification code")]
1113
public void AuthSteps_GetExtendedTimeout(string text)
1214
{
13-
Assert.Equal(WizardTimeouts.AuthTimeoutMs, WizardTimeouts.ForStep(text, string.Empty));
15+
Assert.Equal(WizardTimeouts.SlowStepTimeoutMs, WizardTimeouts.ForStep(text, string.Empty));
1416
}
1517

1618
[Fact]
1719
public void AuthHint_DetectedInMessage()
1820
{
1921
Assert.Equal(
20-
WizardTimeouts.AuthTimeoutMs,
22+
WizardTimeouts.SlowStepTimeoutMs,
2123
WizardTimeouts.ForStep("Setup", "Visit the device authorization page"));
2224
}
2325

26+
[Theory]
27+
[InlineData("Setup", "Downloading plugin package", "")]
28+
[InlineData("Setup", "Installing integration", "")]
29+
[InlineData("Setup", "Working", "install-channel-plugin")]
30+
public void SlowSetupSteps_GetExtendedTimeout(string title, string message, string stepId)
31+
{
32+
Assert.Equal(
33+
WizardTimeouts.SlowStepTimeoutMs,
34+
WizardTimeouts.ForStep(title, message, stepId));
35+
}
36+
37+
[Theory]
38+
[InlineData("opaque-value", "Microsoft Teams", "")]
39+
[InlineData("teams", "Collaboration", "")]
40+
[InlineData("opaque-value", "Collaboration", "Download and configure the plugin")]
41+
public void SelectedSlowOptionMetadata_GetsExtendedTimeout(string value, string label, string hint)
42+
{
43+
var selected = new WizardOptionValue(
44+
value,
45+
label,
46+
hint,
47+
JsonSerializer.SerializeToElement(value));
48+
49+
Assert.Equal(
50+
WizardTimeouts.SlowStepTimeoutMs,
51+
WizardTimeouts.ForStep("Choose an integration", "Pick one.", selectedOptions: [selected]));
52+
}
53+
2454
[Theory]
2555
[InlineData("Choose a connector")]
2656
[InlineData("Enter a friendly name")]
@@ -30,6 +60,60 @@ public void OrdinarySteps_GetDefaultTimeout(string text)
3060
Assert.Equal(WizardTimeouts.DefaultTimeoutMs, WizardTimeouts.ForStep(text, string.Empty));
3161
}
3262

63+
[Fact]
64+
public void OrdinarySelectedOption_KeepsDefaultTimeout()
65+
{
66+
var selected = new WizardOptionValue(
67+
"matrix",
68+
"Matrix",
69+
"Configure an existing connection",
70+
JsonSerializer.SerializeToElement("matrix"));
71+
72+
Assert.Equal(
73+
WizardTimeouts.DefaultTimeoutMs,
74+
WizardTimeouts.ForStep("Choose an integration", "Pick one.", selectedOptions: [selected]));
75+
}
76+
77+
[Theory]
78+
[InlineData("__skip__", "Skip for now", "")]
79+
[InlineData("matrix", "Matrix", "Existing connection")]
80+
[InlineData("browser", "Open in browser", "")]
81+
public void ChannelSelector_NonSlowOption_KeepsDefaultTimeout(string value, string label, string hint)
82+
{
83+
var selected = new WizardOptionValue(
84+
value,
85+
label,
86+
hint,
87+
JsonSerializer.SerializeToElement(value));
88+
89+
Assert.Equal(
90+
WizardTimeouts.DefaultTimeoutMs,
91+
WizardTimeouts.ForStep(
92+
"Choose a channel",
93+
"Select where OpenClaw should send messages.",
94+
"select-channel-quickstart",
95+
[selected]));
96+
}
97+
98+
[Fact]
99+
public void ProgressStep_WithIncidentalOptions_UsesStepMetadata()
100+
{
101+
var incidentalOption = new WizardOptionValue(
102+
"details",
103+
"Show details",
104+
"",
105+
JsonSerializer.SerializeToElement("details"));
106+
107+
Assert.Equal(
108+
WizardTimeouts.SlowStepTimeoutMs,
109+
WizardTimeouts.ForGatewayStep(
110+
"Setup",
111+
"Working",
112+
"install-channel-plugin",
113+
"progress",
114+
[incidentalOption]));
115+
}
116+
33117
[Fact]
34118
public void ProgressPollBudget_AllowsSingleLongSetupStepToUseTotalBudget()
35119
{

tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,15 @@ public void WizardSecondaryButton_DoesNotSkipEntireWizardInErrorState()
597597
Assert.Contains("SendCurrentAnswerAsync(skip: true)", method);
598598
}
599599

600+
[Fact]
601+
public void WizardProgressPolling_UsesStepIdForTimeoutClassification()
602+
{
603+
var root = TestRepositoryPaths.GetRepositoryRoot();
604+
var source = File.ReadAllText(Path.Combine(root, "src", "OpenClaw.SetupEngine.UI", "Pages", "WizardPage.xaml.cs"));
605+
606+
Assert.Contains("WizardTimeouts.ForStep(title, message, _stepId)", source);
607+
}
608+
600609
[Fact]
601610
public void WizardCompletion_AppliesWindowsNodeContextBeforeSummary()
602611
{

0 commit comments

Comments
 (0)