From e27c9a9a41e1328bec90215a4ba2abec6fa71872 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sat, 20 Jun 2026 02:41:58 +0100 Subject: [PATCH] fix: centralize dotenv env cleanup --- src/infra/dotenv.test.ts | 58 +++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index c5684b4a6748..448cc2f982e0 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -12,6 +12,7 @@ import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plug import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js"; import { listKnownProviderAuthEnvVarNames } from "../secrets/provider-env-vars.js"; +import { captureFullEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; import { loadDotEnv, loadWorkspaceDotEnvFile } from "./dotenv.js"; const loggerMocks = vi.hoisted(() => ({ @@ -80,7 +81,7 @@ async function writeEnvFile(filePath: string, contents: string) { function clearEnv(keys: readonly string[]) { for (const key of keys) { - delete process.env[key]; + deleteTestEnvValue(key); } } @@ -91,23 +92,12 @@ function expectEnvUndefined(keys: readonly string[]) { } async function withIsolatedEnvAndCwd(run: () => Promise) { - const prevEnv = { ...process.env }; + const envSnapshot = captureFullEnv(); try { await run(); } finally { vi.restoreAllMocks(); - for (const key of Object.keys(process.env)) { - if (!(key in prevEnv)) { - delete process.env[key]; - } - } - for (const [key, value] of Object.entries(prevEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } + envSnapshot.restore(); } } @@ -169,7 +159,7 @@ async function withDotEnvFixture(run: (fixture: DotEnvFixture) => Promise) const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-dotenv-test-")); const cwdDir = path.join(base, "cwd"); const stateDir = path.join(base, "state"); - process.env.OPENCLAW_STATE_DIR = stateDir; + setTestEnvValue("OPENCLAW_STATE_DIR", stateDir); await fs.mkdir(cwdDir, { recursive: true }); await fs.mkdir(stateDir, { recursive: true }); await run({ base, cwdDir, stateDir }); @@ -228,9 +218,9 @@ describe("loadDotEnv", () => { it("loads the Ubuntu gateway.env compatibility fallback after ~/.openclaw/.env", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ base, cwdDir }) => { - process.env.HOME = base; + setTestEnvValue("HOME", base); const defaultStateDir = path.join(base, ".openclaw"); - process.env.OPENCLAW_STATE_DIR = defaultStateDir; + setTestEnvValue("OPENCLAW_STATE_DIR", defaultStateDir); await writeEnvFile(path.join(defaultStateDir, ".env"), "FOO=from-global\n"); await writeEnvFile( path.join(base, ".config", "openclaw", "gateway.env"), @@ -259,7 +249,7 @@ describe("loadDotEnv", () => { it("does not warn about dotenv conflicts when the key is already set", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ base, cwdDir, stateDir }) => { - process.env.HOME = base; + setTestEnvValue("HOME", base); process.env.FOO = "from-shell"; await writeEnvFile(path.join(stateDir, ".env"), "FOO=from-global\n"); await writeEnvFile( @@ -314,7 +304,7 @@ describe("loadDotEnv", () => { delete process.env.NODE_REPL_EXTERNAL_MODULE; delete process.env.NODE_REPL_HISTORY; delete process.env.NODE_V8_COVERAGE; - delete process.env.OPENCLAW_CONFIG_PATH; + deleteTestEnvValue("OPENCLAW_CONFIG_PATH"); delete process.env.ANTHROPIC_BASE_URL; delete process.env.CLOUDSDK_PYTHON; delete process.env.EXAMPLE_API_HOST; @@ -396,9 +386,9 @@ describe("loadDotEnv", () => { ].join("\n"), ); - delete process.env.OPENCLAW_STATE_DIR; + deleteTestEnvValue("OPENCLAW_STATE_DIR"); delete process.env.STATE_DIRECTORY; - delete process.env.OPENCLAW_CONFIG_PATH; + deleteTestEnvValue("OPENCLAW_CONFIG_PATH"); loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); @@ -453,7 +443,7 @@ describe("loadDotEnv", () => { ].join("\n"), ); - delete process.env.OPENCLAW_AGENT_DIR; + deleteTestEnvValue("OPENCLAW_AGENT_DIR"); delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; delete process.env.OPENCLAW_OAUTH_DIR; delete process.env.PI_CODING_AGENT_DIR; @@ -548,7 +538,7 @@ describe("loadDotEnv", () => { await withDotEnvFixture(async ({ cwdDir }) => { await writeEnvFile(path.join(cwdDir, ".env"), `${key}=./evil/npm-cli.js\n`); - delete process.env[key]; + deleteTestEnvValue(key); loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); @@ -656,7 +646,7 @@ describe("loadCliDotEnv", () => { // Delete the fixture-provided value so the blocking must come from // the workspace blocklist, not the "already set" skip. - delete process.env.OPENCLAW_STATE_DIR; + deleteTestEnvValue("OPENCLAW_STATE_DIR"); vi.spyOn(process, "cwd").mockReturnValue(cwdDir); loadCliDotEnv({ quiet: true }); @@ -669,9 +659,9 @@ describe("loadCliDotEnv", () => { it("loads the gateway.env compatibility fallback during CLI startup", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ base, cwdDir }) => { - process.env.HOME = base; + setTestEnvValue("HOME", base); const defaultStateDir = path.join(base, ".openclaw"); - process.env.OPENCLAW_STATE_DIR = defaultStateDir; + setTestEnvValue("OPENCLAW_STATE_DIR", defaultStateDir); await writeEnvFile(path.join(defaultStateDir, ".env"), "FOO=from-global\n"); await writeEnvFile( path.join(base, ".config", "openclaw", "gateway.env"), @@ -693,9 +683,9 @@ describe("loadCliDotEnv", () => { it("can defer global dotenv while loading only workspace env", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ base, cwdDir }) => { - process.env.HOME = base; + setTestEnvValue("HOME", base); const defaultStateDir = path.join(base, ".openclaw"); - process.env.OPENCLAW_STATE_DIR = defaultStateDir; + setTestEnvValue("OPENCLAW_STATE_DIR", defaultStateDir); await writeEnvFile(path.join(cwdDir, ".env"), "BAZ=from-workspace\n"); await writeEnvFile(path.join(defaultStateDir, ".env"), "FOO=from-global\n"); await writeEnvFile( @@ -721,8 +711,8 @@ describe("loadCliDotEnv", () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ base, cwdDir }) => { const customStateDir = path.join(base, "custom-state"); - process.env.HOME = base; - process.env.OPENCLAW_STATE_DIR = customStateDir; + setTestEnvValue("HOME", base); + setTestEnvValue("OPENCLAW_STATE_DIR", customStateDir); await writeEnvFile( path.join(base, ".config", "openclaw", "gateway.env"), "FOO=from-gateway\n", @@ -745,8 +735,8 @@ describe("loadCliDotEnv", () => { const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-dotenv-legacy-")); const cwdDir = path.join(base, "cwd"); const legacyStateDir = path.join(base, ".clawdbot"); - process.env.HOME = base; - delete process.env.OPENCLAW_STATE_DIR; + setTestEnvValue("HOME", base); + deleteTestEnvValue("OPENCLAW_STATE_DIR"); delete process.env.OPENCLAW_TEST_FAST; await fs.mkdir(cwdDir, { recursive: true }); await writeEnvFile(path.join(legacyStateDir, ".env"), "LEGACY_ONLY=from-legacy\n"); @@ -800,7 +790,7 @@ describe("loadCliDotEnv", () => { vi.spyOn(process, "cwd").mockReturnValue(cwdDir); delete process.env.SAFE_KEY; - delete process.env.OPENCLAW_CONFIG_PATH; + deleteTestEnvValue("OPENCLAW_CONFIG_PATH"); delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; delete process.env.NODE_OPTIONS; delete process.env.NODE_REDIRECT_WARNINGS; @@ -970,7 +960,7 @@ describe("workspace .env blocklist completeness", () => { ); for (const key of runtimeControlKeys) { - delete process.env[key]; + deleteTestEnvValue(key); } loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true });