diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index 08f1a3d65..76857867f 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -120,6 +120,39 @@ describe("sanitizeHostExecEnv", () => { expect(env[" BAD KEY"]).toBeUndefined(); expect(env["NOT-PORTABLE"]).toBeUndefined(); }); + + it("can allow PATH overrides when explicitly opted out of blocking", () => { + const env = sanitizeHostExecEnv({ + baseEnv: { + PATH: "/usr/bin:/bin", + }, + overrides: { + PATH: "/custom/bin", + }, + blockPathOverrides: false, + }); + + expect(env.PATH).toBe("/custom/bin"); + expect(env.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE); + }); + + it("drops non-string inherited values and non-portable inherited keys", () => { + const env = sanitizeHostExecEnv({ + baseEnv: { + PATH: "/usr/bin:/bin", + GOOD: "1", + // oxlint-disable-next-line typescript/no-explicit-any + BAD_NUMBER: 1 as any, + "NOT-PORTABLE": "x", + }, + }); + + expect(env).toEqual({ + OPENCLAW_CLI: OPENCLAW_CLI_ENV_VALUE, + PATH: "/usr/bin:/bin", + GOOD: "1", + }); + }); }); describe("isDangerousHostEnvOverrideVarName", () => { @@ -174,6 +207,33 @@ describe("sanitizeSystemRunEnvOverrides", () => { LC_ALL: "C", }); }); + + it("returns undefined when no shell-wrapper overrides survive", () => { + expect( + sanitizeSystemRunEnvOverrides({ + shellWrapper: true, + overrides: { + TOKEN: "abc", + }, + }), + ).toBeUndefined(); + expect(sanitizeSystemRunEnvOverrides({ shellWrapper: true })).toBeUndefined(); + }); + + it("keeps allowlisted shell-wrapper overrides case-insensitively", () => { + expect( + sanitizeSystemRunEnvOverrides({ + shellWrapper: true, + overrides: { + lang: "C", + ColorTerm: "truecolor", + }, + }), + ).toEqual({ + lang: "C", + ColorTerm: "truecolor", + }); + }); }); describe("shell wrapper exploit regression", () => { diff --git a/src/infra/restart-sentinel.test.ts b/src/infra/restart-sentinel.test.ts index 76b9e53b5..c28504685 100644 --- a/src/infra/restart-sentinel.test.ts +++ b/src/infra/restart-sentinel.test.ts @@ -5,9 +5,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { captureEnv } from "../test-utils/env.js"; import { consumeRestartSentinel, + formatDoctorNonInteractiveHint, formatRestartSentinelMessage, readRestartSentinel, resolveRestartSentinelPath, + summarizeRestartSentinel, trimLogTail, writeRestartSentinel, } from "./restart-sentinel.js"; @@ -59,6 +61,15 @@ describe("restart sentinel", () => { await expect(fs.stat(filePath)).rejects.toThrow(); }); + it("drops structurally invalid sentinel payloads", async () => { + const filePath = resolveRestartSentinelPath(); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, JSON.stringify({ version: 2, payload: null }), "utf-8"); + + await expect(readRestartSentinel()).resolves.toBeNull(); + await expect(fs.stat(filePath)).rejects.toThrow(); + }); + it("formatRestartSentinelMessage uses custom message when present", () => { const payload = { kind: "config-apply" as const, @@ -93,6 +104,26 @@ describe("restart sentinel", () => { expect(result).toContain("Gateway restart"); }); + it("formats summary, distinct reason, and doctor hint together", () => { + const payload = { + kind: "config-patch" as const, + status: "error" as const, + ts: Date.now(), + message: "Patch failed", + doctorHint: "Run openclaw doctor", + stats: { mode: "patch", reason: "validation failed" }, + }; + + expect(formatRestartSentinelMessage(payload)).toBe( + [ + "Gateway restart config-patch error (patch)", + "Patch failed", + "Reason: validation failed", + "Run openclaw doctor", + ].join("\n"), + ); + }); + it("trims log tails", () => { const text = "a".repeat(9000); const trimmed = trimLogTail(text, 8000); @@ -115,6 +146,18 @@ describe("restart sentinel", () => { expect(textA).toContain("Gateway restart restart ok"); expect(textA).not.toContain('"ts"'); }); + + it("summarizes restart payloads and trims log tails without trailing whitespace", () => { + expect( + summarizeRestartSentinel({ + kind: "update", + status: "skipped", + ts: 1, + }), + ).toBe("Gateway restart update skipped"); + expect(trimLogTail("hello\n")).toBe("hello"); + expect(trimLogTail(undefined)).toBeNull(); + }); }); describe("restart sentinel message dedup", () => { @@ -145,4 +188,10 @@ describe("restart sentinel message dedup", () => { expect(result).toContain("Restart requested by /restart"); expect(result).toContain("Reason: /restart"); }); + + it("formats the non-interactive doctor command", () => { + expect(formatDoctorNonInteractiveHint({ PATH: "/usr/bin:/bin" })).toContain( + "openclaw doctor --non-interactive", + ); + }); });