Skip to content

fix: reap test writers before buffer-overrun teardown - #110

Merged
unbraind merged 1 commit into
mainfrom
fix/reap-test-writers-before-cleanup
Sep 6, 2026
Merged

fix: reap test writers before buffer-overrun teardown#110
unbraind merged 1 commit into
mainfrom
fix/reap-test-writers-before-cleanup

Conversation

@unbraind

@unbraind unbraind commented Sep 6, 2026

Copy link
Copy Markdown
Owner

When an outer command capture exceeds its output limit, the fake pm wrapper could exit while its delegated process was still writing tracker history. Capture the delegated output until that process has exited, then forward it. Isolate negative fixtures from global settings and production observability.

A real-process regression fails with the old wrapper and passes with the fix on Node 22.23.2 and Node 26.7.0. Full coverage passes all 225 tests with exact 100% line, branch, and function coverage; static checks, all three linked verification plans, production audit, and package dry run pass.

Tracked in pm-csv-yriu. This fixes a demonstrated test-writer race relevant to the CI history-write error discussed in unbraind/pm-cli#1209. The precise historical ENOENT was not reproduced, so that attribution remains an inference.

Final head f9c13a7: both hosted Node matrix jobs, CodeQL, Semgrep, DeepScan, and static checks pass. CodeRabbit completed with no actionable findings; Greptile reviewed all seven files with no comments. Sourcery findings about inner-child reaping and ESM dependency evaluation were disproved with real-process checks and answered inline; all threads are resolved. Cubic skipped review and is not counted as approval.

@sourcery-ai

sourcery-ai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes a delegated-writer race by making the fake pm wrapper reap and fully capture its real process before exposing output to an outer buffer limit, adds a real-process regression, isolates negative fixtures from global settings and telemetry, and updates cleanup documentation and release tracking.

Sequence diagram for delegated output capture before teardown

sequenceDiagram
    participant Outer as Outer command
    participant Wrapper as Fake pm wrapper
    participant Child as Delegated process
    participant Tracker as Tracker history

    Outer->>Wrapper: Request delegated command
    Wrapper->>Child: Start delegated process
    Child->>Tracker: Write tracker history
    Outer->>Wrapper: Enforce output buffer limit
    Wrapper->>Child: Wait for process exit
    Child-->>Wrapper: Exit after final write
    Wrapper-->>Outer: Forward fully captured output
Loading

File-Level Changes

Change Details Files
Ensure delegated test processes finish before output-overrun teardown can begin.
  • Change the fake pm wrapper to synchronously capture delegated stdout and stderr with a large buffer, then forward output after the delegated process exits.
  • Add a real-process regression covering outer-buffer ENOBUFS, writer completion, and child reaping.
  • Update cleanup guidance to distinguish process completion from bounded filesystem-removal retries.
test/atomic.test.ts
test/support/remove-tree.ts
Isolate negative process and I/O fixtures from developer state and production observability.
  • Initialize an isolated global settings directory and disable Sentry and telemetry before host SDK initialization.
  • Apply the isolation setup to affected test modules and clean up the temporary global directory after the suite.
test/support/isolated-environment.ts
test/atomic.test.ts
test/buffer-regression.test.ts
Document the fix and update its tracked issue metadata.
  • Move the changelog entry to Unreleased and describe the writer-lifetime fix.
  • Record the implementation and verification details in the issue history and issue file.
CHANGELOG.md
.agents/pm/history/pm-csv-yriu.jsonl
.agents/pm/issues/pm-csv-yriu.toon

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Sep 6, 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: ASSERTIVE

Plan: Team

Run ID: 2955cbb2-e2a5-4c85-bfc4-b2b800df5056

📥 Commits

Reviewing files that changed from the base of the PR and between ca8fc8d and f9c13a7.

📒 Files selected for processing (7)
  • .agents/pm/history/pm-csv-yriu.jsonl
  • .agents/pm/issues/pm-csv-yriu.toon
  • CHANGELOG.md
  • test/atomic.test.ts
  • test/buffer-regression.test.ts
  • test/support/isolated-environment.ts
  • test/support/remove-tree.ts

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


Summary by CodeRabbit

  • Bug Fixes

    • Fixed a CI teardown race that could cause temporary-file cleanup failures when delegated processes continued writing after termination.
    • Improved test-process handling so delegated output is completed before cleanup begins.
    • Reduced unintended telemetry and tracker-state interference during isolated test scenarios.
  • Documentation

    • Updated the unreleased changelog with the teardown reliability fix.

Walkthrough

The test wrapper now captures delegated pm output until the real process exits. Regression coverage verifies writer completion and process reaping during an outer ENOBUFS failure. Tests use isolated global state and disabled observability.

Changes

Delegated writer race

Layer / File(s) Summary
Isolated test state
test/support/isolated-environment.ts, test/buffer-regression.test.ts
Tests use a temporary PM_GLOBAL_PATH and disable Sentry, telemetry, and OpenTelemetry.
Wrapper reaping and regression coverage
test/atomic.test.ts
The fake wrapper captures delegated output until real pm exits. The regression test verifies the writer completed and was reaped after an outer ENOBUFS failure.
Teardown contract and release records
test/support/remove-tree.ts, CHANGELOG.md, .agents/pm/issues/pm-csv-yriu.toon, .agents/pm/history/pm-csv-yriu.jsonl
Documentation requires writer completion before cleanup. Changelog and issue records describe the fixed delegated-writer race.

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

Merge Risk: ⚪ Minimal · up to f9c13

The test wrapper now prevents delegated writers from surviving buffer-overrun handling, with regression coverage for completion and process reaping. No current merge-blocking risk is identified.

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant FakePmWrapper
  participant RealPm
  participant WriterProgram
  Test->>FakePmWrapper: Spawn with 16-byte maxBuffer
  FakePmWrapper->>RealPm: Spawn with captured output
  RealPm->>WriterProgram: Run delegated writer
  WriterProgram-->>RealPm: Write chunk and completion marker
  RealPm-->>FakePmWrapper: Exit with captured output
  FakePmWrapper->>FakePmWrapper: Forward output with writeSync
  Test-->>FakePmWrapper: Observe ENOBUFS after writer exit
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 4 files. (3 skipped: 3 …
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.
Title check ✅ Passed The title clearly and concisely describes the main change: reaping delegated test writers before buffer-overrun teardown.
Description check ✅ Passed The description directly explains the test-wrapper race, the output-capture fix, test isolation, regression coverage, and verification results.
✨ 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/reap-test-writers-before-cleanup

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.

@unbraind

unbraind commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

The guide correctly identifies the delegated-process lifetime boundary and test isolation. The real-process regression verifies completion and ESRCH before cleanup.

@unbraind

unbraind commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="test/atomic.test.ts" line_range="298" />
<code_context>
-    "const r = spawnSync(realPm, args, { stdio: 'inherit' });",
+    // Do not expose output until the real process exits. Otherwise an outer
+    // capture overrun kills this wrapper while real pm is still writing history.
+    "const r = spawnSync(realPm, args, { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });",
+    "if (r.stdout) writeSync(1, r.stdout);",
+    "if (r.stderr) writeSync(2, r.stderr);",
     "process.exit(r.status == null ? 1 : r.status);",
</code_context>
<issue_to_address>
**issue (bug_risk):** The fake wrapper still terminates the delegated `pm` process when either stream exceeds its own 64 MiB `maxBuffer`; it then forwards only the truncated output and exits with a synthetic status. A real command producing more than 64 MiB therefore remains vulnerable to the same delegated-writer teardown race and can be misclassified by the outer test capture.

**Triggers:** When the delegated command emits more than 64 MiB on stdout or stderr.

**Suggested fix:** Use a sufficiently unbounded capture strategy, or explicitly drain the delegated process streams without imposing a smaller buffer cap before forwarding the bytes.

```suggestion
    "const r = spawnSync(realPm, args, { encoding: 'utf-8', maxBuffer: Infinity });",
```
</issue_to_address>

### Comment 2
<location path="test/atomic.test.ts" line_range="1" />
<code_context>
+import "./support/isolated-environment.ts";
+
 import assert from "node:assert/strict";
</code_context>
<issue_to_address>
**issue (bug_risk):** The isolation module is imported with a static ESM import, but static dependencies such as `../index.ts` and `@unbrained/pm-cli/sdk` are evaluated before the imported module's top-level body runs. Any host SDK initialization that reads global tracker or observability settings during module evaluation therefore sees the developer's original environment, not the values assigned by `isolated-environment.ts`.

**Triggers:** When the host SDK reads `PM_GLOBAL_PATH` or telemetry/Sentry settings during module initialization.

**Suggested fix:** Set the environment in the test runner before loading static dependencies, or replace the host SDK imports with dynamic imports performed after the isolation module has initialized.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: test/atomic.test.ts:298, test/atomic.test.ts:1


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread test/atomic.test.ts
Comment thread test/atomic.test.ts
@greptile-apps

greptile-apps Bot commented Sep 6, 2026

Copy link
Copy Markdown

Greptile Summary

The PR prevents the fake pm wrapper from being terminated by an outer output limit while its delegated writer is still running.

  • Captures delegated output until the real process exits, then forwards both streams.
  • Adds a real-process regression that verifies writer completion and process reaping.
  • Isolates negative fixtures from developer-global tracker state, Sentry, telemetry, and OpenTelemetry.
  • Corrects teardown documentation and updates issue and changelog records.

Confidence Score: 5/5

The PR appears safe to merge; the delegated-writer lifetime race is covered without introducing an actionable regression.

The wrapper now waits for delegated process completion before exposing output to the outer capture limit, environment mutations are restored, and teardown occurs only after relevant child processes have exited.

Important Files Changed

Filename Overview
test/atomic.test.ts Buffers delegated output until process completion and adds a regression proving the delegated writer finishes and is reaped before teardown.
test/support/isolated-environment.ts Creates disposable global tracker state and disables inherited production observability for negative subprocess fixtures.
test/buffer-regression.test.ts Imports the isolated test environment before executing buffer-overrun fixtures.
test/support/remove-tree.ts Clarifies that retrying filesystem removal does not itself establish process termination.

Sequence Diagram

sequenceDiagram
    participant Test as Outer test capture
    participant Wrapper as Fake pm wrapper
    participant PM as Delegated process
    participant FS as Tracker history

    Test->>Wrapper: spawnSync(pm), maxBuffer 16
    Wrapper->>PM: spawnSync(real command), capture output
    PM->>FS: complete history writes
    PM-->>Wrapper: exit and captured output
    Wrapper-->>Test: forward captured output
    Note over Test,Wrapper: Outer ENOBUFS can occur only after PM exits
    Test->>FS: safely remove fixture
Loading

Reviews (1): Last reviewed commit: "fix: reap test writers before buffer-ove..." | Re-trigger Greptile

@unbraind

unbraind commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Both findings were checked and answered inline. The inner synchronous call waits for its direct child even on its own overrun, and the separate first ESM dependency executes before later sibling dependencies. Neither finding demonstrates a regression in this patch.

@unbraind

unbraind commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Completed review for f9c13a7 acknowledged. No actionable findings remain. Both hosted Node matrix jobs passed all 225 tests with exact 100% coverage, including the process-lifetime regression.

@unbraind

unbraind commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

The requested review completed and its current-head result has been checked.

@unbraind
unbraind merged commit fad7e30 into main Sep 6, 2026
11 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