- Add AutomationProperties.AutomationId to all interactive controls
- Add AutomationProperties.Name to 4 icon-only WebChat toolbar buttons
- Localize all SetupWizard strings via LocalizationHelper + .resw
- Replace hardcoded colors with ThemeResource brushes
- Replace raw FontSize with typography styles where appropriate
Prepares codebase for automated UI testing (winapp ui) and
proper Dark/Light/HighContrast theme support.
Add SetupWizardWindow with 3-step onboarding flow:
- Step 1: Paste setup code (auto-decodes URL+token) or manual entry
with connection test that understands pairing-required as success
- Step 2: Optional node mode with device ID and approve instructions
- Step 3: Done - saves settings and reconnects
Integration:
- Replace WelcomeDialog with wizard on first run (empty token)
- Add Setup Guide menu item to tray menu
- Add openclaw://setup deep link
- Guard node service against empty token (no crash)
- Contextual error messages for token mismatch, origin rejection,
rate limiting, and pairing required
Addresses #199
Tighten weak assertions identified during the three-model test-suite audit and enable the Shared integration test lane in CI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace LINQ Select/Any with direct loops in three hot-path sites:
- ShellQuoting.FormatExecCommand: argv.Select(FormatSingleArg) →
preallocated string[] + for loop; avoids enumerator state-machine alloc
- LocalCommandRunner.BuildProcessArgs: request.Args.Select(...) →
preallocated string[] + for loop; same fix; drop unused System.Linq import
- ExecApprovalPolicy.Evaluate: rule.Shells.Any(...) → foreach with early
break; avoids closure + enumerator alloc on every command approval check
All three sites are in the system.run execution path and can be called
repeatedly. The for-loop variants are allocation-free for the iteration
itself and improve throughput under load.
586 Shared + 122 Tray tests pass.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Math.Round with C# default banker's rounding (MidpointRounding.ToEven) can
produce counterintuitive results at display-threshold boundaries:
- 59.5 minutes -> Math.Round(59.5) = 60 -> displayed as '60m ago'
instead of '59m ago' (or the correct transition to '1h ago')
- 47.5 hours -> Math.Round(47.5) = 48 -> displayed as '48h ago'
instead of '47h ago' (near the 48h/days boundary)
Using integer truncation ((int)delta.TotalX) matches the idiomatic
convention for age display: show the floor of the elapsed time, which
is consistent, predictable, and never exceeds the guard condition.
Adds three regression tests covering:
- 59.5-minute boundary (was '60m ago', now '59m ago')
- 47.5-hour boundary (was '48h ago', now '47h ago')
- Exactly 60 seconds (correctly '1m ago')
Test status: Shared.Tests 589 passed, 20 skipped; Tray.Tests 122 passed.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace per-character foreach+switch with System.Buffers.SearchValues<char>,
which uses SSE2/AVX2 vectorized IndexOfAny to detect shell metacharacters
in a single SIMD pass instead of a character-by-character loop.
- Add static SearchValues<char> s_shellMetachars (same 25-char set as former
- Replace NeedsQuoting foreach body with arg.AsSpan().IndexOfAny(s_shellMetachars) >= 0
- Remove private IsShellMetachar helper (no longer needed)
- Add using System.Buffers (SearchValues<T> namespace)
SearchValues<char> is available since .NET 8; this project targets net10.0.
All existing ShellQuotingTests pass unchanged.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
BuildProviderSummary is called on every gateway usage-status update.
The previous implementation allocated a List<string> (up to 3 entries)
and called string.Join, producing a heap-allocated list wrapper plus
join enumeration in addition to the final string.
With at most 2 provider slots + an optional overflow suffix, the
combinations fit into two nullable string variables and a switch
expression, producing only the final string allocation.
Also adds 9 unit tests covering all branch paths:
empty providers, single-provider with usage/error/no-windows,
two providers, three providers (overflow), missing display name,
all-empty providers, and overflow with one valid provider.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SessionInfo.DisplayText: replace List<string> + string.Join with direct
string interpolation. With at most 3 segments (prefix, channel,
activity/status), the allocation-free conditional composition avoids a
List heap alloc plus the Join enumeration on every tray UI update.
GetSessionListInternal: replace new List(_sessions.Values).ToArray()
with a single direct array allocation + Values.CopyTo + Array.Sort with
a static comparer. Drops one heap allocation per session list query.
Both changes are under the covered test surface (586 Shared + 122 Tray).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RichDisplayText is called for every active session on every tray menu
rebuild, tooltip update, and activity stream refresh. The previous
implementation allocated a List<string> (up to 9 entries), populated
it via nine conditional Add() calls, then called string.Join to produce
the final string.
Replace with a stack-discipline string?[9] array accumulator + the
string.Join(string, string?[], int, int) overload that takes an offset
and count, producing a single array allocation with no List wrapper.
Tests (+4):
- RichDisplayText_PreservesPartOrder_ChannelModelCtxThinkVerboseSystemAbortedActivity
(all 9 slots populated; verifies channel<model<ctx<think<verbose<system<aborted<activity)
- RichDisplayText_ActivityTakesPrecedenceOverStatus
- RichDisplayText_IncludesContextSummary_WhenBothTokensSet
- RichDisplayText_OmitsContextSummary_WhenContextTokensZero
Result: 590 Shared passed, 20 skipped; 122 Tray passed (was 586+122)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the 5-slot List<string> accumulator in GatewayNodeInfo.DetailText
with a fixed-size string?[5] array joined via string.Join(sep, array, 0, count).
This avoids a heap allocation on every node-list render, consistent with the
same pattern applied to GatewayUsageInfo.DisplayText and SessionInfo.RichDisplayText.
Also add two missing tests:
- DetailText_IgnoresWhitespaceOnlyModeAndPlatform
- DetailText_IgnoresZeroCounts
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ParseChannelHealth has six distinct status derivation paths based on the
combination of running/configured/linked/probeOk/hasError flags, plus
separate property-parsing for error/authAge/type/IsLinked fields. Only
the 'status field takes priority' path was previously tested.
Adds 13 new tests covering:
Status derivation:
- running=true → 'running'
- lastError present (non-null) → 'error'
- lastError=null excluded from hasError → falls through to 'ready'
- configured && probe.ok=true → 'ready'
- configured && linked=true → 'ready' + IsLinked=true
- nothing set → 'not configured'
- error priority over running flag
Property parsing:
- error, authAge, type fields populated correctly
- linked=false → IsLinked=false
- probe.ok=false + configured=true → 'ready' (second ready clause)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CS8767 (ExecApprovalPolicyTests): ExecTestLogger.Error parameter 'ex'
was declared as non-nullable 'Exception' but IOpenClawLogger.Error
uses 'Exception? ex = null'; align signature to match interface
- CS8604 (OpenClawGatewayClientTests): SetPrivateField takes 'object'
(non-nullable) but SetOperatorDeviceId passes 'string?'; change
parameter to 'object?' to match realistic usage
- xUnit2013 (CapabilityTests): Assert.Equal(0, collection.Count()) is
an anti-pattern flagged by the xUnit analyser; use Assert.Empty
Test status: 586 Shared + 122 Tray pass, 0 warnings
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes#184 by blocking dangerous environment overrides and by re-evaluating nested shell-wrapper payloads and chained commands against the exec approval policy.
This extends the partial env-only approach discussed in PR #186 so the Windows node closes both vectors called out in the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ChannelMap, IntentMap, and CategoryTitles are static read-only lookup
tables that are never mutated after startup. Switching from plain
Dictionary to FrozenDictionary is consistent with the pattern already
used in OpenClawGatewayClient (s_toolKindMap) and App.xaml.cs
(s_notifTypeMap), and gives optimal read-time performance with
guaranteed immutability.
All 606 Shared.Tests (586 pass, 20 skip) and 122 Tray.Tests pass.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Better git checkout (fetch all branches, full history)
- Status messages for workflow progress
- Protected files fallback to issue
- Rainbow footer with workflow run links
- Min integrity: none for full issue/PR access
Adds a reusable string-array parsing helper for node capabilities, keeps system.which whitespace handling intact, and expands Shared coverage for helper parsing and WindowsNodeClient behavior.
Semantically ports the useful pairing-event support from #144 onto current master while preserving the safer hello-ok fallback behavior and keeping the #150 logging cleanup intact.
Enable XPlat Code Coverage and TRX logs for both test projects in the CI test job, and upload the combined TestResults artifact on every run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove verbose debug block in SendNodeConnectAsync that logged sensitive
data (auth token prefix, Ed25519 signature, full connect payload) on every
connect attempt; this ran in all builds, not just debug.
- Eliminate redundant BuildDebugPayload() call (was called twice: once for
logging, once inside SignPayload) — only the SignPayload call remains.
- Remove duplicate JSON serialization: msg was serialized twice (once with
WriteIndented for the debug log, once compact to actually send); collapse
to a single compact serialization and remove the now-unused s_indentedOptions
field.
- Remove HandleResponse debug log that allocated a full payload string just
to truncate it for a Debug()-level message.
No functional change; connection, signing, and registration behaviour are
identical. Test status: 525 Shared passed, 99 Tray passed.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds tests for previously uncovered scenarios:
- HandleResponse with stored device token fires Paired (not Pending)
- HandleResponse with ok:false raises ConnectionStatus.Error
- HandleResponse with missing payload does not throw
- ShortDeviceId truncates to 16-char prefix of FullDeviceId
- IsPaired returns false when no device token stored
- IsPaired returns true after device token stored
- RegisterCapability adds to Capabilities list and Registration
- RegisterCapability deduplicates categories in NodeRegistration
- IsPendingApproval is false initially
- FullDeviceId is non-empty after construction
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GatewayNodeInfo and SessionInfo both implemented identical age-formatting
logic (just now / Xm ago / Xh ago / Xd ago). Consolidate into a single
ModelFormatting.FormatAge(DateTime) helper.
- Add ModelFormatting.FormatAge(DateTime timestampUtc)
- SessionInfo.AgeText now delegates to ModelFormatting.FormatAge
- GatewayNodeInfo.FormatAge now delegates to ModelFormatting.FormatAge
- No behaviour change; all existing tests pass (525 Shared, 99 Tray)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two small coding improvements:
1. SettingsData.FromJson(string? json) — null/empty guard
- Change parameter from non-nullable 'string' to 'string?'
- Return null immediately for null or empty input rather than
forwarding to JsonSerializer.Deserialize which would throw
ArgumentNullException (not caught by the existing JsonException
handler)
- Adds two regression tests: null and empty string both return null
2. GatewayUrlHelper.RemoveUserInfo — static readonly char array
- Replace the inline 'new[] { '/', '?', '#' }' array literal
in RemoveUserInfo with a 'private static readonly' field
- The method is called on every URL normalisation and display
sanitisation; the previous form allocated a new char array on
each call, which is now eliminated
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'CI/CD pipeline completed' was not classified as build because
ClassifyByKeywords only checked for 'ci ' (trailing space), missing
the 'CI/CD' style where a slash follows 'CI'.
Adding the 'ci/' check catches:
- 'CI/CD pipeline completed'
- 'CI/CD workflow triggered'
- Any pattern where 'CI' is immediately followed by a slash
False-positive analysis: common English words containing 'ci' (recipe,
social, special, facial...) are followed by letters, not '/', so the
new check introduces no false matches.
Regression tests: two new InlineData entries in KeywordFallback_BackwardCompatible.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 15 unit tests covering the two public error-message builder methods on
OpenClawGatewayClient that previously had zero test coverage.
BuildMissingScopeFixCommands (10 tests):
- Null/empty/whitespace scope defaults to 'operator.write'
- Specific scope is preserved in output
- Empty _grantedOperatorScopes shows '(none reported by gateway)' placeholder
- Populated scopes are listed correctly
- _operatorDeviceId present vs absent (placeholder shown when missing)
- node.* scopes trigger 'node token' warning (case-insensitive match)
- Operator-only scopes produce no node-token warning
BuildPairingApprovalFixCommands (5 tests):
- _operatorDeviceId used when set
- Falls back to DeviceIdentity.DeviceId when _operatorDeviceId is null
- Empty scopes shows '(none reported by gateway yet)' placeholder
- Populated scopes are listed correctly
- Output always contains pairing approval instructions
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use invariant culture in token and cost display formatting
FormatLargeNumber and CostUsd were using the current thread culture,
producing "2,5M" or "$0,25" on non-English locales instead of the
expected "2.5M" / "$0.25".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add regression for invariant culture numeric formatting
Covers non-English culture formatting so token and cost display stays
as 2.5M / .25 rather than locale-specific 2,5M / ,25.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: AlexAlves87 <alexalves87@github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reuses same cache key as test job. Saves redundant NuGet downloads on build runners.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Test hardening (Codex code-review findings across 8 test files):
- OpenClawGatewayClientTests: constructor tests now verify URL normalization instead of just NotNull; renamed misleading GetSessionList_SortsMainSessionFirst to _ReturnsEmpty_ForFreshClient
- ExecApprovalPolicyTests: ExecApprovalsGet now deserializes and asserts enabled/defaultAction/rules fields; WithoutPolicy test verifies enabled=false
- CapabilityTests: screen list test validates screens[] array fields (name/primary/bounds); camera list validates cameras[] entries (DeviceId/Name/IsDefault); error-path tests assert 'not available' error message
- NodeCapabilitiesTests: ExecuteAsync verifies payload echoes command; CanSet verifies payload value; added Int32 overflow test
- DeviceIdentityTests: signature length tightened from >40 to ==86; public key test decodes base64url and asserts 32-byte Ed25519 length
- GatewayUrlHelperTests: added malformed percent-encoding negative tests; tightened ValidationMessage assertion
- ReadmeValidationTests: validates ALL matching allowCommands JSON blocks, not just first
- ModelsTests: replaced overly permissive OR assertion with platform-aware exact checks
Production fix:
- NodeCapabilities.GetIntArg: catch FormatException on Int32 overflow and return default value instead of throwing
Add missing code style rules that codify the idioms already used in the
codebase, making IDE suggestions consistent for all contributors:
- csharp_style_prefer_range_operator / prefer_index_operator: prefer
range indexers ([..n], [n..], [^1]) over Substring / Length-based
arithmetic (directly reinforces the refactoring in GatewayUrlHelper)
- csharp_style_prefer_switch_expression: prefer switch expressions
over if/else chains (already used throughout Models.cs etc.)
- csharp_style_prefer_pattern_matching / prefer_not_pattern
- csharp_prefer_simple_using_statement: prefer declaration-style
'using var x = ...' without a nested block
- dotnet_style_prefer_simplified_boolean_expressions
- dotnet_style_prefer_inferred_tuple_names /
prefer_inferred_anonymous_type_member_names
No functional changes. 521 shared + 99 tray tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- GatewayUrlHelper: replace Substring calls with C# range indexers ([..n],
[n..]) and use string.Concat(ReadOnlySpan<char>, ...) for RemoveUserInfo
to avoid an intermediate string allocation
- SettingsData.FromJson: narrow bare catch to catch (JsonException) so
unexpected non-JSON errors (e.g. OutOfMemoryException) are not silently
swallowed and masked
521 shared + 99 tray tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When PreferStructuredCategories is false, classification skips structured metadata (Intent, Channel) and goes straight to user rules + keyword fallback.
5 new tests. 521 shared tests pass.
Reimplements #104 on current master.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #105 introduced ToFrozenDictionary but missed the using directive
for the WinUI project. Fixes build break on master.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HashSet lookups with OrdinalIgnoreCase instead of pattern matching on lowered strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>