From 100c9f7942de8c4d6cbb1dc7dc5586682ed4c6f5 Mon Sep 17 00:00:00 2001 From: leafOfTree Date: Sat, 17 Oct 2026 16:38:01 +0800 Subject: [PATCH 1/3] Start each desktop E2E cycle with the helper windows apart again Exit now remembers the group dragged together during a cycle, so the next cycle's startup brought the surviving helper windows back as one group. Check that the group was remembered, then forget it before the next startup. --- tests/DesktopE2E.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/DesktopE2E.cs b/tests/DesktopE2E.cs index a84d43db..452665cc 100644 --- a/tests/DesktopE2E.cs +++ b/tests/DesktopE2E.cs @@ -512,9 +512,13 @@ static async Task Run(int switches, int cycles, int durationMinutes) var saved=Json.Deserialize>(File.ReadAllText(Path.Combine(Root,"WindowTabsSettings.json"))); Check(saved.ContainsKey("version") && !string.IsNullOrEmpty((string)saved["version"]),"Startup did not persist its settings version"); Check((string)saved["alignment"]=="Left" && !(bool)saved["enableTabbingByDefault"],"Fixture settings were lost"); + Check(saved.ContainsKey("rememberedGroups") && Json.Serialize(saved["rememberedGroups"]).Contains("WindowTabs E2E "),"Exit did not remember the dragged group"); Check(!HasCrash(),"Application logged an exception during the cycle"); completed++; Log("PASS: cycle "+completed+"; original entry point, drag grouping, switching, close and normal exit"); // The next cycle reuses the saved portable configuration and reacquires the real singleton. + // Forget the dragged group so the surviving helpers start apart and are dragged together again. + saved.Remove("rememberedGroups"); + File.WriteAllText(Path.Combine(Root,"WindowTabsSettings.json"),Json.Serialize(saved)); } } [STAThread] static int Main(string[] args) From c717978d9e7bb53c15c08d636befb6b09105766d Mon Sep 17 00:00:00 2001 From: leafOfTree Date: Sat, 17 Oct 2026 16:45:21 +0800 Subject: [PATCH 2/3] Run the full desktop E2E with restart on every pull request Pull requests ran the one-cycle Quick profile, so a defect that only shows after WindowTabs restarts first failed the release. Run Full, as the release does, on pull requests and as the manual default; it adds about a minute. --- .github/workflows/desktop-e2e-hosted.yml | 6 +++--- docs/desktop-e2e.md | 8 +++++--- docs/testing.md | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/desktop-e2e-hosted.yml b/.github/workflows/desktop-e2e-hosted.yml index 4d807c53..a9223486 100644 --- a/.github/workflows/desktop-e2e-hosted.yml +++ b/.github/workflows/desktop-e2e-hosted.yml @@ -11,7 +11,7 @@ on: description: 'Test depth: Quick, Full with exit/restart, or 30-minute Soak' type: choice options: [Quick, Full, Soak] - default: Quick + default: Full seed: description: 'Soak random seed; leave empty to generate one' type: string @@ -40,11 +40,11 @@ concurrency: jobs: desktop: - name: Desktop E2E (${{ inputs.profile || (github.event_name == 'schedule' && 'Soak') || 'Quick' }}) + name: Desktop E2E (${{ inputs.profile || (github.event_name == 'schedule' && 'Soak') || 'Full' }}) runs-on: windows-2022 timeout-minutes: ${{ (inputs.profile == 'Soak' || github.event_name == 'schedule') && 50 || 15 }} env: - E2E_PROFILE: ${{ inputs.profile || (github.event_name == 'schedule' && 'Soak') || 'Quick' }} + E2E_PROFILE: ${{ inputs.profile || (github.event_name == 'schedule' && 'Soak') || 'Full' }} steps: - uses: actions/checkout@v6 with: diff --git a/docs/desktop-e2e.md b/docs/desktop-e2e.md index f931173e..9757aaea 100644 --- a/docs/desktop-e2e.md +++ b/docs/desktop-e2e.md @@ -231,7 +231,7 @@ Median, p95 and maximum are diagnostic observations, not compositor presentation latency or a portable speed benchmark. A switch has a 2-second correctness timeout. Normal CI compiles this harness with `-BuildOnly`. Actual input tests require -an unlocked interactive Windows desktop. The hosted Quick workflow checks this +an unlocked interactive Windows desktop. The hosted workflow checks this on GitHub's Windows VM; unusable hosted/service sessions fail rather than skip or silently report success. The deterministic navigation and modifier snapshot regressions also run in the regular Architecture suite. @@ -240,10 +240,12 @@ snapshot regressions also run in the regular Architecture suite. `.github/workflows/desktop-e2e-hosted.yml` provides **Desktop E2E Hosted** on GitHub's `windows-2022` virtual machine, without a local runner. Every pull -request runs Quick as the stable **Desktop E2E (Quick)** check. No path filters +request runs Full as the stable **Desktop E2E (Full)** check, the same profile +the release runs, so a defect that only appears after restart fails the pull +request rather than the release; it adds about a minute over Quick. No path filters skip this check. Soak runs every day at 18:00 UTC on the default branch, which also catches changes in the hosted Windows image, and manual dispatch offers -Quick, Full or Soak. These runs build their own Release binary. The release +Quick, Full (default) or Soak. These runs build their own Release binary. The release workflow calls the same workflow with Full and `executable-artifact`, so it tests the very `WindowTabs.exe` the draft will carry; the draft is created only after the regression suites, the smoke test, the version check and that run all pass. diff --git a/docs/testing.md b/docs/testing.md index c74a3188..91900d14 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -101,9 +101,9 @@ The separate [desktop E2E suite](desktop-e2e.md) exercises the actual Release entry point, real mouse drag grouping, frequent foreground switching, maximized tabs, closing the active window, tray-menu exit and restart. It requires an idle interactive desktop and is not part of the focus-safe default suite. -GitHub's separate **Desktop E2E Hosted** workflow runs Quick on pull requests -using an isolated `windows-2022` VM; manual Full verifies exit/restart and 1,320 -switches. Both profiles have passed on hosted runners. See the linked E2E guide +GitHub's separate **Desktop E2E Hosted** workflow runs Full on pull requests +using an isolated `windows-2022` VM, verifying exit/restart and 1,320 switches +like the release does; Quick remains a manual choice. See the linked E2E guide for run evidence, activation requirements and remaining limitations. | Risk | Existing evidence | Next validation | From 75b500cae218fde6dbf9dcc158d432278e904523 Mon Sep 17 00:00:00 2001 From: leafOfTree Date: Sat, 17 Oct 2026 16:49:49 +0800 Subject: [PATCH 3/3] Clear the group kept on the E2E helper windows before restarting WindowTabs also keeps each window's group on the window itself, so helpers that survive into the next cycle came back grouped even with the settings record removed. Check that exit kept it, then remove it with the remembered group. --- tests/DesktopE2E.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/DesktopE2E.cs b/tests/DesktopE2E.cs index 452665cc..81b16a08 100644 --- a/tests/DesktopE2E.cs +++ b/tests/DesktopE2E.cs @@ -26,6 +26,8 @@ static partial class DesktopE2E [DllImport("user32.dll")] static extern bool GetGUIThreadInfo(uint thread, ref GuiThread info); [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hwnd); [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hwnd, int command); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern IntPtr GetProp(IntPtr hwnd, string name); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern IntPtr RemoveProp(IntPtr hwnd, string name); [DllImport("user32.dll")] static extern bool IsWindow(IntPtr hwnd); [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr hwnd, uint command); [DllImport("user32.dll")] static extern int GetWindowLong(IntPtr hwnd, int index); @@ -516,7 +518,11 @@ static async Task Run(int switches, int cycles, int durationMinutes) Check(!HasCrash(),"Application logged an exception during the cycle"); completed++; Log("PASS: cycle "+completed+"; original entry point, drag grouping, switching, close and normal exit"); // The next cycle reuses the saved portable configuration and reacquires the real singleton. - // Forget the dragged group so the surviving helpers start apart and are dragged together again. + // Forget the dragged group, in the settings and on the windows themselves, so the + // surviving helpers start apart and are dragged together again. + var survivors=Forms.Where(f=>!f.IsDisposed).ToList(); + Check(survivors.All(f=>GetProp(f.Handle,"WindowTabs.Group.v1")!=IntPtr.Zero),"Exit did not keep the group on its windows"); + foreach(var f in survivors) RemoveProp(f.Handle,"WindowTabs.Group.v1"); saved.Remove("rememberedGroups"); File.WriteAllText(Path.Combine(Root,"WindowTabsSettings.json"),Json.Serialize(saved)); }