PDX-513: fix(mcp): stream sf child stdio to files to prevent ENOBUFS#222
Conversation
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.
Quality Orchestrator🟢 LOW · 🧪 Tests to Run · Running 3 of 57 tests
▶ Run commandnpx vitest run \
unit/mcp/automationTools.test.ts \
unit/mcp/qualityHubTools.test.ts \
unit/mcp/sfSpawn.test.ts
|
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.
Adversarial review round (codex + Claude)Ran an independent adversarial review (OpenAI Codex
Acknowledged, not fixed (not blocking): detached grandchild fd-inheritance under Gate after fixes: |
Summary
runSfCommand(src/mcp/tools/sfSpawn.ts) buffered the child's entire stdout/stderr in memory viaspawnSync({ maxBuffer: 50 MB }). A verboseDETAILEDSalesforce UI run overflows the cap andspawnSyncaborts withENOBUFS, losing the run even though Provar wrote results to disk.stdio: ['ignore', outFd, errFd], nomaxBuffer) and read them back after exit. No in-memory ceiling ⇒maxBuffer-inducedENOBUFSis structurally impossible; writing straight to fds also avoids the Windowsshell:trueOS-level pipeENOBUFS.handleSpawnErrornow translates any residualENOBUFSinto an actionable error (points to on-disk results +--jsonfallback + loweringtestOutputLevel) with adetails.suggestion, instead of the opaquespawnSync … ENOBUFS.runSfCommandsignature 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 compilepassesmcp-smoke.cjspasses (60/60)yarn lintpasses (0 errors, 0 warnings)prettier --checkclean on all changed filesChanges
src/mcp/tools/sfSpawn.ts: retireMAX_BUFFER; extractcaptureSpawnToFilesthat streams stdio to a temp dir and reads it back, removing the in-memory buffer.src/mcp/tools/automationTools.ts:handleSpawnErrorspecial-casesENOBUFSwith an actionable message + suggestion; replace the inaccuratemaxBuffer/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'sfs.readFileSyncthrow-stub so it doesn't intercept the capture temp files.docs/mcp.md: document the streaming output handling + residualENOBUFSbehaviour onprovar_automation_testrun.docs/bug-reports/testrun-enobufs-stream-to-file.md: source bug report / rationale.Notes for reviewers
docs/provar-mcp-public-docs.md, university course) changes required; behaviour change is internal robustness. The user-visible improvement is that verboseDETAILEDtestruns no longer fail withENOBUFS.TOTAL_EXPECTEDinscripts/mcp-smoke.cjsis unchanged.