diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 41c88c773..41a210038 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -398,10 +398,18 @@ A positive result enables and initially selects the first **Install a local nati gateway** card with the accent highlight and **Recommended** badge. A negative capability result leaves that recommended choice visible but disabled after WSL and **Connect to an existing gateway**, with Windows Update guidance directly below the -choice list; reopening the page rechecks support. The pinned SDK documents -Insider build **26340.9212** as its baseline. This is update guidance, not a -hardcoded admission floor or a promise that a particular feature is enabled. -The native boolean cannot distinguish every OS API failure from missing support. +choice list; reopening the page rechecks support. The guidance tells users to +update to a Windows version that supports the native Gateway. The guidance says +support is coming soon to all devices and, until broad availability, directs +users to join the Windows Insider Program, select the Beta or Experimental +channel, and choose Windows 11, version 26H1 under **Advanced options** during +enrollment. It warns that returning to Windows 11, version 25H2/26H2 requires +a clean reinstall. After enrolling, users return to +**Settings > Windows Update** and check for updates; the update may take a few +minutes to appear. The guidance notes that feature availability can vary by +device and tells users to reopen the page after updating to check support again. +The live probe remains authoritative. The native boolean cannot distinguish +every OS API failure from missing support. Missing executables, malformed results and probe errors instead offer retry or Companion repair, not an assertion that Windows must be updated. diff --git a/docs/ONBOARDING_WIZARD.md b/docs/ONBOARDING_WIZARD.md index b8307a61b..409ae396a 100644 --- a/docs/ONBOARDING_WIZARD.md +++ b/docs/ONBOARDING_WIZARD.md @@ -805,9 +805,20 @@ outside the disabled native option so the Windows Update action stays usable. The support card is hidden when native capability is available. When capability is unavailable, **Open Windows Update** opens -`ms-settings:windowsupdate`. Guidance names Insider build **26340.9212**, the -baseline documented by the pinned MXC SDK, or a newer supported build. Reopening -the page reruns the check; the Welcome page has no **Check again** button. +`ms-settings:windowsupdate`. Guidance tells users to update to a Windows version +that supports the native Gateway. It says support is coming soon to all devices +and, until broad availability, directs users to join the Windows Insider +Program, select the Beta or Experimental channel, and choose Windows 11, version +26H1 under **Advanced options** during enrollment. The guidance warns that +returning to Windows 11, version 25H2/26H2 requires a clean reinstall, as described +in [Microsoft's Windows core version guidance](https://learn.microsoft.com/en-us/windows-insider/flighting#understanding-windows-core-versions). +After enrolling, users return to **Settings > Windows Update**, check for +updates, and may need to wait a few minutes for the update to appear. The copy +notes that feature availability can vary by device and tells users to reopen +the page after updating to check support again. The Welcome page has no +**Check again** button. `NativeGatewayEligibilityTextTests` exercises the production +rich-text renderer, complete automation name, and repeated rich/plain transitions +on real WinUI `TextBlock` controls; the gallery also uses that renderer. Feature rollout varies; a negative native API result is not proof that the build alone is the cause. Probe failures or missing/invalid metadata offer retry/Companion repair rather than misleading update advice. Windows Server diff --git a/docs/SETUP_ENGINE_REDESIGN.md b/docs/SETUP_ENGINE_REDESIGN.md index b5c4cdd5a..31d13e5f4 100644 --- a/docs/SETUP_ENGINE_REDESIGN.md +++ b/docs/SETUP_ENGINE_REDESIGN.md @@ -168,9 +168,17 @@ substitute. The [Welcome recommendation policy](ONBOARDING_WIZARD.md#welcome) now checks `wxc-exec --probe` session capability before recommending the existing native Gateway. `NativeGatewaySetupEligibility` owns admission and selection policy. -Unavailable capability offers Windows Update with the pinned SDK's Insider -baseline (26340.9212); failed probes offer retry/repair instead. WSL is always -visible as the second option after native, with existing-gateway connection third. +Unavailable capability offers Windows Update guidance. It says native Gateway +support is coming soon to all devices and, until broad availability, directs +users to join the Windows Insider Program, select the Beta or Experimental +channel, and choose Windows 11, version 26H1 under **Advanced options** during +enrollment. It warns that returning to Windows 11, version 25H2/26H2 requires +a clean reinstall. After enrolling, users return to +**Settings > Windows Update** and check for updates. The copy notes that feature +availability can vary by device and tells users to reopen the page after +updating to check support again. The live probe remains +authoritative; failed probes offer retry/repair instead. WSL is always visible +as the second option after native, with existing-gateway connection third. Explicit WSL and existing-gateway choices survive late native probe results. Welcome has no manual recheck button; reopening the page checks again. The separate isolation warning/checkbox is removed by the 2026-09-18 product decision; general security consent remains. This is not diff --git a/src/OpenClaw.SetupEngine.UI/NativeGatewayEligibilityText.cs b/src/OpenClaw.SetupEngine.UI/NativeGatewayEligibilityText.cs index 46d6169f6..874db8097 100644 --- a/src/OpenClaw.SetupEngine.UI/NativeGatewayEligibilityText.cs +++ b/src/OpenClaw.SetupEngine.UI/NativeGatewayEligibilityText.cs @@ -1,14 +1,77 @@ +using Microsoft.UI.Text; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Documents; + namespace OpenClaw.SetupEngine.UI; internal static class NativeGatewayEligibilityText { + private static readonly string[] UnavailableEmphasisKeys = + [ + "Onboarding_Native_SupportUnavailableLead", + "Onboarding_Native_SupportUnavailableComingSoon", + "Onboarding_Native_SupportUnavailableInsiderProgram", + "Onboarding_Native_SupportUnavailableChannels", + "Onboarding_Native_SupportUnavailableWindowsVersion", + "Onboarding_Native_SupportUnavailableWindowsUpdate", + ]; + internal static string Get(NativeGatewayEligibility eligibility) => eligibility switch { NativeGatewayEligibility.Available => SetupLocalization.GetString("Onboarding_Native_SupportAvailable"), - NativeGatewayEligibility.CapabilityUnavailable => SetupLocalization.Format( - "Onboarding_Native_SupportUnavailable", NativeGatewaySetupEligibility.InsiderBuild), + NativeGatewayEligibility.CapabilityUnavailable => + SetupLocalization.GetString("Onboarding_Native_SupportUnavailable"), NativeGatewayEligibility.UnsupportedPlatform => SetupLocalization.GetString("Onboarding_Native_UnsupportedPlatform"), NativeGatewayEligibility.CheckFailed => SetupLocalization.GetString("Onboarding_Native_SupportCheckFailed"), _ => throw new ArgumentOutOfRangeException(nameof(eligibility)), }; + + internal static void Apply(TextBlock target, NativeGatewayEligibility eligibility) + { + string text = Get(eligibility); + + if (eligibility != NativeGatewayEligibility.CapabilityUnavailable || + !TryGetEmphasisRanges(text, out var ranges)) + { + ApplyPlain(target, text); + return; + } + + target.Inlines.Clear(); + AutomationProperties.SetName(target, text); + int position = 0; + foreach (var range in ranges) + { + if (range.Start > position) + target.Inlines.Add(new Run { Text = text[position..range.Start] }); + + target.Inlines.Add(new Run + { + Text = text.Substring(range.Start, range.Length), + FontWeight = FontWeights.SemiBold, + }); + position = range.Start + range.Length; + } + + if (position < text.Length) + target.Inlines.Add(new Run { Text = text[position..] }); + } + + internal static void ApplyPlain(TextBlock target, string text) + { + target.Inlines.Clear(); + target.ClearValue(AutomationProperties.NameProperty); + target.Text = text; + } + + private static bool TryGetEmphasisRanges( + string text, + out List<(int Start, int Length)> ranges) + { + return NativeGatewayEmphasisRanges.TryGet( + text, + UnavailableEmphasisKeys.Select(SetupLocalization.GetString), + out ranges); + } } diff --git a/src/OpenClaw.SetupEngine.UI/NativeGatewayEmphasisRanges.cs b/src/OpenClaw.SetupEngine.UI/NativeGatewayEmphasisRanges.cs new file mode 100644 index 000000000..df902ec5c --- /dev/null +++ b/src/OpenClaw.SetupEngine.UI/NativeGatewayEmphasisRanges.cs @@ -0,0 +1,28 @@ +namespace OpenClaw.SetupEngine.UI; + +internal static class NativeGatewayEmphasisRanges +{ + internal static bool TryGet( + string text, + IEnumerable emphases, + out List<(int Start, int Length)> ranges) + { + ranges = []; + foreach (string emphasis in emphases) + { + if (string.IsNullOrWhiteSpace(emphasis)) + return false; + + int start = text.IndexOf(emphasis, StringComparison.Ordinal); + if (start < 0) + return false; + + ranges.Add((start, emphasis.Length)); + } + + ranges.Sort((left, right) => left.Start.CompareTo(right.Start)); + return ranges.Zip(ranges.Skip(1), (left, right) => + left.Start + left.Length <= right.Start) + .All(nonOverlapping => nonOverlapping); + } +} diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs index ac10eb584..8eb3e850a 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs +++ b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs @@ -95,7 +95,9 @@ private async Task CheckNativeSupportAsync() WindowsUpdateButton.Visibility = Visibility.Collapsed; NativeCheckProgress.IsActive = true; NativeCheckProgress.Visibility = Visibility.Visible; - NativeSupportStatus.Text = SetupLocalization.GetString("Onboarding_Native_CheckingSupport"); + NativeGatewayEligibilityText.ApplyPlain( + NativeSupportStatus, + SetupLocalization.GetString("Onboarding_Native_CheckingSupport")); ApplySelection(); NativeGatewayEligibility eligibility; @@ -121,7 +123,10 @@ private async Task CheckNativeSupportAsync() NativeSupportStatusPanel.Visibility = available ? Visibility.Collapsed : Visibility.Visible; WindowsUpdateButton.Visibility = eligibility == NativeGatewayEligibility.CapabilityUnavailable ? Visibility.Visible : Visibility.Collapsed; - NativeSupportStatus.Text = available ? "" : NativeGatewayEligibilityText.Get(eligibility); + if (available) + NativeGatewayEligibilityText.ApplyPlain(NativeSupportStatus, ""); + else + NativeGatewayEligibilityText.Apply(NativeSupportStatus, eligibility); NativeCheckProgress.IsActive = false; NativeCheckProgress.Visibility = Visibility.Collapsed; SetChoice(NativeGatewaySetupEligibility.ResolveSelection(_selectedChoice, eligibility)); @@ -185,7 +190,9 @@ private void ShowWindowsUpdateError() { NativeSupportCard.Visibility = Visibility.Visible; NativeSupportStatusPanel.Visibility = Visibility.Visible; - NativeSupportStatus.Text = SetupLocalization.GetString("Onboarding_Native_UpdateLaunchFailed"); + NativeGatewayEligibilityText.ApplyPlain( + NativeSupportStatus, + SetupLocalization.GetString("Onboarding_Native_UpdateLaunchFailed")); } } diff --git a/src/OpenClaw.SetupEngine/NativeGatewaySetupEligibility.cs b/src/OpenClaw.SetupEngine/NativeGatewaySetupEligibility.cs index 0b1224da9..d1d8b5dfc 100644 --- a/src/OpenClaw.SetupEngine/NativeGatewaySetupEligibility.cs +++ b/src/OpenClaw.SetupEngine/NativeGatewaySetupEligibility.cs @@ -9,10 +9,6 @@ public enum GatewaySetupChoice { Native, Existing, Wsl } /// Onboarding admission only. Capability does not imply that the Gateway runs in an MXC session. public static class NativeGatewaySetupEligibility { - // Session baseline documented by the pinned @microsoft/mxc-sdk README. - // The live probe, not a build-number comparison, remains authoritative. - public const string InsiderBuild = "26340.9212"; - public static NativeGatewayEligibility Probe(IOpenClawLogger? logger = null) => Evaluate(MxcAvailability.Probe(logger)); diff --git a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw index 08e8e3ded..a517710ba 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw @@ -7777,7 +7777,17 @@ Your settings, gateway, files, and models stay where they are. Install a local WSL gateway Checking native gateway support... Native gateway support is available on this device. - Native gateway support is unavailable. Install Windows Insider build {0} or a newer supported build in Windows Update, then reopen this page. Feature availability can vary by device. + To use the native gateway, update your device to a Windows version that supports it. + +Native gateway support is coming soon to all devices. Until it is broadly available, join the Windows Insider Program, select the Beta or Experimental channel, and choose Windows 11, version 26H1 under Advanced options during enrollment. Returning to Windows 11, version 25H2/26H2 requires a clean reinstall. + +After enrolling, return to Settings > Windows Update and check for updates. It may take a few minutes for the update to appear. Feature availability can vary by device. After updating, reopen this page to check support again. + To use the native gateway, update your device to a Windows version that supports it. + coming soon to all devices. + Windows Insider Program + Beta or Experimental + Windows 11, version 26H1 + Settings > Windows Update Could not verify native gateway support. Reopen this page to retry. If the check keeps failing, update or repair Companion. Native gateway setup requires a supported Windows client. Windows Server is not supported. Open Windows Update diff --git a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw index 402fc35c9..4e7bdd309 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw @@ -7713,7 +7713,17 @@ Vos paramètres, votre passerelle, vos fichiers et vos modèles restent en place Installer un Gateway WSL local Vérification de la prise en charge du Gateway natif... Le Gateway natif est pris en charge sur cet appareil. - Le Gateway natif n'est pas pris en charge. Installez la build Windows Insider {0} ou une build plus récente prise en charge via Windows Update, puis rouvrez cette page. La disponibilité de cette fonctionnalité peut varier selon l'appareil. + Pour utiliser le Gateway natif, mettez votre appareil à jour vers une version de Windows compatible. + +La prise en charge du Gateway natif sera bientôt disponible sur tous les appareils. En attendant sa disponibilité générale, rejoignez le programme Windows Insider, sélectionnez le canal Bêta ou Expérimental, puis choisissez Windows 11 version 26H1 dans les Options avancées lors de l'inscription. Le retour à Windows 11 version 25H2/26H2 nécessite une réinstallation complète. + +Après votre inscription, revenez dans Paramètres > Windows Update et recherchez les mises à jour. L'affichage de la mise à jour peut prendre quelques minutes. La disponibilité de cette fonctionnalité peut varier selon l'appareil. Après la mise à jour, rouvrez cette page pour vérifier à nouveau la prise en charge. + Pour utiliser le Gateway natif, mettez votre appareil à jour vers une version de Windows compatible. + sera bientôt disponible sur tous les appareils. + programme Windows Insider + Bêta ou Expérimental + Windows 11 version 26H1 + Paramètres > Windows Update Impossible de vérifier la prise en charge du Gateway natif. Rouvrez cette page pour réessayer. Si la vérification échoue à nouveau, mettez à jour ou réparez Companion. La configuration du Gateway natif nécessite une version cliente de Windows prise en charge. Windows Server n'est pas pris en charge. Ouvrir Windows Update diff --git a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw index 65163da30..e30144c4c 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw @@ -7714,7 +7714,17 @@ Uw instellingen, gateway, bestanden en modellen blijven op hun plaats. Een lokale WSL Gateway installeren Ondersteuning voor de native Gateway controleren... De native Gateway wordt ondersteund op dit apparaat. - De native Gateway wordt niet ondersteund. Installeer Windows Insider-build {0} of een nieuwere ondersteunde build via Windows Update en open deze pagina opnieuw. De beschikbaarheid van deze functie kan per apparaat verschillen. + Als u de native Gateway wilt gebruiken, werkt u uw apparaat bij naar een Windows-versie die dit ondersteunt. + +Ondersteuning voor de native Gateway is binnenkort beschikbaar op alle apparaten. Totdat deze algemeen beschikbaar is, neemt u deel aan het Windows Insider-programma, selecteert u het Beta- of Experimental-kanaal en kiest u tijdens de inschrijving Windows 11, versie 26H1 onder Geavanceerde opties. Terugkeren naar Windows 11, versie 25H2/26H2 vereist een schone herinstallatie. + +Ga na inschrijving terug naar Instellingen > Windows Update en controleer op updates. Het kan enkele minuten duren voordat de update wordt weergegeven. De beschikbaarheid van deze functie kan per apparaat verschillen. Open deze pagina na de update opnieuw om de ondersteuning nogmaals te controleren. + Als u de native Gateway wilt gebruiken, werkt u uw apparaat bij naar een Windows-versie die dit ondersteunt. + is binnenkort beschikbaar op alle apparaten. + Windows Insider-programma + Beta- of Experimental + Windows 11, versie 26H1 + Instellingen > Windows Update De ondersteuning voor de native Gateway kon niet worden gecontroleerd. Open deze pagina opnieuw om het nogmaals te proberen. Als de controle blijft mislukken, werk Companion dan bij of herstel de app. Voor het instellen van de native Gateway is een ondersteunde Windows-client vereist. Windows Server wordt niet ondersteund. Windows Update openen diff --git a/src/OpenClaw.Tray.WinUI/Strings/pt-br/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/pt-br/Resources.resw index 49430bd91..9dc90974e 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/pt-br/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/pt-br/Resources.resw @@ -7767,7 +7767,17 @@ Suas configurações, gateway, arquivos e modelos permanecem onde estão.Instalar um Gateway WSL local Verificando o suporte ao Gateway nativo... O suporte ao Gateway nativo está disponível neste dispositivo. - O suporte ao Gateway nativo não está disponível. Instale a compilação {0} do Windows Insider ou uma compilação mais recente compatível pelo Windows Update e reabra esta página. A disponibilidade do recurso pode variar conforme o dispositivo. + Para usar o Gateway nativo, atualize seu dispositivo para uma versão do Windows compatível. + +O suporte ao Gateway nativo estará disponível em breve para todos os dispositivos. Até que esteja amplamente disponível, ingresse no Programa Windows Insider, selecione o canal Beta ou Experimental e escolha Windows 11, versão 26H1 em Opções avançadas durante a inscrição. Voltar ao Windows 11, versão 25H2/26H2 exige uma reinstalação limpa. + +Após a inscrição, volte para Configurações > Windows Update e verifique se há atualizações. Pode levar alguns minutos para a atualização aparecer. A disponibilidade do recurso pode variar conforme o dispositivo. Após a atualização, reabra esta página para verificar o suporte novamente. + Para usar o Gateway nativo, atualize seu dispositivo para uma versão do Windows compatível. + estará disponível em breve para todos os dispositivos. + Programa Windows Insider + Beta ou Experimental + Windows 11, versão 26H1 + Configurações > Windows Update Não foi possível verificar o suporte ao Gateway nativo. Reabra esta página para tentar novamente. Se a verificação continuar falhando, atualize ou repare o Companion. A configuração do Gateway nativo requer uma versão cliente do Windows compatível. O Windows Server não é compatível. Abrir Windows Update diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw index 8140385cc..a2da95731 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw @@ -7713,7 +7713,17 @@ 安装本地 WSL Gateway 正在检查原生 Gateway 支持情况... 此设备支持原生 Gateway。 - 目前不支持原生 Gateway。请通过 Windows 更新安装 Windows Insider 内部版本 {0} 或更新的受支持版本,然后重新打开此页面。功能可用性可能因设备而异。 + 若要使用原生 Gateway,请将设备更新到支持该功能的 Windows 版本。 + +原生 Gateway 支持即将在所有设备上推出。在全面推出之前,请加入 Windows 预览体验计划,选择 Beta 或 Experimental 渠道,并在加入时通过“高级选项”选择 Windows 11 版本 26H1。若要返回 Windows 11 版本 25H2/26H2,必须全新安装系统。 + +加入后,请返回“设置 > Windows 更新”并检查更新。更新可能需要几分钟才会显示。功能可用性可能因设备而异。更新后,请重新打开此页面以再次检查支持情况。 + 若要使用原生 Gateway,请将设备更新到支持该功能的 Windows 版本。 + 即将在所有设备上推出。 + Windows 预览体验计划 + Beta 或 Experimental + Windows 11 版本 26H1 + 设置 > Windows 更新 无法验证原生 Gateway 支持情况。请重新打开此页面以重试。如果检查持续失败,请更新或修复 Companion。 原生 Gateway 设置需要受支持的 Windows 客户端系统。不支持 Windows Server。 打开 Windows 更新 diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw index 2d0a4796b..db6beb159 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw @@ -7713,7 +7713,17 @@ 安裝本機 WSL Gateway 正在檢查原生 Gateway 支援情況... 此裝置支援原生 Gateway。 - 目前不支援原生 Gateway。請透過 Windows Update 安裝 Windows Insider 組建 {0} 或更新的受支援組建,然後重新開啟此頁面。功能可用性可能因裝置而異。 + 若要使用原生 Gateway,請將裝置更新至支援此功能的 Windows 版本。 + +原生 Gateway 支援即將在所有裝置上推出。在全面推出之前,請加入 Windows 測試人員計畫,選取 Beta 或 Experimental 通道,並在加入時透過「進階選項」選取 Windows 11 版本 26H1。若要返回 Windows 11 版本 25H2/26H2,必須全新安裝系統。 + +加入後,請返回「設定 > Windows Update」並檢查更新。更新可能需要幾分鐘才會顯示。功能可用性可能因裝置而異。更新後,請重新開啟此頁面以再次檢查支援情況。 + 若要使用原生 Gateway,請將裝置更新至支援此功能的 Windows 版本。 + 即將在所有裝置上推出。 + Windows 測試人員計畫 + Beta 或 Experimental + Windows 11 版本 26H1 + 設定 > Windows Update 無法驗證原生 Gateway 支援情況。請重新開啟此頁面以重試。如果檢查持續失敗,請更新或修復 Companion。 原生 Gateway 設定需要受支援的 Windows 用戶端系統。不支援 Windows Server。 開啟 Windows Update diff --git a/tests/OpenClaw.Tray.Tests/NativeGatewayEmphasisRangesTests.cs b/tests/OpenClaw.Tray.Tests/NativeGatewayEmphasisRangesTests.cs new file mode 100644 index 000000000..23918509f --- /dev/null +++ b/tests/OpenClaw.Tray.Tests/NativeGatewayEmphasisRangesTests.cs @@ -0,0 +1,32 @@ +using OpenClaw.SetupEngine.UI; + +namespace OpenClaw.Tray.Tests; + +public sealed class NativeGatewayEmphasisRangesTests +{ + [Fact] + public void TryGet_SortsNonOverlappingRanges() + { + Assert.True(NativeGatewayEmphasisRanges.TryGet( + "first middle last", + ["last", "first", "middle"], + out var ranges)); + + Assert.Equal([(0, 5), (6, 6), (13, 4)], ranges); + } + + [Theory] + [MemberData(nameof(InvalidEmphases))] + public void TryGet_RejectsInvalidEmphases(string text, string[] emphases) + { + Assert.False(NativeGatewayEmphasisRanges.TryGet(text, emphases, out _)); + } + + public static TheoryData InvalidEmphases => new() + { + { "complete guidance", [""] }, + { "complete guidance", ["missing"] }, + { "complete guidance", ["complete", "complete guidance"] }, + { "complete guidance", ["complete", "complete"] }, + }; +} diff --git a/tests/OpenClaw.Tray.Tests/NativeGatewaySetupUxContractTests.cs b/tests/OpenClaw.Tray.Tests/NativeGatewaySetupUxContractTests.cs index 889181365..dd537b51d 100644 --- a/tests/OpenClaw.Tray.Tests/NativeGatewaySetupUxContractTests.cs +++ b/tests/OpenClaw.Tray.Tests/NativeGatewaySetupUxContractTests.cs @@ -351,8 +351,79 @@ public void Welcome_NativeBadgeSharesHeadingWithoutRedundantSuccessCopy() Assert.Contains("VisualStateManager.GoToState(this, \"WslRecommendedState\", false)", source); Assert.Contains("available ? \"NativeRecommendedState\" : \"WslRecommendedState\"", source); Assert.Contains("NativeSupportStatusPanel.Visibility = available ? Visibility.Collapsed : Visibility.Visible", source); - Assert.Contains("NativeSupportStatus.Text = available ? \"\" : NativeGatewayEligibilityText.Get(eligibility)", source); + Assert.Contains("NativeGatewayEligibilityText.Apply(NativeSupportStatus, eligibility)", source); + Assert.Equal(3, source.Split("NativeGatewayEligibilityText.ApplyPlain(", StringSplitOptions.None).Length - 1); Assert.Contains("CreatePeerForElement(NativeSupportStatus)", source); + var eligibilityText = File.ReadAllText(Path.Combine( + TestRepositoryPaths.GetRepositoryRoot(), + "src", "OpenClaw.SetupEngine.UI", "NativeGatewayEligibilityText.cs")); + Assert.Contains("AutomationProperties.SetName(target, text)", eligibilityText); + Assert.Contains("target.ClearValue(AutomationProperties.NameProperty)", eligibilityText); + } + + [Fact] + public void Welcome_NativeUnavailableEmphasisTokensRemainLocalizedSubstrings() + { + var stringsDir = Path.Combine( + TestRepositoryPaths.GetRepositoryRoot(), "src", "OpenClaw.Tray.WinUI", "Strings"); + + foreach (var directory in Directory.GetDirectories(stringsDir)) + { + var resources = XDocument.Load(Path.Combine(directory, "Resources.resw")) + .Descendants("data") + .ToDictionary( + element => (string)element.Attribute("name")!, + element => element.Element("value")?.Value ?? ""); + string guidance = resources["Onboarding_Native_SupportUnavailable"]; + var ranges = new List<(int Start, int Length)>(); + + foreach (var suffix in new[] + { + "Lead", + "ComingSoon", + "InsiderProgram", + "Channels", + "WindowsVersion", + "WindowsUpdate", + }) + { + string emphasis = resources[$"Onboarding_Native_SupportUnavailable{suffix}"]; + Assert.False(string.IsNullOrWhiteSpace(emphasis)); + int start = guidance.IndexOf(emphasis, StringComparison.Ordinal); + Assert.True(start >= 0, $"{suffix} is not a substring in {directory}"); + ranges.Add((start, emphasis.Length)); + } + + ranges.Sort((left, right) => left.Start.CompareTo(right.Start)); + Assert.All(ranges.Zip(ranges.Skip(1)), pair => + Assert.True(pair.First.Start + pair.First.Length <= pair.Second.Start)); + } + } + + [Theory] + [InlineData("en-us", "Advanced options", "clean reinstall", "vary by device", "reopen this page")] + [InlineData("fr-fr", "Options avancées", "réinstallation complète", "varier selon l'appareil", "rouvrez cette page")] + [InlineData("nl-nl", "Geavanceerde opties", "schone herinstallatie", "per apparaat verschillen", "Open deze pagina")] + [InlineData("pt-br", "Opções avançadas", "reinstalação limpa", "variar conforme o dispositivo", "reabra esta página")] + [InlineData("zh-cn", "高级选项", "全新安装系统", "因设备而异", "重新打开此页面")] + [InlineData("zh-tw", "進階選項", "全新安裝系統", "因裝置而異", "重新開啟此頁面")] + public void Welcome_NativeUnavailableGuidanceIncludesEnrollmentTradeoffAndRecheck( + string locale, string advancedOptions, string cleanReinstall, string rollout, string reopen) + { + var document = XDocument.Load(Path.Combine( + TestRepositoryPaths.GetRepositoryRoot(), "src", "OpenClaw.Tray.WinUI", + "Strings", locale, "Resources.resw")); + string guidance = document.Descendants("data") + .Single(element => (string?)element.Attribute("name") == "Onboarding_Native_SupportUnavailable") + .Element("value")!.Value; + + Assert.Contains(advancedOptions, guidance); + Assert.Contains("26H1", guidance); + Assert.Contains("25H2/26H2", guidance); + Assert.Contains(cleanReinstall, guidance); + Assert.Contains(rollout, guidance); + Assert.Contains(reopen, guidance); + Assert.Equal(3, guidance.Split("\n\n", StringSplitOptions.None).Length); } [Fact] diff --git a/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj b/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj index da88f29fa..d434a46d2 100644 --- a/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj +++ b/tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/tests/OpenClaw.Tray.UITests/NativeGatewayEligibilityTextTests.cs b/tests/OpenClaw.Tray.UITests/NativeGatewayEligibilityTextTests.cs new file mode 100644 index 000000000..811363062 --- /dev/null +++ b/tests/OpenClaw.Tray.UITests/NativeGatewayEligibilityTextTests.cs @@ -0,0 +1,138 @@ +using System.Reflection; +using Microsoft.UI.Text; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Automation.Peers; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Documents; +using OpenClaw.SetupEngine; +using OpenClaw.SetupEngine.UI; +using Xunit.Abstractions; + +namespace OpenClaw.Tray.UITests; + +[Collection(UICollection.Name)] +public sealed class NativeGatewayEligibilityTextTests(UIThreadFixture ui, ITestOutputHelper output) +{ + [Fact] + public async Task Apply_UnavailablePreservesLocalizedParagraphsEmphasisAndAccessibleName() + { + await ui.ResetContainerAsync(); + await ui.RunOnUIAsync(async () => + { + var target = new TextBlock { Width = 560, TextWrapping = TextWrapping.Wrap }; + ui.Container.Children.Add(target); + try + { + await TestSupport.WaitForRenderedConditionAsync(() => target.IsLoaded, "native guidance Loaded"); + AssertRich(target); + var runCount = target.Inlines.Count; + AssertRich(target); + Assert.Equal(runCount, target.Inlines.Count); + ui.Container.UpdateLayout(); + await ui.YieldToRenderAsync(); + Assert.True(target.ActualHeight > 0); + Assert.Equal(3, target.Text.Replace("\r\n", "\n", StringComparison.Ordinal) + .Split("\n\n", StringSplitOptions.None).Length); + output.WriteLine($"Rendered guidance: {target.Text}"); + output.WriteLine($"Runs={runCount}; semibold=6; automation name matches full localized text."); + await OnboardingArtworkRenderingTests.SaveProofAsync(target, "native-gateway-guidance", output); + } + finally + { + ui.Container.Children.Clear(); + } + }); + } + + [Theory] + [InlineData(NativeGatewayEligibility.Available)] + [InlineData(NativeGatewayEligibility.UnsupportedPlatform)] + [InlineData(NativeGatewayEligibility.CheckFailed)] + public async Task Apply_OtherEligibilityClearsRichTextAndAutomationOverride(NativeGatewayEligibility eligibility) + { + await ui.RunOnUIAsync(() => + { + var target = new TextBlock(); + AssertRich(target); + NativeGatewayEligibilityTextTestAccess.Apply(target, eligibility); + AssertPlain(target, NativeGatewayEligibilityTextTestAccess.Get(eligibility)); + AssertRich(target); + }); + } + + [Theory] + [InlineData("Onboarding_Native_CheckingSupport")] + [InlineData("Onboarding_Native_UpdateLaunchFailed")] + [InlineData("")] + public async Task ApplyPlain_ClearsRichTextAndAutomationOverride(string resourceKey) + { + await ui.RunOnUIAsync(() => + { + var target = new TextBlock(); + AssertRich(target); + string text = resourceKey.Length == 0 ? "" : NativeGatewayEligibilityTextTestAccess.GetString(resourceKey); + if (resourceKey.Length > 0) + Assert.NotEqual(resourceKey, text); + NativeGatewayEligibilityTextTestAccess.ApplyPlain(target, text); + AssertPlain(target, text); + AssertRich(target); + }); + } + + private static void AssertRich(TextBlock target) + { + string text = NativeGatewayEligibilityTextTestAccess.Get(NativeGatewayEligibility.CapabilityUnavailable); + Assert.NotEqual("Onboarding_Native_SupportUnavailable", text); + NativeGatewayEligibilityTextTestAccess.Apply(target, NativeGatewayEligibility.CapabilityUnavailable); + var runs = target.Inlines.Select(inline => Assert.IsType(inline)).ToArray(); + Assert.Equal(text, string.Concat(runs.Select(run => run.Text))); + Assert.Equal(text, target.Text); + Assert.Equal(text, AutomationProperties.GetName(target)); + Assert.Equal(text, Peer(target).GetName()); + var emphasized = runs.Where(run => run.FontWeight == FontWeights.SemiBold).ToArray(); + Assert.Equal(6, emphasized.Length); + foreach (string suffix in new[] { "Lead", "ComingSoon", "InsiderProgram", "Channels", "WindowsVersion", "WindowsUpdate" }) + { + string key = $"Onboarding_Native_SupportUnavailable{suffix}"; + string emphasis = NativeGatewayEligibilityTextTestAccess.GetString(key); + Assert.NotEqual(key, emphasis); + Assert.Contains(emphasized, run => run.Text == emphasis); + } + } + + private static void AssertPlain(TextBlock target, string text) + { + Assert.Equal(text, target.Text); + Assert.Equal(text, string.Concat(target.Inlines.Select(inline => Assert.IsType(inline).Text))); + Assert.Same(DependencyProperty.UnsetValue, target.ReadLocalValue(AutomationProperties.NameProperty)); + Assert.Equal(text, Peer(target).GetName()); + Assert.All(target.Inlines, inline => Assert.Equal(FontWeights.Normal, Assert.IsType(inline).FontWeight)); + } + + private static AutomationPeer Peer(TextBlock target) => + FrameworkElementAutomationPeer.FromElement(target) + ?? FrameworkElementAutomationPeer.CreatePeerForElement(target); +} + +// A friend-assembly grant also exposes Setup's linked FluentIconCatalog, which conflicts +// with the Tray copy in this test assembly. Bind only the production methods under test. +internal static class NativeGatewayEligibilityTextTestAccess +{ + internal static readonly Action Apply = + Bind>("NativeGatewayEligibilityText", "Apply"); + internal static readonly Action ApplyPlain = + Bind>("NativeGatewayEligibilityText", "ApplyPlain"); + internal static readonly Func Get = + Bind>("NativeGatewayEligibilityText", "Get"); + internal static readonly Func GetString = + Bind>("SetupLocalization", "GetString"); + + private static T Bind(string typeName, string methodName) where T : Delegate + { + var type = typeof(SetupWindow).Assembly.GetType($"OpenClaw.SetupEngine.UI.{typeName}", throwOnError: true)!; + var method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMethodException(type.FullName, methodName); + return method.CreateDelegate(); + } +} diff --git a/tests/OpenClaw.Tray.UITests/OnboardingSetupGalleryTests.cs b/tests/OpenClaw.Tray.UITests/OnboardingSetupGalleryTests.cs index 93b35298a..508bf3bd4 100644 --- a/tests/OpenClaw.Tray.UITests/OnboardingSetupGalleryTests.cs +++ b/tests/OpenClaw.Tray.UITests/OnboardingSetupGalleryTests.cs @@ -534,8 +534,13 @@ await WaitAsync(() => !Find(welcome, "NativeCheckProgress").IsActi available ? Visibility.Collapsed : Visibility.Visible; Find(welcome, "NativeSupportStatusPanel").Visibility = available ? Visibility.Collapsed : Visibility.Visible; - Find(welcome, "NativeSupportStatus").Text = - available ? "" : "Synthetic unavailable host. Windows update required."; + var nativeStatus = Find(welcome, "NativeSupportStatus"); + if (available) + NativeGatewayEligibilityTextTestAccess.ApplyPlain(nativeStatus, ""); + else + NativeGatewayEligibilityTextTestAccess.Apply(nativeStatus, NativeGatewayEligibility.CapabilityUnavailable); + Find