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("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();
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland