Skip to content

fix(validate-utils): write the error report to stderr synchronously so process.exit cannot truncate it - #1293

Merged
mrbobbytables merged 1 commit into
mainfrom
quality/test-report-and-exit-sync-stderr
Oct 11, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
quality/test-report-and-exit-sync-stderr

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Test Improvement

reportAndExit() in scripts/lib/validate-utils.mjs printed every error with
console.error() and then called process.exit(1). Node documents stderr
writes as asynchronous on POSIX when stderr is a pipe, and process.exit()
does not wait for pending writes, so the tail of a long validator report can be
dropped before the parent reads it. All 12 validate-*.mjs scripts reach this
path, and tests/e2e-data-fixture-validity.test.mjs compares a validator's full
piped output against a memoized baseline, so a truncated baseline reports the
lost lines as new errors.

Change

  • reportAndExit() now builds each report into one string and writes it to
    stderr's file descriptor with fs.writeSync(), which is synchronous whatever
    stderr is connected to. Short writes are resumed and EAGAIN is retried.
  • The warning report takes the same path, so a report carrying both severities
    cannot lose its warning half to the exit below and the two halves cannot
    interleave.

Why not process.exitCode = 1

The alternative suggested in the issue thread is not safe here, and I verified
why before discarding it: every validator prints a Validated N ... success
line immediately after its top-level reportAndExit() call
(validate-members.mjs:433, validate-architecture-assets.mjs:419, and ten
more). Letting the script fall through would print a success line after a failed
run. tests/validate-utils.test.mjs now pins that invariant, so the incorrect
fix cannot be reintroduced silently.

Tests

tests/validate-utils.test.mjs captures fs.writeSync instead of
console.error/console.warn, and adds:

  • each report reaches stderr in a single write, warnings first;
  • the module no longer routes its report through console;
  • a partially accepted write is resumed until the report is complete;
  • a write refused with EAGAIN is retried, and a non-EAGAIN failure propagates;
  • a 2000-line report spawned through a real pipe arrives whole and exits 1;
  • every validator that calls reportAndExit() prints a success line afterwards.

Verification at this head: npm run test:unit:coverage:check passes with 2324
tests green; scripts/lib/validate-utils.mjs and tests/validate-utils.test.mjs
are both at 100.00% lines and 100.00% regions, and the src files totals stay at
100.00/100.00. Prettier is clean on both files.

One honest caveat: the piped-report test passes against the old
console.error implementation too in this environment — the truncation is
load-dependent, which is why it surfaced in a merge_group runner and not
locally. The deterministic pins are the fd-write, resume and retry tests; the
piped test guards the end-to-end contract.

Related Issue

Closes #1286


Human review required.

— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88 requested_by=@mrbobbytables

reportAndExit() printed every error with console.error() and then called
process.exit(1). Node documents stderr writes as asynchronous on POSIX when
stderr is a pipe, and process.exit() does not wait for pending writes, so the
tail of a long validator report can be dropped before the parent reads it --
the shape of the merge_group failure on run 38079181421, where the 18 missing
lines were the alphabetical tail of one report.

Build each report into a single string and write it to stderr's file
descriptor with fs.writeSync(), which is synchronous whatever stderr is
connected to, resuming short writes and retrying EAGAIN. Warnings share the
same path so a report carrying both severities cannot lose its warning half to
the exit below, and the two halves cannot interleave.

process.exitCode = 1 would not work here: every validator prints a
"Validated N ..." success line immediately after reportAndExit(), which must
stay unreachable once errors have been reported. A new test pins that.

Closes #1286

Co-authored-by: mrbobbytables <6500863+mrbobbytables@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: quality <quality@hive.kubestellar.io>
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "quality" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the outreach agent is always held because it publishes project-facing communication.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant