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:
kumarabhirup 2026-03-05 10:46:16 -08:00
parent 4bcd47b848
commit f41c0411eb
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
5 changed files with 249 additions and 16 deletions

View File

@ -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" },
});
});

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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
View 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);
});
});