Skip to content

fix(cli): coerce ServerSupervisor exit code to number — prevents TypeError on Node.js v24 (#3748) - #3750

Merged
diegosouzapw merged 1 commit into
release/v3.8.24from
fix/3748-supervisor-string-exit-code
Jun 13, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.24from
fix/3748-supervisor-string-exit-code

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Closes #3748

Summary

Node.js v24 throws TypeError [ERR_INVALID_ARG_TYPE]: The 'code' argument must be of type number when process.exit() receives a string. The spawn error event passes err.code (e.g. 'ENOENT') via err.code ?? -1 — nullish coalescing doesn't help since 'ENOENT' is neither null nor undefined.

Root cause in processSupervisor.mjs:

  • Line 45: this.handleExit(err.code ?? -1, err) → passes 'ENOENT' (string)
  • Lines 55/74: process.exit(code || 0) / process.exit(code ?? 1) → both receive a string → TypeError on Node.js v24

Fix:

  • error callback now passes -1 unconditionally (err.code is an OS error string, not a meaningful exit code)
  • handleExit() normalises code to a number at the top: const exitCode = typeof code === 'number' ? code : null
  • All process.exit() calls use exitCode with safe numeric defaults

Regression Test

tests/unit/cli-process-supervisor.test.ts — new test calls supervisor.handleExit('ENOENT') directly (mimicking the spawn error path) and asserts that process.exit receives a number, not a string. Confirmed failing before fix, passing after.

✔ ServerSupervisor.handleExit com string code não passa string para process.exit (#3748)

…Error on Node.js v24 (#3748)

Node.js v24 added strict type checking to process.exit() and throws
TypeError [ERR_INVALID_ARG_TYPE] when given a non-number. The spawn
'error' event passes err.code (e.g. 'ENOENT') — a string, not a number
— via `err.code ?? -1` (nullish coalescing doesn't help since 'ENOENT'
is not null/undefined). handleExit() now normalises the code to a number
at the top; the 'error' callback passes -1 unconditionally.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses a Node.js v24 compatibility issue where process.exit() throws an error if passed a string instead of a number. The ServerSupervisor.handleExit method was updated to normalize exit codes, and a corresponding unit test was added. The review feedback suggests wrapping the global stubbing of process.exit in a try...finally block within the unit test to guarantee the original function is restored even if assertions fail.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +203 to +219
const exits: Array<number | string | undefined> = [];
const origExit = process.exit.bind(process);
// @ts-ignore
process.exit = (code?: number | string) => exits.push(code);

const supervisor = new ServerSupervisor({
serverPath: "/fake/server.js",
env: {},
maxRestarts: 0,
});
// Simulates the 'error' event on child spawn failure: err.code = 'ENOENT' (string, not number).
// maxRestarts=0 → restartCount(0) >= maxRestarts(0) → process.exit() is called immediately.
supervisor.startedAt = Date.now() - 100;
supervisor.handleExit("ENOENT" as any);

// @ts-ignore
process.exit = origExit;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Stubbing process.exit globally without a try...finally block is risky. If any assertion fails or an unexpected error is thrown during the test execution, process.exit will remain stubbed. This can cause subsequent tests to behave unpredictably or prevent the test runner from exiting properly. Wrapping the test logic in a try...finally block ensures that process.exit is always restored to its original implementation.

Suggested change
const exits: Array<number | string | undefined> = [];
const origExit = process.exit.bind(process);
// @ts-ignore
process.exit = (code?: number | string) => exits.push(code);
const supervisor = new ServerSupervisor({
serverPath: "/fake/server.js",
env: {},
maxRestarts: 0,
});
// Simulates the 'error' event on child spawn failure: err.code = 'ENOENT' (string, not number).
// maxRestarts=0 → restartCount(0) >= maxRestarts(0) → process.exit() is called immediately.
supervisor.startedAt = Date.now() - 100;
supervisor.handleExit("ENOENT" as any);
// @ts-ignore
process.exit = origExit;
const exits: Array<number | string | undefined> = [];
const origExit = process.exit;
try {
// @ts-ignore
process.exit = (code?: number | string) => { exits.push(code); };
const supervisor = new ServerSupervisor({
serverPath: "/fake/server.js",
env: {},
maxRestarts: 0,
});
// Simulates the 'error' event on child spawn failure: err.code = 'ENOENT' (string, not number).
// maxRestarts=0 → restartCount(0) >= maxRestarts(0) → process.exit() is called immediately.
supervisor.startedAt = Date.now() - 100;
supervisor.handleExit("ENOENT" as any);
} finally {
process.exit = origExit;
}

const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs");

const exits: Array<number | string | undefined> = [];
const origExit = process.exit.bind(process);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Narrow the stub result type and assert the expected exit code

exits is typed to allow strings, and the test only asserts that the value is a number. Use number[] and assert exits[0] === 1 so the regression pins both Node.js v24 compatibility and the intended failure exit code.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
tests/unit/cli-process-supervisor.test.ts 204 The regression test should pin the expected numeric exit code (1) instead of only asserting that process.exit received a number.
Other Observations (not in diff)

No additional unchanged-code issues identified.

Files Reviewed (3 files)
  • bin/cli/runtime/processSupervisor.mjs - 0 issues
  • tests/unit/cli-process-supervisor.test.ts - 1 issue
  • CHANGELOG.md - 0 issues

Fix these issues in Kilo Cloud


Reviewed by nex-n2-pro:free · 372,926 tokens

@diegosouzapw
diegosouzapw merged commit 33667fc into release/v3.8.24 Jun 13, 2026
2 checks passed
@diegosouzapw
diegosouzapw deleted the fix/3748-supervisor-string-exit-code branch June 13, 2026 04:15
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…Error on Node.js v24 (diegosouzapw#3748) (diegosouzapw#3750)

Node.js v24 added strict type checking to process.exit() and throws
TypeError [ERR_INVALID_ARG_TYPE] when given a non-number. The spawn
'error' event passes err.code (e.g. 'ENOENT') — a string, not a number
— via `err.code ?? -1` (nullish coalescing doesn't help since 'ENOENT'
is not null/undefined). handleExit() now normalises the code to a number
at the top; the 'error' callback passes -1 unconditionally.
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