Skip to content

test(cli): serialize the tests that change the process directory - #3494

Merged
kojiwakayama merged 7 commits into
mainfrom
fix/serialize-test-cwd
Aug 9, 2026
Merged

test(cli): serialize the tests that change the process directory#3494
kojiwakayama merged 7 commits into
mainfrom
fix/serialize-test-cwd

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

cli/commands/skills/validate.test.ts failed once on main, on a commit that touched only CSP, and passed on the next run. It resolves a relative path through Deno.chdir, and five test files do the same. The working directory is process-global, so when a shard groups two of them the second chdir lands while the first is still awaiting, and the first resolves against the wrong directory.

Adds withCwd, and moves every process-directory mutation in the suite onto it. No test calls Deno.chdir directly any more.

The queue could not have worked

The first version of this PR queued callers in module state. That cannot serialize anything across files. Four probes against deno test --parallel:

probe result
same process across test files yes — identical Deno.pid
module state shared no — each file gets its own instance
globalThis shared no — each file sees only its own writes
cwd shared yes — file B observed file A's temp dir

Each test file is its own isolate sharing one OS process. cwd is process state; let queue is not. Every file held a private queue and raced every other one — including the one file that had been migrated to it.

The turn is now taken from the operating system, the only mutex those isolates can both see: a directory created with Deno.mkdir, which fails atomically when it already exists. It is keyed on the pid, because the directory it guards is per-process — two concurrent deno test runs have no reason to wait for each other, and no run can inherit a lock from an earlier one. The in-isolate queue stays in front of it so callers within a file keep arrival order.

Restoring no longer reads Deno.cwd()

That read was unsafe under exactly the concurrency it guards. The directory it reports may belong to a sibling, and if the sibling has since removed it, the call throws rather than returning a stale path:

B: Deno.cwd() THREW => NotFound: No such file or directory (os error 2)

This was the bug behind a migration I reverted earlier in this PR: webhook/handler.test.ts passed alone and failed in the suite. That file had grown its own queue and deliberately never read Deno.cwd(), so the code I reverted to was more correct than the helper replacing it. Callers now return to a root derived from the module URL. The afterEach restores are gone for the same reason — reaching for the directory outside a turn takes it from whichever file holds it now.

restores the previous directory was written on that same assumption, capturing Deno.cwd() before the call, and now asserts what holds while a peer is running.

Tests

src/testing/cwd-exclusion-{a,b}.test.ts pin the cross-file property. They come as a pair because one file cannot observe it: inside a single isolate the module queue is already sufficient, so the bug is invisible by construction. Against the previous implementation the pair fails with OVERLAP.

Nested calls now reject with a registered nested-cwd-scope rather than a generic Error, so the contract does not rest on message text.

Notes

  • src/platform/compat/process.test.ts was a fifth chdir site not previously listed; it moved the process to /tmp mid-suite.
  • The error registry is reachable from the RSC client bundle, so adding a slug changes its bytes and generate:manifests:check fails until regenerated.

Full suite green: 3773 passed, 0 failed. lint, lint:test-typecheck, typecheck, fmt --check all clean.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability when operations run from temporary working directories.
    • Prevented conflicting or nested working-directory changes and ensured directories are restored after errors.
  • Documentation

    • Documented the new nested working-directory scope error and updated error references.
  • Tests

    • Expanded coverage for concurrent, nested, and failure scenarios involving working-directory changes.

`cli/commands/skills/validate.test.ts` failed once on main, on a commit that
touched only CSP, and passed on the next run. It resolves a relative path
through `Deno.chdir`, and five CLI test files do the same. The working
directory is process-global, so when a shard groups two of them the second
`chdir` lands while the first is still awaiting, and the first resolves against
the wrong directory. Which files a shard groups decides whether it happens,
which is why it fails rarely and somewhere unrelated to the change under test.

Adds `withCwd`, which queues callers so at most one holds the directory at a
time and each is restored before the next begins, and moves the test that
failed onto it.

The other four files still call `Deno.chdir` directly, so the race is narrowed
rather than closed. `webhook/handler.test.ts` has grown its own queue for this,
which says the hazard was already felt -- and also why a per-file queue is not
the answer, since it orders only its own callers while every other file races
it. I tried moving that file onto the shared helper and it broke a test that
passes in isolation and fails in the full suite, so it is left alone rather
than half-understood. `router.test.ts`, `app/operations/project-creation.test.ts`
and `commands/schedule/handler.test.ts` set the directory in setup and restore
it in a distant `finally`, which means restructuring each test rather than
swapping a helper.

Noted here so the next person does not read one migration as the whole job.
@kojiwakayama
kojiwakayama requested a review from kwakayama as a code owner August 9, 2026 09:23
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c43e89fd-6890-4ed7-91e6-06782eb6ee03

📥 Commits

Reviewing files that changed from the base of the PR and between a7fe6d7 and 48921b3.

⛔ Files ignored due to path filters (1)
  • src/server/services/rsc/endpoints/rsc-bundles.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (14)
  • cli/app/operations/project-creation.test.ts
  • cli/commands/schedule/handler.test.ts
  • cli/commands/webhook/handler.test.ts
  • cli/router.test.ts
  • docs/api-reference/veryfront/errors.md
  • src/errors/error-registry.test.ts
  • src/errors/error-registry/general.ts
  • src/errors/index.ts
  • src/platform/compat/process.test.ts
  • src/testing/cwd-exclusion-a.test.ts
  • src/testing/cwd-exclusion-b.test.ts
  • src/testing/cwd-exclusion-probe.ts
  • src/testing/cwd.test.ts
  • src/testing/cwd.ts

📝 Walkthrough

Walkthrough

The change adds a shared withCwd utility that serializes working-directory changes across tests and Deno isolates. It adds the NESTED_CWD_SCOPE error, migrates affected tests, and adds coverage for restoration, queueing, failures, and cross-file exclusion.

Changes

Current-directory test handling

Layer / File(s) Summary
Queued working-directory utility
src/testing/cwd.ts
Adds local queueing, filesystem locking, nested-call rejection, timeout handling, directory restoration, and queue recovery.
Nested-scope error contract
src/errors/..., docs/api-reference/veryfront/errors.md, src/errors/error-registry.test.ts
Adds and documents NESTED_CWD_SCOPE and updates error-registry counts.
Utility behavior and isolate validation
src/testing/cwd.test.ts, src/testing/cwd-exclusion-*
Tests directory restoration, serialization, queueing, nested calls, callback failures, and cross-file working-directory exclusion.
Working-directory test migrations
cli/..., src/platform/compat/process.test.ts
Replaces manual directory changes and restoration with scoped withCwd calls in affected tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: kwakayama

🚥 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 summarizes the main change: serializing tests that modify the process working directory.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ 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 fix/serialize-test-cwd

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

@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

🤖 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 `@src/testing/cwd.ts`:
- Around line 31-45: The withCwd queue deadlocks when its callback awaits
another withCwd call. Define and implement nested-call behavior in withCwd,
preferably failing fast before enqueueing or otherwise supporting reentrancy,
and add a focused regression test that exercises an awaited nested call.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2bd5018e-e30d-4792-81a5-19bb30d3afd8

📥 Commits

Reviewing files that changed from the base of the PR and between e00217a and c0082b2.

📒 Files selected for processing (2)
  • cli/commands/skills/validate.test.ts
  • src/testing/cwd.ts

Comment thread src/testing/cwd.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0082b2ddb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/testing/cwd.ts
A nested call waited for the queue, which waited for the outer call, which
waited for the inner one, so the suite hung rather than failed. Reentrancy is
not the fix either: the inner chdir would move the directory out from under the
outer caller, which is the hazard this helper exists to prevent. It now rejects
with a named error.

Adds the tests the helper should have had when it was introduced: restore,
serialization of overlapping callers, the nested rejection, and the queue still
advancing after a caller throws.

@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: 2

🤖 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 `@src/testing/cwd.ts`:
- Around line 40-43: Replace the generic error in src/testing/cwd.ts lines 40-43
with the registered VeryfrontError for nested withCwd calls, using its defined
slug. Update src/testing/cwd.test.ts lines 41-45 to assert the VeryfrontError
type and expected slug rather than matching only the message text.
- Around line 39-44: Update withCwd’s ownership tracking so nested-call
rejection is scoped to the current execution context rather than the
process-global held flag; independent callers must wait through the existing
queue while another callback is awaiting. Preserve rejection for true
same-context nesting, and add coverage where one callback signals start, waits
on a gate, and a second caller starts and runs only after the gate opens.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74955929-a53e-4f7f-9d0e-d0200856bea3

📥 Commits

Reviewing files that changed from the base of the PR and between c0082b2 and a7fe6d7.

📒 Files selected for processing (2)
  • src/testing/cwd.test.ts
  • src/testing/cwd.ts

Comment thread src/testing/cwd.ts Outdated
Comment thread src/testing/cwd.ts Outdated
The nested-call guard I added used a process-global flag, which rejects any
caller that arrives while another callback is awaiting -- and that caller is
usually an independent test, for which queueing is exactly the right answer.
Worse than the deadlock it replaced, and my own serialization test could not
see it: both calls were made synchronously before either body ran, so the flag
was still clear.

Ownership is tracked by async context now, so only code running inside a
callback is treated as nested. The regression test starts a callback, waits for
it to signal, then calls from outside while it is still awaiting, and asserts
the second caller runs after the first rather than being rejected. It fails
against the global-flag version.
`withCwd` rejected nested calls with a generic `Error`, which left the
contract resting on message text -- callers wanting to distinguish it from
any other failure had to substring-match the wording.

Registers `nested-cwd-scope` under GENERAL so the condition has a name that
survives rewording. Both count assertions move with it: the total and the
per-category one, which is easy to miss because the failure names only the
category.

The RSC client bundle is regenerated because the error registry is reachable
from it, so any registry entry changes its bytes.
The queue only ever ordered its own file. Under `deno test --parallel` each
test file runs in its own isolate, sharing neither module state nor
`globalThis` with its peers, while the working directory it mutates belongs
to the process they all share. So every file held a private queue and raced
every other one -- the helper could not serialize the failing test against
the peers that actually race it, which is the whole reason it exists.

The turn is now taken from the operating system, the only mutex those
isolates can both see: a directory created with `Deno.mkdir`, which fails
atomically when it already exists. It is keyed on the pid because the
directory it guards is per-process; two concurrent `deno test` runs have no
reason to wait for each other, and no run can inherit a lock from an earlier
one. The in-isolate queue stays in front of it so callers within a file
still take their turn in arrival order.

Restoring no longer reads `Deno.cwd()`. That read was unsafe under exactly
the concurrency this guards: the directory it reports may belong to a
sibling, and if the sibling has since removed it the call throws `NotFound`
outright. Callers return to a root derived from the module URL instead.
That was the bug behind the migration reverted earlier -- it broke a webhook
test that passed alone and failed in the suite.

`restores the previous directory` was written on that same unsafe
assumption, capturing `Deno.cwd()` before the call, so it now asserts what
holds under a peer instead.

The cross-file property needs two test files to be observable at all; inside
one isolate the module queue is already sufficient and the bug is invisible.
Hence the exclusion pair, which fails against the previous implementation.
No test calls `Deno.chdir` directly any more, which is what the helper needs
to be worth having: a queue that half the callers ignore serializes nothing.

`webhook/handler.test.ts` had grown its own queue for this, which is why the
hazard was already felt there. Its queue ordered only its own callers, so it
is replaced rather than kept.

The others held the directory from setup to a distant `finally`, spanning
work that never needed it. Each now scopes it to the call that actually
resolves a relative path, so the turn is held for a command rather than a
test.

The `afterEach` restores are gone too. They were the same hazard in a
quieter form: reaching for the directory outside a turn, which takes it from
whichever file holds it now.

`src/platform/compat/process.test.ts` is included -- it moved the process to
/tmp mid-suite. It tests `chdir` itself, so it takes the turn first and
exercises the call inside it.
`src/errors/index.ts` re-exports every registered error, so adding one makes
`docs/api-reference/veryfront/errors.md` stale and fails `ci (lint)`. The
line pins shift with it; regenerated rather than hand-edited.
@kojiwakayama
kojiwakayama enabled auto-merge August 9, 2026 10:31
@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 9d0938a Aug 9, 2026
31 checks passed
@kojiwakayama
kojiwakayama deleted the fix/serialize-test-cwd branch August 9, 2026 10:46
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