mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 06:30:43 +00:00
* feat: support operator-managed proxy routing * docs: add network proxy changelog entry * fix(proxy): restrict gateway bypass to loopback IPs * fix(cli): harden container proxy URL checks * docs(proxy): clarify gateway bypass scope * docs: remove proxy changelog entry * fix(proxy): clear startup CI guard failures * fix(proxy): harden gateway proxy policy parsing * fix(proxy): honor update shorthand proxy policy * fix(cli): redact proxy URL suffixes * test(proxy): keep gateway help off proxy startup * fix(proxy): keep overlapping lifecycle active * docs: add proxy changelog entry --------- Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
752 lines
18 KiB
TypeScript
752 lines
18 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
maybeRunCliInContainer,
|
|
parseCliContainerArgs,
|
|
resolveCliContainerTarget,
|
|
} from "./container-target.js";
|
|
|
|
describe("parseCliContainerArgs", () => {
|
|
it("extracts a root --container flag before the command", () => {
|
|
expect(
|
|
parseCliContainerArgs(["node", "openclaw", "--container", "demo", "status", "--deep"]),
|
|
).toEqual({
|
|
ok: true,
|
|
container: "demo",
|
|
argv: ["node", "openclaw", "status", "--deep"],
|
|
});
|
|
});
|
|
|
|
it("accepts the equals form", () => {
|
|
expect(parseCliContainerArgs(["node", "openclaw", "--container=demo", "health"])).toEqual({
|
|
ok: true,
|
|
container: "demo",
|
|
argv: ["node", "openclaw", "health"],
|
|
});
|
|
});
|
|
|
|
it("rejects a missing container value", () => {
|
|
expect(parseCliContainerArgs(["node", "openclaw", "--container"])).toEqual({
|
|
ok: false,
|
|
error: "--container requires a value",
|
|
});
|
|
});
|
|
|
|
it("does not consume an adjacent flag as the container value", () => {
|
|
expect(
|
|
parseCliContainerArgs(["node", "openclaw", "--container", "--no-color", "status"]),
|
|
).toEqual({
|
|
ok: false,
|
|
error: "--container requires a value",
|
|
});
|
|
});
|
|
|
|
it("leaves argv unchanged when the flag is absent", () => {
|
|
expect(parseCliContainerArgs(["node", "openclaw", "status"])).toEqual({
|
|
ok: true,
|
|
container: null,
|
|
argv: ["node", "openclaw", "status"],
|
|
});
|
|
});
|
|
|
|
it("extracts --container after the command like other root options", () => {
|
|
expect(
|
|
parseCliContainerArgs(["node", "openclaw", "status", "--container", "demo", "--deep"]),
|
|
).toEqual({
|
|
ok: true,
|
|
container: "demo",
|
|
argv: ["node", "openclaw", "status", "--deep"],
|
|
});
|
|
});
|
|
|
|
it("stops parsing --container after the -- terminator", () => {
|
|
expect(
|
|
parseCliContainerArgs([
|
|
"node",
|
|
"openclaw",
|
|
"nodes",
|
|
"run",
|
|
"--",
|
|
"docker",
|
|
"run",
|
|
"--container",
|
|
"demo",
|
|
"alpine",
|
|
]),
|
|
).toEqual({
|
|
ok: true,
|
|
container: null,
|
|
argv: [
|
|
"node",
|
|
"openclaw",
|
|
"nodes",
|
|
"run",
|
|
"--",
|
|
"docker",
|
|
"run",
|
|
"--container",
|
|
"demo",
|
|
"alpine",
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("resolveCliContainerTarget", () => {
|
|
it("uses argv first and falls back to OPENCLAW_CONTAINER", () => {
|
|
expect(
|
|
resolveCliContainerTarget(["node", "openclaw", "--container", "demo", "status"], {}),
|
|
).toBe("demo");
|
|
expect(resolveCliContainerTarget(["node", "openclaw", "status"], {})).toBeNull();
|
|
expect(
|
|
resolveCliContainerTarget(["node", "openclaw", "status"], {
|
|
OPENCLAW_CONTAINER: "demo",
|
|
} as NodeJS.ProcessEnv),
|
|
).toBe("demo");
|
|
});
|
|
});
|
|
|
|
describe("maybeRunCliInContainer", () => {
|
|
it("passes through when no container target is provided", () => {
|
|
expect(maybeRunCliInContainer(["node", "openclaw", "status"], { env: {} })).toEqual({
|
|
handled: false,
|
|
argv: ["node", "openclaw", "status"],
|
|
});
|
|
});
|
|
|
|
it("uses OPENCLAW_CONTAINER when the flag is absent", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(
|
|
maybeRunCliInContainer(["node", "openclaw", "status"], {
|
|
env: { OPENCLAW_CONTAINER: "demo" } as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
}),
|
|
).toEqual({
|
|
handled: true,
|
|
exitCode: 0,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"podman",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"--env",
|
|
"OPENCLAW_CONTAINER_HINT=demo",
|
|
"--env",
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
"demo",
|
|
"openclaw",
|
|
"status",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: {
|
|
OPENCLAW_CONTAINER: "",
|
|
},
|
|
},
|
|
);
|
|
});
|
|
|
|
it("clears inherited host routing and gateway env before execing into the child CLI", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
maybeRunCliInContainer(["node", "openclaw", "status"], {
|
|
env: {
|
|
OPENCLAW_CONTAINER: "demo",
|
|
OPENCLAW_PROFILE: "work",
|
|
OPENCLAW_GATEWAY_PORT: "19001",
|
|
OPENCLAW_GATEWAY_URL: "ws://127.0.0.1:18789",
|
|
OPENCLAW_GATEWAY_TOKEN: "token",
|
|
OPENCLAW_GATEWAY_PASSWORD: "password",
|
|
} as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"podman",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"--env",
|
|
"OPENCLAW_CONTAINER_HINT=demo",
|
|
"--env",
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
"demo",
|
|
"openclaw",
|
|
"status",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: {
|
|
OPENCLAW_CONTAINER: "",
|
|
},
|
|
},
|
|
);
|
|
});
|
|
|
|
it("passes the proxy URL env fallback into the child container CLI", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
maybeRunCliInContainer(["node", "openclaw", "status"], {
|
|
env: {
|
|
OPENCLAW_CONTAINER: "demo",
|
|
OPENCLAW_PROXY_URL: " http://proxy.internal:3128 ",
|
|
} as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"podman",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"--env",
|
|
"OPENCLAW_CONTAINER_HINT=demo",
|
|
"--env",
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
"--env",
|
|
"OPENCLAW_PROXY_URL=http://proxy.internal:3128",
|
|
"demo",
|
|
"openclaw",
|
|
"status",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: {
|
|
OPENCLAW_CONTAINER: "",
|
|
OPENCLAW_PROXY_URL: " http://proxy.internal:3128 ",
|
|
},
|
|
},
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"http://127.0.0.1:3128",
|
|
"http://127.1:3128",
|
|
"http://127.0.0.01:3128",
|
|
"http://localhost.:3128",
|
|
"http://[::1]:3128",
|
|
"http://[::ffff:127.0.0.1]:3128",
|
|
])("fails before forwarding loopback proxy URL %s into a child container CLI", (proxyUrl) => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(() =>
|
|
maybeRunCliInContainer(["node", "openclaw", "status"], {
|
|
env: {
|
|
OPENCLAW_CONTAINER: "demo",
|
|
OPENCLAW_PROXY_URL: ` ${proxyUrl} `,
|
|
} as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
}),
|
|
).toThrow("127.0.0.1 inside a container points at the container");
|
|
|
|
expect(spawnSync).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("redacts proxy URL credentials and URL suffixes before rejecting loopback container proxy forwarding", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
});
|
|
|
|
let message = "";
|
|
try {
|
|
maybeRunCliInContainer(["node", "openclaw", "status"], {
|
|
env: {
|
|
OPENCLAW_CONTAINER: "demo",
|
|
OPENCLAW_PROXY_URL:
|
|
"http://proxy-user:proxy-secret@127.1:3128?token=proxy-query-secret#proxy-fragment-secret",
|
|
} as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
});
|
|
} catch (err) {
|
|
message = err instanceof Error ? err.message : String(err);
|
|
}
|
|
|
|
expect(message).toContain("OPENCLAW_PROXY_URL=http://redacted:redacted@127.0.0.1:3128/");
|
|
expect(message).not.toContain("proxy-user");
|
|
expect(message).not.toContain("proxy-secret");
|
|
expect(message).not.toContain("proxy-query-secret");
|
|
expect(message).not.toContain("proxy-fragment-secret");
|
|
expect(message).not.toContain("?token=");
|
|
expect(message).not.toContain("#");
|
|
expect(spawnSync).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("allows explicitly overridden loopback proxy URL forwarding into a child container CLI", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
maybeRunCliInContainer(["node", "openclaw", "status"], {
|
|
env: {
|
|
OPENCLAW_CONTAINER: "demo",
|
|
OPENCLAW_PROXY_URL: " http://127.0.0.1:3128 ",
|
|
OPENCLAW_CONTAINER_ALLOW_LOOPBACK_PROXY_URL: "1",
|
|
} as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"podman",
|
|
expect.arrayContaining(["OPENCLAW_PROXY_URL=http://127.0.0.1:3128"]),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it("executes through podman when the named container is running", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "status"], {
|
|
env: {},
|
|
spawnSync,
|
|
}),
|
|
).toEqual({
|
|
handled: true,
|
|
exitCode: 0,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
1,
|
|
"podman",
|
|
["inspect", "--format", "{{.State.Running}}", "demo"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"podman",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"--env",
|
|
"OPENCLAW_CONTAINER_HINT=demo",
|
|
"--env",
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
"demo",
|
|
"openclaw",
|
|
"status",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: { OPENCLAW_CONTAINER: "" },
|
|
},
|
|
);
|
|
});
|
|
|
|
it("falls back to docker when podman does not have the container", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "health"], {
|
|
env: { USER: "openclaw" } as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
}),
|
|
).toEqual({
|
|
handled: true,
|
|
exitCode: 0,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
2,
|
|
"docker",
|
|
["inspect", "--format", "{{.State.Running}}", "demo"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"docker",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"-e",
|
|
"OPENCLAW_CONTAINER_HINT=demo",
|
|
"-e",
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
"demo",
|
|
"openclaw",
|
|
"health",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: { USER: "openclaw", OPENCLAW_CONTAINER: "" },
|
|
},
|
|
);
|
|
});
|
|
|
|
it("checks docker after podman and before failing", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "status"], {
|
|
env: { USER: "somalley" } as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
}),
|
|
).toEqual({
|
|
handled: true,
|
|
exitCode: 0,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
1,
|
|
"podman",
|
|
["inspect", "--format", "{{.State.Running}}", "demo"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
2,
|
|
"docker",
|
|
["inspect", "--format", "{{.State.Running}}", "demo"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"docker",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"-e",
|
|
"OPENCLAW_CONTAINER_HINT=demo",
|
|
"-e",
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
"demo",
|
|
"openclaw",
|
|
"status",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: { USER: "somalley", OPENCLAW_CONTAINER: "" },
|
|
},
|
|
);
|
|
expect(spawnSync).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("does not try any sudo podman fallback for regular users", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(() =>
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "status"], {
|
|
env: { USER: "somalley" } as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
}),
|
|
).toThrow('No running container matched "demo" under podman or docker.');
|
|
|
|
expect(spawnSync).toHaveBeenCalledTimes(2);
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
1,
|
|
"podman",
|
|
["inspect", "--format", "{{.State.Running}}", "demo"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
2,
|
|
"docker",
|
|
["inspect", "--format", "{{.State.Running}}", "demo"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
});
|
|
|
|
it("rejects ambiguous matches across runtimes", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(() =>
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "status"], {
|
|
env: { USER: "somalley" } as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
}),
|
|
).toThrow(
|
|
'Container "demo" is running under multiple runtimes (podman, docker); use a unique container name.',
|
|
);
|
|
});
|
|
|
|
it("allocates a tty for interactive terminal sessions", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "setup"], {
|
|
env: {},
|
|
spawnSync,
|
|
stdinIsTTY: true,
|
|
stdoutIsTTY: true,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
3,
|
|
"podman",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"-t",
|
|
"--env",
|
|
"OPENCLAW_CONTAINER_HINT=demo",
|
|
"--env",
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
"demo",
|
|
"openclaw",
|
|
"setup",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: { OPENCLAW_CONTAINER: "" },
|
|
},
|
|
);
|
|
});
|
|
|
|
it("prefers --container over OPENCLAW_CONTAINER", () => {
|
|
const spawnSync = vi
|
|
.fn()
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 1,
|
|
stdout: "",
|
|
})
|
|
.mockReturnValueOnce({
|
|
status: 0,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "flag-demo", "health"], {
|
|
env: { OPENCLAW_CONTAINER: "env-demo" } as NodeJS.ProcessEnv,
|
|
spawnSync,
|
|
}),
|
|
).toEqual({
|
|
handled: true,
|
|
exitCode: 0,
|
|
});
|
|
|
|
expect(spawnSync).toHaveBeenNthCalledWith(
|
|
1,
|
|
"podman",
|
|
["inspect", "--format", "{{.State.Running}}", "flag-demo"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
});
|
|
|
|
it("throws when the named container is not running", () => {
|
|
const spawnSync = vi.fn().mockReturnValue({
|
|
status: 1,
|
|
stdout: "",
|
|
});
|
|
|
|
expect(() =>
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "status"], {
|
|
env: {},
|
|
spawnSync,
|
|
}),
|
|
).toThrow('No running container matched "demo" under podman or docker.');
|
|
});
|
|
|
|
it("skips recursion when the bypass env is set", () => {
|
|
expect(
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "status"], {
|
|
env: { OPENCLAW_CLI_CONTAINER_BYPASS: "1" } as NodeJS.ProcessEnv,
|
|
}),
|
|
).toEqual({
|
|
handled: false,
|
|
argv: ["node", "openclaw", "--container", "demo", "status"],
|
|
});
|
|
});
|
|
|
|
it("blocks updater commands from running inside the container", () => {
|
|
const spawnSync = vi.fn().mockReturnValue({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
});
|
|
|
|
expect(() =>
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "update"], {
|
|
env: {},
|
|
spawnSync,
|
|
}),
|
|
).toThrow(
|
|
"openclaw update is not supported with --container; rebuild or restart the container image instead.",
|
|
);
|
|
expect(spawnSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("blocks update after interleaved root flags", () => {
|
|
const spawnSync = vi.fn().mockReturnValue({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
});
|
|
|
|
expect(() =>
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "--no-color", "update"], {
|
|
env: {},
|
|
spawnSync,
|
|
}),
|
|
).toThrow(
|
|
"openclaw update is not supported with --container; rebuild or restart the container image instead.",
|
|
);
|
|
expect(spawnSync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("blocks the --update shorthand from running inside the container", () => {
|
|
const spawnSync = vi.fn().mockReturnValue({
|
|
status: 0,
|
|
stdout: "true\n",
|
|
});
|
|
|
|
expect(() =>
|
|
maybeRunCliInContainer(["node", "openclaw", "--container", "demo", "--update"], {
|
|
env: {},
|
|
spawnSync,
|
|
}),
|
|
).toThrow(
|
|
"openclaw update is not supported with --container; rebuild or restart the container image instead.",
|
|
);
|
|
expect(spawnSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|