diff --git a/src/gateway/server-runtime-services.ts b/src/gateway/server-runtime-services.ts index 0b79f3e02fc..dd5cf2460df 100644 --- a/src/gateway/server-runtime-services.ts +++ b/src/gateway/server-runtime-services.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { isVitestRuntimeEnv } from "../infra/env.js"; import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js"; import type { ChannelHealthMonitor } from "./channel-health-monitor.js"; import { startChannelHealthMonitor } from "./channel-health-monitor.js"; @@ -87,7 +88,7 @@ export function startGatewayRuntimeServices(params: { heartbeatRunner: createNoopHeartbeatRunner(), channelHealthMonitor, stopModelPricingRefresh: - !params.minimalTestGateway && process.env.VITEST !== "1" + !params.minimalTestGateway && !isVitestRuntimeEnv() ? startGatewayModelPricingRefresh({ config: params.cfgAtStart }) : () => {}, }; diff --git a/src/gateway/server-runtime-subscriptions.ts b/src/gateway/server-runtime-subscriptions.ts index 5ef062d071e..c442133a32e 100644 --- a/src/gateway/server-runtime-subscriptions.ts +++ b/src/gateway/server-runtime-subscriptions.ts @@ -15,7 +15,6 @@ import { } from "./server-session-events.js"; export function startGatewayEventSubscriptions(params: { - minimalTestGateway: boolean; broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void; broadcastToConnIds: ( event: string, @@ -33,47 +32,39 @@ export function startGatewayEventSubscriptions(params: { sessionMessageSubscribers: SessionMessageSubscriberRegistry; chatAbortControllers: Map; }) { - const agentUnsub = params.minimalTestGateway - ? null - : onAgentEvent( - createAgentEventHandler({ - broadcast: params.broadcast, - broadcastToConnIds: params.broadcastToConnIds, - nodeSendToSession: params.nodeSendToSession, - agentRunSeq: params.agentRunSeq, - chatRunState: params.chatRunState, - resolveSessionKeyForRun: params.resolveSessionKeyForRun, - clearAgentRunContext: params.clearAgentRunContext, - toolEventRecipients: params.toolEventRecipients, - sessionEventSubscribers: params.sessionEventSubscribers, - isChatSendRunActive: (runId) => params.chatAbortControllers.has(runId), - }), - ); + const agentUnsub = onAgentEvent( + createAgentEventHandler({ + broadcast: params.broadcast, + broadcastToConnIds: params.broadcastToConnIds, + nodeSendToSession: params.nodeSendToSession, + agentRunSeq: params.agentRunSeq, + chatRunState: params.chatRunState, + resolveSessionKeyForRun: params.resolveSessionKeyForRun, + clearAgentRunContext: params.clearAgentRunContext, + toolEventRecipients: params.toolEventRecipients, + sessionEventSubscribers: params.sessionEventSubscribers, + isChatSendRunActive: (runId) => params.chatAbortControllers.has(runId), + }), + ); - const heartbeatUnsub = params.minimalTestGateway - ? null - : onHeartbeatEvent((evt) => { - params.broadcast("heartbeat", evt, { dropIfSlow: true }); - }); + const heartbeatUnsub = onHeartbeatEvent((evt) => { + params.broadcast("heartbeat", evt, { dropIfSlow: true }); + }); - const transcriptUnsub = params.minimalTestGateway - ? null - : onSessionTranscriptUpdate( - createTranscriptUpdateBroadcastHandler({ - broadcastToConnIds: params.broadcastToConnIds, - sessionEventSubscribers: params.sessionEventSubscribers, - sessionMessageSubscribers: params.sessionMessageSubscribers, - }), - ); + const transcriptUnsub = onSessionTranscriptUpdate( + createTranscriptUpdateBroadcastHandler({ + broadcastToConnIds: params.broadcastToConnIds, + sessionEventSubscribers: params.sessionEventSubscribers, + sessionMessageSubscribers: params.sessionMessageSubscribers, + }), + ); - const lifecycleUnsub = params.minimalTestGateway - ? null - : onSessionLifecycleEvent( - createLifecycleEventBroadcastHandler({ - broadcastToConnIds: params.broadcastToConnIds, - sessionEventSubscribers: params.sessionEventSubscribers, - }), - ); + const lifecycleUnsub = onSessionLifecycleEvent( + createLifecycleEventBroadcastHandler({ + broadcastToConnIds: params.broadcastToConnIds, + sessionEventSubscribers: params.sessionEventSubscribers, + }), + ); return { agentUnsub, diff --git a/src/gateway/server.auth.control-ui.suite.ts b/src/gateway/server.auth.control-ui.suite.ts index 0c7598c5e1e..9eec1d8aa6b 100644 --- a/src/gateway/server.auth.control-ui.suite.ts +++ b/src/gateway/server.auth.control-ui.suite.ts @@ -310,33 +310,36 @@ export function registerControlUiAndPairingSuite(): void { }); }); - test("allows localhost control ui without device identity when insecure auth is enabled", async () => { + test("allows localhost ui clients without device identity when insecure auth is enabled", async () => { testState.gatewayControlUi = { allowInsecureAuth: true }; - const { server, ws, prevToken } = await startControlUiServerWithClient("secret", { + const { server, ws, port, prevToken } = await startControlUiServerWithClient("secret", { wsHeaders: { origin: "http://127.0.0.1" }, }); - await connectControlUiWithoutDeviceAndExpectOk({ ws, token: "secret" }); - ws.close(); - await server.close(); - restoreGatewayToken(prevToken); - }); + let tuiWs: WebSocket | undefined; + try { + await connectControlUiWithoutDeviceAndExpectOk({ ws, token: "secret" }); - test("allows localhost tui without device identity when insecure auth is enabled", async () => { - testState.gatewayControlUi = { allowInsecureAuth: true }; - const { server, ws, prevToken } = await startControlUiServerWithClient("secret"); - await connectControlUiWithoutDeviceAndExpectOk({ - ws, - token: "secret", - client: { - id: GATEWAY_CLIENT_NAMES.TUI, - version: "1.0.0", - platform: "darwin", - mode: GATEWAY_CLIENT_MODES.UI, - }, - }); - ws.close(); - await server.close(); - restoreGatewayToken(prevToken); + tuiWs = await openWs(port); + await connectControlUiWithoutDeviceAndExpectOk({ + ws: tuiWs, + token: "secret", + client: { + id: GATEWAY_CLIENT_NAMES.TUI, + version: "1.0.0", + platform: "darwin", + mode: GATEWAY_CLIENT_MODES.UI, + }, + }); + } finally { + ws.close(); + tuiWs?.close(); + await Promise.all([ + waitForWsClose(ws, 1_000), + ...(tuiWs ? [waitForWsClose(tuiWs, 1_000)] : []), + ]); + await server.close(); + restoreGatewayToken(prevToken); + } }); test("allows control ui password-only auth on localhost when insecure auth is enabled", async () => { @@ -1322,16 +1325,35 @@ export function registerControlUiAndPairingSuite(): void { } }); - test("allows local gateway backend shared-auth connections without device pairing", async () => { - const { server, ws, prevToken } = await startControlUiServerWithClient("secret"); + test("allows gateway backend loopback shared-auth connections without device pairing", async () => { + const { server, ws, port, prevToken } = await startControlUiServerWithClient("secret"); + const sockets = [ws]; try { - const localBackend = await connectReq(ws, { - token: "secret", - client: BACKEND_GATEWAY_CLIENT, - }); - expect(localBackend.ok).toBe(true); + const backendCases: Array<{ + name: string; + headers?: Record; + socket?: WebSocket; + }> = [ + { name: "default host", socket: ws }, + { name: "remote-looking host", headers: { host: "gateway.example" } }, + { name: "private host", headers: { host: "172.17.0.2:18789" } }, + ]; + + for (const backendCase of backendCases) { + const socket = backendCase.socket ?? (await openWs(port, backendCase.headers)); + if (!backendCase.socket) { + sockets.push(socket); + } + const backendConnect = await connectReq(socket, { + token: "secret", + client: BACKEND_GATEWAY_CLIENT, + }); + expect(backendConnect.ok, backendCase.name).toBe(true); + } } finally { - ws.close(); + for (const socket of sockets) { + socket.close(); + } await server.close(); restoreGatewayToken(prevToken); } diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index df43a301298..2bbc3367c0a 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -18,7 +18,7 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { clearAgentRunContext } from "../infra/agent-events.js"; import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js"; -import { logAcceptedEnvOption } from "../infra/env.js"; +import { isVitestRuntimeEnv, logAcceptedEnvOption } from "../infra/env.js"; import { ensureOpenClawCliOnPath } from "../infra/path-env.js"; import { setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck } from "../infra/restart.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; @@ -208,7 +208,7 @@ export async function startGatewayServer( opts: GatewayServerOptions = {}, ): Promise { const minimalTestGateway = - process.env.VITEST === "1" && process.env.OPENCLAW_TEST_MINIMAL_GATEWAY === "1"; + isVitestRuntimeEnv() && process.env.OPENCLAW_TEST_MINIMAL_GATEWAY === "1"; // Ensure all default port derivations (browser/canvas) see the actual runtime port. process.env.OPENCLAW_GATEWAY_PORT = String(port); @@ -599,7 +599,6 @@ export async function startGatewayServer( Object.assign( runtimeState, startGatewayEventSubscriptions({ - minimalTestGateway, broadcast, broadcastToConnIds, nodeSendToSession, diff --git a/src/gateway/server.models-voicewake-misc.test.ts b/src/gateway/server.models-voicewake-misc.test.ts index d6f3ec8c0af..fba65ecdc16 100644 --- a/src/gateway/server.models-voicewake-misc.test.ts +++ b/src/gateway/server.models-voicewake-misc.test.ts @@ -542,9 +542,11 @@ describe("gateway server misc", () => { "utf-8", ); - const autoPort = await getFreePort(); - const autoServer = await startGatewayServer(autoPort); - await autoServer.close(); + await withEnvAsync({ OPENCLAW_TEST_MINIMAL_GATEWAY: undefined }, async () => { + const autoPort = await getFreePort(); + const autoServer = await startGatewayServer(autoPort); + await autoServer.close(); + }); const updated = JSON.parse(await fs.readFile(configPath, "utf-8")) as Record; const channels = updated.channels as Record | undefined; diff --git a/src/gateway/server.reload.test.ts b/src/gateway/server.reload.test.ts index 765562bd61a..b8ad945c47f 100644 --- a/src/gateway/server.reload.test.ts +++ b/src/gateway/server.reload.test.ts @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { WebSocket } from "ws"; import { resolveMainSessionKeyFromConfig } from "../config/sessions.js"; import { drainSystemEvents } from "../infra/system-events.js"; +import { withEnvAsync } from "../test-utils/env.js"; import { TALK_TEST_PROVIDER_API_KEY_PATH, TALK_TEST_PROVIDER_ID, @@ -369,8 +370,16 @@ describe("gateway hot reload", () => { ); } + async function withNonMinimalGatewayServer( + fn: Parameters[0], + ): ReturnType { + return await withEnvAsync({ OPENCLAW_TEST_MINIMAL_GATEWAY: undefined }, async () => + withGatewayServer(fn), + ); + } + it("applies hot reload actions and emits restart signal", async () => { - await withGatewayServer(async () => { + await withNonMinimalGatewayServer(async () => { const onHotReload = hoisted.getOnHotReload(); expect(onHotReload).toBeTypeOf("function"); @@ -473,7 +482,7 @@ describe("gateway hot reload", () => { await writeEnvRefConfig(); process.env.OPENAI_API_KEY = "sk-startup"; // pragma: allowlist secret - await withGatewayServer(async () => { + await withNonMinimalGatewayServer(async () => { const onHotReload = hoisted.getOnHotReload(); expect(onHotReload).toBeTypeOf("function"); const sessionKey = resolveMainSessionKeyFromConfig(); diff --git a/src/infra/env.ts b/src/infra/env.ts index 1b87d730e9c..716f67e18ae 100644 --- a/src/infra/env.ts +++ b/src/infra/env.ts @@ -67,6 +67,16 @@ export function isTruthyEnvValue(value?: string): boolean { } } +export function isVitestRuntimeEnv(env: NodeJS.ProcessEnv = process.env): boolean { + return ( + env.VITEST === "true" || + env.VITEST === "1" || + env.VITEST_POOL_ID !== undefined || + env.VITEST_WORKER_ID !== undefined || + env.NODE_ENV === "test" + ); +} + export function normalizeEnv(): void { normalizeZaiEnv(); }