Skip to content

chore(e2e): stop and reap leaked isolated Obsidian E2E instances#1368

Merged
chhoumann merged 2 commits into
masterfrom
chhoumann/e2e-autocleanup-orca
Jun 16, 2026
Merged

chore(e2e): stop and reap leaked isolated Obsidian E2E instances#1368
chhoumann merged 2 commits into
masterfrom
chhoumann/e2e-autocleanup-orca

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 16, 2026

Copy link
Copy Markdown
Owner

What & why

Ports the isolated-Obsidian-E2E auto-cleanup feature that PodNotes shipped in 13ab5eb into QuickAdd. Today, removing/merging a worktree leaves its isolated Obsidian instance running — leaking a whole Obsidian process tree plus a /tmp/quickadd-obsidian-e2e/<vault>-<hash>/ profile dir. This adds three layers so that never happens, and a manual escape hatch.

Tooling only — zero plugin behavior change (no src/** touched).

What changed

  • scripts/stop-obsidian-e2e-instance.mjs (new) + pnpm run stop:e2e-obsidian: stops this worktree's instance — finds the Obsidian process tree bound to its private --user-data-dir token (a per-worktree 12-hex hash), terminates it (SIGTERM → grace → SIGKILL for stragglers), and removes its profile dir. Flags: --dry-run, --prune (also reap orphans), --json. Exposes reapOrphanedInstances which reaps only instances whose backing worktree is GONE.
  • scripts/start-obsidian-e2e-instance.mjs + scripts/obsidian-e2e-cli.mjs: write a quickadd-e2e-instance.json marker (records worktreePath) in prepareObsidianProfile, and reap-on-start — a best-effort self-healing pass that runs before launch/reuse and never blocks a start (lazy import() keeps the module graph acyclic).
  • orca.yaml (new): archive hook runs the teardown on orca worktree rm --run-hooks.
  • Tests: tests/stop-obsidian-e2e-instance.test.ts (new, mirrors the reference; 23 cases) + a marker-producer assertion in tests/start-obsidian-e2e-instance.test.ts. (Placed in tests/ per QuickAdd's existing convention for e2e-script tests.)
  • AGENTS.md: documents the stop/reap workflow.

Safety — only ever this worktree, only ever a gone worktree

  • Kill scope: matches solely the --user-data-dir=<instancePath>/ flag (trailing slash → no prefix bleed), in both /tmp and /private/tmp (macOS firmlink) forms. The shared dev vault (~/Library/Application Support/obsidian) isn't even under the profile root; sibling worktrees and PodNotes instances have different tokens → excluded.
  • rm scope: assertSafeInstancePath requires an absolute path whose basename matches -[0-9a-f]{12}$ and is a direct child of the profile root — refuses /tmp, relative paths, or anything outside the e2e tree.
  • Reap signal: primary = marker says the worktree is gone (reaps even a leaked-but-running instance); fallback for pre-marker instances = every registered vault path is gone; stays conservative (spares) when state is unreadable; never reaps the current instance.

Verification (in this worktree's own isolated vault, hermetic private profile root)

Gates: tsc ✓ · pnpm run lint ✓ · pnpm run check ✓ (0 errors) · pnpm test2394 passed / 31 skipped (incl. 23 new + marker-producer).

End-to-end (real Obsidian, evidence captured):

  1. Stop — started a real instance (4-proc tree: main on /tmp, helpers on /private/tmp); stop:e2e-obsidian killed the whole tree and removed the profile dir. Dev vault + 4 sibling-worktree instances untouched; shared root unchanged (16 dirs).
  2. Reap-on-start — seeded an orphan (marker worktree GONE), a live instance (marker worktree EXISTS), and a non-instance dir; start reaped only the orphan, spared the live one + the non-instance dir + my own instance.
  3. Orca hook — ran the exact orca.yaml archive command (cwd=worktree, $ORCA_WORKTREE_PATH=worktree); it invoked the stop script and tore down the running instance.
  4. Real-data classifier scan (read-only) over the live shared root correctly flagged 10 worktree-gone instances as ORPHAN (incl. two leaked-but-running ones) while sparing every live worktree and the dev vault.

Review

Reviewed by an ultracode multi-lens pass + 2 opposite-model (Codex) adversarial reviewers (Skeptic + Architect) → both CONTESTED, no high-severity. Accepted fixes: moved the test to tests/ (QuickAdd convention) and dropped a redundant vitest-include change; added the marker-producer test. Findings rejected as by-design parity with the reference (and documented): the pre-marker vault-path fallback, ps-snapshot PID-reuse TOCTOU, symlink path-vs-identity, matcher-not-name-scoped, the pre-existing start always-open -n behavior, and the lazy-import seam.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a command to stop isolated Obsidian E2E instances, with optional pruning of orphaned instances.
    • Improved E2E startup by automatically reaping stale instances to reduce process and disk resource buildup.
    • Added an Orca lifecycle hook to trigger cleanup during worktree archiving.
  • Documentation

    • Updated instructions on safely stopping E2E Obsidian instances and preventing temp profile leaks.
  • Tests

    • Added/expanded coverage for instance shutdown, orphan detection, safety checks, and cleanup behavior.

Add stop:e2e-obsidian + reap-on-start (worktree-gone marker) + an orca.yaml
archive hook so removed/merged worktrees no longer leak an Obsidian process
tree or a /tmp profile dir.

- scripts/stop-obsidian-e2e-instance.mjs: stop THIS worktree's instance
  (terminate its process tree found via the private --user-data-dir token,
  SIGTERM->SIGKILL, then remove its profile dir) + reapOrphanedInstances which
  reaps only instances whose backing worktree is GONE. Path guards refuse to rm
  anything that is not a direct child of the profile root matching the instance
  hash pattern.
- start/cli: write a quickadd-e2e-instance.json marker recording the worktree,
  and reap stale instances on the next start/run (best-effort, never blocks).
- orca.yaml archive hook runs the teardown on `orca worktree rm --run-hooks`.

Targets only this worktree's instance; never touches the shared dev vault,
other live worktrees, or PodNotes. Tooling only; no plugin behavior change.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09b44fb8-370d-40bd-b4bc-cd6240055649

📥 Commits

Reviewing files that changed from the base of the PR and between 7d94c7d and 2e1acfb.

📒 Files selected for processing (1)
  • tests/stop-obsidian-e2e-instance.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/stop-obsidian-e2e-instance.test.ts

📝 Walkthrough

Walkthrough

Adds a complete Obsidian E2E instance teardown system: a new stop-obsidian-e2e-instance.mjs CLI kills the instance process tree and removes its temp profile directory, detects and reaps orphaned instances, and a marker file written at profile creation enables orphan detection. Reaping runs automatically on next start and via a new Orca archive lifecycle hook.

Changes

Obsidian E2E Teardown System

Layer / File(s) Summary
Instance marker file
scripts/start-obsidian-e2e-instance.mjs, tests/start-obsidian-e2e-instance.test.ts
Exports INSTANCE_MARKER_FILE constant and writes quickadd-e2e-instance.json containing worktreePath, vaultName, and vaultPath into the instance root during prepareObsidianProfile. Test asserts the sidecar is written with correct fields.
Stop script: arg parsing, process-tree discovery, and stopInstance
scripts/stop-obsidian-e2e-instance.mjs, tests/stop-obsidian-e2e-instance.test.ts
New CLI parses options, matches Obsidian processes to a specific instance via --user-data-dir (with macOS /private prefix normalization), walks the PID tree via ps, validates instance paths with safety guards, and implements SIGTERM → poll → SIGKILL → fs.rm in stopInstance. Tests cover escalation, dry-run, path-safety rejection, no-process cleanup, EPERM tolerance, and profile-root scoping.
Orphan detection and reapOrphanedInstances
scripts/stop-obsidian-e2e-instance.mjs, tests/stop-obsidian-e2e-instance.test.ts
Adds readInstanceVaultPaths, readInstanceMarker, and isInstanceOrphaned (prefers marker worktree check, falls back to vault path existence). reapOrphanedInstances scans the profile root and calls stopInstance per orphaned directory with per-instance error isolation. Tests cover selective reaping, failure-continues behavior, unreadable-registration conservatism, and missing profile root.
Reap-on-start integration, CLI main, and lifecycle wiring
scripts/start-obsidian-e2e-instance.mjs, scripts/obsidian-e2e-cli.mjs, scripts/stop-obsidian-e2e-instance.mjs, package.json, orca.yaml, AGENTS.md
Exports reapStaleInstances (lazy-imports reaper, suppresses failures) and calls it before provisioning in both start scripts. Stop script main() resolves paths, stops the target, optionally prunes, and emits JSON or human-readable output. package.json adds stop:e2e-obsidian, orca.yaml adds a node-guarded archive hook, and AGENTS.md documents teardown commands and automatic defenses.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • chhoumann/quickadd#1354: Introduced obsidian-e2e-cli.mjs and the start-obsidian-e2e-instance.mjs flow that this PR extends with reapStaleInstances and the pre-start cleanup call.

Poem

🐇 Hop hop, the instances scatter and flee,
Old temp dirs piling up under /private/tmp tree,
But now a brave script hunts each stale PID,
SIGTERM, then SIGKILL — no ghost goes unhid!
The marker file whispers "my worktree is gone,"
The reaper arrives, and the cleanup rolls on. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding functionality to stop and reap leaked Obsidian E2E instances, which is the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chhoumann/e2e-autocleanup-orca

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 16, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2e1acfb
Status: ✅  Deploy successful!
Preview URL: https://f295722f.quickadd.pages.dev
Branch Preview URL: https://chhoumann-e2e-autocleanup-or.quickadd.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/stop-obsidian-e2e-instance.test.ts (1)

351-353: ⚡ Quick win

Use the shared marker constant instead of duplicating the filename literal.

Hardcoding "quickadd-e2e-instance.json" here can drift from the producer/consumer contract. Reuse INSTANCE_MARKER_FILE to keep tests aligned automatically.

♻️ Proposed refactor
+import { INSTANCE_MARKER_FILE } from "../scripts/start-obsidian-e2e-instance.mjs";
...
-			path.join(spared, "quickadd-e2e-instance.json"),
+			path.join(spared, INSTANCE_MARKER_FILE),
...
-			path.join(leaked, "quickadd-e2e-instance.json"),
+			path.join(leaked, INSTANCE_MARKER_FILE),

Also applies to: 362-364

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/stop-obsidian-e2e-instance.test.ts` around lines 351 - 353, The code
uses a hardcoded filename string "quickadd-e2e-instance.json" instead of the
shared constant INSTANCE_MARKER_FILE, which can cause drift between producer and
consumer contracts. Replace the hardcoded string literal with the
INSTANCE_MARKER_FILE constant in the path.join call at lines 351-353 and also at
lines 362-364 where this same pattern appears, ensuring all references to the
instance marker file use the same constant throughout the test file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/start-obsidian-e2e-instance.mjs`:
- Line 355: The reapStaleInstances call is being awaited at the startup point in
scripts/start-obsidian-e2e-instance.mjs, which blocks the entire startup process
and causes launch latency to grow with orphan count. Remove the await keyword
from the reapStaleInstances(options) call so it runs asynchronously in the
background without blocking startup. Additionally, check
scripts/obsidian-e2e-cli.mjs at line 173 for a similar await on reaping that
should also be converted to fire-and-forget behavior to ensure non-blocking
startup throughout the codebase.

---

Nitpick comments:
In `@tests/stop-obsidian-e2e-instance.test.ts`:
- Around line 351-353: The code uses a hardcoded filename string
"quickadd-e2e-instance.json" instead of the shared constant
INSTANCE_MARKER_FILE, which can cause drift between producer and consumer
contracts. Replace the hardcoded string literal with the INSTANCE_MARKER_FILE
constant in the path.join call at lines 351-353 and also at lines 362-364 where
this same pattern appears, ensuring all references to the instance marker file
use the same constant throughout the test file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b845ec2b-69cf-428f-a6c9-465bfc66a895

📥 Commits

Reviewing files that changed from the base of the PR and between 273e4c0 and 7d94c7d.

📒 Files selected for processing (8)
  • AGENTS.md
  • orca.yaml
  • package.json
  • scripts/obsidian-e2e-cli.mjs
  • scripts/start-obsidian-e2e-instance.mjs
  • scripts/stop-obsidian-e2e-instance.mjs
  • tests/start-obsidian-e2e-instance.test.ts
  • tests/stop-obsidian-e2e-instance.test.ts

Comment thread scripts/start-obsidian-e2e-instance.mjs
Addresses CodeRabbit review on #1368: replace the hardcoded
quickadd-e2e-instance.json literal with the shared INSTANCE_MARKER_FILE
export so the test cannot drift from the producer/consumer contract.
@chhoumann chhoumann merged commit a3e0d88 into master Jun 16, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant