Skip to content

fix(tests): live-e2e の scratch 削除を検証付きへ — rmSync force:true の無音 no-op を解消 (#2154) - #2548

Merged
j5ik2o merged 5 commits into
mainfrom
fix-2154-verified-removal
Aug 8, 2026
Merged

fix(tests): live-e2e の scratch 削除を検証付きへ — rmSync force:true の無音 no-op を解消 (#2154)#2548
j5ik2o merged 5 commits into
mainfrom
fix-2154-verified-removal

Conversation

@j5ik2o

@j5ik2o j5ik2o commented Aug 8, 2026

Copy link
Copy Markdown
Member

概要

live-e2e scratch ツリーの削除が並行 remover 競合時に無音 no-op になる欠陥(#2154)を、検証付き削除ヘルパーで解消する。

Closes #2154

機序(クロスレビュー2名で確定済み)

rmSync(root, {recursive:true, force:true}) は、並行 remover が walk と unlink の間にエントリを消したときの一過性 ENOENT を握り潰し、ツリーを残したまま正常 return する。呼び出し元は削除成功を検証していないため、t-codex-exec-live-helper のロールバック検証が確率的に赤(reviewer-1 の再現: 88/1000 @ remover 4本)。

変更内容(tests/ のみ、packages/ 無変更)

  1. tests/harness/live-e2e/testing/remove-tree-verified.ts(新設): remove → existsSync 検証 → 10ms 待機で最大3回 → 残存なら loud throw。非 ENOENT(EACCES 等)は伝播。様式は kiro-tui.ts #removeScratch(既存の実証済み async 版)の同期形
  2. 9 callsite を置換: codex-exec-live.ts + live-e2e の claude-sdk / codex×2 / claude-tui / claude×2 / kiro / testing/fakeskiro-tui は既に検証付きのため不変更)
  3. rollback 経路の元エラーマスク解消: catch (error) { cleanup(); throw error; } 形 5 callsite で、cleanup 失敗時に AggregateError([setupError, cleanupError]) — 元エラーも cleanup 失敗もどちらも失わない
  4. テスト: t-remove-tree-verified.test.ts(happy / 冪等 / 削除不能ツリーで loud 失敗)+ t-codex-exec-live-helper へ AggregateError 回帰ケース追加

検証

  • TDD 赤→緑: ヘルパー未実装で module 不在の赤 → 実装後 3 pass。AggregateError は fix SHA 明示の checkout 限定切替で pre-fix 赤を実測(赤の逐語が「cleanup の EACCES が元の setup エラーを置換していた」ことを直接示す)→ 復元後 5 pass・残渣ゼロ(git diff 空)
  • 統計的落ちる実証(scratch、barrier 同期 remover 4本 × 300 trial): 素の rmSync(force:true) = 265/300 無音残存 / ヘルパー = 0/300(8件は retry 枯渇の loud throw — 無音でない失敗は契約どおり)
  • t-remove-tree-verified / t-codex-exec-live-helper / typecheck / lint 全て exit 0、consumer integration 11ファイル 93 pass / 0 fail
  • 同型の全数棚卸し(grep -rn "rmSync(.*recursive.*force" tests/harness/): 置換9 / 対象外12(kiro-tui 既検証・lock 解放系・fixture teardown 系 — 分類は Issue クロスレビューと整合)。live-e2e scratch 削除クラスの取り残し 0

スコープ外

tests/ 全体の同型 ~725 箇所(reviewer-2 指摘の広義クラス)は本 PR の所有境界外。live-e2e 面の閉包を先行し、広域は別判断。

関連

#2154 / #1565(同根の過去事例・CLOSED)/ PR #2159(対象ファイルの改名元)


Note

Low Risk
Test-harness-only changes; no production packages touched. Slightly stricter teardown may surface real filesystem permission issues as loud failures instead of silent leaks.

Overview
Fixes flaky live-e2e / codex-exec helper tests where rmSync(..., { recursive: true, force: true }) could leave scratch dirs behind when concurrent removers race (silent no-op on swallowed ENOENT, #2154).

Adds removeTreeVerified: remove, existsSync check, up to three attempts with a short delay, then throw if the tree remains; non-ENOENT errors still propagate. Live-e2e harness and codex-exec-live scratch teardown now call this instead of raw rmSync (nine sites).

On setup rollback, failed cleanup no longer hides the original error: AggregateError carries both the setup failure and cleanup failure (rollback cleanup failed after setup failure).

New coverage: t-remove-tree-verified.test.ts and an AggregateError regression in t-codex-exec-live-helper.test.ts.

Reviewed by Cursor Bugbot for commit f42651f. Configure here.

j5ik2o added 2 commits August 8, 2026 21:10
`rmSync(root, { recursive: true, force: true })` swallows the transient ENOENT
a concurrent remover produces between the directory walk and the unlink. The
swallowed error turns the removal into a silent no-op, leaving the scratch root
behind while the caller believes it is gone — the mechanism behind the
probabilistic red in tests/integration/t-codex-exec-live-helper.test.ts.

Add tests/harness/live-e2e/testing/remove-tree-verified.ts, a synchronous
helper that removes, verifies with existsSync, retries up to three times, and
throws with the surviving path when the tree will not go. Non-ENOENT failures
(EACCES, EPERM, …) still propagate. It mirrors the async pattern already proven
in live-e2e/kiro-tui.ts #removeScratch, which is left unchanged.

Apply it at the seven synchronous scratch-removal callsites: the codex-exec
home cleanup closure, the five same-root cleanup paths (claude-sdk, codex,
claude-tui, claude, testing/fakes), and the kiro allocate-failure path.

Barrier-synchronised probe over 300 trials with four concurrent removers:
plain rmSync silently left the root behind 265 times; removeTreeVerified never
returned silently with a surviving root (0/300), failing loudly 8 times instead.

Refs #2154
Verified removal can now throw, and the setup-failure rollback paths ran it as
`catch (error) { cleanup(); throw error; }` — a cleanup throw would replace the
original setup error and lose the reason the rollback was running at all.

Wrap each rollback so both errors survive: on a cleanup failure the callsite
throws `AggregateError([setupError, cleanupError])`. When cleanup succeeds the
original error is rethrown unchanged, so existing rollback assertions are
untouched.

Also apply removeTreeVerified at the two allocate-failure paths missed in the
first pass (live-e2e/claude.ts, live-e2e/codex.ts) — same defect class as the
kiro allocate path already converted.

Cover the composition with an integration test that pins a subtree read-only so
the rollback fails with EACCES, and assert the AggregateError carries the
original setup failure first.

Refs #2154
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@j5ik2o, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6635ee69-8016-4d0e-a153-5a00312ff30d

📥 Commits

Reviewing files that changed from the base of the PR and between d3fd311 and 04bb13c.

📒 Files selected for processing (11)
  • tests/harness/codex-exec-live.ts
  • tests/harness/live-e2e/claude-sdk.ts
  • tests/harness/live-e2e/claude-tui.ts
  • tests/harness/live-e2e/claude.ts
  • tests/harness/live-e2e/codex.ts
  • tests/harness/live-e2e/kiro.ts
  • tests/harness/live-e2e/testing/fakes.ts
  • tests/harness/live-e2e/testing/remove-tree-verified.ts
  • tests/integration/t-codex-exec-live-helper.test.ts
  • tests/integration/t-remove-tree-verified.test.ts
  • tests/unit/t-remove-tree-verified-retry.test.ts

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

j5ik2o added 3 commits August 8, 2026 21:35
…2154)

The retry and exhaustion branches of removeTreeVerified are only reachable when
a concurrent remover wins the race, so no real-filesystem test can force them on
demand and they landed uncovered in the patch coverage gate.

Add a RemoveTreeIo seam (rm/exists/sleep) with the real filesystem as the
default argument, so every callsite stays unchanged, and drive the branches from
a test-side fake: first-attempt success, one retry, two retries, budget
exhaustion (asserting the surviving path is in the message), and propagation of
a removal error. The fake lives entirely in the test; no test-only branch enters
the helper.

The fake never touches the filesystem, so the new suite is a unit test; the
existing real-filesystem suite stays in integration unchanged.

Refs #2154
…aths (#2154)

Both codex-exec setup paths carried the same eight-line rollback block, and the
setupCodexExecHome copy is unreachable from a test: its only fallible statement
is a mkdirSync on a directory the preceding mkdtempSync just created, so no
external fixture can fail one without failing the other first. Those lines were
the patch coverage gate's remaining violations.

Extract rollbackOrAggregate into remove-tree-verified.ts, documented as always
throwing: the original error is rethrown unchanged when cleanup succeeds, and
only a failing cleanup produces the AggregateError. Both codex-exec callsites
now delegate to it, so the composition is exercised once by the existing
setupCodexExecProject rollback-failure test.

Collapse the unreachable Home catch onto one line so it occupies a single lcov
row, and record next to it why no fixture can reach it.

Refs #2154
#2154)

Sharing rollbackOrAggregate removed the duplicated composition but left each
setup with its own catch clause, and the setupCodexExecHome one is unreachable
from any fixture: its only fallible statement is a mkdirSync against a directory
the preceding mkdtempSync just created. That clause stayed uncovered, and the
patch coverage gate is zero-tolerance.

Add withRollback(cleanup, work), which owns the catch, and run both setups
through it. The clause now lives in one place that the existing
setupCodexExecProject rollback tests exercise, so no unreachable catch remains
in the patch. rollbackOrAggregate keeps its contract and still composes the
errors.

Value and exception semantics are unchanged: work's value is returned as-is, a
successful cleanup rethrows the original error, and only a failing cleanup
raises the AggregateError — pinned by the five existing helper tests.

Patch coverage over the branch diff for both files: 34 measured added lines, 34
covered, 0 uncovered.

Refs #2154
@j5ik2o
j5ik2o merged commit 4801929 into main Aug 8, 2026
15 checks passed
@j5ik2o
j5ik2o deleted the fix-2154-verified-removal branch August 8, 2026 12:57
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.

flaky: t-codex-exec-live-helper のロールバック検証がフルスイート並列実行下で確率的に赤になる(scratch root 残存)

1 participant