From 3ddb110caece0a134efd7ef4f456fda73df73013 Mon Sep 17 00:00:00 2001 From: dietrich ayala Date: Mon, 14 Sep 2026 12:17:06 +0200 Subject: [PATCH] Tag background test log/staging paths per worktree, so concurrent runs don't collide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The :bg test family wrote to fixed /tmp paths (test-electron.log, its .exit/.pid siblings, test-spec, test-grep-pattern). Two worktrees — or a worktree and the primary checkout — running the gate at the same time silently overwrote each other's output, so a run could report another checkout's result. scripts/tagged-path.sh derives a suffix from the calling checkout's own worktree root (basename of `git rev-parse --show-toplevel`) and prints the tagged path for a given basename, so the path is deterministic from repo location alone — no environment variable has to be set or exported for it to resolve, unlike ${TMPDIR:-/tmp} alone. apps/desktop/package.json's :bg scripts and test:log now resolve their paths through it; scripts/bg-run.sh stays path-agnostic. Docs citing the old literal /tmp/test-electron.log path are updated to point at yarn test:log / scripts/tagged-path.sh instead. Claude-Session: https://claude.ai/code/session_017eGkdjW7RoMwGCssQpLgfS --- .claude/skills/test/SKILL.md | 7 ++-- README.md | 2 +- apps/desktop/CLAUDE.md | 17 +++++---- apps/desktop/features/hello-world/home.html | 2 +- apps/desktop/main/tile-ipc-gate.ts | 2 +- apps/desktop/package.json | 10 +++--- notes/desktop-test-flakes-xvfb.md | 2 +- scripts/bg-run.sh | 7 ++++ scripts/tagged-path.sh | 39 +++++++++++++++++++++ 9 files changed, 70 insertions(+), 18 deletions(-) create mode 100755 scripts/tagged-path.sh diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md index 2726d825..23a48668 100644 --- a/.claude/skills/test/SKILL.md +++ b/.claude/skills/test/SKILL.md @@ -10,7 +10,7 @@ When the user invokes /test: | Argument | Command | Notes | |----------|---------|-------| - | (none) or `full` / `desktop` | `yarn test:desktop:electron:bg` | **Canonical full suite — all 6 runners, ~3300+ tests**, ~10 min, bg, logs to `/tmp/test-electron.log` | + | (none) or `full` / `desktop` | `yarn test:desktop:electron:bg` | **Canonical full suite — all 6 runners, ~3300+ tests**, ~10 min, bg, tail with `yarn test:log` | | `unit` | `yarn test:unit` | unit runners only (main-process + node, ~2600), background. A *subset* of the full suite | | `grep` / single | `yarn test:grep ""` | one test, source-only, fast inner loop | | `tauri` | `yarn test:desktop:tauri` | off-critical-path Tauri desktop full suite (frontend + Rust) | @@ -26,7 +26,10 @@ When the user invokes /test: - **NEVER run `npx playwright test` directly** — it steals focus and blocks the user - The full gate is **`yarn test:desktop:electron`** (or `:bg`). It runs the **packaged** build (`electron-builder --dir`) and **all six runners** — unit, components, and editor included. The Playwright `desktop`+`desktop-serial` pair alone (~549) is **~17% of the suite**, not the suite; never report it as "full". -- Always use `yarn test:desktop:electron:bg` for the full background run (logs to `/tmp/test-electron.log`); read that log with the Read tool. +- Always use `yarn test:desktop:electron:bg` for the full background run. Its log path is + tagged per checkout (`scripts/tagged-path.sh test-electron.log` prints it, or run + `yarn test:log` to tail it directly) so concurrent runs from different worktrees don't + share one log; read the log with the Read tool. - Use `yarn test:desktop:electron:x` if the user wants stop-on-first-failure mode. - Source-only escape hatch (no packaging, runners 3+4 only): `yarn test:desktop:electron:source`. Single-test fast iteration: `yarn test:grep ""` (source). - **`test:desktop:electron:spec` / `:spec:bg` build nothing.** They run `PACKAGED=1` against whatever `electron-builder` last produced, so after a source edit they exercise a stale binary and "the fix didn't work" is indistinguishable from "the fix was never in the build". Run `yarn workspace @peek/desktop build && npx electron-builder --dir` first, or use `yarn test:grep`, which rebuilds from source. Also note some failures reproduce only under the full desktop project's load and ordering — a green single-file run does not clear a failure the gate found. diff --git a/README.md b/README.md index 7ff899c2..55de45c1 100644 --- a/README.md +++ b/README.md @@ -409,7 +409,7 @@ peek/ **Lex** — Lexicon feature: UI logic and background processing. -Background test commands: `yarn test:desktop:electron:bg` runs the full desktop (electron) gate headless in background (logs to `/tmp/test-electron.log`). +Background test commands: `yarn test:desktop:electron:bg` runs the full desktop (electron) gate headless in background (tail the log with `yarn test:log`). ### Notes diff --git a/apps/desktop/CLAUDE.md b/apps/desktop/CLAUDE.md index 24bccf76..fe074bbc 100644 --- a/apps/desktop/CLAUDE.md +++ b/apps/desktop/CLAUDE.md @@ -122,8 +122,9 @@ anchoring each project to its own `tests//`. ```bash yarn test:desktop:electron # CANONICAL: full build + all 6 runners (~3190) -yarn test:desktop:electron:bg # same, backgrounded → tail /tmp/test-electron.log - # pass/fail = /tmp/test-electron.exit, NOT the :bg exit code +yarn test:desktop:electron:bg # same, backgrounded → tail with yarn test:log + # pass/fail = the log's .exit sentinel, NOT the :bg exit code + # (path is worktree-tagged: scripts/tagged-path.sh test-electron.log) yarn test:grep "" # Fast inner-loop: source-only, single test yarn test:desktop:electron:source # Escape hatch: source-only, no packaging (runners 3+4 only) yarn test:desktop:electron:x # Packaged desktop project, stop on first failure @@ -320,15 +321,17 @@ the below was covered — every one became a user-visible regression. Invariants the decision/state via the hybrid test bridge, not real focus. Coverage added: `hybrid-show-activate`, `hybrid-activation-policy`, `hybrid-close-shortcut`, `hybrid-shortcut-routing`. The `:bg` test family detaches cross-platform via -`scripts/bg-run.sh` (macOS `nohup` / Linux `setsid -f`), backgrounding the gate to -`/tmp/test-electron.log` (tail with `yarn test:log`). +`scripts/bg-run.sh` (macOS `nohup` / Linux `setsid -f`), backgrounding the gate to a log +(tail with `yarn test:log`) at a path tagged per checkout by `scripts/tagged-path.sh` — +`scripts/tagged-path.sh test-electron.log` prints it — so two worktrees (or a worktree and +the primary checkout) running the gate at once don't share one log. **A `:bg` launch returning 0 means the run STARTED, not that it passed.** The script's own status can only report the launch; the run outlives it. To judge a `:bg` run: -- **`/tmp/test-electron.exit`** — the sentinel: the gate's real exit code, written by the - child on completion. Absent = still running. This is the machine-readable pass/fail; - don't grep the log for "passed". +- **The log's `.exit` sentinel** (`scripts/tagged-path.sh test-electron.exit`) — the gate's + real exit code, written by the child on completion. Absent = still running. This is the + machine-readable pass/fail; don't grep the log for "passed". - The log always opens with `bg-run: STARTED @` and closes with `bg-run: EXIT `. A log with STARTED but no EXIT is still in flight; an empty log means the launcher itself never ran. (Before those banners an empty-looking log was indistinguishable from a quiet diff --git a/apps/desktop/features/hello-world/home.html b/apps/desktop/features/hello-world/home.html index 4f0c11a0..db4f5dab 100644 --- a/apps/desktop/features/hello-world/home.html +++ b/apps/desktop/features/hello-world/home.html @@ -86,7 +86,7 @@ api.commands.register({

Unit tests — pure logic, no Electron needed:

yarn test:unit

Electron integration tests — full app, headless:

-
yarn test:electron:bg   # background, logs to /tmp/test-electron.log
+        
yarn test:electron:bg   # background, tail the log with yarn test:log
 yarn test:electron      # foreground headless

Minimal Playwright test for a tile command:

test('hello world command shows window', async ({ page }) => {
diff --git a/apps/desktop/main/tile-ipc-gate.ts b/apps/desktop/main/tile-ipc-gate.ts
index ec35d853..a8778887 100644
--- a/apps/desktop/main/tile-ipc-gate.ts
+++ b/apps/desktop/main/tile-ipc-gate.ts
@@ -208,7 +208,7 @@ export function emitGateRejected(reason: GateRejectReason, ctx: GateRejectContex
     // Never propagate telemetry failures — drop silently + log.
     console.error('[tile-ipc-gate] Failed to publish gate:rejected:', err);
   }
-  // Also mirror to console.warn so /tmp/test-electron.log carries the
+  // Also mirror to console.warn so the :bg test run's log carries the
   // full drop context (rate-limited publishes hide this from pubsub
   // subscribers that arrived late).
   console.warn(`[tile-ipc-gate] gate:rejected reason=${reason} op=${ctx.op} tileId=${ctx.tileId ?? ''}`);
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 7cddf84f..dffc4c88 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -24,7 +24,7 @@
     "test:packaged:debug": "cd ../.. && yarn kill:packaged && HEADLESS=1 PACKAGED=1 DEBUG=1 ./scripts/playwright-with-display.sh test --config apps/desktop/playwright.config.ts --project=desktop",
     "test:desktop:disruptive": "cd ../.. && ./scripts/timed.sh sh -c 'yarn workspace @peek/desktop build && HEADLESS=1 BACKEND=electron ./scripts/playwright-with-display.sh test --config apps/desktop/playwright.config.ts --project=desktop-disruptive --workers=1'",
     "test:default-browser:linux": "cd ../.. && ./scripts/timed.sh sh -c 'yarn workspace @peek/desktop build && yarn workspace @peek/desktop build:electron:dist && ./scripts/smoke-default-browser.sh'",
-    "test:default-browser:linux:bg": "../../scripts/bg-run.sh /tmp/smoke-default-browser.log yarn test:default-browser:linux",
+    "test:default-browser:linux:bg": "../../scripts/bg-run.sh \"$(../../scripts/tagged-path.sh smoke-default-browser.log)\" yarn test:default-browser:linux",
     "test:unit": "../../scripts/timed.sh sh -c 'yarn build && NODE_ENV=test ELECTRON_RUN_AS_NODE=1 npx electron --test dist/main/*.test.js && NODE_ENV=test node --test tests/unit/*.test.js'",
     "test:unit:modes": "../../scripts/timed.sh sh -c 'yarn build && ELECTRON_RUN_AS_NODE=1 npx electron --test dist/main/modes.test.js'",
     "test:unit:shortcuts": "../../scripts/timed.sh sh -c 'yarn build && ELECTRON_RUN_AS_NODE=1 npx electron --test dist/main/shortcuts.test.js'",
@@ -34,10 +34,10 @@
     "test:grep": "../../scripts/timed.sh sh -c 'yarn build && cd ../.. && HEADLESS=1 BACKEND=electron ./scripts/playwright-with-display.sh test --config apps/desktop/playwright.config.ts apps/desktop/tests/desktop/ --grep \"$0\"'",
     "test:desktop:electron:grep": "cd ../.. && HEADLESS=1 PACKAGED=1 BACKEND=electron ./scripts/playwright-with-display.sh test --config apps/desktop/playwright.config.ts apps/desktop/tests/desktop/ --reporter=line --grep \"$0\"",
     "test:desktop:electron:spec": "cd ../.. && HEADLESS=1 PACKAGED=1 BACKEND=electron ./scripts/playwright-with-display.sh test --config apps/desktop/playwright.config.ts --reporter=line \"$0\"",
-    "test:desktop:electron:spec:bg": "../../scripts/bg-run.sh /tmp/test-electron.log sh -c 'read SPEC < /tmp/test-spec && exec yarn test:desktop:electron:spec \"$SPEC\"'",
-    "test:desktop:electron:bg": "../../scripts/bg-run.sh /tmp/test-electron.log yarn test:desktop:electron",
-    "test:desktop:electron:grep:bg": "../../scripts/bg-run.sh /tmp/test-electron.log sh -c 'read PATTERN < /tmp/test-grep-pattern && exec yarn test:desktop:electron:grep \"$PATTERN\"'",
-    "test:log": "tail -f /tmp/test-electron.log",
+    "test:desktop:electron:spec:bg": "../../scripts/bg-run.sh \"$(../../scripts/tagged-path.sh test-electron.log)\" sh -c 'read SPEC < \"$(../../scripts/tagged-path.sh test-spec)\" && exec yarn test:desktop:electron:spec \"$SPEC\"'",
+    "test:desktop:electron:bg": "../../scripts/bg-run.sh \"$(../../scripts/tagged-path.sh test-electron.log)\" yarn test:desktop:electron",
+    "test:desktop:electron:grep:bg": "../../scripts/bg-run.sh \"$(../../scripts/tagged-path.sh test-electron.log)\" sh -c 'read PATTERN < \"$(../../scripts/tagged-path.sh test-grep-pattern)\" && exec yarn test:desktop:electron:grep \"$PATTERN\"'",
+    "test:log": "tail -f \"$(../../scripts/tagged-path.sh test-electron.log)\"",
     "//-- Manual debugging harness --//": "",
     "harness": "../../scripts/timed.sh sh -c 'yarn build && cd ../.. && BACKEND=electron HEADLESS=0 npx playwright test --config apps/desktop/playwright.config.ts --project=manual --workers=1 --headed'",
     "harness:stop": "mkdir -p ../../tmp && touch ../../tmp/harness-stop",
diff --git a/notes/desktop-test-flakes-xvfb.md b/notes/desktop-test-flakes-xvfb.md
index 4ea957f2..fb5d9e50 100644
--- a/notes/desktop-test-flakes-xvfb.md
+++ b/notes/desktop-test-flakes-xvfb.md
@@ -140,7 +140,7 @@ trigger.
 
 ```
 yarn test:electron:bg                      # full chain (desktop + serial)
-tail -F /tmp/test-electron.log
+yarn test:log                              # tail the run's log
 
 # or isolated:
 HEADLESS=1 BACKEND=electron ./scripts/playwright-with-display.sh \
diff --git a/scripts/bg-run.sh b/scripts/bg-run.sh
index a8f5d962..6026e14b 100755
--- a/scripts/bg-run.sh
+++ b/scripts/bg-run.sh
@@ -32,6 +32,13 @@
 #   e.g. scripts/bg-run.sh /tmp/test-electron.log yarn test:desktop:electron
 # Side files, derived from : .exit (real exit code, written on
 # completion) and .pid (child pid, for the liveness check).
+#
+#  is taken as-is — this script has no opinion on the path. Callers (e.g.
+# apps/desktop/package.json's `:bg` scripts) tag it via scripts/tagged-path.sh, which
+# derives a suffix from the calling checkout's own worktree root, so two worktrees (or a
+# worktree and the primary checkout) running the same gate at the same time don't share a
+# log or a staging file. Do not flatten those tagged paths back to constants — that's what
+# let one run's output silently clobber another's.
 set -euo pipefail
 
 # ---------------------------------------------------------------------------
diff --git a/scripts/tagged-path.sh b/scripts/tagged-path.sh
new file mode 100755
index 00000000..9834273b
--- /dev/null
+++ b/scripts/tagged-path.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# tagged-path.sh — print the worktree-tagged /tmp path a :bg script (or a caller staging
+# its input) uses in THIS checkout.
+#
+# Concurrent :bg runs from different worktrees (or the primary checkout) used to share one
+# fixed /tmp path, so whichever run finished last silently overwrote the other's log and
+# exit/pid sentinels. Each caller tags its path with the checkout's own identity instead:
+#
+#   tag="$(basename "$(git rev-parse --show-toplevel)")"
+#
+# — the worktree directory's basename, or the primary checkout's directory name. That's
+# deterministic from repo location alone: no env var has to be set or exported for it to
+# resolve correctly, so it holds for the primary checkout and for any shell that never
+# inherited a runtime namespace, which `${TMPDIR:-/tmp}` alone would not — and unlike
+# $TMPDIR, it isn't redirected to a per-user directory on macOS, so agents reading the log
+# with the Read tool can predict the path without inspecting the environment.
+#
+# Usage: scripts/tagged-path.sh 
+#   scripts/tagged-path.sh test-electron.log   ->  /tmp/test-electron-.log
+#   scripts/tagged-path.sh test-spec           ->  /tmp/test-spec-
+# The tag is inserted before the last extension when  has one, else appended.
+set -euo pipefail
+
+if [ "$#" -ne 1 ]; then
+  echo "usage: tagged-path.sh " >&2
+  exit 2
+fi
+
+tag="$(basename "$(git rev-parse --show-toplevel)")"
+name="$1"
+
+case "$name" in
+  *.*)
+    echo "/tmp/${name%.*}-${tag}.${name##*.}"
+    ;;
+  *)
+    echo "/tmp/${name}-${tag}"
+    ;;
+esac
-- 
knotmirror 0.0.1