test(gateway): restore startup environment after servers (#120137)

* fix(test): restore gateway startup environment

* fix(test): centralize gateway startup env cleanup

* test(gateway): restore live startup environment

* fix(test): complete gateway startup env lifecycle

* test(gateway): restore raw server owner environment

* test(qa): restore gateway producer environment
This commit is contained in:
Peter Steinberger
2026-08-07 01:06:16 -07:00
committed by Vincent Koc
parent 576c70e778
commit fb04abdd0f
28 changed files with 212 additions and 132 deletions
@@ -17,6 +17,7 @@ import {
disconnectGatewayClient,
getFreeGatewayPort,
} from "../gateway/test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "../gateway/test-helpers.env.js";
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { withTimeout } from "../utils/with-timeout.js";
@@ -25,10 +26,10 @@ import type { ExecApprovalFollowupOutcome } from "./bash-tools.exec-types.js";
const TEST_ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PORT",
"OPENCLAW_SKIP_CHANNELS",
"OPENCLAW_SKIP_GMAIL_WATCHER",
"OPENCLAW_SKIP_CRON",
+1 -1
View File
@@ -47,7 +47,7 @@ const DEFAULT_LIVE_PARENT_MODEL = "openai/gpt-5.4";
type LiveAcpAgent = "claude" | "codex" | "droid" | "gemini" | "opencode";
function snapshotAcpBindLiveEnv(): LiveEnvSnapshot {
return snapshotLiveEnv(["CODEX_HOME", "OPENCLAW_GATEWAY_PORT"]);
return snapshotLiveEnv(["CODEX_HOME"]);
}
function resolveLiveTimeoutMs(raw: string | undefined, fallback: number): number {
@@ -41,7 +41,7 @@ const LIVE_TIMEOUT_MS = resolvePositiveInteger(
);
function snapshotAcpSpawnDefaultsLiveEnv(): LiveEnvSnapshot {
return snapshotLiveEnv(["CODEX_HOME", "OPENCLAW_GATEWAY_PORT"]);
return snapshotLiveEnv(["CODEX_HOME"]);
}
function resolvePositiveInteger(raw: string | undefined, fallback: number): number {
@@ -3,6 +3,8 @@
*/
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { testing as cliBackendsTesting } from "../agents/cli-backends.test-support.js";
import { captureEnv } from "../test-utils/env.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
vi.mock("./client-start-readiness.js", () => ({
startGatewayClientWhenEventLoopReady: async (client: { start: () => void }) => {
@@ -12,6 +14,7 @@ vi.mock("./client-start-readiness.js", () => ({
}));
describe("gateway cli backend live helpers", () => {
const gatewayStartupEnv = captureEnv([...GATEWAY_STARTUP_MUTATED_ENV_KEYS]);
let liveHelpers: typeof import("./gateway-cli-backend.live-helpers.js");
beforeAll(async () => {
@@ -24,6 +27,7 @@ describe("gateway cli backend live helpers", () => {
afterEach(() => {
vi.useRealTimers();
gatewayStartupEnv.restore();
cliBackendsTesting.resetDepsForTest();
delete process.env.OPENCLAW_SKIP_CHANNELS;
delete process.env.OPENCLAW_SKIP_PROVIDERS;
@@ -53,9 +57,15 @@ describe("gateway cli backend live helpers", () => {
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "old-minimal";
process.env.ANTHROPIC_API_KEY = "old-anthropic";
process.env.ANTHROPIC_API_KEY_OLD = "old-anthropic-old";
process.env.PATH = "old-path";
process.env.OPENCLAW_GATEWAY_PORT = "old-port";
process.env.OPENCLAW_PATH_BOOTSTRAPPED = "old-bootstrap";
const snapshot = snapshotCliBackendLiveEnv();
applyCliBackendLiveEnv(new Set<string>());
process.env.PATH = "gateway-path";
process.env.OPENCLAW_GATEWAY_PORT = "gateway-port";
process.env.OPENCLAW_PATH_BOOTSTRAPPED = "1";
expect(process.env.OPENCLAW_SKIP_CHANNELS).toBe("1");
expect(process.env.OPENCLAW_SKIP_PROVIDERS).toBe("1");
@@ -80,6 +90,9 @@ describe("gateway cli backend live helpers", () => {
expect(process.env.OPENCLAW_TEST_MINIMAL_GATEWAY).toBe("old-minimal");
expect(process.env.ANTHROPIC_API_KEY).toBe("old-anthropic");
expect(process.env.ANTHROPIC_API_KEY_OLD).toBe("old-anthropic-old");
expect(process.env.PATH).toBe("old-path");
expect(process.env.OPENCLAW_GATEWAY_PORT).toBe("old-port");
expect(process.env.OPENCLAW_PATH_BOOTSTRAPPED).toBe("old-bootstrap");
});
it("defaults the model switch probe to Claude Sonnet -> Opus", async () => {
+10 -51
View File
@@ -26,6 +26,7 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-cha
import { sleep } from "../utils/sleep.js";
import { startGatewayClientWhenEventLoopReady } from "./client-start-readiness.js";
import { GatewayClient, type GatewayClientOptions } from "./client.js";
import { restoreLiveEnv, snapshotLiveEnv, type LiveEnvSnapshot } from "./live-env-test-helpers.js";
// Aggregate docker live runs can contend on startup enough that the gateway
// websocket handshake needs a wider budget than the single-provider reruns.
@@ -49,21 +50,7 @@ export type CliBackendLiveModelSelection = {
agentRuntime: { id: string };
};
export type CliBackendLiveEnvSnapshot = {
configPath?: string;
stateDir?: string;
token?: string;
skipChannels?: string;
skipProviders?: string;
skipGmail?: string;
skipCron?: string;
skipCanvas?: string;
skipBrowserControl?: string;
bundledPluginsDir?: string;
minimalGateway?: string;
anthropicApiKey?: string;
anthropicApiKeyOld?: string;
};
export type CliBackendLiveEnvSnapshot = LiveEnvSnapshot;
export const CLI_BACKEND_LIVE_PROVIDER_SKIP_ENV = "OPENCLAW_LIVE_CLI_BACKEND_ALLOW_PROVIDER_SKIP";
export const CLI_BACKEND_LIVE_ADVISORY_ENV = "OPENCLAW_LIVE_CLI_BACKEND_ADVISORY";
@@ -550,21 +537,13 @@ function isRetryableGatewayConnectError(error: Error): boolean {
}
export function snapshotCliBackendLiveEnv(): CliBackendLiveEnvSnapshot {
return {
configPath: process.env.OPENCLAW_CONFIG_PATH,
stateDir: process.env.OPENCLAW_STATE_DIR,
token: process.env.OPENCLAW_GATEWAY_TOKEN,
skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
skipProviders: process.env.OPENCLAW_SKIP_PROVIDERS,
skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
skipCron: process.env.OPENCLAW_SKIP_CRON,
skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
skipBrowserControl: process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER,
bundledPluginsDir: process.env.OPENCLAW_BUNDLED_PLUGINS_DIR,
minimalGateway: process.env.OPENCLAW_TEST_MINIMAL_GATEWAY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
anthropicApiKeyOld: process.env.ANTHROPIC_API_KEY_OLD,
};
return snapshotLiveEnv([
"OPENCLAW_SKIP_PROVIDERS",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_TEST_MINIMAL_GATEWAY",
"ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY_OLD",
]);
}
export function applyCliBackendLiveEnv(preservedEnv: ReadonlySet<string>): void {
@@ -584,27 +563,7 @@ export function applyCliBackendLiveEnv(preservedEnv: ReadonlySet<string>): void
}
export function restoreCliBackendLiveEnv(snapshot: CliBackendLiveEnvSnapshot): void {
restoreEnvVar("OPENCLAW_CONFIG_PATH", snapshot.configPath);
restoreEnvVar("OPENCLAW_STATE_DIR", snapshot.stateDir);
restoreEnvVar("OPENCLAW_GATEWAY_TOKEN", snapshot.token);
restoreEnvVar("OPENCLAW_SKIP_CHANNELS", snapshot.skipChannels);
restoreEnvVar("OPENCLAW_SKIP_PROVIDERS", snapshot.skipProviders);
restoreEnvVar("OPENCLAW_SKIP_GMAIL_WATCHER", snapshot.skipGmail);
restoreEnvVar("OPENCLAW_SKIP_CRON", snapshot.skipCron);
restoreEnvVar("OPENCLAW_SKIP_CANVAS_HOST", snapshot.skipCanvas);
restoreEnvVar("OPENCLAW_SKIP_BROWSER_CONTROL_SERVER", snapshot.skipBrowserControl);
restoreEnvVar("OPENCLAW_BUNDLED_PLUGINS_DIR", snapshot.bundledPluginsDir);
restoreEnvVar("OPENCLAW_TEST_MINIMAL_GATEWAY", snapshot.minimalGateway);
restoreEnvVar("ANTHROPIC_API_KEY", snapshot.anthropicApiKey);
restoreEnvVar("ANTHROPIC_API_KEY_OLD", snapshot.anthropicApiKeyOld);
}
function restoreEnvVar(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name];
return;
}
process.env[name] = value;
restoreLiveEnv(snapshot);
}
export async function ensurePairedTestGatewayClientIdentity(params?: {
@@ -80,6 +80,8 @@ import { deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js";
import { getFreePort, isPortFree } from "../test-utils/ports.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { GatewayClient } from "./client.js";
import { restoreLiveEnv, snapshotLiveEnv } from "./live-env-test-helpers.js";
import type { GatewayServer } from "./server-public.js";
type ProviderThinkingModelCompat = {
thinkingFormat?: string;
@@ -92,7 +94,6 @@ import {
shouldRetryExecReadProbe,
shouldRetryToolReadProbe,
} from "./live-tool-probe.test-helpers.js";
import { startGatewayServer } from "./server.impl.js";
import { readSessionMessagesAsync } from "./session-transcript-readers.js";
import { loadSessionEntry } from "./session-utils.js";
@@ -4570,25 +4571,19 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
);
}
const [ultraUpstreamBaseUrl] = [...ultraUpstreamBaseUrls];
const previous = {
configPath: process.env.OPENCLAW_CONFIG_PATH,
token: process.env.OPENCLAW_GATEWAY_TOKEN,
skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
skipCron: process.env.OPENCLAW_SKIP_CRON,
skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
disableBonjour: process.env.OPENCLAW_DISABLE_BONJOUR,
logLevel: process.env.OPENCLAW_LOG_LEVEL,
agentDir: process.env.OPENCLAW_AGENT_DIR,
stateDir: process.env.OPENCLAW_STATE_DIR,
};
const previousEnv = snapshotLiveEnv([
"OPENCLAW_DISABLE_BONJOUR",
"OPENCLAW_LOG_LEVEL",
"OPENCLAW_AGENT_DIR",
]);
const { startGatewayServer } = await import("./server.impl.js");
let runtimeEnv: ReturnType<typeof enterProductionEnvForLiveRun> | undefined;
let cleanupTempStateDir: string | undefined;
let cleanupTempAgentDir: string | undefined;
let cleanupToolProbePath: string | undefined;
let cleanupTempDir: string | undefined;
let ultraWireCapture: OpenAIUltraWireCapture | undefined;
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
let server: GatewayServer | undefined;
let client: GatewayClient | undefined;
try {
@@ -5481,16 +5476,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
if (runtimeEnv) {
restoreProductionEnvForLiveRun(runtimeEnv);
}
restoreOptionalEnv("OPENCLAW_CONFIG_PATH", previous.configPath);
restoreOptionalEnv("OPENCLAW_GATEWAY_TOKEN", previous.token);
restoreOptionalEnv("OPENCLAW_SKIP_CHANNELS", previous.skipChannels);
restoreOptionalEnv("OPENCLAW_SKIP_GMAIL_WATCHER", previous.skipGmail);
restoreOptionalEnv("OPENCLAW_SKIP_CRON", previous.skipCron);
restoreOptionalEnv("OPENCLAW_SKIP_CANVAS_HOST", previous.skipCanvas);
restoreOptionalEnv("OPENCLAW_DISABLE_BONJOUR", previous.disableBonjour);
restoreOptionalEnv("OPENCLAW_LOG_LEVEL", previous.logLevel);
restoreOptionalEnv("OPENCLAW_AGENT_DIR", previous.agentDir);
restoreOptionalEnv("OPENCLAW_STATE_DIR", previous.stateDir);
restoreLiveEnv(previousEnv);
}
}
}
@@ -5747,16 +5733,8 @@ describeLive("gateway live (dev agent, profile keys)", () => {
}
clearRuntimeConfigSnapshot();
const runtimeEnv = enterProductionEnvForLiveRun();
const previous = {
configPath: process.env.OPENCLAW_CONFIG_PATH,
token: process.env.OPENCLAW_GATEWAY_TOKEN,
skipChannels: process.env.OPENCLAW_SKIP_CHANNELS,
skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER,
skipCron: process.env.OPENCLAW_SKIP_CRON,
skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST,
agentDir: process.env.OPENCLAW_AGENT_DIR,
stateDir: process.env.OPENCLAW_STATE_DIR,
};
const previousEnv = snapshotLiveEnv(["OPENCLAW_AGENT_DIR"]);
const { startGatewayServer } = await import("./server.impl.js");
process.env.OPENCLAW_SKIP_CHANNELS = "1";
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
@@ -5766,7 +5744,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
const token = `test-${randomUUID()}`;
process.env.OPENCLAW_GATEWAY_TOKEN = token;
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
let server: GatewayServer | undefined;
let client: GatewayClient | undefined;
let toolProbePath: string | undefined;
let tempDir: string | undefined;
@@ -5972,14 +5950,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
});
}
restoreOptionalEnv("OPENCLAW_CONFIG_PATH", previous.configPath);
restoreOptionalEnv("OPENCLAW_GATEWAY_TOKEN", previous.token);
restoreOptionalEnv("OPENCLAW_SKIP_CHANNELS", previous.skipChannels);
restoreOptionalEnv("OPENCLAW_SKIP_GMAIL_WATCHER", previous.skipGmail);
restoreOptionalEnv("OPENCLAW_SKIP_CRON", previous.skipCron);
restoreOptionalEnv("OPENCLAW_SKIP_CANVAS_HOST", previous.skipCanvas);
restoreOptionalEnv("OPENCLAW_AGENT_DIR", previous.agentDir);
restoreOptionalEnv("OPENCLAW_STATE_DIR", previous.stateDir);
restoreLiveEnv(previousEnv);
}
}, 180_000);
});
@@ -15,10 +15,12 @@ import {
disconnectGatewayClient,
getFreeGatewayPort,
} from "./test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
const GATEWAY_E2E_TIMEOUT_MS = 90_000;
const ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
+3 -11
View File
@@ -28,6 +28,7 @@ import {
getFreeGatewayPort,
startGatewayWithClient,
} from "./test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
import { installOpenAiResponsesMock } from "./test-helpers.openai-mock.js";
import { buildMockOpenAiResponsesProvider } from "./test-openai-responses-model.js";
@@ -36,6 +37,7 @@ const GATEWAY_E2E_TIMEOUT_MS = 90_000;
let gatewayTestSeq = 0;
const GATEWAY_TEST_ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
@@ -986,17 +988,7 @@ module.exports = {
{ timeout: GATEWAY_E2E_TIMEOUT_MS },
async () => {
const envSnapshot = captureEnv([
"HOME",
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_SKIP_CHANNELS",
"OPENCLAW_SKIP_GMAIL_WATCHER",
"OPENCLAW_SKIP_CRON",
"OPENCLAW_SKIP_CANVAS_HOST",
"OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
"OPENCLAW_SKIP_PROVIDERS",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
...GATEWAY_TEST_ENV_KEYS,
"OPENCLAW_TEST_MINIMAL_GATEWAY",
"DISCORD_BOT_TOKEN",
]);
+2
View File
@@ -2,8 +2,10 @@
* Environment snapshot helpers for live gateway tests.
*/
import { deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
const COMMON_LIVE_ENV_NAMES = [
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_AGENT_RUNTIME",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
@@ -24,15 +24,16 @@ import {
disconnectGatewayClient,
getFreeGatewayPort,
} from "./test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
const TEST_ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_URL",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PASSWORD",
"OPENCLAW_GATEWAY_PORT",
];
type Cleanup = () => Promise<void> | void;
@@ -22,15 +22,16 @@ import {
disconnectGatewayClient,
getFreeGatewayPort,
} from "../test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "../test-helpers.env.js";
const TEST_ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_URL",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PASSWORD",
"OPENCLAW_GATEWAY_PORT",
];
describe("plugin.approval.request delivery routing (real gateway)", () => {
@@ -11,9 +11,11 @@ import { PROXY_ENV_KEYS } from "../infra/net/proxy-env.js";
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js";
import { startGatewayServer } from "./server.js";
import { getFreeGatewayPort } from "./test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
const NETWORK_GATEWAY_ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
+1 -2
View File
@@ -30,8 +30,6 @@ export async function resetPreparedModelCatalogForTest(): Promise<void> {
await resetPreparedModelCatalogForTestLocal();
}
ensureOpenClawCliOnPath();
const loadGatewayStartupEarlyModule = createLazyRuntimeModule(
() => import("./server-startup-early.js"),
);
@@ -108,6 +106,7 @@ export async function startGatewayServer(
port = 18789,
opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
ensureOpenClawCliOnPath();
let releasePostReadyWork: () => void = () => {};
const postReadyWorkBarrier = new Promise<void>((resolve) => {
releasePostReadyWork = resolve;
@@ -12,8 +12,8 @@ import {
signDevicePayload,
} from "../infra/device-identity.js";
import { buildDeviceAuthPayload } from "./device-auth.js";
import { shouldRetainControlUiDeviceAuthMigrationSession } from "./server-public.js";
import { CONTROL_UI_CLIENT } from "./server.auth.test-helpers.js";
import { shouldRetainControlUiDeviceAuthMigrationSession } from "./server.impl.js";
import {
connectReq,
createGatewaySuiteHarness,
+43 -17
View File
@@ -15,6 +15,7 @@ import {
signDevicePayload,
} from "../infra/device-identity.js";
import { rawDataToString } from "../infra/ws.js";
import { captureEnv } from "../test-utils/env.js";
import { getDeterministicFreePortBlock } from "../test-utils/ports.js";
import {
GATEWAY_CLIENT_MODES,
@@ -25,6 +26,7 @@ import {
import { GatewayClient } from "./client.js";
import { buildDeviceAuthPayloadV3 } from "./device-auth.js";
import { startGatewayServer } from "./server.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
/** Reserve a deterministic free port block for Gateway E2E tests. */
export async function getFreeGatewayPort(): Promise<number> {
@@ -263,23 +265,47 @@ export async function startGatewayWithClient(params: {
token: string;
clientDisplayName?: string;
}) {
await writeFile(params.configPath, `${JSON.stringify(params.cfg, null, 2)}\n`);
process.env.OPENCLAW_CONFIG_PATH = params.configPath;
clearRuntimeConfigSnapshot();
clearConfigCache();
clearSessionStoreCacheForTest();
const gatewayStartupEnv = captureEnv([...GATEWAY_STARTUP_MUTATED_ENV_KEYS]);
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
try {
await writeFile(params.configPath, `${JSON.stringify(params.cfg, null, 2)}\n`);
process.env.OPENCLAW_CONFIG_PATH = params.configPath;
clearRuntimeConfigSnapshot();
clearConfigCache();
clearSessionStoreCacheForTest();
const port = await getFreeGatewayPort();
const server = await startGatewayServer(port, {
bind: "loopback",
auth: { mode: "token", token: params.token },
controlUiEnabled: false,
});
const client = await connectGatewayClient({
url: `ws://127.0.0.1:${port}`,
token: params.token,
clientDisplayName: params.clientDisplayName,
});
const port = await getFreeGatewayPort();
const startedServer = await startGatewayServer(port, {
bind: "loopback",
auth: { mode: "token", token: params.token },
controlUiEnabled: false,
});
server = startedServer;
const client = await connectGatewayClient({
url: `ws://127.0.0.1:${port}`,
token: params.token,
clientDisplayName: params.clientDisplayName,
});
return { port, server, client };
return {
port,
client,
server: {
close: async (...args: Parameters<typeof startedServer.close>) => {
try {
await startedServer.close(...args);
} finally {
gatewayStartupEnv.restore();
}
},
},
};
} catch (error) {
try {
await server?.close({ reason: "gateway E2E client setup failed" });
} finally {
gatewayStartupEnv.restore();
}
throw error;
}
}
+12
View File
@@ -0,0 +1,12 @@
// Gateway startup rewrites these process-wide values. Manual in-process test
// owners must snapshot them so later files never inherit a closed server or stale PATH.
export const GATEWAY_STARTUP_MUTATED_ENV_KEYS = [
"PATH",
"OPENCLAW_GATEWAY_PORT",
"OPENCLAW_PATH_BOOTSTRAPPED",
] as const;
/** Captures values that in-process Gateway startup can mutate. */
export function snapshotGatewayStartupEnv(): Record<string, string | undefined> {
return Object.fromEntries(GATEWAY_STARTUP_MUTATED_ENV_KEYS.map((key) => [key, process.env[key]]));
}
@@ -0,0 +1,68 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js";
import { disconnectGatewayClient, startGatewayWithClient } from "./test-helpers.e2e.js";
import { installGatewayTestHooks, withGatewayServer } from "./test-helpers.server.js";
const envBeforeSuite = {
PATH: process.env.PATH,
OPENCLAW_GATEWAY_PORT: process.env.OPENCLAW_GATEWAY_PORT,
OPENCLAW_PATH_BOOTSTRAPPED: process.env.OPENCLAW_PATH_BOOTSTRAPPED,
};
installGatewayTestHooks();
describe("Gateway test environment lifecycle", () => {
it("records the process-wide startup environment", async () => {
await withGatewayServer(async ({ port }) => {
expect(process.env.OPENCLAW_GATEWAY_PORT).toBe(String(port));
expect(process.env.OPENCLAW_PATH_BOOTSTRAPPED).toBe("1");
});
});
it("restores startup-owned environment before the next test", () => {
expect({
PATH: process.env.PATH,
OPENCLAW_GATEWAY_PORT: process.env.OPENCLAW_GATEWAY_PORT,
OPENCLAW_PATH_BOOTSTRAPPED: process.env.OPENCLAW_PATH_BOOTSTRAPPED,
}).toEqual(envBeforeSuite);
});
it("restores startup-owned environment when a direct E2E server closes", async () => {
const stateDir = process.env.OPENCLAW_STATE_DIR;
if (!stateDir) {
throw new Error("OPENCLAW_STATE_DIR is required");
}
setTestEnvValue("PATH", process.env.PATH ?? "");
deleteTestEnvValue("OPENCLAW_PATH_BOOTSTRAPPED");
const envBeforeServer = {
PATH: process.env.PATH,
OPENCLAW_GATEWAY_PORT: process.env.OPENCLAW_GATEWAY_PORT,
OPENCLAW_PATH_BOOTSTRAPPED: process.env.OPENCLAW_PATH_BOOTSTRAPPED,
};
const token = "test-gateway-token-1234567890";
for (const attempt of ["first", "second"]) {
const started = await startGatewayWithClient({
cfg: { gateway: { auth: { mode: "token", token } } },
configPath: path.join(stateDir, "openclaw.json"),
token,
});
try {
expect(process.env.OPENCLAW_GATEWAY_PORT).toBe(String(started.port));
expect(process.env.OPENCLAW_PATH_BOOTSTRAPPED).toBe("1");
} finally {
await disconnectGatewayClient(started.client).catch(() => undefined);
await started.server.close({
reason: `${attempt} direct E2E environment proof complete`,
});
}
expect({
PATH: process.env.PATH,
OPENCLAW_GATEWAY_PORT: process.env.OPENCLAW_GATEWAY_PORT,
OPENCLAW_PATH_BOOTSTRAPPED: process.env.OPENCLAW_PATH_BOOTSTRAPPED,
}).toEqual(envBeforeServer);
}
});
});
+2
View File
@@ -60,6 +60,7 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-cha
import { buildDeviceAuthPayloadV3 } from "./device-auth.js";
import type { GatewayServerOptions } from "./server.js";
import { invalidateSessionSharingSnapshot } from "./session-sharing.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "./test-helpers.env.js";
import { resetTestPluginRegistry } from "./test-helpers.plugin-registry.js";
import {
agentCommand,
@@ -80,6 +81,7 @@ const getServerModule = createLazyRuntimeModule(() => import("./server.js"));
const GATEWAY_TEST_ENV_KEYS = [
"HOME",
"USERPROFILE",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_AGENT_DIR",
+10 -1
View File
@@ -4,6 +4,10 @@ import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { loadPersistedAuthProfileStore } from "../agents/auth-profiles/persisted.js";
import {
GATEWAY_STARTUP_MUTATED_ENV_KEYS,
snapshotGatewayStartupEnv,
} from "../gateway/test-helpers.env.js";
import {
closeOpenClawAgentDatabaseByPath,
openOpenClawAgentDatabase,
@@ -12,7 +16,7 @@ import {
closeOpenClawStateDatabaseByPath,
openOpenClawStateDatabase,
} from "../state/openclaw-state-db.js";
import { withEnvAsync } from "./env.js";
import { setTestEnvValue, withEnvAsync } from "./env.js";
import { createOpenClawTestState, withOpenClawTestState } from "./openclaw-test-state.js";
async function expectPathMissing(targetPath: string): Promise<void> {
@@ -31,6 +35,7 @@ describe("openclaw test state", () => {
const previousOpenClawHome = process.env.OPENCLAW_HOME;
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
const previousConfigPath = process.env.OPENCLAW_CONFIG_PATH;
const previousGatewayStartupEnv = snapshotGatewayStartupEnv();
const state = await createOpenClawTestState({
label: "unit",
@@ -49,6 +54,9 @@ describe("openclaw test state", () => {
expect(process.env.HOME).toBe(state.home);
expect(process.env.OPENCLAW_HOME).toBe(state.home);
expect(JSON.parse(await fs.readFile(state.configPath, "utf8"))).toStrictEqual({});
for (const key of GATEWAY_STARTUP_MUTATED_ENV_KEYS) {
setTestEnvValue(key, `mutated-${key}`);
}
} finally {
await state.cleanup();
}
@@ -57,6 +65,7 @@ describe("openclaw test state", () => {
expect(process.env.OPENCLAW_HOME).toBe(previousOpenClawHome);
expect(process.env.OPENCLAW_STATE_DIR).toBe(previousStateDir);
expect(process.env.OPENCLAW_CONFIG_PATH).toBe(previousConfigPath);
expect(snapshotGatewayStartupEnv()).toEqual(previousGatewayStartupEnv);
await expectPathMissing(state.root);
});
+2
View File
@@ -7,6 +7,7 @@ import { resolveAuthProfileDatabasePath } from "../agents/auth-profiles/sqlite.j
import { saveAuthProfileStore } from "../agents/auth-profiles/store.js";
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
import * as configRuntime from "../config/config.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "../gateway/test-helpers.env.js";
import { isPathInside } from "../infra/path-guards.js";
import {
closeOpenClawAgentDatabaseByPath,
@@ -72,6 +73,7 @@ const ENV_KEYS = [
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_HOME",
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
@@ -18,6 +18,7 @@ import {
disconnectGatewayClient,
getFreeGatewayPort,
} from "../../../../src/gateway/test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "../../../../src/gateway/test-helpers.env.js";
import { closeOpenClawAgentDatabasesForTest } from "../../../../src/state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../../../src/state/openclaw-state-db.js";
import { createTaskRecord, deleteTaskRecordById } from "../../../../src/tasks/task-registry.js";
@@ -120,11 +121,11 @@ vi.mock("../../../../src/gateway/server-request-context.js", async () => {
const ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PASSWORD",
"OPENCLAW_GATEWAY_PORT",
"OPENCLAW_SKIP_CHANNELS",
"OPENCLAW_SKIP_GMAIL_WATCHER",
"OPENCLAW_SKIP_CRON",
@@ -15,6 +15,7 @@ import {
disconnectGatewayClient,
getFreeGatewayPort,
} from "../../../../src/gateway/test-helpers.e2e.js";
import { snapshotGatewayStartupEnv } from "../../../../src/gateway/test-helpers.env.js";
import {
registerPluginHttpRoute,
withPluginHttpRouteRegistry,
@@ -139,6 +140,7 @@ describe("Gateway hosted web surfaces", () => {
await withEnvAsync(
{
...snapshotGatewayStartupEnv(),
HOME: root,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { snapshotGatewayStartupEnv } from "../../../../src/gateway/test-helpers.env.js";
import {
assertGatewayLoopbackLanProof,
parseGatewayLoopbackLanOptions,
@@ -36,7 +37,9 @@ describe("Gateway loopback and LAN access producer", () => {
});
it("proves real loopback isolation, LAN reachability, and shared-token authentication", async () => {
const gatewayStartupEnv = snapshotGatewayStartupEnv();
const proof = await runGatewayLoopbackLanProof();
expect(snapshotGatewayStartupEnv()).toEqual(gatewayStartupEnv);
expect(proof).toEqual({
loopback: {
authenticatedHealthRpc: true,
@@ -13,6 +13,7 @@ import { clearSessionStoreCacheForTest } from "../../../../src/config/sessions/s
import { pickPrimaryLanIPv4 } from "../../../../src/gateway/net.js";
import { startGatewayServer, type GatewayServer } from "../../../../src/gateway/server.js";
import { getFreeGatewayPort } from "../../../../src/gateway/test-helpers.e2e.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "../../../../src/gateway/test-helpers.env.js";
import { resetAgentEventsForTest } from "../../../../src/infra/agent-events.js";
import { rawDataToString } from "../../../../src/infra/ws.js";
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../../../src/test-utils/env.js";
@@ -27,6 +28,7 @@ const SCENARIO_ID = "gateway-loopback-lan-access";
const PROBE_TIMEOUT_MS = 10_000;
const ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
@@ -9,6 +9,7 @@ import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { snapshotGatewayStartupEnv } from "../../../../src/gateway/test-helpers.env.js";
import { withEnvAsync } from "../../../../src/test-utils/env.js";
import { waitForFile } from "../../../helpers/process-wait.js";
import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js";
@@ -171,11 +172,13 @@ describeOnTestbox("Gateway SSH tunnel QA producer", () => {
it("proves real forwarding, cleanup, and operator diagnostics", async () => {
const artifactBase = tempDirs.make("openclaw-gateway-ssh-evidence-");
const gatewayStartupEnv = snapshotGatewayStartupEnv();
const evidence = await runGatewaySshTunnels({
artifactBase,
repoRoot: process.cwd(),
});
expect(snapshotGatewayStartupEnv()).toEqual(gatewayStartupEnv);
expect(evidence.entries).toHaveLength(1);
expect(evidence.entries[0]?.result.status).toBe("pass");
const summary = JSON.parse(
@@ -13,6 +13,7 @@ import {
import { gatewayStatusCommand } from "../../../../src/commands/gateway-status.js";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../../../../src/config/config.js";
import { startGatewayServer } from "../../../../src/gateway/server.js";
import { snapshotGatewayStartupEnv } from "../../../../src/gateway/test-helpers.env.js";
import { formatErrorMessage } from "../../../../src/infra/errors.js";
import type { OutputRuntimeEnv } from "../../../../src/runtime.js";
import { withEnvAsync } from "../../../../src/test-utils/env.js";
@@ -581,6 +582,7 @@ export async function runGatewaySshTunnels(
const result = await withEnvAsync(
{
...snapshotGatewayStartupEnv(),
HOME: homeDir,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { snapshotGatewayStartupEnv } from "../../../../src/gateway/test-helpers.env.js";
import { runGatewayTlsPinningProducer } from "./gateway-tls-pinning.js";
const tempDirs: string[] = [];
@@ -14,12 +15,14 @@ describe("Gateway TLS pinning evidence", () => {
it("proves the live listener fingerprint and public client pin policy", async () => {
const artifactBase = await fs.mkdtemp(path.join(os.tmpdir(), "gateway-tls-pinning-evidence-"));
tempDirs.push(artifactBase);
const gatewayStartupEnv = snapshotGatewayStartupEnv();
const evidence = await runGatewayTlsPinningProducer({
artifactBase,
repoRoot: process.cwd(),
});
expect(snapshotGatewayStartupEnv()).toEqual(gatewayStartupEnv);
expect(evidence.entries[0]?.result.status).toBe("pass");
const proof = JSON.parse(
await fs.readFile(path.join(artifactBase, "gateway-tls-pinning-summary.json"), "utf8"),
@@ -13,6 +13,7 @@ import {
} from "../../../../extensions/qa-lab/api.js";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../../../../src/config/config.js";
import { startGatewayServer } from "../../../../src/gateway/server.js";
import { GATEWAY_STARTUP_MUTATED_ENV_KEYS } from "../../../../src/gateway/test-helpers.env.js";
import { formatErrorMessage } from "../../../../src/infra/errors.js";
import { normalizeFingerprint } from "../../../../src/infra/tls/fingerprint.js";
import { loadGatewayTlsRuntime } from "../../../../src/infra/tls/gateway.js";
@@ -24,6 +25,7 @@ const DISCOVERY_PLUGIN_ID = "tls-discovery-proof";
const CONNECTION_TIMEOUT_MS = 15_000;
const ENV_KEYS = [
"HOME",
...GATEWAY_STARTUP_MUTATED_ENV_KEYS,
"NODE_ENV",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_STATE_DIR",