Skip to content

fix(onboard): format dashboard-port conflict as CLI error, not stack trace - #2220

Merged
ericksoa merged 3 commits into
NVIDIA:mainfrom
latenighthackathon:fix/onboard-port-conflict-error-format
Apr 24, 2026
Merged

fix(onboard): format dashboard-port conflict as CLI error, not stack trace#2220
ericksoa merged 3 commits into
NVIDIA:mainfrom
latenighthackathon:fix/onboard-port-conflict-error-format

Conversation

@latenighthackathon

@latenighthackathon latenighthackathon commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

When a user runs nemoclaw onboard for a second sandbox while the first sandbox already forwards port 18789, ensureDashboardForward() threw a raw Error. The top-level IIFE in nemoclaw.ts has no catch, so the user saw a Node unhandled-rejection stack trace originating at onboard.js:6022 instead of a clean preflight-style message.

This PR matches the established preflight pattern (console.error + process.exit(1)), so the user now sees:

  Port 18789 is already forwarded for sandbox 'test21'.
  Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:18790)
  before onboarding a second sandbox.

Related Issue

Closes #2169

Changes

  • src/lib/onboard.ts — replaced throw new Error(...) in ensureDashboardForward() with the standard console.error(...) + process.exit(1) pattern used by every other user-facing preflight failure in this file (e.g. missing Docker, openshell version gate).
  • Extracted the forward-list column parsing into a pure findDashboardForwardOwner(output, port) helper so the parse logic is directly unit-testable without exercising the exit path. Exported it.

Testing

  • npx vitest run test/onboard.test.ts — 134 tests pass (+1 new for findDashboardForwardOwner)
  • npm run build:cli + npm run typecheck:cli clean

Executed:

  • New test case covers: canonical column format match, port-not-in-list → null, empty/null/undefined inputs → null, and a false-positive substring guard (port number appearing inside a sandbox name).

Checklist

Signed-off-by: latenighthackathon latenighthackathon@users.noreply.github.com

Summary by CodeRabbit

  • Bug Fixes

    • Improved onboarding port-conflict handling: conflicts now emit a clear, multi-line message and exit cleanly, avoiding raw stack traces.
    • More accurate detection of which sandbox owns a forwarded port to prevent false positives in port resolution.
  • Tests

    • Added regression tests covering forwarded-port parsing and related onboarding scenarios.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Introduces exported helper findDashboardForwardOwner() to parse openshell forward list output and refactors ensureDashboardForward() to use it. Port-conflict handling now prints a formatted message via console.error() and exits with process.exit(1) instead of throwing an Error.

Changes

Cohort / File(s) Summary
Port Forwarding Helper & Error Handling
src/lib/onboard.ts
Added exported findDashboardForwardOwner(forwardListOutput, portToStop) to locate which sandbox owns a forwarded port from columnar openshell forward list output. ensureDashboardForward() now delegates parsing to this helper and handles port conflicts by printing a multi-line console.error() and calling process.exit(1) (replacing a thrown Error).
Unit Tests
test/onboard.test.ts
Added tests for findDashboardForwardOwner() validating column-aligned matches, absent/empty inputs ("", null, undefined), and avoiding false-positive substring matches. Tests import the function from dist/lib/onboard for direct verification.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I peeked the forward list, neat and bright,
Found which sandbox holds the port tonight.
When ports collide I whisper, not rage—
I print a note and close the stage.
Hops, carrots, calm exits—no stack-trace sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: converting a dashboard port conflict from an unhandled error stack trace to a formatted CLI error message.
Linked Issues check ✅ Passed The PR fully addresses issue #2169: replaces thrown Error with console.error + process.exit(1) pattern and extracts parsing logic into testable helper.
Out of Scope Changes check ✅ Passed All changes are scoped to port-conflict error handling in onboard.ts and its corresponding test coverage; no unrelated modifications present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/onboard.ts (1)

6006-6016: Harden findDashboardForwardOwner parsing against non-table lines.

Current parsing reads parts[2] from every trimmed line. If openshell forward list includes non-table output, this can false-match and incorrectly block onboarding. Consider validating row shape (and header skip) like src/lib/sandbox-session-state.ts:59-87.

Suggested patch
 function findDashboardForwardOwner(forwardListOutput, portToStop) {
-  if (!forwardListOutput) return null;
-  const portLine = forwardListOutput
-    .split("\n")
-    .map((l) => l.trim())
-    .find((l) => {
-      const parts = l.split(/\s+/);
-      return parts[2] === portToStop;
-    });
-  return portLine ? (portLine.split(/\s+/)[0] ?? null) : null;
+  if (!forwardListOutput || portToStop === null || portToStop === undefined) return null;
+  const targetPort = String(portToStop);
+  const lines = String(forwardListOutput)
+    .split("\n")
+    .map((l) => l.trim())
+    .filter(Boolean);
+
+  for (const line of lines) {
+    if (/^\s*SANDBOX\s/i.test(line)) continue;
+    const parts = line.split(/\s+/);
+    if (parts.length < 4) continue;
+    if (parts[2] === targetPort && /^\d+$/.test(parts[3])) {
+      return parts[0] ?? null;
+    }
+  }
+  return null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 6006 - 6016, The function
findDashboardForwardOwner currently indexes parts[2] on every trimmed line which
can mis-parse non-table or header lines; update it to first skip empty lines and
known header lines, then split each line and validate the row shape (e.g.,
require parts.length >= 3 and that parts[2] matches the expected port format or
pattern) before comparing to portToStop, and only then return parts[0] as the
owner; reference the function name findDashboardForwardOwner and variables
forwardListOutput and portToStop when making the change and follow the
header/row-shape checks used in sandbox-session-state.ts as a model.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/onboard.test.ts`:
- Around line 5369-5373: Replace the CommonJS require/require.cache usage with
ESM dynamic import: compute the onboard module file URL from import.meta.url
(use pathToFileURL on the computed onboardPath) and await import(...) to load
it, using a cache-busting query string (e.g., ?t=Date.now()) if you need to
reload the module during tests; then destructure findDashboardForwardOwner from
the imported module. Remove any references to require.cache and require; use
import(...) with the file:// URL built from repoRoot/onboardPath and
import.meta.url instead to satisfy ESM test rules.

---

Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 6006-6016: The function findDashboardForwardOwner currently
indexes parts[2] on every trimmed line which can mis-parse non-table or header
lines; update it to first skip empty lines and known header lines, then split
each line and validate the row shape (e.g., require parts.length >= 3 and that
parts[2] matches the expected port format or pattern) before comparing to
portToStop, and only then return parts[0] as the owner; reference the function
name findDashboardForwardOwner and variables forwardListOutput and portToStop
when making the change and follow the header/row-shape checks used in
sandbox-session-state.ts as a model.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dccb482a-19f5-4821-8a89-ed1f6c8d600e

📥 Commits

Reviewing files that changed from the base of the PR and between eda521e and e09f6cb.

📒 Files selected for processing (2)
  • src/lib/onboard.ts
  • test/onboard.test.ts

Comment thread test/onboard.test.ts Outdated
latenighthackathon added a commit to latenighthackathon/NemoClaw that referenced this pull request Apr 22, 2026
…A#2220 CR)

Address CodeRabbit review on NVIDIA#2220:

> Use ESM loading instead of require in test/ TypeScript files. This
> test uses CommonJS module loading (require/require.cache), which
> violates the test ESM rule.

Promote findDashboardForwardOwner to the top-of-file static import
list and drop the require/require.cache block. The regex parser is
pure, so cache-busting via `await import(url + '?t=...')` isn't
needed — a static import keeps the test simple and matches the
ESM convention documented in AGENTS.md.

Tests
- test/onboard.test.ts -t "NVIDIA#2169" still passes (1/1).

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
@latenighthackathon

Copy link
Copy Markdown
Collaborator Author

Applied CodeRabbit's ESM suggestion — promoted findDashboardForwardOwner to the top-of-file static import list and dropped the require/require.cache block. The regex parser is pure so cache-busting isn't needed. Signed commit 03d283bd. Targeted test still passes. Cheers!

…trace (closes NVIDIA#2169)

When a user runs `nemoclaw onboard` for a second sandbox while the first
sandbox already forwards port 18789, ensureDashboardForward() threw a
raw Error. The top-level IIFE in nemoclaw.ts has no catch, so the user
saw a Node unhandled-rejection stack trace from onboard.js:6022 instead
of a clean preflight-style message.

Match the established preflight pattern (console.error + process.exit(1))
so the output is:

  Port 18789 is already forwarded for sandbox 'test21'.
  Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:18790)
  before onboarding a second sandbox.

Extract the forward-list column parsing into a pure helper
findDashboardForwardOwner() so the parse logic is directly unit-testable
without exercising the process-exit path. Export it for the new test.

Tests
- test/onboard.test.ts: +1 new case covering canonical forward-list
  format, port-in-list (match), port-not-in-list (null), empty/null/
  undefined inputs (null), and a false-positive substring guard.
- Full suite: 134 tests pass (was 133 before this change).

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
…A#2220 CR)

Address CodeRabbit review on NVIDIA#2220:

> Use ESM loading instead of require in test/ TypeScript files. This
> test uses CommonJS module loading (require/require.cache), which
> violates the test ESM rule.

Promote findDashboardForwardOwner to the top-of-file static import
list and drop the require/require.cache block. The regex parser is
pure, so cache-busting via `await import(url + '?t=...')` isn't
needed — a static import keeps the test simple and matches the
ESM convention documented in AGENTS.md.

Tests
- test/onboard.test.ts -t "NVIDIA#2169" still passes (1/1).

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
@latenighthackathon
latenighthackathon force-pushed the fix/onboard-port-conflict-error-format branch from 32cc9c8 to ea86d3c Compare April 22, 2026 03:55
@latenighthackathon

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main to drop the unsigned merge commit 32cc9c8b that Update Branch added. Two signed commits (4a49ea89 port-conflict formatting + ea86d3cc test ESM import) replay cleanly. Cheers!

@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this PR that proposes a fix for the dashboard-port conflict error — this could help provide a cleaner error message for users.


Related open issues:

@ericksoa ericksoa 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.

Clean fix. throw new Errorconsole.error + process.exit(1) matches the established preflight pattern. The findDashboardForwardOwner extraction is good — pure helper, directly testable, column-based parsing avoids substring false positives. Test coverage is solid.

LGTM.

…IDIA#2221 tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ericksoa

Copy link
Copy Markdown
Contributor

Merge conflicts resolved. The contributor's fork has branch protection that prevents direct pushes, so the resolved branch is at NVIDIA/NemoClaw:fix/2220-port-conflict-resolved.

@latenighthackathon — to pick up the resolution:

git fetch upstream fix/2220-port-conflict-resolved
git reset --hard upstream/fix/2220-port-conflict-resolved
git push --force-with-lease

Both your findDashboardForwardOwner test and the formatOnboardConfigSummary test from #2221 (which merged into main) are preserved.

@copy-pr-bot

copy-pr-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@latenighthackathon

Copy link
Copy Markdown
Collaborator Author

@ericksoa thanks for resolving the conflicts and walking me through the pickup — fetched fix/2220-port-conflict-resolved, git reset --hard, force-push-with-lease worked cleanly. #2220 is now at 8c0a9270 and MERGEABLE, both the findDashboardForwardOwner test (#2169) and formatOnboardConfigSummary test (from the now-merged #2221) preserved.

Also loosened the fork ruleset to exempt non-default branches from the admin-only rule, so direct maintainer pushes (not just the Update Branch merge button) should work on future PRs without needing the detour. Cheers!

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/onboard.ts`:
- Around line 6150-6152: The error message currently prints a hardcoded example
port; update the console.error call in src/lib/onboard.ts (the console.error
that suggests "Set CHAT_UI_URL ... e.g. http://127.0.0.1:18790") to avoid
hardcoding 18790 — instead derive the suggested port dynamically from the
existing portToStop variable (e.g., suggest portToStop + 1) or use a generic
placeholder (e.g., "http://127.0.0.1:<port>") so users are not pointed back to
the conflicting port; modify the console.error invocation to interpolate the
computed port or placeholder accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0ac2e71b-dce1-475f-92ea-3366ff9f0e0f

📥 Commits

Reviewing files that changed from the base of the PR and between ea86d3c and 8c0a927.

📒 Files selected for processing (2)
  • src/lib/onboard.ts
  • test/onboard.test.ts

Comment thread src/lib/onboard.ts
Comment on lines +6150 to 6152
console.error(
` Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:18790)`,
);

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.

⚠️ Potential issue | 🟡 Minor

Avoid hardcoding the example port in the conflict message.

If the user already set CHAT_UI_URL to 18790 and that port conflicts, this message still suggests http://127.0.0.1:18790, which points them back to the failing port. Derive the example from portToStop or make it generic.

💡 Proposed fix
-    console.error(
-      `  Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:18790)`,
-    );
+    const examplePort = Number.isFinite(Number(portToStop))
+      ? String(Number(portToStop) + 1)
+      : "18790";
+    console.error(
+      `  Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:${examplePort})`,
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error(
` Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:18790)`,
);
const examplePort = Number.isFinite(Number(portToStop))
? String(Number(portToStop) + 1)
: "18790";
console.error(
` Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:${examplePort})`,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 6150 - 6152, The error message currently
prints a hardcoded example port; update the console.error call in
src/lib/onboard.ts (the console.error that suggests "Set CHAT_UI_URL ... e.g.
http://127.0.0.1:18790") to avoid hardcoding 18790 — instead derive the
suggested port dynamically from the existing portToStop variable (e.g., suggest
portToStop + 1) or use a generic placeholder (e.g., "http://127.0.0.1:<port>")
so users are not pointed back to the conflicting port; modify the console.error
invocation to interpolate the computed port or placeholder accordingly.

@ericksoa
ericksoa merged commit 7e89bc1 into NVIDIA:main Apr 24, 2026
1 check passed
@latenighthackathon
latenighthackathon deleted the fix/onboard-port-conflict-error-format branch April 24, 2026 03:33
@cv cv added the v0.0.25 label Apr 24, 2026
DemianHeyGen pushed a commit to DemianHeyGen/NemoClaw that referenced this pull request Apr 30, 2026
…trace (NVIDIA#2220)

## Summary

When a user runs `nemoclaw onboard` for a second sandbox while the first
sandbox already forwards port 18789, `ensureDashboardForward()` threw a
raw `Error`. The top-level IIFE in `nemoclaw.ts` has no catch, so the
user saw a Node unhandled-rejection stack trace originating at
`onboard.js:6022` instead of a clean preflight-style message.

This PR matches the established preflight pattern (`console.error` +
`process.exit(1)`), so the user now sees:

```
  Port 18789 is already forwarded for sandbox 'test21'.
  Set CHAT_UI_URL to a different local port (e.g. http://127.0.0.1:18790)
  before onboarding a second sandbox.
```

## Related Issue

Closes NVIDIA#2169

## Changes

- **`src/lib/onboard.ts`** — replaced `throw new Error(...)` in
`ensureDashboardForward()` with the standard `console.error(...)` +
`process.exit(1)` pattern used by every other user-facing preflight
failure in this file (e.g. missing Docker, openshell version gate).
- Extracted the forward-list column parsing into a pure
`findDashboardForwardOwner(output, port)` helper so the parse logic is
directly unit-testable without exercising the exit path. Exported it.

## Testing

- [x] `npx vitest run test/onboard.test.ts` — 134 tests pass (+1 new for
`findDashboardForwardOwner`)
- [x] `npm run build:cli` + `npm run typecheck:cli` clean

Executed:
- New test case covers: canonical column format match, port-not-in-list
→ `null`, empty/`null`/`undefined` inputs → `null`, and a false-positive
substring guard (port number appearing inside a sandbox name).

## Checklist

- [x] Follows [Conventional
Commits](https://www.conventionalcommits.org/)
- [x] Commit is signed (SSH)
- [x] DCO Signed-off-by trailer present

Signed-off-by: latenighthackathon
<latenighthackathon@users.noreply.github.com>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved onboarding port-conflict handling: conflicts now emit a
clear, multi-line message and exit cleanly, avoiding raw stack traces.
* More accurate detection of which sandbox owns a forwarded port to
prevent false positives in port resolution.

* **Tests**
* Added regression tests covering forwarded-port parsing and related
onboarding scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
Co-authored-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
Co-authored-by: Aaron Erickson <aerickson@nvidia.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wscurran wscurran added area: cli Command line interface, flags, terminal UX, or output bug-fix PR fixes a bug or regression and removed NemoClaw CLI labels Jun 3, 2026
@wscurran wscurran added the NV QA Bugs found by the NVIDIA QA Team label Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Command line interface, flags, terminal UX, or output bug-fix PR fixes a bug or regression NV QA Bugs found by the NVIDIA QA Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Jetson][Onboard] nemoclaw onboard second sandbox: port 18789 conflict shows raw JS stack trace instead of user-friendly error

4 participants