test: add port isolation tests for DenchClaw gateway fix
- New src/config/paths.test.ts: 11 tests covering resolveGatewayPort profile-aware precedence (env > config > profile > global default) - Extend bootstrap-external.test.ts: 15 new tests for readExistingGatewayPort (config reading, fallback, edge cases) and isPersistedPortAcceptable (18789 rejection guard, end-to-end composition) - Extend web-runtime-command.test.ts: 2 new tests verifying the fallback returns 19001 when manifest has no lastGatewayPort or is null - Update test fixtures across all test files to expect port 19001 All 5 critical mutations verified: removing profile check, removing 18789 guard, reverting fallback, changing constant, breaking config reader -- each caught by at least 2 tests.
This commit is contained in:
parent
4bcd47b848
commit
f41c0411eb
@ -63,7 +63,7 @@ describe("profiles API", () => {
|
||||
mockReadFile.mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
if (s.includes("openclaw.json")) {
|
||||
return JSON.stringify({ gateway: { mode: "local", port: 18789 } }) as never;
|
||||
return JSON.stringify({ gateway: { mode: "local", port: 19001 } }) as never;
|
||||
}
|
||||
return "" as never;
|
||||
});
|
||||
@ -83,7 +83,7 @@ describe("profiles API", () => {
|
||||
name: "work",
|
||||
stateDir: STATE_DIR,
|
||||
isActive: true,
|
||||
gateway: { mode: "local", port: 18789, url: "ws://127.0.0.1:18789" },
|
||||
gateway: { mode: "local", port: 19001, url: "ws://127.0.0.1:19001" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -199,7 +199,7 @@ describe("agent-runner", () => {
|
||||
describe("buildConnectParams", () => {
|
||||
it("uses a client.id that the Gateway actually accepts (prevents connect rejection)", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:18789" }) as {
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:19001" }) as {
|
||||
client: { id: string; mode: string };
|
||||
};
|
||||
expect(VALID_GATEWAY_CLIENT_IDS.has(params.client.id)).toBe(true);
|
||||
@ -207,7 +207,7 @@ describe("agent-runner", () => {
|
||||
|
||||
it("uses a client.mode the Gateway accepts (prevents schema validation failure)", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:18789" }) as {
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:19001" }) as {
|
||||
client: { id: string; mode: string };
|
||||
};
|
||||
expect(VALID_GATEWAY_CLIENT_MODES.has(params.client.mode)).toBe(true);
|
||||
@ -216,7 +216,7 @@ describe("agent-runner", () => {
|
||||
it("includes auth.token when settings have a token", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
url: "ws://127.0.0.1:19001",
|
||||
token: "secret-token",
|
||||
}) as { auth?: { token?: string; password?: string } };
|
||||
expect(params.auth?.token).toBe("secret-token");
|
||||
@ -225,7 +225,7 @@ describe("agent-runner", () => {
|
||||
it("includes auth.password when settings have a password", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
url: "ws://127.0.0.1:19001",
|
||||
password: "secret-pass",
|
||||
}) as { auth?: { token?: string; password?: string } };
|
||||
expect(params.auth?.password).toBe("secret-pass");
|
||||
@ -233,7 +233,7 @@ describe("agent-runner", () => {
|
||||
|
||||
it("omits auth when no token or password is set", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:18789" }) as {
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:19001" }) as {
|
||||
auth?: unknown;
|
||||
};
|
||||
expect(params.auth).toBeUndefined();
|
||||
@ -241,7 +241,7 @@ describe("agent-runner", () => {
|
||||
|
||||
it("requests protocol version 3 (current Gateway protocol)", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:18789" }) as {
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:19001" }) as {
|
||||
minProtocol: number;
|
||||
maxProtocol: number;
|
||||
};
|
||||
@ -251,7 +251,7 @@ describe("agent-runner", () => {
|
||||
|
||||
it("uses backend mode so sessions.patch is allowed", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:18789" }) as {
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:19001" }) as {
|
||||
client: { mode: string };
|
||||
};
|
||||
expect(params.client.mode).toBe("backend");
|
||||
@ -259,7 +259,7 @@ describe("agent-runner", () => {
|
||||
|
||||
it("advertises tool-events capability for tool stream parity", async () => {
|
||||
const { buildConnectParams } = await import("./agent-runner.js");
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:18789" }) as {
|
||||
const params = buildConnectParams({ url: "ws://127.0.0.1:19001" }) as {
|
||||
caps?: string[];
|
||||
};
|
||||
expect(Array.isArray(params.caps)).toBe(true);
|
||||
|
||||
@ -5,6 +5,8 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
buildBootstrapDiagnostics,
|
||||
checkAgentAuth,
|
||||
isPersistedPortAcceptable,
|
||||
readExistingGatewayPort,
|
||||
resolveBootstrapRolloutStage,
|
||||
isLegacyFallbackEnabled,
|
||||
type BootstrapDiagnostics,
|
||||
@ -67,8 +69,8 @@ describe("bootstrap-external diagnostics", () => {
|
||||
profile: "dench",
|
||||
openClawCliAvailable: true,
|
||||
openClawVersion: "2026.3.1",
|
||||
gatewayPort: 18789,
|
||||
gatewayUrl: "ws://127.0.0.1:18789",
|
||||
gatewayPort: 19001,
|
||||
gatewayUrl: "ws://127.0.0.1:19001",
|
||||
gatewayProbe: { ok: true as const },
|
||||
webPort: 3100,
|
||||
webReachable: true,
|
||||
@ -293,3 +295,114 @@ describe("bootstrap-external rollout env helpers", () => {
|
||||
expect(isLegacyFallbackEnabled({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readExistingGatewayPort", () => {
|
||||
let stateDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
stateDir = createTempStateDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(stateDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("reads numeric port from openclaw.json (normal config path)", () => {
|
||||
writeConfig(stateDir, { gateway: { port: 19001 } });
|
||||
expect(readExistingGatewayPort(stateDir)).toBe(19001);
|
||||
});
|
||||
|
||||
it("falls back to config.json when openclaw.json is absent (legacy config support)", () => {
|
||||
writeFileSync(
|
||||
path.join(stateDir, "config.json"),
|
||||
JSON.stringify({ gateway: { port: 19005 } }),
|
||||
);
|
||||
expect(readExistingGatewayPort(stateDir)).toBe(19005);
|
||||
});
|
||||
|
||||
it("prefers openclaw.json over config.json when both exist (config precedence)", () => {
|
||||
writeConfig(stateDir, { gateway: { port: 19001 } });
|
||||
writeFileSync(
|
||||
path.join(stateDir, "config.json"),
|
||||
JSON.stringify({ gateway: { port: 19099 } }),
|
||||
);
|
||||
expect(readExistingGatewayPort(stateDir)).toBe(19001);
|
||||
});
|
||||
|
||||
it("returns undefined when no config files exist (fresh install)", () => {
|
||||
expect(readExistingGatewayPort(stateDir)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when config has no gateway section (incomplete config)", () => {
|
||||
writeConfig(stateDir, { agents: {} });
|
||||
expect(readExistingGatewayPort(stateDir)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("parses string port values (handles config.set serialization)", () => {
|
||||
writeConfig(stateDir, { gateway: { port: "19001" } });
|
||||
expect(readExistingGatewayPort(stateDir)).toBe(19001);
|
||||
});
|
||||
|
||||
it("rejects zero and negative ports (invalid port values)", () => {
|
||||
writeConfig(stateDir, { gateway: { port: 0 } });
|
||||
expect(readExistingGatewayPort(stateDir)).toBeUndefined();
|
||||
|
||||
writeConfig(stateDir, { gateway: { port: -1 } });
|
||||
expect(readExistingGatewayPort(stateDir)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for malformed JSON (handles corrupt config gracefully)", () => {
|
||||
writeFileSync(path.join(stateDir, "openclaw.json"), "not valid json{{{");
|
||||
expect(readExistingGatewayPort(stateDir)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns 18789 when config has it (reader does not filter; caller must guard)", () => {
|
||||
writeConfig(stateDir, { gateway: { port: 18789 } });
|
||||
expect(readExistingGatewayPort(stateDir)).toBe(18789);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPersistedPortAcceptable", () => {
|
||||
let stateDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
stateDir = createTempStateDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(stateDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("rejects 18789 (prevents OpenClaw port hijack on launchd restart)", () => {
|
||||
expect(isPersistedPortAcceptable(18789)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts DenchClaw's own port range (normal operation)", () => {
|
||||
expect(isPersistedPortAcceptable(19001)).toBe(true);
|
||||
expect(isPersistedPortAcceptable(19002)).toBe(true);
|
||||
expect(isPersistedPortAcceptable(19100)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects undefined (no persisted port to reuse)", () => {
|
||||
expect(isPersistedPortAcceptable(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects zero and negative values (invalid ports)", () => {
|
||||
expect(isPersistedPortAcceptable(0)).toBe(false);
|
||||
expect(isPersistedPortAcceptable(-1)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects corrupted 18789 from config (end-to-end: read + guard prevents port hijack)", () => {
|
||||
writeConfig(stateDir, { gateway: { port: 18789 } });
|
||||
const port = readExistingGatewayPort(stateDir);
|
||||
expect(port).toBe(18789);
|
||||
expect(isPersistedPortAcceptable(port)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts valid 19001 from config (end-to-end: read + guard allows DenchClaw port)", () => {
|
||||
writeConfig(stateDir, { gateway: { port: 19001 } });
|
||||
const port = readExistingGatewayPort(stateDir);
|
||||
expect(port).toBe(19001);
|
||||
expect(isPersistedPortAcceptable(port)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -28,7 +28,7 @@ const webRuntimeMocks = vi.hoisted(() => ({
|
||||
deployedAt: "2026-01-01T00:00:00.000Z",
|
||||
sourceStandaloneServer: "/tmp/server.js",
|
||||
lastPort: 3100,
|
||||
lastGatewayPort: 18789,
|
||||
lastGatewayPort: 19001,
|
||||
})),
|
||||
resolveCliPackageRoot: vi.fn(() => "/tmp/pkg"),
|
||||
resolveManagedWebRuntimeServerPath: vi.fn(() => "/tmp/.openclaw-dench/web-runtime/app/server.js"),
|
||||
@ -143,7 +143,7 @@ describe("updateWebRuntimeCommand", () => {
|
||||
deployedAt: "2026-01-01T00:00:00.000Z",
|
||||
sourceStandaloneServer: "/tmp/server.js",
|
||||
lastPort: 3100,
|
||||
lastGatewayPort: 18789,
|
||||
lastGatewayPort: 19001,
|
||||
}));
|
||||
webRuntimeMocks.startManagedWebRuntime.mockReset();
|
||||
webRuntimeMocks.startManagedWebRuntime.mockImplementation(() => ({
|
||||
@ -325,12 +325,38 @@ describe("startWebRuntimeCommand", () => {
|
||||
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith({
|
||||
stateDir: "/tmp/.openclaw-dench",
|
||||
port: 3100,
|
||||
gatewayPort: 18789,
|
||||
gatewayPort: 19001,
|
||||
});
|
||||
expect(webRuntimeMocks.ensureManagedWebRuntime).not.toHaveBeenCalled();
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
expect(summary.started).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to DenchClaw port 19001 when manifest has no lastGatewayPort (prevents 18789 hijack)", async () => {
|
||||
webRuntimeMocks.readManagedWebRuntimeManifest.mockReturnValue({
|
||||
schemaVersion: 1,
|
||||
deployedDenchVersion: "2.1.0",
|
||||
deployedAt: "2026-01-01T00:00:00.000Z",
|
||||
sourceStandaloneServer: "/tmp/server.js",
|
||||
lastPort: 3100,
|
||||
});
|
||||
const runtime = runtimeStub();
|
||||
await startWebRuntimeCommand({ webPort: "3100" }, runtime);
|
||||
|
||||
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ gatewayPort: 19001 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to DenchClaw port 19001 when manifest is null (fresh install, prevents 18789 hijack)", async () => {
|
||||
webRuntimeMocks.readManagedWebRuntimeManifest.mockReturnValue(null);
|
||||
const runtime = runtimeStub();
|
||||
await startWebRuntimeCommand({ webPort: "3100" }, runtime);
|
||||
|
||||
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ gatewayPort: 19001 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("restartWebRuntimeCommand", () => {
|
||||
@ -378,7 +404,7 @@ describe("restartWebRuntimeCommand", () => {
|
||||
expect(webRuntimeMocks.startManagedWebRuntime).toHaveBeenCalledWith({
|
||||
stateDir: "/tmp/.openclaw-dench",
|
||||
port: 3100,
|
||||
gatewayPort: 18789,
|
||||
gatewayPort: 19001,
|
||||
});
|
||||
expect(webRuntimeMocks.ensureManagedWebRuntime).not.toHaveBeenCalled();
|
||||
expect(summary.started).toBe(true);
|
||||
|
||||
94
src/config/paths.test.ts
Normal file
94
src/config/paths.test.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
resolveGatewayPort,
|
||||
DEFAULT_GATEWAY_PORT,
|
||||
DENCHCLAW_DEFAULT_GATEWAY_PORT,
|
||||
} from "./paths.js";
|
||||
|
||||
describe("resolveGatewayPort", () => {
|
||||
it("returns DenchClaw port when profile is dench and no config/env override (prevents OpenClaw port hijack)", () => {
|
||||
const port = resolveGatewayPort(undefined, { OPENCLAW_PROFILE: "dench" });
|
||||
expect(port).toBe(19001);
|
||||
expect(port).not.toBe(DEFAULT_GATEWAY_PORT);
|
||||
});
|
||||
|
||||
it("returns OpenClaw default when no profile is set (preserves host gateway default)", () => {
|
||||
expect(resolveGatewayPort(undefined, {})).toBe(18789);
|
||||
});
|
||||
|
||||
it("env OPENCLAW_GATEWAY_PORT overrides profile-based default (supports runtime override)", () => {
|
||||
expect(
|
||||
resolveGatewayPort(undefined, {
|
||||
OPENCLAW_PROFILE: "dench",
|
||||
OPENCLAW_GATEWAY_PORT: "19500",
|
||||
}),
|
||||
).toBe(19500);
|
||||
});
|
||||
|
||||
it("legacy env CLAWDBOT_GATEWAY_PORT is still honoured (backwards compatibility)", () => {
|
||||
expect(
|
||||
resolveGatewayPort(undefined, { CLAWDBOT_GATEWAY_PORT: "19500" }),
|
||||
).toBe(19500);
|
||||
});
|
||||
|
||||
it("config port overrides profile-based default (honours persisted config)", () => {
|
||||
expect(
|
||||
resolveGatewayPort(
|
||||
{ gateway: { port: 19005 } },
|
||||
{ OPENCLAW_PROFILE: "dench" },
|
||||
),
|
||||
).toBe(19005);
|
||||
});
|
||||
|
||||
it("env var takes precedence over config port (explicit runtime override wins)", () => {
|
||||
expect(
|
||||
resolveGatewayPort(
|
||||
{ gateway: { port: 19005 } },
|
||||
{ OPENCLAW_GATEWAY_PORT: "19500" },
|
||||
),
|
||||
).toBe(19500);
|
||||
});
|
||||
|
||||
it("ignores non-numeric env values and falls through to profile default (malformed input)", () => {
|
||||
expect(
|
||||
resolveGatewayPort(undefined, {
|
||||
OPENCLAW_PROFILE: "dench",
|
||||
OPENCLAW_GATEWAY_PORT: "not-a-number",
|
||||
}),
|
||||
).toBe(DENCHCLAW_DEFAULT_GATEWAY_PORT);
|
||||
});
|
||||
|
||||
it("ignores zero and negative config ports (invalid config)", () => {
|
||||
expect(resolveGatewayPort({ gateway: { port: 0 } }, { OPENCLAW_PROFILE: "dench" })).toBe(
|
||||
DENCHCLAW_DEFAULT_GATEWAY_PORT,
|
||||
);
|
||||
expect(resolveGatewayPort({ gateway: { port: -1 } }, { OPENCLAW_PROFILE: "dench" })).toBe(
|
||||
DENCHCLAW_DEFAULT_GATEWAY_PORT,
|
||||
);
|
||||
});
|
||||
|
||||
it("treats whitespace-only env as absent (trims before parsing)", () => {
|
||||
expect(
|
||||
resolveGatewayPort(undefined, {
|
||||
OPENCLAW_PROFILE: "dench",
|
||||
OPENCLAW_GATEWAY_PORT: " ",
|
||||
}),
|
||||
).toBe(DENCHCLAW_DEFAULT_GATEWAY_PORT);
|
||||
});
|
||||
|
||||
it("undefined config falls through to profile/global default", () => {
|
||||
expect(resolveGatewayPort(undefined, {})).toBe(DEFAULT_GATEWAY_PORT);
|
||||
expect(resolveGatewayPort({}, {})).toBe(DEFAULT_GATEWAY_PORT);
|
||||
expect(resolveGatewayPort({ gateway: {} }, { OPENCLAW_PROFILE: "dench" })).toBe(
|
||||
DENCHCLAW_DEFAULT_GATEWAY_PORT,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("port constants", () => {
|
||||
it("DenchClaw default port is distinct from OpenClaw default (prevents port collision)", () => {
|
||||
expect(DENCHCLAW_DEFAULT_GATEWAY_PORT).not.toBe(DEFAULT_GATEWAY_PORT);
|
||||
expect(DENCHCLAW_DEFAULT_GATEWAY_PORT).toBe(19001);
|
||||
expect(DEFAULT_GATEWAY_PORT).toBe(18789);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user