Skip to content

Commit 77db6fb

Browse files
zhiyanliuprekshivyascjagwani
authored
fix(sandbox): clear gateway marker when the start supervisor exits (#5569)
## Summary Fixes #5568. On OpenShell docker-driver sandboxes, `scripts/nemoclaw-start.sh` is not PID 1 — OpenShell's `sleep infinity` keeps the container alive as a sibling. The Docker HEALTHCHECK's pidfile fallback (#4952) trusts `/tmp/nemoclaw-gateway.pid`, which is refreshed only by `record_gateway_pid` inside this script's launch and respawn paths. When the supervise loop exits, the container lives on but nothing refreshes the pidfile. The in-container gateway marker stays in place, so the healthcheck keeps trusting a now-stale PID forever and reports a working sandbox as permanently `unhealthy`. ## Change - Add `clear_in_container_gateway_marker`, symmetric to `mark_in_container_gateway`. - Arm it via `trap ... EXIT` right after the existing `SIGTERM`/`SIGINT` trap, in both the non-root and root supervise paths. The EXIT trap covers every way the supervisor leaves: a clean gateway exit, a forwarded signal (`cleanup_on_signal` ends in `exit`), and errexit. This keeps the fix inside `nemoclaw-start.sh` and out of the shared `cleanup_on_signal` in `sandbox-init.sh`, which `agents/hermes/start.sh` also sources. The marker is re-dropped at each (re)launch by `mark_in_container_gateway`, so the respawn loop — which never exits the script — keeps it in place; only a supervisor *exit* clears it. The marker semantics become: "a supervisor is actively managing the gateway and keeping the pidfile fresh." Once the marker is gone, the healthcheck takes the marker-absent → healthy branch (#4503) instead of trusting a stale PID. ## Tests Extends `test/nemoclaw-start-gateway-marker.test.ts`, reusing its existing `extractShellFunctionFromSource` / `safeTmpHelpers` harness so each case runs the real shell functions in isolation against a temp marker path: - `clear_in_container_gateway_marker` removes the marker and is a no-op when it is absent. - The marker is dropped when the supervisor reaches a clean exit (real `trap ... EXIT` wiring, not a direct helper call). - The marker is dropped when the supervisor is terminated by a signal, exercising the `cleanup_on_signal` → `exit` → EXIT-trap path. - The marker survives respawns while the supervisor keeps running, and is cleared only once the script finally exits. `shfmt`, `shellcheck`, Biome, and `typecheck:cli` pass; the new and existing marker tests pass (14/14). Happy to revisit the choice of an EXIT trap over explicit cleanup at each exit site if maintainers prefer a different shape. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved gateway supervisor shutdown to reliably remove the in-container gateway marker on normal exit, SIGTERM/SIGINT termination, early launch failures, and `errexit`/`EXIT` flows (including non-PID 1 scenarios). * Reduces risk of stale marker files misleading container health checks. * **Tests** * Added shell-based behavioral coverage for marker cleanup on both launch paths, SIGTERM teardown, and marker persistence across respawn iterations until final exit. * **Documentation** * Clarified the marker lifecycle in the deployment/architecture reference. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> --------- Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> Signed-off-by: Charan Jagwani <cjagwani@nvidia.com> Co-authored-by: Prekshi Vyas <prekshiv@nvidia.com> Co-authored-by: Charan Jagwani <cjagwani@nvidia.com>
1 parent 89e19f0 commit 77db6fb

4 files changed

Lines changed: 446 additions & 67 deletions

File tree

docs/reference/architecture.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ Per-user units, partial units, and user-manager or bus outages do not take over
8787
That compatibility fallback remains until supported upgrade paths no longer include pre-service OpenShell installs and the package-managed handoff has direct nightly coverage.
8888
On Apple Silicon macOS, NemoClaw starts the OpenShell Docker-driver gateway and creates the sandbox as a Docker container.
8989
In both Docker-driver modes, the sandbox is a Docker container, not a Kubernetes pod.
90-
The in-container `/tmp/nemoclaw-gateway-local` marker is written only by entrypoint paths that actually launch an in-container gateway.
90+
Entrypoint supervisors create the in-container `/tmp/nemoclaw-gateway-local` marker only when they actually launch an in-container gateway, and they normally keep it present while that supervisor is active.
91+
On normal exits, handled `SIGTERM`/`SIGINT`, startup failures, and shell `errexit` termination through the `EXIT` trap, the supervisor removes the marker on a best-effort basis so the Docker health check does not keep trusting a stale gateway PID.
9192
Terminal runtimes may not write it.
9293
NemoClaw does not treat sandbox environment hints such as `OPENSHELL_DRIVERS` as authoritative for gateway ownership.
9394
Legacy non-Docker-driver installs still use the k3s-based gateway path.

scripts/nemoclaw-start.sh

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,22 @@ mark_in_container_gateway() {
350350
_nemoclaw_safe_create_tmp_file /tmp/nemoclaw-gateway-local 600 "" best-effort 2>/dev/null || true
351351
}
352352

353+
# Drop the in-container gateway marker (#4952). The HEALTHCHECK's pidfile
354+
# fallback trusts /tmp/nemoclaw-gateway.pid, which is refreshed *only* by
355+
# record_gateway_pid inside this supervisor's launch/respawn paths. On
356+
# OpenShell docker-driver sandboxes this script is NOT PID 1 -- OpenShell's
357+
# `sleep infinity` keeps the container alive as a sibling -- so when the
358+
# supervise loop exits, the container lives on but nothing refreshes the
359+
# pidfile. The marker would otherwise stay in place, leaving the healthcheck
360+
# trusting a stale PID forever (permanent false `unhealthy`). Tying marker
361+
# removal to supervisor exit completes the #4710 marker semantics: the marker
362+
# means "a supervisor is actively managing the gateway and keeping the pidfile
363+
# fresh". Once it is gone the healthcheck takes the marker-absent -> healthy
364+
# branch (#4503) instead. Best-effort: failure must never block teardown.
365+
clear_in_container_gateway_marker() {
366+
rm -f /tmp/nemoclaw-gateway-local 2>/dev/null || true
367+
}
368+
353369
# Record the PID/starttime identity of the live in-container gateway so the
354370
# Docker HEALTHCHECK
355371
# can confirm the actual gateway process (not merely *some* `openclaw`
@@ -4049,7 +4065,9 @@ GUARDENVEOF
40494065
# cleanup_on_signal is provided by sandbox-init.sh. It reads
40504066
# SANDBOX_CHILD_PIDS (array of all PIDs) and SANDBOX_WAIT_PID (the
40514067
# primary process whose exit status is returned).
4052-
# Each code path below sets these before registering the trap.
4068+
# Each code path arms the trap before launching the gateway. These values are
4069+
# populated as children start; cleanup refreshes and validates them before
4070+
# signaling anything.
40534071

40544072
# Keep per-user rc files out of runtime proxy wiring. Older images and prior
40554073
# entrypoint versions wrote a two-line shim into .bashrc/.profile; remove that
@@ -4872,7 +4890,24 @@ wait_for_openclaw_gateway_internal() {
48724890
return 1
48734891
}
48744892

4893+
arm_openclaw_gateway_supervisor_cleanup() {
4894+
# Bash does not run an EXIT trap when an untrapped SIGTERM/SIGINT terminates
4895+
# the shell, so both traps must be live before the marker is written.
4896+
trap cleanup_openclaw_on_signal SIGTERM SIGINT
4897+
trap clear_in_container_gateway_marker EXIT
4898+
}
4899+
48754900
launch_openclaw_gateway() {
4901+
# Drop the gateway marker whenever this supervisor exits -- clean gateway
4902+
# exit (`exit 0` below), a forwarded signal (cleanup_openclaw_on_signal ends
4903+
# in `cleanup_on_signal` -> `exit`), or errexit. This is the #4952 fix: on
4904+
# docker-driver sandboxes this script is not PID 1, so it can exit while the
4905+
# container lives on; a surviving marker would leave the HEALTHCHECK trusting
4906+
# a stale pidfile. Arm this before marking so early launch failures cannot
4907+
# leave the marker behind. The marker is re-dropped at each launch
4908+
# (mark_in_container_gateway), so the respawn loop -- which never exits the
4909+
# script -- keeps it in place.
4910+
arm_openclaw_gateway_supervisor_cleanup
48764911
mark_in_container_gateway
48774912
nohup "${STEP_DOWN_PREFIX_GATEWAY[@]}" sh -c \
48784913
'umask 0007; exec "$@" >>/tmp/gateway.log 2>&1' sh \
@@ -4894,6 +4929,16 @@ launch_openclaw_gateway() {
48944929
echo "[gateway] openclaw gateway launched as 'gateway' user (pid $GATEWAY_PID)" >&2
48954930
}
48964931

4932+
launch_openclaw_gateway_non_root() {
4933+
arm_openclaw_gateway_supervisor_cleanup
4934+
mark_in_container_gateway
4935+
nohup "$OPENCLAW" gateway run --port "${_DASHBOARD_PORT}" >/tmp/gateway.log 2>&1 &
4936+
GATEWAY_PID=$!
4937+
capture_openclaw_pid_start_identity "$GATEWAY_PID" GATEWAY_PID_START_IDENTITY || exit 1
4938+
record_gateway_pid "$GATEWAY_PID" "$GATEWAY_PID_START_IDENTITY"
4939+
echo "[gateway] openclaw gateway launched (pid $GATEWAY_PID)" >&2
4940+
}
4941+
48974942
openclaw_supervised_aux_pid_is_live() {
48984943
local pid="$1"
48994944
local expected_identity="$2"
@@ -5492,12 +5537,7 @@ if [ "$(id -u)" -ne 0 ]; then
54925537
# to healthy — see the mark_in_container_gateway comment near the top of this
54935538
# file for the #4710 rationale (why the marker is tied to the launch site
54945539
# rather than an env-var conditional at startup).
5495-
mark_in_container_gateway
5496-
nohup "$OPENCLAW" gateway run --port "${_DASHBOARD_PORT}" >/tmp/gateway.log 2>&1 &
5497-
GATEWAY_PID=$!
5498-
capture_openclaw_pid_start_identity "$GATEWAY_PID" GATEWAY_PID_START_IDENTITY || exit 1
5499-
record_gateway_pid "$GATEWAY_PID" "$GATEWAY_PID_START_IDENTITY"
5500-
echo "[gateway] openclaw gateway launched (pid $GATEWAY_PID)" >&2
5540+
launch_openclaw_gateway_non_root
55015541
# Diagnostic: mirror gateway log to PID 1's stderr — see root-mode block
55025542
# below for rationale (NVIDIA/NemoClaw#2484).
55035543
{ tail -n +1 -F /tmp/gateway.log 2>/dev/null | sed -u 's/^/[gateway-log:] /' >&2; } &
@@ -5515,7 +5555,6 @@ if [ "$(id -u)" -ne 0 ]; then
55155555
refresh_openclaw_supervised_child_pids
55165556
# shellcheck disable=SC2034 # read by cleanup_on_signal from sandbox-init.sh
55175557
SANDBOX_WAIT_PID="$GATEWAY_PID"
5518-
trap cleanup_openclaw_on_signal SIGTERM SIGINT
55195558
print_dashboard_urls
55205559

55215560
# Auto-respawn gateway on unexpected death (NVIDIA/NemoClaw#2757). Without
@@ -5720,6 +5759,7 @@ validate_nemoclaw_tmp_permissions
57205759
# Marking, privilege step-down, log redirection, and PID recording are kept in
57215760
# one reusable launch primitive so PID 1 owns initial start, crash respawn, and
57225761
# host-requested restart identically.
5762+
# The launch primitive arms signal and EXIT cleanup before writing the marker.
57235763
launch_openclaw_gateway
57245764

57255765
# Diagnostic: mirror gateway log to PID 1's stderr so its content surfaces in
@@ -5769,7 +5809,6 @@ start_gateway_serving_watchdog
57695809
refresh_openclaw_supervised_child_pids
57705810
# shellcheck disable=SC2034 # read by cleanup_on_signal from sandbox-init.sh
57715811
SANDBOX_WAIT_PID="$GATEWAY_PID"
5772-
trap cleanup_openclaw_on_signal SIGTERM SIGINT
57735812
if ! gateway_control_init; then
57745813
echo "[gateway-control] privileged gateway control unavailable" >&2
57755814
fi

test/nemoclaw-start-gateway-health.test.ts

Lines changed: 56 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ function extractShellFunction(src: string, name: string): string {
4747
return `${name}() {${body.slice(0, closing?.index ?? 0)}\n}`;
4848
}
4949

50+
function gatewayMarkerFunction(src: string, name: string, markerPath: string): string {
51+
return extractShellFunction(src, name).replaceAll("/tmp/nemoclaw-gateway-local", markerPath);
52+
}
53+
5054
function extractGatewayLogAppendFunction(src: string, gatewayLog: string): string {
5155
const functionSource = extractShellFunction(src, "append_openclaw_gateway_log_line");
5256
const marker = ' local log_file="/tmp/gateway.log"';
@@ -101,6 +105,7 @@ function watchdogFunctions(gatewayLog: string): string {
101105
function rootGatewayLifecycleFunctions(src: string, gatewayLog: string): string {
102106
return [
103107
pidIdentityFunctions(src),
108+
extractShellFunction(src, "arm_openclaw_gateway_supervisor_cleanup"),
104109
extractShellFunction(src, "launch_openclaw_gateway").replaceAll("/tmp/gateway.log", gatewayLog),
105110
extractShellFunction(src, "openclaw_supervised_aux_pid_is_live"),
106111
extractShellFunction(src, "stop_openclaw_supervised_gateway"),
@@ -111,6 +116,18 @@ function rootGatewayLifecycleFunctions(src: string, gatewayLog: string): string
111116
].join("\n");
112117
}
113118

119+
function gatewayLaunchBlock(src: string, kind: "non-root" | "root", gatewayLog: string): string {
120+
const startMarker =
121+
kind === "non-root"
122+
? "# Start gateway in background, auto-pair, then wait"
123+
: "# Start the gateway as the 'gateway' user.";
124+
const start = src.indexOf(startMarker);
125+
const end = src.indexOf('SANDBOX_WAIT_PID="$GATEWAY_PID"', start);
126+
expect(start, `Expected ${kind} gateway launch block in scripts/nemoclaw-start.sh`).not.toBe(-1);
127+
expect(end, `Expected ${kind} gateway launch block in scripts/nemoclaw-start.sh`).not.toBe(-1);
128+
return src.slice(start, src.indexOf("\n", end)).replaceAll("/tmp/gateway.log", gatewayLog);
129+
}
130+
114131
// Drive the watchdog end-to-end against a real background process standing in
115132
// for the gateway. `curlPlan` is the sequence of curl exit codes the stubbed
116133
// probe returns, one per watchdog cycle; the last entry repeats forever.
@@ -994,12 +1011,8 @@ describe("healthcheck marker (#4503, #4710)", () => {
9941011
});
9951012
});
9961013

997-
// Behavioral wiring coverage: run the real launch block of each entrypoint
998-
// mode with the real marker/pidfile/watchdog helpers and assert their
999-
// runtime effects. This replaces source-text assertions (banned by
1000-
// ci/source-shape-test-budget.json) and locks the #4748 regression
1001-
// behaviorally: OPENSHELL_DRIVERS is exported during the run and must have
1002-
// no influence on whether the marker is dropped.
1014+
// Run both real launch paths with their marker, pidfile, and watchdog helpers.
1015+
// This behaviorally covers the driver-env marker regression (#4748).
10031016
describe("gateway launch wiring (#4710)", () => {
10041017
it("exits PID 1 without signaling when gateway identity capture fails", () => {
10051018
const src = fs.readFileSync(START_SCRIPT, "utf-8");
@@ -1021,10 +1034,12 @@ describe("gateway launch wiring (#4710)", () => {
10211034
"GATEWAY_PID=0",
10221035
"GATEWAY_PID_START_IDENTITY=",
10231036
"mark_in_container_gateway() { :; }",
1037+
"clear_in_container_gateway_marker() { :; }\ncleanup_openclaw_on_signal() { :; }",
10241038
"capture_openclaw_pid_start_identity() { return 1; }",
10251039
'clear_gateway_pid_record() { printf "clear\\n" >>"$EVENT_LOG"; }',
10261040
'kill() { printf "unexpected-kill:%s\\n" "$*" >>"$EVENT_LOG"; }',
10271041
'wait() { printf "unexpected-wait:%s\\n" "$*" >>"$EVENT_LOG"; }',
1042+
extractShellFunction(src, "arm_openclaw_gateway_supervisor_cleanup"),
10281043
launch,
10291044
"launch_openclaw_gateway",
10301045
].join("\n"),
@@ -1038,20 +1053,6 @@ describe("gateway launch wiring (#4710)", () => {
10381053
fs.rmSync(tmpDir, { recursive: true, force: true });
10391054
});
10401055

1041-
function launchBlock(src: string, kind: "non-root" | "root"): string {
1042-
const startMarker =
1043-
kind === "non-root"
1044-
? "# Start gateway in background, auto-pair, then wait"
1045-
: "# Start the gateway as the 'gateway' user.";
1046-
const start = src.indexOf(startMarker);
1047-
const trap = src.indexOf("trap cleanup_openclaw_on_signal SIGTERM SIGINT", start);
1048-
expect(start, `Expected ${kind} gateway launch block in scripts/nemoclaw-start.sh`).not.toBe(
1049-
-1,
1050-
);
1051-
expect(trap, `Expected ${kind} gateway launch block in scripts/nemoclaw-start.sh`).not.toBe(-1);
1052-
return src.slice(start, src.indexOf("\n", trap));
1053-
}
1054-
10551056
function runLaunchWiring(kind: "non-root" | "root") {
10561057
const src = fs.readFileSync(START_SCRIPT, "utf-8");
10571058
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `nemoclaw-launch-wiring-${kind}-`));
@@ -1074,9 +1075,11 @@ describe("gateway launch wiring (#4710)", () => {
10741075

10751076
const realFunctions = [
10761077
safeTmpHelpers(src),
1077-
extractShellFunction(src, "mark_in_container_gateway").replaceAll(
1078-
"/tmp/nemoclaw-gateway-local",
1079-
markerPath,
1078+
gatewayMarkerFunction(src, "mark_in_container_gateway", markerPath),
1079+
gatewayMarkerFunction(src, "clear_in_container_gateway_marker", markerPath),
1080+
extractShellFunction(src, "launch_openclaw_gateway_non_root").replaceAll(
1081+
"/tmp/gateway.log",
1082+
gatewayLog,
10801083
),
10811084
extractShellFunction(src, "record_gateway_pid"),
10821085
extractShellFunction(src, "gateway_pid_is_openclaw_gateway"),
@@ -1106,7 +1109,8 @@ describe("gateway launch wiring (#4710)", () => {
11061109
"STEP_DOWN_PREFIX_SANDBOX=(gosu sandbox)",
11071110
"STEP_DOWN_PREFIX_GATEWAY=(gosu gateway)",
11081111
realFunctions,
1109-
launchBlock(src, kind).replaceAll("/tmp/gateway.log", gatewayLog),
1112+
gatewayLaunchBlock(src, kind, gatewayLog),
1113+
`if [ -f ${JSON.stringify(markerPath)} ]; then printf "MARKER_PRESENT=1\\n"; fi`,
11101114
`for _ in $(command seq 1 100); do [ -s ${JSON.stringify(openclawLog)} ] && break; command sleep 0.1; done`,
11111115
'printf "GATEWAY_PID=%s\\n" "$GATEWAY_PID"',
11121116
'printf "WATCHDOG_PID=%s\\n" "${GATEWAY_WATCHDOG_PID:-}"',
@@ -1124,20 +1128,31 @@ describe("gateway launch wiring (#4710)", () => {
11241128
const watchdogPid = stdout.match(/^WATCHDOG_PID=(\d+)$/m)?.[1];
11251129
const childPids = (stdout.match(/^CHILD_PIDS=(.+)$/m)?.[1] ?? "").split(/\s+/);
11261130
const pidFileContent = readFileIfPresent(pidFile)?.trim() ?? null;
1131+
const markerPresent = stdout.includes("MARKER_PRESENT=1");
11271132
const markerExists = readFileIfPresent(markerPath) !== null;
11281133
fs.rmSync(tmpDir, { recursive: true, force: true });
1129-
return { result, stdout, gatewayPid, watchdogPid, childPids, pidFileContent, markerExists };
1134+
return {
1135+
result,
1136+
stdout,
1137+
gatewayPid,
1138+
watchdogPid,
1139+
childPids,
1140+
pidFileContent,
1141+
markerPresent,
1142+
markerExists,
1143+
};
11301144
}
11311145

11321146
it.each([
11331147
"non-root",
11341148
"root",
1135-
] as const)("%s launch drops the marker, records the gateway PID, and starts the tracked watchdog", (kind) => {
1149+
] as const)("%s launch clears the marker on supervisor exit after recording the gateway PID", (kind) => {
11361150
const run = runLaunchWiring(kind);
11371151
expect(run.result.status, `script failed: ${run.result.stderr}`).toBe(0);
1138-
// Marker dropped by the launch site, even with OPENSHELL_DRIVERS=docker
1139-
// exported — env hints must not gate it (#4748 was a no-op for this).
1140-
expect(run.markerExists).toBe(true);
1152+
expect(run.markerPresent).toBe(true);
1153+
// The supervisor EXIT trap clears the in-container marker when this fixture
1154+
// exits, returning healthchecks to the marker-absent branch (#4952).
1155+
expect(run.markerExists).toBe(false);
11411156
// The watchdog reads the gateway PID from the pidfile each cycle.
11421157
expect(run.gatewayPid).toBeDefined();
11431158
expect(run.pidFileContent?.split(" ")[0]).toBe(run.gatewayPid);
@@ -1356,27 +1371,11 @@ describe("respawn loop pidfile refresh (#4710)", () => {
13561371
});
13571372
});
13581373

1359-
// Launch-path signal handling and child-PID tracking for both entrypoint
1360-
// modes. Moved from test/nemoclaw-start.test.ts so the legacy file stays
1361-
// under its ratcheted size budget; this file owns gateway-launch coverage.
1374+
// Launch-path signal handling and child-PID tracking for both entrypoint modes.
1375+
// This file owns gateway launch coverage to keep the legacy test within budget.
13621376
describe("nemoclaw-start gateway launch signal handling", () => {
13631377
const src = fs.readFileSync(START_SCRIPT, "utf-8");
13641378

1365-
function launchBlock(kind: "non-root" | "root", gatewayLog: string): string {
1366-
const startMarker =
1367-
kind === "non-root"
1368-
? "# Start gateway in background, auto-pair, then wait"
1369-
: "# Start the gateway as the 'gateway' user.";
1370-
const start = src.indexOf(startMarker);
1371-
const trap = src.indexOf("trap cleanup_openclaw_on_signal SIGTERM SIGINT", start);
1372-
expect(start, `Expected ${kind} gateway launch block in scripts/nemoclaw-start.sh`).not.toBe(
1373-
-1,
1374-
);
1375-
expect(trap, `Expected ${kind} gateway launch block in scripts/nemoclaw-start.sh`).not.toBe(-1);
1376-
const lineEnd = src.indexOf("\n", trap);
1377-
return src.slice(start, lineEnd).replaceAll("/tmp/gateway.log", gatewayLog);
1378-
}
1379-
13801379
function runLaunchBlock(kind: "non-root" | "root") {
13811380
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `nemoclaw-launch-${kind}-`));
13821381
const fakeBin = path.join(tmpDir, "bin");
@@ -1417,20 +1416,20 @@ describe("nemoclaw-start gateway launch signal handling", () => {
14171416
"start_plugin_registry_refresh() { :; }",
14181417
"cleanup_on_signal() { :; }",
14191418
safeTmpHelpers(src),
1420-
extractShellFunction(src, "mark_in_container_gateway").replaceAll(
1421-
"/tmp/nemoclaw-gateway-local",
1422-
markerPath,
1423-
),
1424-
// #4710: the launch block also records the gateway PID for the
1425-
// serving watchdog and starts the watchdog alongside the other
1426-
// background services. Stub both — watchdog behavior has its own
1427-
// suite in test/nemoclaw-start-gateway-health.test.ts.
1419+
gatewayMarkerFunction(src, "mark_in_container_gateway", markerPath),
1420+
gatewayMarkerFunction(src, "clear_in_container_gateway_marker", markerPath),
1421+
// Stub PID recording and the serving watchdog; each has focused tests
1422+
// elsewhere in this suite (#4710).
14281423
"record_gateway_pid() { :; }",
14291424
'start_gateway_serving_watchdog() { sleep 30 & GATEWAY_WATCHDOG_PID=$!; capture_openclaw_pid_start_identity "$GATEWAY_WATCHDOG_PID" GATEWAY_WATCHDOG_PID_START_IDENTITY; }',
1425+
extractShellFunction(src, "launch_openclaw_gateway_non_root").replaceAll(
1426+
"/tmp/gateway.log",
1427+
gatewayLog,
1428+
),
14301429
rootGatewayLifecycleFunctions(src, gatewayLog),
14311430
"STEP_DOWN_PREFIX_SANDBOX=(gosu sandbox)",
14321431
"STEP_DOWN_PREFIX_GATEWAY=(gosu gateway)",
1433-
launchBlock(kind, gatewayLog),
1432+
gatewayLaunchBlock(src, kind, gatewayLog),
14341433
kind === "root"
14351434
? `for _ in ${waitForLaunchLogIterations}; do [ -s ${JSON.stringify(gosuLog)} ] && [ -s ${JSON.stringify(openclawLog)} ] && break; sleep 0.1; done`
14361435
: `for _ in ${waitForLaunchLogIterations}; do [ -s ${JSON.stringify(openclawLog)} ] && break; sleep 0.1; done`,
@@ -1457,7 +1456,7 @@ describe("nemoclaw-start gateway launch signal handling", () => {
14571456

14581457
it("registers child PIDs, redirects gateway output, and traps signals in non-root mode", () => {
14591458
const { result, openclaw, gateway } = runLaunchBlock("non-root");
1460-
expect(result.status).toBe(0);
1459+
expect(result.status, result.stderr).toBe(0);
14611460
expect(openclaw).toContain("gateway run --port 19000");
14621461
expect(openclaw).toContain("marker=present");
14631462
expect(openclaw).not.toContain("marker=absent");

0 commit comments

Comments
 (0)