From 54b2b7a9c747ae9a5939a8f3b4c00d6d959b0b3d Mon Sep 17 00:00:00 2001 From: pbean Date: Tue, 16 Jun 2026 12:56:44 -0700 Subject: [PATCH 1/2] test(e2e): discover app IPC socket for local live-sync rendezvous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI live-sync suite pinned a unique per-run NOTEY_SOCKET_PATH so the test app and CLI rendezvous on an isolated endpoint. That relies on the env var reaching the tauri-driver-launched app. It holds on CI's older Ubuntu webkit2gtk-driver, but a modern WebKitWebDriver (webkit2gtk 2.52.4) resets the launched app's environment — stripping NOTEY_SOCKET_PATH and forcing XDG_RUNTIME_DIR back to the real session value — and tauri-driver 2.0.6 has no env passthrough. So locally the app binds the default $XDG_RUNTIME_DIR/notey.sock while the CLI looked at the per-run /tmp path: `notey add` failed exit-2 ("not running"), leaving the suite 14/16 locally though green on CI. Discover the socket the app actually bound instead of dictating it: probe both the configured path and $XDG_RUNTIME_DIR/notey.sock for liveness, then point the CLI at whichever the app is serving. Add a pre-flight guard that cleanly skips (not hijacks) the suite when a real notey instance already owns the default socket. CI behavior is unchanged (it matches the first candidate); local now runs 16/16 under xvfb. See DW-95. Co-Authored-By: Claude Opus 4.8 --- e2e/run.mjs | 90 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/e2e/run.mjs b/e2e/run.mjs index f1ae344..51db4fe 100644 --- a/e2e/run.mjs +++ b/e2e/run.mjs @@ -15,6 +15,7 @@ import { spawn } from 'child_process'; import path from 'path'; import os from 'os'; import fs from 'fs'; +import net from 'node:net'; import { fileURLToPath } from 'url'; // Force WebKitGTK software rendering before anything spawns the driver. Under a @@ -68,6 +69,9 @@ let tauriDriver; let sessionId; let passed = 0; let failed = 0; +// Set once, before any test app launches: whether a real notey instance already +// owns the default IPC socket (see the live-sync suite's guard for why it matters). +let realInstancePresent = false; async function test(name, fn) { try { @@ -260,19 +264,61 @@ async function waitForMarkerInNoteList(marker, timeoutMs = 5000) { } /** - * Poll until the app's IPC socket file exists. The app's IPC server binds during - * Tauri's setup hook, which can lag a freshly-created WebDriver session on a cold - * CI runner — without this gate the first `notey add` can race the bind and fail - * exit-2 ("not running") as a flake. Best-effort: returns after the deadline - * regardless, leaving the CLI's own connect timeout to report a genuine absence. + * Paths the app's IPC server may have bound, in priority order. The app resolves + * its socket as NOTEY_SOCKET_PATH → `$XDG_RUNTIME_DIR/notey.sock` → temp fallback + * (mirrors `socket_server::socket_path`). We export a unique per-run + * NOTEY_SOCKET_PATH, but a modern WebKitWebDriver (webkit2gtk ≥2.52) resets the + * launched app's environment — stripping NOTEY_SOCKET_PATH and forcing + * XDG_RUNTIME_DIR back to the real session value — and tauri-driver has no env + * passthrough, so the harness cannot steer the app's path: it must discover it. + * On CI's older driver the env survives, so the first candidate wins and behavior + * is unchanged. */ -async function waitForSocket(timeoutMs = 5000) { - const sock = process.env.NOTEY_SOCKET_PATH; +function appSocketCandidates() { + const xdg = process.env.XDG_RUNTIME_DIR; + return [process.env.NOTEY_SOCKET_PATH, xdg ? path.join(xdg, 'notey.sock') : null].filter(Boolean); +} + +/** + * Whether a Unix socket path is *accepting connections* (a live server), as + * opposed to a leftover stale socket file. A probe connect that immediately + * disconnects is harmless: the IPC worker sees EOF before any frame and exits. + */ +function isSocketLive(p, timeoutMs = 500) { + return new Promise((resolve) => { + const sock = net.connect(p); + let settled = false; + const timer = setTimeout(() => done(false), timeoutMs); + function done(live) { + if (settled) return; + settled = true; + clearTimeout(timer); + sock.destroy(); + resolve(live); + } + sock.once('connect', () => done(true)); + sock.once('error', () => done(false)); + }); +} + +/** + * Poll until one of the app's candidate IPC sockets is *live*, and return that + * path. The app's IPC server binds during Tauri's setup hook, which can lag a + * freshly-created WebDriver session on a cold runner — without this gate the first + * `notey add` races the bind and fails exit-2 ("not running"). Liveness (not mere + * file existence) is checked so a leftover stale socket never wins over the path + * the app actually bound. Returns null after the deadline, leaving the CLI's own + * connect timeout to report a genuine absence. + */ +async function waitForAppSocket(timeoutMs = 5000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - if (sock && fs.existsSync(sock)) return; + for (const c of appSocketCandidates()) { + if (fs.existsSync(c) && (await isSocketLive(c))) return c; + } await pause(150); } + return null; } // --- Setup --- @@ -488,6 +534,20 @@ async function purgeCliNote(marker) { async function cliLiveSyncTests() { console.log('\nP1-E2E-003: CLI Live Sync'); + // A modern WebKitWebDriver resets the test app's env, forcing it onto the + // default `$XDG_RUNTIME_DIR/notey.sock` — the same endpoint a real desktop notey + // uses. If a real instance was already listening there before we launched, the + // CLI would talk to IT (polluting the real DB) instead of the test app. Skip + // rather than hijack; CI never trips this (no real instance, env survives). + if (realInstancePresent) { + console.log( + ' ⚠ skipped: a running notey instance owns the default IPC socket. ' + + 'Close it to run the live-sync suite locally — this WebKitWebDriver build ' + + 'resets the app env, preventing per-run socket isolation.', + ); + return; + } + // Unique per run so the marker note is identifiable in a non-isolated dev DB. const marker = `E2E-CLISYNC-${Date.now()}`; let addSucceeded = false; @@ -502,7 +562,12 @@ async function cliLiveSyncTests() { }); await test('notey add (CLI, separate process) exits 0', async () => { - await waitForSocket(); // let the app's IPC server finish binding before connecting + // Discover where the app actually bound (its env may not survive the + // WebDriver launch) and point the CLI at that exact socket. The CLI reads + // NOTEY_SOCKET_PATH at spawn, so updating it here steers `runCli` below. + const appSocket = await waitForAppSocket(); + assert(appSocket, `app IPC socket never came up; tried: ${appSocketCandidates().join(', ')}`); + process.env.NOTEY_SOCKET_PATH = appSocket; const res = await runCli(['add', marker]); assert(res.code === 0, `CLI exited ${res.code}; stderr: "${res.stderr.trim()}"`); addSucceeded = true; @@ -525,6 +590,13 @@ async function cliLiveSyncTests() { // --- Main --- async function main() { + // Detect a real notey instance on the default socket BEFORE launching the test + // app (which, under a modern WebKitWebDriver, binds that same default path). Once + // the test app is up we can't tell its socket apart from a pre-existing one. + const xdg = process.env.XDG_RUNTIME_DIR; + const defaultSock = xdg ? path.join(xdg, 'notey.sock') : null; + realInstancePresent = !!(defaultSock && fs.existsSync(defaultSock) && (await isSocketLive(defaultSock))); + console.log('Starting tauri-driver...'); await startDriver(); From 7e451ae320cbb94f98bf89f9c6853a8d207e1b92 Mon Sep 17 00:00:00 2001 From: pbean Date: Tue, 16 Jun 2026 12:56:44 -0700 Subject: [PATCH 2/2] chore(retro): log DW-95 and correct epic-6 item-1 E2E tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local verification of the epic-6 retro surfaced two tracking errors and one new limitation: - epic-6-retro-item-1 cited HEAD 5be24c7 as the "green" E2E run, but that commit's CI is FAILURE and predates the live-sync suite (added in 322e570). The green run is HEAD 3a76bac (run 27566817463), 16/16 incl. live-sync. - The note labeled CLI live-sync "deferred (DW-91/DW-92)"; those are trash-suite hygiene items and the live-sync suite actually shipped. - DW-95: modern WebKitWebDriver resets the test app's env, preventing per-run IPC socket isolation locally — documented alongside the test-only fix. Co-Authored-By: Claude Opus 4.8 --- _bmad-output/implementation-artifacts/deferred-work.md | 7 +++++++ _bmad-output/implementation-artifacts/sprint-status.yaml | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/_bmad-output/implementation-artifacts/deferred-work.md b/_bmad-output/implementation-artifacts/deferred-work.md index 5cef46d..910731d 100644 --- a/_bmad-output/implementation-artifacts/deferred-work.md +++ b/_bmad-output/implementation-artifacts/deferred-work.md @@ -735,3 +735,10 @@ origin: code review of spec-dw-85-ipc-worker-budget.md (blind hunter), 2026-06-1 location: src-tauri/src/ipc/socket_server.rs (unblock_conn, #[cfg(windows)] branch) reason: DW-85's reaper unblocks a worker stalled in `read_frame` via `libc::shutdown(fd, SHUT_RDWR)` on Unix (empirically confirmed — `int_dw85_watchdog_reclaims_stalled_slots` passes on Linux) and `CancelIoEx(handle, None)` on Windows. The Windows path is `#[cfg(windows)]` and was NOT compiled or run on the Linux dev/CI host, so it is unverified that `CancelIoEx` actually cancels the worker thread's pending synchronous `ReadFile` on an `interprocess` 2.4.2 named-pipe handle (cancellation semantics differ for handles opened without `FILE_FLAG_OVERLAPPED`; `CancelIoEx` is cross-thread, unlike `CancelIo`, which is the reason it was chosen). Also confirm the `RawHandle as *mut _ → HANDLE` cast and the two-arg `CancelIoEx` signature compile under `windows = 0.61`. If `CancelIoEx` does not unblock the read, the budget cap still bounds concurrency on Windows but stalled half-open slots are never reclaimed there (degrades to budget-only on Windows). Needs a Windows CI job or manual Windows run to validate; pairs with the broader cross-platform IPC QA (RISK-E6-007). status: open + +### DW-95: Modern WebKitWebDriver resets the test app's env, preventing per-run IPC socket isolation in local E2E + +origin: local verification of epic-6 retro item-1 (E2E), 2026-06-16 +location: e2e/run.mjs (cliLiveSyncTests, waitForAppSocket / realInstancePresent guard) +reason: The CLI live-sync suite originally pinned a unique per-run `NOTEY_SOCKET_PATH` so the test app and CLI rendezvous on their own endpoint, isolated from any real notey instance. This relies on the env var propagating to the tauri-driver-launched app. It holds on CI's older Ubuntu `webkit2gtk-driver`, but a modern WebKitWebDriver (verified locally on `webkit2gtk-4.1 2.52.4`) **resets the launched app's environment** — stripping `NOTEY_SOCKET_PATH` and forcing `XDG_RUNTIME_DIR` back to the real session value — and `tauri-driver 2.0.6` forwards only `{binary, args}` (no env passthrough). So locally the app always binds the default `$XDG_RUNTIME_DIR/notey.sock`, diverging from the per-run path the CLI used → `notey add` failed exit-2 ("not running") and the suite was 14/16 locally while green on CI. Fixed test-only by discovering the socket the app actually bound (probe configured path + `$XDG_RUNTIME_DIR/notey.sock` for liveness) and pointing the CLI there — local now 16/16, CI unchanged (it matches the first candidate). Residual limitation: because the test app falls back to the *default* socket locally, the suite cannot isolate from a real running notey; it now detects a live pre-existing instance and skips the live-sync suite with an actionable message rather than hijacking it. Proper isolation would need either an env-passthrough in a newer tauri-driver or an app-level `--socket-path` arg routed via `tauri:options.args` (app source change; deferred). Test-harness robustness, not a product defect. +status: open diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index d532a9f..712303e 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -40,7 +40,7 @@ # - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended) generated: 2026-04-03 -last_updated: 2026-06-15 # epic-6-retro item 5 (DW-85 IPC worker budget + watchdog) → done (spec-dw-85-ipc-worker-budget.md); items 1 (E2E) + 2 (singleflight) previously → done +last_updated: 2026-06-16 # epic-6-retro item-1 (E2E) note corrected + local live-sync E2E fixed (DW-95; 16/16 local); item 5 (DW-85) → done; items 1+2 previously → done # Note: epic-2-retrospective rewritten fresh on 2026-04-04 project: notey project_key: NOKEY @@ -191,7 +191,7 @@ development_status: epic-5-retro-item-4-permission-toml-auto-gen-check: done # MOOT at Epic 6 retro — Epic 6 added no new Tauri commands (IPC delegates to existing services), gotcha did not recur # Epic 6 Retro Items (2026-06-14): - epic-6-retro-item-1-first-feature-e2e: done # HIGH — trash-lifecycle feature E2E GENUINELY GREEN ON CI: run 27527258028 (HEAD 5be24c7), Linux job success incl. "Build debug binary (Linux)" AND "E2E tests (Linux)". The 2.11 stack alignment fixed the prior "Build debug binary (Linux)" failure; CI's WebKit software-rendering env (WEBKIT_DISABLE_COMPOSITING_MODE/DMABUF + LIBGL_ALWAYS_SOFTWARE) runs the suite that crashes under local xvfb. spec-6-retro-1-first-feature-e2e.md status done. CLI live-sync + export-dialog E2E deferred (DW-91/DW-92 logged). Pinkyd-owned. + epic-6-retro-item-1-first-feature-e2e: done # HIGH — feature E2E green on CI at HEAD 3a76bac (run 27566817463, Linux job): 16/16 incl. trash-lifecycle (P1-E2E-002) AND CLI live-sync (P1-E2E-003). [Corrected 2026-06-16: the earlier note cited HEAD 5be24c7 as the green run, but that commit's CI is FAILURE and predates the live-sync suite (added in 322e570); and the live-sync suite shipped — it was never "deferred". DW-91/DW-92 are trash-suite hygiene follow-ups, not a live-sync deferral.] Specs spec-6-retro-1-first-feature-e2e.md + spec-cli-live-sync-e2e.md status done. Local verification 2026-06-16: 16/16 under xvfb after fixing a modern-WebKitWebDriver socket-rendezvous bug (env-reset; see DW-95) — the suite no longer "crashes under local xvfb". Export-dialog E2E remains the one unbuilt path. Pinkyd-owned. epic-6-retro-item-2-singleflight-guard-helper: done # HIGH — shared singleflight helper extracted (src/lib/singleflight.ts) + all 7 hand-rolled guards migrated (realtimeSync refreshInFlight/refreshQueued, command-palette isCreatingNote/isTrashingNote/isTogglingTheme/isTogglingLayoutMode, export isExporting); spec-singleflight-guard-helper.md status done, merged b6a0c06/86f2ff4. Verified 2026-06-15: acceptance grep clean (zero legacy-guard hits outside singleflight.ts), tsc clean, 572 FE tests pass. Absorbs epic-5-retro-item-2. Pinkyd-owned. epic-6-retro-item-3-per-story-dw-logging: backlog # MEDIUM — log every real out-of-scope review dismissal to deferred-work.md PER STORY as part of the coding-assistant workflow (not swept at retro) epic-6-retro-item-4-close-out-tracking-discipline: done # MEDIUM — reconcile status on close-out not at next retro; Epic 5 items 1/2/4 reconciled this retro