import path from "node:path" ;
import { Command } from "commander" ;
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
import { withTempSecretFiles } from "../../test-utils/secret-file-fixture.js" ;
import { createCliRuntimeCapture } from "../test-runtime-capture.js" ;
const startGatewayServer = vi.fn(async (_port: number, _opts?: unknown) => ({
close: vi.fn(async () => {}),
}));
const setGatewayWsLogStyle = vi.fn((_style: string) => undefined);
const setVerbose = vi.fn((_enabled: boolean ) => undefined);
const setConsoleSubsystemFilter = vi.fn((_filters: string[]) => undefined);
const forceFreePortAndWait = vi.fn(async (_port: number, _opts: unknown) => ({
killed: [],
waitedMs: 0 ,
escalatedToSigkill: false ,
}));
const waitForPortBindable = vi.fn(async (_port: number, _opts?: unknown) => 0 );
const ensureDevGatewayConfig = vi.fn(async (_opts?: unknown) => {});
const runGatewayLoop = vi.fn(async ({ start }: { start: () => Promise<unknown> }) => {
await start();
});
const gatewayLogMessages = vi.hoisted(() => [] as string[]);
const configState = vi.hoisted(() => ({
cfg: {} as Record<string, unknown>,
snapshot: { exists: false } as Record<string, unknown>,
}));
const recoverConfigFromLastKnownGood = vi.fn<(params?: unknown) => Promise<boolean >>(
async (_params?: unknown) => false ,
);
const recoverConfigFromJsonRootSuffix = vi.fn<(snapshot?: unknown) => Promise<boolean >>(
async (_snapshot?: unknown) => false ,
);
const writeRestartSentinel = vi.fn<(payload?: unknown) => Promise<string>>(
async (_payload?: unknown) => "/tmp/restart-sentinel.json" ,
);
const writeDiagnosticStabilityBundleForFailureSync = vi.fn((_reason: string, _error: unknown) => ({
status: "written" as const ,
message: "wrote stability bundle: /tmp/openclaw-stability.json" ,
path: "/tmp/openclaw-stability.json" ,
}));
const controlUiState = vi.hoisted(() => ({
root: "/tmp/openclaw-control-ui" as string | null ,
}));
const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../../config/config.js" , () => ({
getConfigPath: () => "/tmp/openclaw-test-missing-config.json" ,
readBestEffortConfig: async () => configState.cfg,
readConfigFileSnapshot: async () => configState.snapshot,
recoverConfigFromLastKnownGood: (params: unknown) => recoverConfigFromLastKnownGood(params),
recoverConfigFromJsonRootSuffix: (snapshot: unknown) => recoverConfigFromJsonRootSuffix(snapshot),
resolveStateDir: () => "/tmp" ,
resolveGatewayPort: (cfg?: { gateway?: { port?: number } }) => cfg?.gateway?.port ?? 18789 ,
}));
vi.mock("../../gateway/auth.js" , () => ({
resolveGatewayAuth: (params: {
authConfig?: { mode?: string; token?: unknown; password?: unknown };
authOverride?: { mode?: string; token?: unknown; password?: unknown };
env?: NodeJS.ProcessEnv;
}) => {
const mode = params.authOverride?.mode ?? params.authConfig?.mode ?? "token" ;
const token =
(typeof params.authOverride?.token === "string" ? params.authOverride.token : undefined) ??
(typeof params.authConfig?.token === "string" ? params.authConfig.token : undefined) ??
params.env?.OPENCLAW_GATEWAY_TOKEN;
const password =
(typeof params.authOverride?.password === "string"
? params.authOverride.password
: undefined) ??
(typeof params.authConfig?.password === "string" ? params.authConfig.password : undefined) ??
params.env?.OPENCLAW_GATEWAY_PASSWORD;
return {
mode,
token,
password,
allowTailscale: false ,
};
},
}));
vi.mock("../../gateway/server.js" , () => ({
startGatewayServer: (port: number, opts?: unknown) => startGatewayServer(port, opts),
}));
vi.mock("../../infra/control-ui-assets.js" , () => ({
resolveControlUiRootSync: () => controlUiState.root,
}));
vi.mock("../../gateway/ws-logging.js" , () => ({
setGatewayWsLogStyle: (style: string) => setGatewayWsLogStyle(style),
}));
vi.mock("../../globals.js" , () => ({
setVerbose: (enabled: boolean ) => setVerbose(enabled),
}));
vi.mock("../../infra/gateway-lock.js" , () => ({
GatewayLockError: class GatewayLockError extends Error {},
}));
vi.mock("../../infra/ports.js" , () => ({
formatPortDiagnostics: () => [],
inspectPortUsage: async () => ({ status: "free" }),
}));
vi.mock("../../infra/restart-sentinel.js" , () => ({
writeRestartSentinel: (payload: unknown) => writeRestartSentinel(payload),
}));
vi.mock("../../logging/console.js" , () => ({
setConsoleSubsystemFilter: (filters: string[]) => setConsoleSubsystemFilter(filters),
setConsoleTimestampPrefix: () => undefined,
}));
vi.mock("../../logging/diagnostic-stability-bundle.js" , () => ({
writeDiagnosticStabilityBundleForFailureSync: (reason: string, error: unknown) =>
writeDiagnosticStabilityBundleForFailureSync(reason, error),
}));
vi.mock("../../logging/subsystem.js" , () => ({
createSubsystemLogger: () => ({
info: (message: string) => {
gatewayLogMessages.push(message);
},
warn: (message: string) => {
gatewayLogMessages.push(message);
},
error: () => undefined,
}),
}));
vi.mock("../../runtime.js" , () => ({
defaultRuntime,
}));
vi.mock("../command-format.js" , () => ({
formatCliCommand: (cmd: string) => cmd,
}));
vi.mock("../ports.js" , () => ({
forceFreePortAndWait: (port: number, opts: unknown) => forceFreePortAndWait(port, opts),
waitForPortBindable: (port: number, opts?: unknown) => waitForPortBindable(port, opts),
}));
vi.mock("./dev.js" , () => ({
ensureDevGatewayConfig: (opts?: unknown) => ensureDevGatewayConfig(opts),
}));
vi.mock("./run-loop.js" , () => ({
runGatewayLoop: (params: { start: () => Promise<unknown> }) => runGatewayLoop(params),
}));
describe("gateway run option collisions" , () => {
let addGatewayRunCommand: typeof import ("./run.js" ).addGatewayRunCommand;
let sharedProgram: Command;
beforeAll(async () => {
({ addGatewayRunCommand } = await import ("./run.js" ));
sharedProgram = new Command();
sharedProgram.exitOverride();
const gateway = addGatewayRunCommand(sharedProgram.command("gateway" ));
addGatewayRunCommand(gateway.command("run" ));
});
beforeEach(() => {
resetRuntimeCapture();
configState.cfg = {};
configState.snapshot = { exists: false };
controlUiState.root = "/tmp/openclaw-control-ui" ;
gatewayLogMessages.length = 0 ;
recoverConfigFromLastKnownGood.mockReset();
recoverConfigFromLastKnownGood.mockResolvedValue(false );
recoverConfigFromJsonRootSuffix.mockReset();
recoverConfigFromJsonRootSuffix.mockResolvedValue(false );
writeRestartSentinel.mockReset();
writeRestartSentinel.mockResolvedValue("/tmp/restart-sentinel.json" );
writeDiagnosticStabilityBundleForFailureSync.mockClear();
startGatewayServer.mockClear();
setGatewayWsLogStyle.mockClear();
setVerbose.mockClear();
setConsoleSubsystemFilter.mockClear();
forceFreePortAndWait.mockClear();
waitForPortBindable.mockClear();
ensureDevGatewayConfig.mockClear();
runGatewayLoop.mockClear();
});
async function runGatewayCli(argv: string[]) {
await sharedProgram.parseAsync(argv, { from: "user" });
}
function expectAuthOverrideMode(mode: string) {
expect(startGatewayServer).toHaveBeenCalledWith(
18789 ,
expect.objectContaining({
auth: expect.objectContaining({
mode,
}),
}),
);
}
it("forwards parent-captured options to `gateway run` subcommand" , async () => {
await runGatewayCli([
"gateway" ,
"run" ,
"--token" ,
"tok_run" ,
"--allow-unconfigured" ,
"--ws-log" ,
"full" ,
"--force" ,
]);
expect(forceFreePortAndWait).toHaveBeenCalledWith(18789 , expect.anything());
expect(waitForPortBindable).toHaveBeenCalledWith(
18789 ,
expect.objectContaining({ intervalMs: 150 , timeoutMs: 3000 }),
);
expect(setGatewayWsLogStyle).toHaveBeenCalledWith("full" );
expect(startGatewayServer).toHaveBeenCalledWith(
18789 ,
expect.objectContaining({
auth: expect.objectContaining({
token: "tok_run" ,
}),
}),
);
});
it.each([
["--cli-backend-logs" , "generic flag" ],
["--claude-cli-logs" , "deprecated alias" ],
])("enables CLI backend log filtering via %s (%s)" , async (flag) => {
delete process.env.OPENCLAW_CLI_BACKEND_LOG_OUTPUT;
await runGatewayCli(["gateway" , "run" , flag, "--allow-unconfigured" ]);
expect(setConsoleSubsystemFilter).toHaveBeenCalledWith(["agent/cli-backend" ]);
expect(process.env.OPENCLAW_CLI_BACKEND_LOG_OUTPUT).toBe("1" );
});
it("starts gateway when token mode has no configured token (startup bootstrap path)" , async () => {
await runGatewayCli(["gateway" , "run" , "--allow-unconfigured" ]);
expect(startGatewayServer).toHaveBeenCalledWith(
18789 ,
expect.objectContaining({
bind: "loopback" ,
}),
);
});
it("logs when first startup will build missing Control UI assets" , async () => {
controlUiState.root = null ;
await runGatewayCli(["gateway" , "run" , "--allow-unconfigured" ]);
expect(gatewayLogMessages).toContain(
"Control UI assets are missing; first startup may spend a few seconds building them before the gateway binds. `pnpm gateway:watch` does not rebuild Control UI assets, so rerun `pnpm ui:build` after UI changes or use `pnpm ui:dev` while developing the Control UI. For a full local dist, run `pnpm build && pnpm ui:build`." ,
);
});
it("does not write startup failure bundles for expected gateway lock conflicts" , async () => {
const err = Object.assign(new Error("gateway already running on port 18789" ), {
name: "GatewayLockError" ,
});
startGatewayServer.mockRejectedValueOnce(err);
await expect(runGatewayCli(["gateway" , "run" , "--allow-unconfigured" ])).rejects.toThrow(
"__exit__:0" ,
);
expect(writeDiagnosticStabilityBundleForFailureSync).not.toHaveBeenCalled();
});
it("blocks startup when the observed snapshot loses gateway.mode even if loadConfig still says local" , async () => {
configState.cfg = {
gateway: {
mode: "local" ,
},
};
configState.snapshot = {
exists: true ,
valid: true ,
config: {
update: { channel: "beta" },
},
parsed: {
update: { channel: "beta" },
},
};
await expect(runGatewayCli(["gateway" , "run" ])).rejects.toThrow("__exit__:78" );
expect(runtimeErrors).toContain(
"Gateway start blocked: existing config is missing gateway.mode. Treat this as suspicious or clobbered config. Re-run `openclaw onboard --mode local` or `openclaw setup`, set gateway.mode=local manually, or pass --allow-unconfigured." ,
);
expect(runtimeErrors).toContain(
`Config write audit: ${path.join("/tmp" , "logs" , "config-audit.jsonl" )}`,
);
expect(startGatewayServer).not.toHaveBeenCalled();
});
it("restores last-known-good config before startup when the effective config is invalid" , async () => {
configState.cfg = {};
configState.snapshot = {
exists: true ,
valid: false ,
path: "/tmp/openclaw-test-missing-config.json" ,
config: {},
parsed: null ,
issues: [{ path: "<root>" , message: "JSON5 parse failed" }],
legacyIssues: [],
};
recoverConfigFromLastKnownGood.mockImplementationOnce(async () => {
configState.snapshot = {
exists: true ,
valid: true ,
path: "/tmp/openclaw-test-missing-config.json" ,
config: {
gateway: {
mode: "local" ,
port: 19170 ,
auth: { mode: "none" },
},
},
parsed: {
gateway: {
mode: "local" ,
port: 19170 ,
auth: { mode: "none" },
},
},
issues: [],
legacyIssues: [],
};
return true ;
});
await runGatewayCli(["gateway" , "run" , "--allow-unconfigured" ]);
expect(recoverConfigFromLastKnownGood).toHaveBeenCalledWith({
snapshot: expect.objectContaining({
exists: true ,
valid: false ,
}),
reason: "gateway-run-invalid-config" ,
});
expect(writeRestartSentinel).toHaveBeenCalledWith({
kind: "config-auto-recovery" ,
status: "ok" ,
ts: expect.any(Number),
message:
"Gateway recovered automatically after a failed config change and restored the last known good configuration." ,
stats: {
mode: "config-auto-recovery" ,
reason: "gateway-run-invalid-config" ,
after: { restoredFrom: "last-known-good" },
},
});
expect(gatewayLogMessages).toContain(
"gateway: restored invalid effective config from last-known-good backup: /tmp/openclaw-test-missing-config.json" ,
);
expect(startGatewayServer).toHaveBeenCalledWith(
19170 ,
expect.objectContaining({
bind: "loopback" ,
auth: undefined,
}),
);
});
it("keeps startup recovery non-fatal when writing the recovery notice fails" , async () => {
configState.cfg = {};
configState.snapshot = {
exists: true ,
valid: false ,
path: "/tmp/openclaw-test-missing-config.json" ,
config: {},
parsed: null ,
issues: [{ path: "<root>" , message: "JSON5 parse failed" }],
legacyIssues: [],
};
recoverConfigFromLastKnownGood.mockImplementationOnce(async () => {
configState.snapshot = {
exists: true ,
valid: true ,
path: "/tmp/openclaw-test-missing-config.json" ,
config: {
gateway: {
mode: "local" ,
},
},
parsed: {
gateway: {
mode: "local" ,
},
},
issues: [],
legacyIssues: [],
};
return true ;
});
writeRestartSentinel.mockRejectedValueOnce(new Error("disk full" ));
await runGatewayCli(["gateway" , "run" ]);
expect(startGatewayServer).toHaveBeenCalledWith(
18789 ,
expect.objectContaining({ bind: "loopback" }),
);
expect(gatewayLogMessages).toContain(
"gateway: failed to persist config auto-recovery notice: disk full" ,
);
});
it.each(["none" , "trusted-proxy" ] as const )("accepts --auth %s override" , async (mode) => {
await runGatewayCli(["gateway" , "run" , "--auth" , mode, "--allow-unconfigured" ]);
expectAuthOverrideMode(mode);
});
it("prints all supported modes on invalid --auth value" , async () => {
await expect(
runGatewayCli(["gateway" , "run" , "--auth" , "bad-mode" , "--allow-unconfigured" ]),
).rejects.toThrow("__exit__:1" );
expect(runtimeErrors).toContain(
'Invalid --auth (use "none", "token", "password", or "trusted-proxy")' ,
);
});
it("allows password mode preflight when password is configured via SecretRef" , async () => {
configState.cfg = {
gateway: {
auth: {
mode: "password" ,
password: { source: "env" , provider: "default" , id: "OPENCLAW_GATEWAY_PASSWORD" },
},
},
secrets: {
defaults: {
env: "default" ,
},
},
};
configState.snapshot = {
exists: true ,
valid: true ,
config: configState.cfg,
parsed: configState.cfg,
};
await runGatewayCli(["gateway" , "run" , "--allow-unconfigured" ]);
expect(startGatewayServer).toHaveBeenCalledWith(
18789 ,
expect.objectContaining({
bind: "loopback" ,
}),
);
});
it("reads gateway password from --password-file" , async () => {
await withTempSecretFiles(
"openclaw-gateway-run-" ,
{ password: "pw_from_file\n" },
async ({ passwordFile }) => {
await runGatewayCli([
"gateway" ,
"run" ,
"--auth" ,
"password" ,
"--password-file" ,
passwordFile ?? "" ,
"--allow-unconfigured" ,
]);
},
);
expect(startGatewayServer).toHaveBeenCalledWith(
18789 ,
expect.objectContaining({
auth: expect.objectContaining({
mode: "password" ,
password: "pw_from_file" , // pragma: allowlist secret
}),
}),
);
expect(runtimeErrors).not.toContain(
"Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD." ,
);
});
it("warns when gateway password is passed inline" , async () => {
await runGatewayCli([
"gateway" ,
"run" ,
"--auth" ,
"password" ,
"--password" ,
"pw_inline" ,
"--allow-unconfigured" ,
]);
expect(runtimeErrors).toContain(
"Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD." ,
);
});
it("rejects using both --password and --password-file" , async () => {
await withTempSecretFiles(
"openclaw-gateway-run-" ,
{ password: "pw_from_file\n" },
async ({ passwordFile }) => {
await expect(
runGatewayCli([
"gateway" ,
"run" ,
"--password" ,
"pw_inline" ,
"--password-file" ,
passwordFile ?? "" ,
"--allow-unconfigured" ,
]),
).rejects.toThrow("__exit__:1" );
},
);
expect(runtimeErrors[0 ]).toContain("Use either --passw***d or --password-file." );
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland