diff --git a/src/lib/sandbox-logs-action.ts b/src/lib/sandbox-logs-action.ts index af66f06641..89d97cd05c 100644 --- a/src/lib/sandbox-logs-action.ts +++ b/src/lib/sandbox-logs-action.ts @@ -2,74 +2,35 @@ // SPDX-License-Identifier: Apache-2.0 import { spawn } from "node:child_process"; -import os from "node:os"; import { ROOT } from "./runner"; import { getOpenshellBinary, runOpenshell } from "./openshell-runtime"; +import { + buildEnableSandboxAuditLogsArgs, + buildSandboxLogsArgs, + buildSandboxOpenclawGatewayLogsArgs, + describeLogProbeResult, + exitCodeFromSignal, + getLogsProbeTimeoutMs, + normalizeSandboxLogsOptions, + type LogProbeResult, +} from "./sandbox-logs-helpers"; import type { SandboxLogsOptions } from "./sandbox-logs-options"; -import { DEFAULT_SANDBOX_LOG_LINES } from "./sandbox-logs-options"; - -const DEFAULT_LOGS_PROBE_TIMEOUT_MS = 5000; -const LOGS_PROBE_TIMEOUT_ENV = "NEMOCLAW_LOGS_PROBE_TIMEOUT_MS"; - -type SpawnLikeResult = { - status: number | null; - stdout?: string; - stderr?: string; - error?: Error; - signal?: NodeJS.Signals | null; -}; /* v8 ignore next -- process exit mapping is covered through CLI subprocess log tests. */ -function exitWithSpawnResult(result: SpawnLikeResult & { signal?: NodeJS.Signals | null }) { +function exitWithSpawnResult(result: LogProbeResult) { if (result.status !== null) { process.exit(result.status); } - if (result.signal) { - const signalNumber = os.constants.signals[result.signal]; - process.exit(signalNumber ? 128 + signalNumber : 1); - } - - process.exit(1); -} - -export function getLogsProbeTimeoutMs(): number { - const rawValue = process.env[LOGS_PROBE_TIMEOUT_ENV]; - if (!rawValue) { - return DEFAULT_LOGS_PROBE_TIMEOUT_MS; - } - const parsed = Number(rawValue); - const timeoutMs = Number.isFinite(parsed) ? Math.floor(parsed) : Number.NaN; - return timeoutMs > 0 ? timeoutMs : DEFAULT_LOGS_PROBE_TIMEOUT_MS; -} - -export function describeLogProbeResult(result: SpawnLikeResult): string { - if (result.error) { - return result.error.message; - } - if (result.signal) { - return `signal ${result.signal}`; - } - return `exit ${result.status ?? "unknown"}`; -} - -export function normalizeSandboxLogsOptions(options: SandboxLogsOptions | boolean): SandboxLogsOptions { - if (typeof options === "boolean") { - return { follow: options, lines: DEFAULT_SANDBOX_LOG_LINES, since: null }; - } - return { - follow: options.follow, - lines: options.lines || DEFAULT_SANDBOX_LOG_LINES, - since: options.since || null, - }; + process.exit(exitCodeFromSignal(result.signal ?? null)); } /* v8 ignore next -- OpenShell subprocess call is covered through CLI subprocess log tests. */ function runOpenclawGatewayLogs( sandboxName: string, options: SandboxLogsOptions, -): SpawnLikeResult { +): LogProbeResult { const args = buildSandboxOpenclawGatewayLogsArgs(sandboxName, options); const result = runOpenshell(args, { stdio: "inherit", @@ -126,11 +87,6 @@ function streamSandboxFollowLogs(sandboxName: string, options: SandboxLogsOption } process.exit(requestedExitCode ?? finalStatus); }; - const exitFromSignal = (signal: NodeJS.Signals | null): number => { - if (!signal) return 1; - const signalNumber = os.constants.signals[signal]; - return signalNumber ? 128 + signalNumber : 1; - }; const markSourceDone = ( source: (typeof sources)[number], status: number, @@ -177,7 +133,7 @@ function streamSandboxFollowLogs(sandboxName: string, options: SandboxLogsOption markSourceDone(source, 1, error.message); }); source.child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => { - markSourceDone(source, code ?? exitFromSignal(signal), signal ? `signal ${signal}` : null); + markSourceDone(source, code ?? exitCodeFromSignal(signal), signal ? `signal ${signal}` : null); }); }; @@ -207,7 +163,7 @@ function enableSandboxAuditLogs(sandboxName: string) { function warnSandboxAuditLogsUnavailable( sandboxName: string, args: string[], - result: SpawnLikeResult, + result: LogProbeResult, ): void { const stderr = String(result.stderr || "").trim(); console.error( @@ -220,33 +176,6 @@ function warnSandboxAuditLogsUnavailable( console.error(" Policy denial events may be missing from OpenShell logs."); } -export function buildEnableSandboxAuditLogsArgs(sandboxName: string): string[] { - return ["settings", "set", sandboxName, "--key", "ocsf_json_enabled", "--value", "true"]; -} - -export function buildSandboxOpenclawGatewayLogsArgs( - sandboxName: string, - options: SandboxLogsOptions, -): string[] { - const args = ["sandbox", "exec", "-n", sandboxName, "--", "tail", "-n", options.lines]; - if (options.follow) { - args.push("-f"); - } - args.push("/tmp/gateway.log"); - return args; -} - -export function buildSandboxLogsArgs(sandboxName: string, options: SandboxLogsOptions): string[] { - const args = ["logs", sandboxName, "-n", options.lines, "--source", "all"]; - if (options.since) { - args.push("--since", options.since); - } - if (options.follow) { - args.push("--tail"); - } - return args; -} - /* v8 ignore next -- external log streaming is covered through CLI subprocess log tests. */ export function showSandboxLogs(sandboxName: string, options: SandboxLogsOptions | boolean) { const logsOptions = normalizeSandboxLogsOptions(options); diff --git a/src/lib/sandbox-logs-action.test.ts b/src/lib/sandbox-logs-helpers.test.ts similarity index 73% rename from src/lib/sandbox-logs-action.test.ts rename to src/lib/sandbox-logs-helpers.test.ts index 71d383c822..723c49e4c4 100644 --- a/src/lib/sandbox-logs-action.test.ts +++ b/src/lib/sandbox-logs-helpers.test.ts @@ -1,28 +1,19 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, describe, expect, it, vi } from "vitest"; - -vi.mock("./runner", () => ({ ROOT: process.cwd() })); -vi.mock("./openshell-runtime", () => ({ - getOpenshellBinary: () => "openshell", - runOpenshell: () => ({ status: 0 }), -})); +import { describe, expect, it } from "vitest"; import { buildEnableSandboxAuditLogsArgs, buildSandboxLogsArgs, buildSandboxOpenclawGatewayLogsArgs, describeLogProbeResult, + exitCodeFromSignal, getLogsProbeTimeoutMs, normalizeSandboxLogsOptions, -} from "./sandbox-logs-action"; - -describe("sandbox logs action helpers", () => { - afterEach(() => { - vi.unstubAllEnvs(); - }); +} from "./sandbox-logs-helpers"; +describe("sandbox logs helpers", () => { it("normalizes boolean and partial logs options", () => { expect(normalizeSandboxLogsOptions(true)).toEqual({ follow: true, lines: "200", since: null }); expect(normalizeSandboxLogsOptions({ follow: false, lines: "", since: "" })).toEqual({ @@ -68,11 +59,14 @@ describe("sandbox logs action helpers", () => { expect(describeLogProbeResult({ status: null, signal: "SIGTERM" })).toBe("signal SIGTERM"); expect(describeLogProbeResult({ status: 7 })).toBe("exit 7"); - vi.stubEnv("NEMOCLAW_LOGS_PROBE_TIMEOUT_MS", "1234"); - expect(getLogsProbeTimeoutMs()).toBe(1234); - vi.stubEnv("NEMOCLAW_LOGS_PROBE_TIMEOUT_MS", "0"); - expect(getLogsProbeTimeoutMs()).toBe(5000); - vi.stubEnv("NEMOCLAW_LOGS_PROBE_TIMEOUT_MS", "not-a-number"); - expect(getLogsProbeTimeoutMs()).toBe(5000); + expect(getLogsProbeTimeoutMs({ NEMOCLAW_LOGS_PROBE_TIMEOUT_MS: "1234" })).toBe(1234); + expect(getLogsProbeTimeoutMs({ NEMOCLAW_LOGS_PROBE_TIMEOUT_MS: "0" })).toBe(5000); + expect(getLogsProbeTimeoutMs({ NEMOCLAW_LOGS_PROBE_TIMEOUT_MS: "not-a-number" })).toBe(5000); + expect(getLogsProbeTimeoutMs({})).toBe(5000); + }); + + it("maps signals to conventional process exit codes", () => { + expect(exitCodeFromSignal(null)).toBe(1); + expect(exitCodeFromSignal("SIGINT")).toBe(130); }); }); diff --git a/src/lib/sandbox-logs-helpers.ts b/src/lib/sandbox-logs-helpers.ts new file mode 100644 index 0000000000..0b998e29cc --- /dev/null +++ b/src/lib/sandbox-logs-helpers.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* v8 ignore start -- pure helper tests exercise this module; orchestration coverage still runs through dist. */ + +import os from "node:os"; + +import type { SandboxLogsOptions } from "./sandbox-logs-options"; +import { DEFAULT_SANDBOX_LOG_LINES } from "./sandbox-logs-options"; + +export const DEFAULT_LOGS_PROBE_TIMEOUT_MS = 5000; +export const LOGS_PROBE_TIMEOUT_ENV = "NEMOCLAW_LOGS_PROBE_TIMEOUT_MS"; + +export type LogProbeResult = { + status: number | null; + stdout?: string; + stderr?: string; + error?: Error; + signal?: NodeJS.Signals | null; +}; + +export function getLogsProbeTimeoutMs( + env: Record = process.env, +): number { + const rawValue = env[LOGS_PROBE_TIMEOUT_ENV]; + if (!rawValue) { + return DEFAULT_LOGS_PROBE_TIMEOUT_MS; + } + const parsed = Number(rawValue); + const timeoutMs = Number.isFinite(parsed) ? Math.floor(parsed) : Number.NaN; + return timeoutMs > 0 ? timeoutMs : DEFAULT_LOGS_PROBE_TIMEOUT_MS; +} + +export function describeLogProbeResult(result: LogProbeResult): string { + if (result.error) { + return result.error.message; + } + if (result.signal) { + return `signal ${result.signal}`; + } + return `exit ${result.status ?? "unknown"}`; +} + +export function exitCodeFromSignal(signal: NodeJS.Signals | null): number { + if (!signal) return 1; + const signalNumber = os.constants.signals[signal]; + return signalNumber ? 128 + signalNumber : 1; +} + +export function normalizeSandboxLogsOptions(options: SandboxLogsOptions | boolean): SandboxLogsOptions { + if (typeof options === "boolean") { + return { follow: options, lines: DEFAULT_SANDBOX_LOG_LINES, since: null }; + } + return { + follow: options.follow, + lines: options.lines || DEFAULT_SANDBOX_LOG_LINES, + since: options.since || null, + }; +} + +export function buildEnableSandboxAuditLogsArgs(sandboxName: string): string[] { + return ["settings", "set", sandboxName, "--key", "ocsf_json_enabled", "--value", "true"]; +} + +export function buildSandboxOpenclawGatewayLogsArgs( + sandboxName: string, + options: SandboxLogsOptions, +): string[] { + const args = ["sandbox", "exec", "-n", sandboxName, "--", "tail", "-n", options.lines]; + if (options.follow) { + args.push("-f"); + } + args.push("/tmp/gateway.log"); + return args; +} + +export function buildSandboxLogsArgs(sandboxName: string, options: SandboxLogsOptions): string[] { + const args = ["logs", sandboxName, "-n", options.lines, "--source", "all"]; + if (options.since) { + args.push("--since", options.since); + } + if (options.follow) { + args.push("--tail"); + } + return args; +} + +/* v8 ignore stop */