From 0b7807a50ebb8766ef59d047e091587c1c00badd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ademir=20Jos=C3=A9=20Ferreira=20J=C3=BAnior?= Date: Mon, 10 Aug 2026 02:07:06 -0300 Subject: [PATCH 1/2] Run the assembled desktop E2E suite in CI The suite sat outside pnpm check and outside CI, so assembled-boundary regressions surfaced only when a maintainer on Windows remembered to run it. The job stays separate from Backend Checks because the E2E build shares no target directory or feature set with it, so serializing them would lengthen the critical path without reusing anything. --- .github/workflows/ci.yml | 63 ++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 6 ++-- docs/architecture.md | 2 +- e2e/desktop/run.ts | 2 +- package.json | 3 +- 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2076909..2f81797 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,3 +86,66 @@ jobs: - name: Audit Rust dependencies run: cargo audit --file src-tauri/Cargo.lock + + test-e2e-desktop: + name: Desktop E2E + runs-on: windows-2025-vs2026 + timeout-minutes: 20 + env: + CARGO_PROFILE_DEV_DEBUG: 0 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Setup Rust + run: rustup toolchain install + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: "./src-tauri -> target/desktop-e2e" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build desktop E2E binary + run: pnpm build:e2e:desktop + + - name: Run desktop E2E suite + run: pnpm test:e2e:desktop:run + + - name: Upload failure evidence + if: failure() + uses: actions/upload-artifact@v7 + with: + name: desktop-e2e-artifacts + path: e2e/desktop/artifacts/ + retention-days: 7 + + - name: Assert clean teardown + if: always() + shell: pwsh + run: | + # WebView2 host processes outlive the suite by a few seconds; they are + # not asserted on, but give them time to exit before sampling. + Start-Sleep -Seconds 10 + + $orphans = Get-Process -Name leafdown-e2e, msedgedriver -ErrorAction SilentlyContinue + $listeners = Get-NetTCPConnection -LocalPort 4445 -State Listen -ErrorAction SilentlyContinue + + if ($orphans -or $listeners) { + $orphans | Format-Table -AutoSize Name, Id, StartTime + $listeners | Format-Table -AutoSize LocalAddress, LocalPort, OwningProcess + throw "Desktop E2E left an orphaned process or a listener on port 4445." + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59742d4..0e12412 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,15 +74,15 @@ Verify a fresh development environment with: pnpm check ``` -Run the Windows-local assembled desktop E2E suite with: +Run the assembled desktop E2E suite, which requires Windows, with: ```powershell pnpm test:e2e:desktop ``` -This explicit suite is not part of `pnpm check`. It builds an isolated debug binary with test-only WebDriver capabilities, then runs one embedded WebDriver worker at a time on port 4445 across fresh application sessions. The embedded provider does not require an external WebDriver. Keep port 4445 available while it runs. The test identifier, persisted store, and target directory are separate from ordinary Leafdown builds. +This explicit suite is not part of `pnpm check`; CI runs it as its own job on every pull request and every push to `main`, so it is enforced without changing what you run locally. It builds an isolated debug binary with test-only WebDriver capabilities, then runs one embedded WebDriver worker at a time on port 4445 across fresh application sessions. The embedded provider does not require an external WebDriver. Keep port 4445 available while it runs. The test identifier, persisted store, and target directory are separate from ordinary Leafdown builds. -The runner resets only the isolated E2E persisted store, leaving the application to write its own defaults, creates temporary filesystem fixtures, and removes both after the suite. Each run writes ignored runner, frontend, backend, and focused diagnostic evidence under `e2e/desktop/artifacts///`. A failed test also captures a screenshot, the real diagnostics summary, the test error, and a semantic UI snapshot that excludes editor content. A failed run additionally writes `fixture-manifest.json` under `e2e/desktop/artifacts//`, recording each temporary fixture's path, expected and actual hash and size, and modification time before cleanup removes it. These artifacts are retained until manually deleted. Treat them as potentially sensitive because diagnostics and errors may contain local paths. +The runner resets only the isolated E2E persisted store, leaving the application to write its own defaults, creates temporary filesystem fixtures, and removes both after the suite. Each run writes ignored runner, frontend, backend, and focused diagnostic evidence under `e2e/desktop/artifacts///`. A failed test also captures a screenshot, the real diagnostics summary, the test error, and a semantic UI snapshot that excludes editor content. A failed run additionally writes `fixture-manifest.json` under `e2e/desktop/artifacts//`, recording each temporary fixture's path, expected and actual hash and size, and modification time before cleanup removes it. Local artifacts are retained until manually deleted. Treat them as potentially sensitive because diagnostics and errors may contain local paths. CI uploads the same evidence only when the job fails, retained for seven days; those artifacts contain runner paths rather than a contributor's. To verify the failure-evidence path, run the suite with the forced-failure flag: diff --git a/docs/architecture.md b/docs/architecture.md index 53eeef2..76694c6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -169,7 +169,7 @@ Automated tests focus on: - Literal HTML rendering and script-execution prevention. - Context popup layout and caret-based marker visibility. -The Windows-local assembled desktop E2E suite complements those component and boundary tests without replacing them. It runs one embedded WebDriver worker at a time against an isolated debug binary and starts fresh application processes for independent scenarios. The suite retains the Help → Diagnostics smoke path, then adds narrow assembled-boundary assertions for the document lifecycle, real folder-watcher refresh, typed backend error propagation, persisted settings across restart, injected frame controls, and the clean window-close handshake. +The assembled desktop E2E suite complements those component and boundary tests without replacing them. It requires Windows and stays outside `pnpm check`, running through an explicit command locally and as its own CI job on every pull request and push to `main`. It runs one embedded WebDriver worker at a time against an isolated debug binary and starts fresh application processes for independent scenarios. The suite retains the Help → Diagnostics smoke path, then adds narrow assembled-boundary assertions for the document lifecycle, real folder-watcher refresh, typed backend error propagation, persisted settings across restart, injected frame controls, and the clean window-close handshake. User-visible acceptance paths use semantic UI interactions. Scenarios decide that an operation happened from state that outlives it — on-disk contents, menu item state, editor contents, or diagnostic records — rather than from an affordance that dismisses on a timer. Where the notification is itself the reported outcome, the desktop E2E build disables toast auto-dismissal so the assertion reads a settled affordance instead of racing it. Direct bridge execution is limited to corroborating diagnostic state, while Node-side filesystem, persisted-store, log, and process access provides deterministic setup or evidence around the native boundary. WebDriver plugins, permissions, and frontend integration remain limited to the dedicated desktop E2E build and are excluded from ordinary application builds. diff --git a/e2e/desktop/run.ts b/e2e/desktop/run.ts index 5b4b96e..d97155d 100644 --- a/e2e/desktop/run.ts +++ b/e2e/desktop/run.ts @@ -125,7 +125,7 @@ const waitForPortRelease = async (timeoutMs = 10_000) => { const main = async () => { if (!process.env.APPDATA) { - throw new Error("APPDATA is required for the Windows-local desktop E2E suite."); + throw new Error("APPDATA is required for the Windows desktop E2E suite."); } await mkdir(artifactsRoot, { recursive: true }); diff --git a/package.json b/package.json index b0ebe4f..8ace8c7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build:frontend": "tsc -b && vite build", "build:frontend:e2e": "tsc -b && vite build --mode desktop-e2e", "build:e2e:desktop": "tauri build --debug --no-bundle --features desktop-e2e --config e2e/desktop/tauri.conf.json -- --target-dir target/desktop-e2e", - "test:e2e:desktop": "pnpm build:e2e:desktop && tsx e2e/desktop/run.ts", + "test:e2e:desktop": "pnpm build:e2e:desktop && pnpm test:e2e:desktop:run", + "test:e2e:desktop:run": "tsx e2e/desktop/run.ts", "preview": "vite preview", "tauri": "tauri", "lint": "pnpm lint:frontend && pnpm lint:backend", From 018df90d8eb3ae027401ef964231c30769876bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ademir=20Jos=C3=A9=20Ferreira=20J=C3=BAnior?= Date: Mon, 10 Aug 2026 02:45:52 -0300 Subject: [PATCH 2/2] Save Rust caches only from main Cache entries written during a pull request run are scoped to that pull request ref, so nothing but a re-run of the same branch can restore them while they consume the repository's shared 10 GB budget and evict by LRU. Pull requests restore main's entry either way. --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f81797..2bebf44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: "./src-tauri -> target" + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: pnpm install --frozen-lockfile @@ -115,6 +116,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: "./src-tauri -> target/desktop-e2e" + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: pnpm install --frozen-lockfile