Skip to content

test(daemon-state): isolate the states dir so a parallel daemon's prune can't delete test files - #131

Merged
Rinse12 merged 1 commit into
masterfrom
test/daemon-state-dir-isolation-130
Aug 17, 2026
Merged

test(daemon-state): isolate the states dir so a parallel daemon's prune can't delete test files#131
Rinse12 merged 1 commit into
masterfrom
test/daemon-state-dir-isolation-130

Conversation

@Rinse12

@Rinse12 Rinse12 commented Aug 17, 2026

Copy link
Copy Markdown
Member

Closes #130

Problem

Windows CI on #129 failed a test that branch does not touch:

FAIL test/common-utils/daemon-state.test.ts > writeDaemonState + readAllDaemonStates
     > should write multiple state files
AssertionError: expected [ 9999903 ] to include 9999902

Ubuntu and macOS passed. Note the actual array: pid2's file is there, pid1's is gone. Nothing failed to write — something deleted pid1's file in the 76 ms between writeDaemonState(pid1) and readAllDaemonStates().

Root cause

src/common-utils/daemon-state.ts:9 resolves the states dir once, at import time, from env-paths' data dir:

const DAEMON_STATES_DIR = path.join(defaults.PKC_DATA_PATH, ".daemon_states");

The test imports that dist module in-process without overriding the env, so — unlike update-install-restart-race.test.ts / update-install-systemd-aware.test.ts, which override HOME/XDG_DATA_HOME for the children they spawn — its state files land in the machine-global .daemon_states dir every daemon shares. And the pids it writes (FAKE_PID_BASE = 9999900) are deliberately dead.

Meanwhile fileParallelism: true runs other files concurrently, and every daemon startup prunes that shared dir: src/cli/commands/daemon.ts:349pruneStaleStates()getAliveDaemonStates(), which unlinks the state file of any pid that is not alive. A synthetic 9999xxx pid is exactly that.

The log timing confirms the overlap: when the assertion fired at 06:42:56, daemon.test.ts (finished 06:43:11) and challenge.test.ts (06:43:00) were both mid-flight starting daemons. Windows-only in practice because it is ~3-4x slower there, which widens the window; the race is latent on Linux and macOS too.

Distinct from #94 / #75, which hardened the daemon against losing the prune race and skipped the pid-reuse tests on Windows — neither stopped these tests from being pruned out from under themselves.

Changes

  1. daemon-state.test.ts — point HOME / XDG_DATA_HOME / LOCALAPPDATA / APPDATA at a tempdir (env-paths reads a different one per platform: XDG_DATA_HOME on linux, HOME on macOS, LOCALAPPDATA on windows). Since PKC_DATA_PATH and DAEMON_STATES_DIR are both computed eagerly at module load, the override has to precede the import — so the two dist imports become dynamic. The type-only import stays static (erased at compile time).
  2. daemon-supervisor.test.ts — same hazard at its writeDaemonState round-trip tests (fake pids 9999700/9999701 in the shared dir), same isolation.

Tests

keeps its own state file when another daemon prunes the shared states dir reproduces the race deterministically instead of hoping to catch it: it writes a state file, then runs pruneStaleStates() in a spawned process carrying the real, un-isolated env — exactly what a daemon starting in a parallel test file does — and asserts the file survives.

  • Red before the isolation, with the same failure mode as CI: expected [] to include 9999902
  • Green after

It is also a standing guard: reverting the isolation makes it fail deterministically on every platform, not just under a lost race on Windows.

The captured real env is stored before the override; child_process drops env keys whose value is undefined, so a var that was never set stays unset in the child. Running the pruner against the shared dir is safe — it removes only dead-pid files, the same best-effort cleanup any daemon start performs.

Verification

  • npm run build && npm run build:test — clean
  • npm run test:cli — 43 files, 339 passed, 1 skipped

Summary by CodeRabbit

  • Tests
    • Improved daemon-state test isolation by using temporary, platform-specific data directories.
    • Added regression coverage to ensure one daemon’s cleanup does not remove another daemon’s valid state.
    • Prevented daemon supervisor tests from sharing machine-global state files.

…ne can't delete test files

The daemon-state tests write state files for synthetic DEAD pids, and the dist
module resolves DAEMON_STATES_DIR once, at import time, from env-paths' data
dir. Left at its default that dir is machine-global: every daemon startup prunes
dead-pid files there (daemon.ts -> pruneStaleStates), so a daemon started by any
test file running in parallel deleted these files mid-test. Windows CI, being
slower, lost the race and failed "should write multiple state files" with
`expected [ 9999903 ] to include 9999902` — pid1's file pruned between the two
writes and the read.

Point HOME / XDG_DATA_HOME / LOCALAPPDATA / APPDATA at a tempdir before
importing the dist module (env-paths reads a different one per platform).
PKC_DATA_PATH and DAEMON_STATES_DIR are both computed eagerly at module load, so
the override has to precede the import — hence the dynamic imports.
daemon-supervisor.test.ts had the same hazard and gets the same isolation.

The regression test reproduces the race deterministically: it writes a state
file, then runs pruneStaleStates() in a spawned process carrying the real,
un-isolated env — exactly what a daemon starting in a parallel test file does —
and asserts the file survives. Red before the isolation (`expected [] to include
9999902`), green after.

Refs #130
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a5121139-7998-42d2-a85d-ef5c1c6d5f0b

📥 Commits

Reviewing files that changed from the base of the PR and between 00b5a3d and 11da835.

📒 Files selected for processing (2)
  • test/common-utils/daemon-state.test.ts
  • test/common-utils/daemon-supervisor.test.ts

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The tests now isolate daemon state in temporary platform-specific data directories before importing eagerly initialized modules. A child-process regression test runs stale-state pruning against the shared directory and confirms that isolated test state remains available.

Changes

Daemon test isolation

Layer / File(s) Summary
Isolate daemon-state tests
test/common-utils/daemon-state.test.ts
The tests configure temporary data paths before dynamic module loading. They run shared-directory pruning in a child process and verify that isolated state files remain present.
Isolate daemon-supervisor tests
test/common-utils/daemon-supervisor.test.ts
The setup configures temporary platform data paths before dynamically importing daemon-state utilities.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 11da8

This change isolates daemon test state files to prevent parallel tests from deleting one another’s fixtures; it is merge-ready after normal checks, with no actionable merge-blocking risk remaining.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes isolating daemon state files to prevent parallel pruning from deleting test files.
Linked Issues check ✅ Passed The changes address issue #130 by isolating both test suites and adding a regression test for the pruning race.
Out of Scope Changes check ✅ Passed All described changes support the linked issue objectives and remain within the scope of daemon-state test isolation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 test/daemon-state-dir-isolation-130

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

test/common-utils/daemon-state.test.ts

Oops! Something went wrong! :(

ESLint: 8.27.0

Error: ESLint configuration in --config » eslint-config-oclif is invalid:

  • Unexpected top-level property "__esModule".

Referenced from: /.eslintrc
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2156:19)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2998:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2963:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3264:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3135:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3074:25)
at _normalizeObjectConfigDataBody.next ()
at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:20)
at _normalizeObjectConfigData.next ()
at ConfigArrayFactory.loadFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2829:16)

test/common-utils/daemon-supervisor.test.ts

Oops! Something went wrong! :(

ESLint: 8.27.0

Error: ESLint configuration in --config » eslint-config-oclif is invalid:

  • Unexpected top-level property "__esModule".

Referenced from: /.eslintrc
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2156:19)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2998:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2963:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3264:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3135:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3074:25)
at _normalizeObjectConfigDataBody.next ()
at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:20)
at _normalizeObjectConfigData.next ()
at ConfigArrayFactory.loadFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2829:16)


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.

@Rinse12
Rinse12 merged commit 846685c into master Aug 17, 2026
4 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.

test: daemon-state tests write into the machine-global .daemon_states dir, so a parallel daemon's startup prune deletes their files (Windows CI flake)

1 participant