diff --git a/installer.iss b/installer.iss index d4dbd9aaf..91722f0e0 100644 --- a/installer.iss +++ b/installer.iss @@ -1,5 +1,22 @@ ; OpenClaw Companion Inno Setup Script (WinUI version) -#define MyAppName "OpenClaw Companion" +; Pass /DDevBuild=1 to produce a side-by-side dev installer. +#ifdef DevBuild + #define MyAppName "OpenClaw Companion (Dev)" + #define MyAppId "{{M0LTB0T-TRAY-4PP1-DEV}" + #define MyInstallDir "OpenClawTray-Dev" + #define MyMutex "OpenClawTray-Dev" + #define MyProtocol "openclaw-dev" + #define MyOutputSuffix "-Dev" + #define MyAutoStartName "OpenClawTray-Dev" +#else + #define MyAppName "OpenClaw Companion" + #define MyAppId "{{M0LTB0T-TRAY-4PP1-D3N7}" + #define MyInstallDir "OpenClawTray" + #define MyMutex "OpenClawTray" + #define MyProtocol "openclaw" + #define MyOutputSuffix "" + #define MyAutoStartName "OpenClawTray" +#endif #define MyAppPublisher "Scott Hanselman" #define MyAppURL "https://github.com/openclaw/openclaw-windows-node" #define MyAppExeName "OpenClaw.Tray.WinUI.exe" @@ -20,17 +37,17 @@ [Setup] ; Inno requires "{{" to emit a literal opening brace in AppId. ; Do not add a second closing brace here; that creates a malformed uninstall registry key. -AppId={{M0LTB0T-TRAY-4PP1-D3N7} +AppId={#MyAppId} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL=https://github.com/openclaw/openclaw-windows-node/issues AppUpdatesURL=https://github.com/openclaw/openclaw-windows-node/releases -DefaultDirName={localappdata}\OpenClawTray +DefaultDirName={localappdata}\{#MyInstallDir} DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes -OutputBaseFilename=OpenClawCompanion-Setup-{#MyAppArch} +OutputBaseFilename=OpenClawCompanion{#MyOutputSuffix}-Setup-{#MyAppArch} Compression={#MyCompression} SolidCompression={#MySolidCompression} WizardStyle=modern @@ -41,7 +58,7 @@ UninstallDisplayIcon={app}\{#MyAppExeName} ; Mutex name matches App.xaml.cs (`new Mutex(true, "OpenClawTray", …)`). ; Tray and Inno run in the same user session, so the bare name resolves ; against Local\OpenClawTray — no Global\ prefix needed. -AppMutex=OpenClawTray +AppMutex={#MyMutex} #if MyAppArch == "arm64" ArchitecturesInstallIn64BitMode=arm64 ArchitecturesAllowed=arm64 @@ -86,17 +103,17 @@ Source: "{#vcRedist}"; DestDir: "{tmp}"; DestName: "vc_redist.exe"; Flags: delet #endif [Registry] -Root: HKCU; Subkey: "Software\Classes\openclaw"; ValueType: string; ValueName: ""; ValueData: "URL:OpenClaw Protocol"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\openclaw"; ValueType: string; ValueName: "URL Protocol"; ValueData: "" -Root: HKCU; Subkey: "Software\Classes\openclaw\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"",0" -Root: HKCU; Subkey: "Software\Classes\openclaw\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1""" +Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}"; ValueType: string; ValueName: ""; ValueData: "URL:OpenClaw Protocol"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}"; ValueType: string; ValueName: "URL Protocol"; ValueData: "" +Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKCU; Subkey: "Software\Classes\{#MyProtocol}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1""" [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{group}\OpenClaw Gateway Setup"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://setup"; IconFilename: "{app}\{#MyAppExeName}" -Name: "{group}\OpenClaw Companion Settings"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://commandcenter"; IconFilename: "{app}\{#MyAppExeName}" -Name: "{group}\OpenClaw Chat"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://chat"; IconFilename: "{app}\{#MyAppExeName}" -Name: "{group}\Check for Updates"; Filename: "{app}\{#MyAppExeName}"; Parameters: "openclaw://check-updates"; IconFilename: "{app}\{#MyAppExeName}" +Name: "{group}\OpenClaw Gateway Setup"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://setup"; IconFilename: "{app}\{#MyAppExeName}" +Name: "{group}\OpenClaw Companion Settings"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://commandcenter"; IconFilename: "{app}\{#MyAppExeName}" +Name: "{group}\OpenClaw Chat"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://chat"; IconFilename: "{app}\{#MyAppExeName}" +Name: "{group}\Check for Updates"; Filename: "{app}\{#MyAppExeName}"; Parameters: "{#MyProtocol}://check-updates"; IconFilename: "{app}\{#MyAppExeName}" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startupicon diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index af833fb82..d3c3d7db9 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -235,7 +235,7 @@ public IntPtr GetHubWindowHandle() private static readonly string DataPath = DataDirOverride ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "OpenClawTray"); + AppIdentity.IsDev ? "OpenClawTray-Dev" : "OpenClawTray"); private static readonly string DeepLinkPipeName = DeepLinkSecurityPolicy.BuildCurrentUserScopedPipeName(DataPath); // Operator/node identity store. In normal installs this is %APPDATA%\OpenClawTray. @@ -245,7 +245,7 @@ public IntPtr GetHubWindowHandle() ?? Path.Combine( Environment.GetEnvironmentVariable("OPENCLAW_TRAY_APPDATA_DIR") ?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "OpenClawTray"); + AppIdentity.IsDev ? "OpenClawTray-Dev" : "OpenClawTray"); private readonly AppCrashLogger _crashLogger = new(Path.Combine(DataPath, "crash.log")); private static readonly AppRunMarker s_runMarker = new(Path.Combine(DataPath, "run.marker")); @@ -332,6 +332,14 @@ private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExce e.Handled = true; // Try to prevent crash } + /// + /// Returns true if looks like a deep link URI for this build variant. + /// Dev builds accept both openclaw-dev:// and openclaw:// for compatibility. + /// + private static bool IsDeepLinkArg(string arg) => + arg.StartsWith($"{AppIdentity.ProtocolScheme}://", StringComparison.OrdinalIgnoreCase) + || (AppIdentity.IsDev && arg.StartsWith("openclaw://", StringComparison.OrdinalIgnoreCase)); + private void OnDomainUnhandledException(object sender, System.UnhandledExceptionEventArgs e) { _crashLogger.Log("DomainUnhandledException", e.ExceptionObject as Exception); @@ -429,7 +437,7 @@ private async Task OnLaunchedAsync(LaunchActivatedEventArgs args) // (round 2, Scott #5). The suffixed test-isolation variant is // intentionally not covered by AppMutex — production installs only // ever use the unsuffixed name. - var mutexName = "OpenClawTray"; + var mutexName = AppIdentity.MutexBaseName; if (DataDirOverride is not null) { var hash = System.Security.Cryptography.SHA256.HashData( @@ -456,10 +464,10 @@ private async Task OnLaunchedAsync(LaunchActivatedEventArgs args) { // Forward deep link args to running instance (command-line or protocol activation) var deepLink = protocolUri - ?? (_startupArgs.Length > 1 && _startupArgs[1].StartsWith("openclaw://", StringComparison.OrdinalIgnoreCase) + ?? (_startupArgs.Length > 1 && IsDeepLinkArg(_startupArgs[1]) ? _startupArgs[1] : null) ?? (string.Equals(_postSetupLaunch, "chat", StringComparison.OrdinalIgnoreCase) - ? "openclaw://chat" : null); + ? $"{AppIdentity.ProtocolScheme}://chat" : null); if (deepLink != null) { SendDeepLinkToRunningInstance(deepLink); @@ -720,7 +728,7 @@ _dispatcherQueue is null // Process startup deep link (command-line or MSIX protocol activation) var startupDeepLink = _pendingProtocolUri - ?? (_startupArgs.Length > 1 && _startupArgs[1].StartsWith("openclaw://", StringComparison.OrdinalIgnoreCase) + ?? (_startupArgs.Length > 1 && IsDeepLinkArg(_startupArgs[1]) ? _startupArgs[1] : null); if (!setupShownDuringStartup && startupDeepLink != null) { @@ -728,7 +736,7 @@ _dispatcherQueue is null } else if (!setupShownDuringStartup && string.Equals(_postSetupLaunch, "chat", StringComparison.OrdinalIgnoreCase)) { - await HandleDeepLinkAsync("openclaw://chat"); + await HandleDeepLinkAsync($"{AppIdentity.ProtocolScheme}://chat"); } Logger.Info("Application started (WinUI 3)"); diff --git a/src/OpenClaw.Tray.WinUI/AppIdentity.cs b/src/OpenClaw.Tray.WinUI/AppIdentity.cs new file mode 100644 index 000000000..b7a300191 --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/AppIdentity.cs @@ -0,0 +1,52 @@ +namespace OpenClawTray; + +/// +/// Compile-time app identity constants that vary between Dev and Release builds, +/// enabling side-by-side installation of both variants (similar to WinUI Gallery). +/// +internal static class AppIdentity +{ +#if DEV_BUILD + /// Human-visible app name shown in tray tooltips, window titles, and notifications. + public const string DisplayName = "OpenClaw Companion (Dev)"; + + /// Short name used in tray tooltip prefix. + public const string TrayName = "OpenClaw Tray (Dev)"; + + /// MSIX package identity name (must differ from release for side-by-side). + public const string PackageIdentityName = "OpenClaw.Companion.Dev"; + + /// Windows Registry auto-start value name (must differ so both can auto-start). + public const string AutoStartRegistryName = "OpenClawTray-Dev"; + + /// Single-instance mutex base name. + public const string MutexBaseName = "OpenClawTray-Dev"; + + /// Protocol scheme for deep links. + public const string ProtocolScheme = "openclaw-dev"; + + /// Whether this is a development build. + public const bool IsDev = true; +#else + /// Human-visible app name shown in tray tooltips, window titles, and notifications. + public const string DisplayName = "OpenClaw Companion"; + + /// Short name used in tray tooltip prefix. + public const string TrayName = "OpenClaw Tray"; + + /// MSIX package identity name. + public const string PackageIdentityName = "OpenClaw.Companion"; + + /// Windows Registry auto-start value name. + public const string AutoStartRegistryName = "OpenClawTray"; + + /// Single-instance mutex base name. + public const string MutexBaseName = "OpenClawTray"; + + /// Protocol scheme for deep links. + public const string ProtocolScheme = "openclaw"; + + /// Whether this is a development build. + public const bool IsDev = false; +#endif +} diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj index c8f29ad05..fe84563ab 100644 --- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj +++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj @@ -14,6 +14,15 @@ win-x64;win-arm64 + + + true + + + $(DefineConstants);DEV_BUILD + + win-x64 @@ -192,6 +201,64 @@ + + + + + + + + + + + ]*?\\bName\\s*=\\s*[\"'])[^\"']+([\"'])", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + text = nameRegex.Replace(text, "${1}" + IdentityName + "$2", 1); + + // Patch Properties/DisplayName element + var propsDisplayRegex = new System.Text.RegularExpressions.Regex( + "()[^<]+()"); + text = propsDisplayRegex.Replace(text, "${1}" + DisplayName + "$2", 2); + + // Patch VisualElements DisplayName attribute + var visualRegex = new System.Text.RegularExpressions.Regex( + "(DisplayName\\s*=\\s*\")[^\"]+(\")"); + text = visualRegex.Replace(text, "${1}" + DisplayName + "$2"); + + // Patch Protocol Name attribute (openclaw -> openclaw-dev) + var protocolRegex = new System.Text.RegularExpressions.Regex( + "( + + + diff --git a/src/OpenClaw.Tray.WinUI/Services/AutoStartManager.cs b/src/OpenClaw.Tray.WinUI/Services/AutoStartManager.cs index 3cdae99a6..77397bdd7 100644 --- a/src/OpenClaw.Tray.WinUI/Services/AutoStartManager.cs +++ b/src/OpenClaw.Tray.WinUI/Services/AutoStartManager.cs @@ -11,7 +11,7 @@ namespace OpenClawTray.Services; public static class AutoStartManager { private const string RegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; - private const string AppName = "OpenClawTray"; + private static readonly string AppName = AppIdentity.AutoStartRegistryName; public static bool IsAutoStartEnabled() { diff --git a/src/OpenClaw.Tray.WinUI/Services/TrayTooltipBuilder.cs b/src/OpenClaw.Tray.WinUI/Services/TrayTooltipBuilder.cs index 6b82c73e0..a44a65613 100644 --- a/src/OpenClaw.Tray.WinUI/Services/TrayTooltipBuilder.cs +++ b/src/OpenClaw.Tray.WinUI/Services/TrayTooltipBuilder.cs @@ -40,7 +40,7 @@ internal string Build() if (HasRelevantMcpStartupError()) warningCount++; if (_snapshot.Channels.Length == 0 && isHealthy) warningCount++; - var tooltip = $"OpenClaw Tray - {statusText}; " + + var tooltip = $"{AppIdentity.TrayName} - {statusText}; " + $"{topology.DisplayName}; " + $"Channels {channelReady}/{_snapshot.Channels.Length}; " + $"Nodes {nodeOnline}/{nodeTotal}; " + @@ -49,7 +49,7 @@ internal string Build() if (_snapshot.CurrentActivity != null && !string.IsNullOrEmpty(_snapshot.CurrentActivity.DisplayText)) { - tooltip = $"OpenClaw Tray - {_snapshot.CurrentActivity.DisplayText}; {statusText}"; + tooltip = $"{AppIdentity.TrayName} - {_snapshot.CurrentActivity.DisplayText}; {statusText}"; } return TrayTooltipFormatter.FitShellTooltip(tooltip); diff --git a/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs b/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs index d06cb2196..0b3e45eaa 100644 --- a/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs +++ b/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs @@ -350,7 +350,7 @@ public void Setup_IsHostedInTrayAndUsesSelfRestartAfterCompletion() Assert.Contains("\"--post-setup-restart\"", source); Assert.Contains("\"--wait-for-pid\"", source); Assert.Contains("\"--post-setup-launch\"", source); - Assert.Contains("? \"openclaw://chat\" : null", source); + Assert.Contains("$\"{AppIdentity.ProtocolScheme}://chat\"", source); Assert.Contains("WaitForRestartSourceIfRequested(Environment.GetCommandLineArgs())", source); AssertInOrder(source, "WaitForRestartSourceIfRequested(Environment.GetCommandLineArgs())", "_mutex = new Mutex"); Assert.DoesNotContain("ResolveSetupEngineUiPath", source); diff --git a/tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs b/tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs index a777cb4b2..a97e88eba 100644 --- a/tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs +++ b/tests/OpenClaw.Tray.Tests/InstallerIssAssertionTests.cs @@ -16,15 +16,17 @@ public sealed class InstallerIssAssertionTests public void Installer_HasAppMutexMatchingTraySingleInstance() { var iss = File.ReadAllText(Path.Combine(TestRepositoryPaths.GetRepositoryRoot(), "installer.iss")); - Assert.Contains("AppMutex=OpenClawTray", iss); + // Release build uses "OpenClawTray" mutex; dev build uses "OpenClawTray-Dev". + // The installer default (non-DevBuild) must match the release mutex. + Assert.Contains("AppMutex={#MyMutex}", iss); + Assert.Contains(@"#define MyMutex ""OpenClawTray""", iss); Assert.Contains("Inno requires \"{{\" to emit a literal opening brace in AppId.", iss); - Assert.Contains("AppId={{M0LTB0T-TRAY-4PP1-D3N7}", iss); - Assert.DoesNotContain("AppId={{M0LTB0T-TRAY-4PP1-D3N7}}", iss); + Assert.Contains(@"#define MyAppId ""{{M0LTB0T-TRAY-4PP1-D3N7}""", iss); - // The matching tray-side mutex name must be present in App.xaml.cs. + // The matching tray-side mutex name must be present in App.xaml.cs via AppIdentity. var appXamlCs = File.ReadAllText(Path.Combine( TestRepositoryPaths.GetRepositoryRoot(), "src", "OpenClaw.Tray.WinUI", "App.xaml.cs")); - Assert.Contains("var mutexName = \"OpenClawTray\";", appXamlCs); + Assert.Contains("var mutexName = AppIdentity.MutexBaseName;", appXamlCs); } [Fact] @@ -46,12 +48,12 @@ public void Installer_CreatesStartMenuEntrypointsForTraySetupAndSupport() Assert.Contains(@"#define MyAppName ""OpenClaw Companion""", iss); Assert.Contains(@"#define MyCompression ""lzma""", iss); Assert.Contains(@"#define MySolidCompression ""yes""", iss); - Assert.Contains("OutputBaseFilename=OpenClawCompanion-Setup-{#MyAppArch}", iss); + Assert.Contains("OutputBaseFilename=OpenClawCompanion{#MyOutputSuffix}-Setup-{#MyAppArch}", iss); Assert.Contains(@"Name: ""{group}\{#MyAppName}""; Filename: ""{app}\{#MyAppExeName}""", iss); - Assert.Contains(@"Name: ""{group}\OpenClaw Gateway Setup""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://setup""", iss); - Assert.Contains(@"Name: ""{group}\OpenClaw Companion Settings""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://commandcenter""", iss); - Assert.Contains(@"Name: ""{group}\OpenClaw Chat""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://chat""", iss); - Assert.Contains(@"Name: ""{group}\Check for Updates""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""openclaw://check-updates""", iss); + Assert.Contains(@"Name: ""{group}\OpenClaw Gateway Setup""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""{#MyProtocol}://setup""", iss); + Assert.Contains(@"Name: ""{group}\OpenClaw Companion Settings""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""{#MyProtocol}://commandcenter""", iss); + Assert.Contains(@"Name: ""{group}\OpenClaw Chat""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""{#MyProtocol}://chat""", iss); + Assert.Contains(@"Name: ""{group}\Check for Updates""; Filename: ""{app}\{#MyAppExeName}""; Parameters: ""{#MyProtocol}://check-updates""", iss); } [Fact] @@ -112,11 +114,14 @@ public void Installer_RegistersOpenClawProtocol() { var iss = File.ReadAllText(Path.Combine(TestRepositoryPaths.GetRepositoryRoot(), "installer.iss")); - Assert.Contains(@"Subkey: ""Software\Classes\openclaw""", iss); + // Protocol registration uses preprocessor variable {#MyProtocol} + Assert.Contains(@"Subkey: ""Software\Classes\{#MyProtocol}""", iss); Assert.Contains(@"ValueName: ""URL Protocol""", iss); - Assert.Contains(@"Subkey: ""Software\Classes\openclaw\shell\open\command""", iss); + Assert.Contains(@"Subkey: ""Software\Classes\{#MyProtocol}\shell\open\command""", iss); Assert.Contains(@"{app}\{#MyAppExeName}", iss); Assert.Contains(@"""%1""", iss); + // Ensure release default protocol is "openclaw" + Assert.Contains(@"#define MyProtocol ""openclaw""", iss); } [Fact] diff --git a/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj b/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj index 11e2caba0..d1fd00c17 100644 --- a/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj +++ b/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj @@ -82,6 +82,7 @@ + diff --git a/tests/OpenClaw.Tray.Tests/Services/TrayTooltipBuilderTests.cs b/tests/OpenClaw.Tray.Tests/Services/TrayTooltipBuilderTests.cs index 897a29ab9..a1932fc7f 100644 --- a/tests/OpenClaw.Tray.Tests/Services/TrayTooltipBuilderTests.cs +++ b/tests/OpenClaw.Tray.Tests/Services/TrayTooltipBuilderTests.cs @@ -1,5 +1,6 @@ using OpenClaw.Shared; using OpenClaw.Connection; +using OpenClawTray; using OpenClawTray.Helpers; using OpenClawTray.Services; using System; @@ -49,7 +50,7 @@ public void Build_ConnectedWithChannelsAndNodes_ContainsExpectedSegments() var result = new TrayTooltipBuilder(snapshot).Build(); - Assert.Contains("OpenClaw Tray - Connected", result); + Assert.Contains($"{AppIdentity.TrayName} - Connected", result); Assert.Contains("Channels 1/2", result); Assert.Contains("Nodes 1/1", result); Assert.Contains("Warnings 0", result); @@ -108,7 +109,7 @@ public void Build_DegradedOverall_DoesNotReadConnected() var result = new TrayTooltipBuilder(snapshot).Build(); - Assert.Contains("OpenClaw Tray - Degraded", result); + Assert.Contains($"{AppIdentity.TrayName} - Degraded", result); Assert.Contains("Warnings 1", result); } @@ -130,7 +131,7 @@ public void Build_LocalMcpOnly_IsExplicit() var result = new TrayTooltipBuilder(snapshot).Build(); - Assert.Contains("OpenClaw Tray - Local MCP only", result); + Assert.Contains($"{AppIdentity.TrayName} - Local MCP only", result); Assert.Contains("Warnings 1", result); } @@ -153,7 +154,7 @@ public void Build_LocalMcpOnly_DoesNotMaskDegradedGatewayLifecycle() var result = new TrayTooltipBuilder(snapshot).Build(); - Assert.Contains("OpenClaw Tray - Degraded", result); + Assert.Contains($"{AppIdentity.TrayName} - Degraded", result); Assert.DoesNotContain("Local MCP only", result); } @@ -174,7 +175,7 @@ public void Build_McpStartupError_IsExplicit() var result = new TrayTooltipBuilder(snapshot).Build(); - Assert.Contains("OpenClaw Tray - Local MCP failed", result); + Assert.Contains($"{AppIdentity.TrayName} - Local MCP failed", result); Assert.Contains("Warnings 2", result); } @@ -194,7 +195,7 @@ public void Build_StaleMcpStartupError_IsIgnoredWhenMcpDisabled() var result = new TrayTooltipBuilder(snapshot).Build(); - Assert.Contains("OpenClaw Tray - Connected", result); + Assert.Contains($"{AppIdentity.TrayName} - Connected", result); Assert.Contains("Warnings 1", result); Assert.DoesNotContain("Local MCP failed", result); }