Skip to content

PDX-513: fix(mcp): stream sf child stdio to files to prevent ENOBUFS#222

Merged
mrdailey99 merged 2 commits into
developfrom
fix/PDX-513-enobufs-stream-to-file
Jun 8, 2026
Merged

PDX-513: fix(mcp): stream sf child stdio to files to prevent ENOBUFS#222
mrdailey99 merged 2 commits into
developfrom
fix/PDX-513-enobufs-stream-to-file

Conversation

@mrdailey99

Copy link
Copy Markdown
Collaborator

Summary

  • runSfCommand (src/mcp/tools/sfSpawn.ts) buffered the child's entire stdout/stderr in memory via spawnSync({ maxBuffer: 50 MB }). A verbose DETAILED Salesforce UI run overflows the cap and spawnSync aborts with ENOBUFS, losing the run even though Provar wrote results to disk.
  • Fix: stream child stdout/stderr to temp files (stdio: ['ignore', outFd, errFd], no maxBuffer) and read them back after exit. No in-memory ceiling ⇒ maxBuffer-induced ENOBUFS is structurally impossible; writing straight to fds also avoids the Windows shell:true OS-level pipe ENOBUFS.
  • Defense-in-depth: handleSpawnError now translates any residual ENOBUFS into an actionable error (points to on-disk results + --json fallback + lowering testOutputLevel) with a details.suggestion, instead of the opaque spawnSync … ENOBUFS.
  • runSfCommand signature is unchanged — all callers (config_load, compile, metadata_download, setup, qualityhub_*, defectTools, probeProvarTopic) still receive { stdout, stderr, exitCode }.

Jira

https://provartesting.atlassian.net/browse/PDX-513

Test plan

  • yarn compile passes
  • full unit suite passes (1485 passing, 1 pending, 0 failing)
  • mcp-smoke.cjs passes (60/60)
  • yarn lint passes (0 errors, 0 warnings)
  • prettier --check clean on all changed files

Changes

  • src/mcp/tools/sfSpawn.ts: retire MAX_BUFFER; extract captureSpawnToFiles that streams stdio to a temp dir and reads it back, removing the in-memory buffer.
  • src/mcp/tools/automationTools.ts: handleSpawnError special-cases ENOBUFS with an actionable message + suggestion; replace the inaccurate maxBuffer/ENOBUFS tool-description lines with a streaming note.
  • test/unit/mcp/{sfSpawn,automationTools,qualityHubTools,defectTools}.test.ts: migrate spawn stubs to the file-backed model (fakes write captured output to the inherited fds); add an over-cap (51 MB) regression test and ENOBUFS positive/negative tests; scope the setup block's fs.readFileSync throw-stub so it doesn't intercept the capture temp files.
  • docs/mcp.md: document the streaming output handling + residual ENOBUFS behaviour on provar_automation_testrun.
  • docs/bug-reports/testrun-enobufs-stream-to-file.md: source bug report / rationale.

Notes for reviewers

  • No public-facing docs (docs/provar-mcp-public-docs.md, university course) changes required; behaviour change is internal robustness. The user-visible improvement is that verbose DETAILED testruns no longer fail with ENOBUFS.
  • No MCP tools added/removed, so TOTAL_EXPECTED in scripts/mcp-smoke.cjs is unchanged.

RCA: runSfCommand buffered the entire child stdout/stderr in memory via spawnSync({ maxBuffer: 50 MB }), so a verbose DETAILED Provar run overflowed the cap and aborted with ENOBUFS, losing the run even though results were written to disk.
Fix: capture child stdout/stderr to temp files via stdio fds with no maxBuffer and read them back after exit, removing the in-memory ceiling; translate any residual ENOBUFS into an actionable error; migrate spawn-stub tests to the file-backed model and add an over-cap regression test.
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Quality Orchestrator

🟢 LOW · 15 / 100 · 1 file(s) with no test coverage detected.


🧪 Tests to Run · Running 3 of 57 tests

  • unit/mcp/automationTools.test.ts
  • unit/mcp/qualityHubTools.test.ts
  • unit/mcp/sfSpawn.test.ts
▶ Run command
npx vitest run \
  unit/mcp/automationTools.test.ts \
  unit/mcp/qualityHubTools.test.ts \
  unit/mcp/sfSpawn.test.ts

⚠️ Missing Coverage · 1 file

These source files have no mapped test — consider adding coverage before merge.

  • src/mcp/tools/spawnErrors.ts

💡 Run locally: qo stub src/mcp/tools/spawnErrors.ts to generate a test scaffold.


⚡ quality-orchestrator  ·  /qo stub <file>  ·  qo analyze-local

RCA: adversarial review found captureSpawnToFiles could leak a descriptor if the second openSync threw, its finally rmSync could throw (Windows EBUSY/EPERM) and mask the real result, the ENOBUFS message was test-run-specific yet shared, and qualityHub's handleSpawnError never got the ENOBUFS branch.
Fix: split capture into captureSpawnToFiles + spawnCapturingToDir so cleanup covers a failed second open and rmSync is best-effort; extract one generic ENOBUFS-aware handleSpawnError into spawnErrors.ts used by both automation and qualityHub; soften the overstated "regardless of size" docs; add a real-subprocess >50MB integration test plus a qualityHub ENOBUFS test.
@mrdailey99

Copy link
Copy Markdown
Collaborator Author

Adversarial review round (codex + Claude)

Ran an independent adversarial review (OpenAI Codex challenge mode, then a second Claude reviewer). Both converged; commit f61fcfd addresses the consensus findings:

Finding Sev Fix
rmSync in finally can throw (Windows EBUSY/EPERM — force:true only suppresses ENOENT) and mask the real result/error High Cleanup is now best-effort (wrapped in try/catch) and never escapes finally
fd leak if the second openSync throws (both opens were before the try) Med Split into captureSpawnToFiles + spawnCapturingToDir; the first fd is closed and the temp dir removed on a failed second open
Shared handleSpawnError ENOBUFS message was test-run-specific but fires for compile/setup/metadata too Med Message genericized
qualityHubTools had its own handleSpawnError that never got the ENOBUFS branch Med Extracted one shared spawnErrors.ts handleSpawnError, used by both automation + Quality Hub (+ qualityHub ENOBUFS test)
Over-cap unit test only proves fs round-trips, not real child-process capture Med Added a real-subprocess integration test: spawns node writing 60 MB through runSfCommand with the helper un-stubbed (479 ms)
Docs/description "regardless of size" overstated (readFileSync still has a ~512 MB string ceiling) High→doc Softened wording; head+tail slicing remains deferred (noted in the bug report)

Acknowledged, not fixed (not blocking): detached grandchild fd-inheritance under shell:true (no detached descendants in the synchronous sf command path); orphaned provar-sf-* temp dirs only on a hard crash between mkdtemp and finally (now far less likely); removing encoding:'utf-8' confirmed behavior-neutral (readFileSync('utf-8') returns the same string, empty→'').

Gate after fixes: yarn compile clean · unit suite 1487 passing / 0 failing · mcp-smoke 60/60 · yarn lint 0/0 · prettier clean.

@mrdailey99
mrdailey99 merged commit 257826b into develop Jun 8, 2026
4 checks passed
@mrdailey99
mrdailey99 deleted the fix/PDX-513-enobufs-stream-to-file branch June 8, 2026 18:42
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