diff --git a/src/cli/browser-cli-extension.test.ts b/src/cli/browser-cli-extension.test.ts index 581813aa2..ab4ed334d 100644 --- a/src/cli/browser-cli-extension.test.ts +++ b/src/cli/browser-cli-extension.test.ts @@ -1,6 +1,7 @@ import path from "node:path"; import { Command } from "commander"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { withEnvAsync } from "../test-utils/env.js"; const copyToClipboard = vi.fn(); const runtime = { @@ -167,11 +168,8 @@ describe("browser extension install (fs-mocked)", () => { }); it("copies extension path to clipboard", async () => { - const prev = process.env.OPENCLAW_STATE_DIR; const tmp = abs("/tmp/openclaw-ext-path"); - process.env.OPENCLAW_STATE_DIR = tmp; - - try { + await withEnvAsync({ OPENCLAW_STATE_DIR: tmp }, async () => { copyToClipboard.mockResolvedValue(true); const dir = path.join(tmp, "browser", "chrome-extension"); @@ -186,12 +184,6 @@ describe("browser extension install (fs-mocked)", () => { await program.parseAsync(["browser", "extension", "path"], { from: "user" }); expect(copyToClipboard).toHaveBeenCalledWith(dir); - } finally { - if (prev === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prev; - } - } + }); }); }); diff --git a/src/config/sessions.test.ts b/src/config/sessions.test.ts index 94d628dcd..13c2f6474 100644 --- a/src/config/sessions.test.ts +++ b/src/config/sessions.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { withEnv } from "../test-utils/env.js"; import { buildGroupDisplayName, deriveSessionKey, @@ -33,6 +34,9 @@ describe("sessions", () => { await fs.rm(fixtureRoot, { recursive: true, force: true }); }); + const withStateDir = (stateDir: string, fn: () => T): T => + withEnv({ OPENCLAW_STATE_DIR: stateDir }, fn); + it("returns normalized per-sender key", () => { expect(deriveSessionKey("per-sender", { From: "whatsapp:+1555" })).toBe("+1555"); }); @@ -428,9 +432,7 @@ describe("sessions", () => { }); it("includes topic ids in session transcript filenames", () => { - const prev = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = "/custom/state"; - try { + withStateDir("/custom/state", () => { const sessionFile = resolveSessionTranscriptPath("sess-1", "main", 123); expect(sessionFile).toBe( path.join( @@ -441,39 +443,23 @@ describe("sessions", () => { "sess-1-topic-123.jsonl", ), ); - } finally { - if (prev === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prev; - } - } + }); }); it("uses agent id when resolving session file fallback paths", () => { - const prev = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = "/custom/state"; - try { + withStateDir("/custom/state", () => { const sessionFile = resolveSessionFilePath("sess-2", undefined, { agentId: "codex", }); expect(sessionFile).toBe( path.join(path.resolve("/custom/state"), "agents", "codex", "sessions", "sess-2.jsonl"), ); - } finally { - if (prev === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prev; - } - } + }); }); it("resolves cross-agent absolute sessionFile paths", () => { - const prev = process.env.OPENCLAW_STATE_DIR; const stateDir = path.resolve("/home/user/.openclaw"); - process.env.OPENCLAW_STATE_DIR = stateDir; - try { + withStateDir(stateDir, () => { const bot2Session = path.join(stateDir, "agents", "bot2", "sessions", "sess-1.jsonl"); // Agent bot1 resolves a sessionFile that belongs to agent bot2 const sessionFile = resolveSessionFilePath( @@ -482,19 +468,11 @@ describe("sessions", () => { { agentId: "bot1" }, ); expect(sessionFile).toBe(bot2Session); - } finally { - if (prev === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prev; - } - } + }); }); it("resolves cross-agent paths when OPENCLAW_STATE_DIR differs from stored paths", () => { - const prev = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.resolve("/different/state"); - try { + withStateDir(path.resolve("/different/state"), () => { const originalBase = path.resolve("/original/state"); const bot2Session = path.join(originalBase, "agents", "bot2", "sessions", "sess-1.jsonl"); // sessionFile was created under a different state dir than current env @@ -504,19 +482,11 @@ describe("sessions", () => { { agentId: "bot1" }, ); expect(sessionFile).toBe(bot2Session); - } finally { - if (prev === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prev; - } - } + }); }); it("rejects absolute sessionFile paths outside agent sessions directories", () => { - const prev = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.resolve("/home/user/.openclaw"); - try { + withStateDir(path.resolve("/home/user/.openclaw"), () => { expect(() => resolveSessionFilePath( "sess-1", @@ -524,13 +494,7 @@ describe("sessions", () => { { agentId: "bot1" }, ), ).toThrow(/within sessions directory/); - } finally { - if (prev === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prev; - } - } + }); }); it("updateSessionStoreEntry merges concurrent patches", async () => { diff --git a/src/infra/session-cost-usage.test.ts b/src/infra/session-cost-usage.test.ts index 5d584eefd..ba9e10b1f 100644 --- a/src/infra/session-cost-usage.test.ts +++ b/src/infra/session-cost-usage.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { withEnvAsync } from "../test-utils/env.js"; import { discoverAllSessions, loadCostUsageSummary, @@ -12,6 +13,9 @@ import { } from "./session-cost-usage.js"; describe("session cost usage", () => { + const withStateDir = async (stateDir: string, fn: () => Promise): Promise => + await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, fn); + it("aggregates daily totals with log cost and pricing fallback", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cost-")); const sessionsDir = path.join(root, "agents", "main", "sessions"); @@ -98,20 +102,12 @@ describe("session cost usage", () => { }, } as unknown as OpenClawConfig; - const originalState = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = root; - try { + await withStateDir(root, async () => { const summary = await loadCostUsageSummary({ days: 30, config }); expect(summary.daily.length).toBe(1); expect(summary.totals.totalTokens).toBe(50); expect(summary.totals.totalCost).toBeCloseTo(0.03003, 5); - } finally { - if (originalState === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalState; - } - } + }); }); it("summarizes a single session file", async () => { @@ -225,22 +221,14 @@ describe("session cost usage", () => { const now = Date.now(); await fs.utimes(sessionFile, now / 1000, now / 1000); - const originalState = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = root; - try { + await withStateDir(root, async () => { const sessions = await discoverAllSessions({ startMs: now - 7 * 24 * 60 * 60 * 1000, endMs: now - 24 * 60 * 60 * 1000, }); expect(sessions.length).toBe(1); expect(sessions[0]?.sessionId).toBe("sess-late"); - } finally { - if (originalState === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalState; - } - } + }); }); it("resolves non-main absolute sessionFile using explicit agentId for cost summary", async () => { @@ -270,9 +258,7 @@ describe("session cost usage", () => { "utf-8", ); - const originalState = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = root; - try { + await withStateDir(root, async () => { const summary = await loadSessionCostSummary({ sessionId: "sess-worker-1", sessionEntry: { @@ -284,13 +270,7 @@ describe("session cost usage", () => { }); expect(summary?.totalTokens).toBe(18); expect(summary?.totalCost).toBeCloseTo(0.01, 5); - } finally { - if (originalState === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalState; - } - } + }); }); it("resolves non-main absolute sessionFile using explicit agentId for timeseries", async () => { @@ -316,9 +296,7 @@ describe("session cost usage", () => { "utf-8", ); - const originalState = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = root; - try { + await withStateDir(root, async () => { const timeseries = await loadSessionUsageTimeSeries({ sessionId: "sess-worker-2", sessionEntry: { @@ -330,13 +308,7 @@ describe("session cost usage", () => { }); expect(timeseries?.points.length).toBe(1); expect(timeseries?.points[0]?.totalTokens).toBe(8); - } finally { - if (originalState === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalState; - } - } + }); }); it("resolves non-main absolute sessionFile using explicit agentId for logs", async () => { @@ -360,9 +332,7 @@ describe("session cost usage", () => { "utf-8", ); - const originalState = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = root; - try { + await withStateDir(root, async () => { const logs = await loadSessionLogs({ sessionId: "sess-worker-3", sessionEntry: { @@ -375,13 +345,7 @@ describe("session cost usage", () => { expect(logs).toHaveLength(1); expect(logs?.[0]?.content).toContain("hello worker"); expect(logs?.[0]?.role).toBe("user"); - } finally { - if (originalState === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalState; - } - } + }); }); it("strips inbound and untrusted metadata blocks from session usage logs", async () => { diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 6d7b155d6..876cbb3a4 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelPlugin } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; +import { withEnvAsync } from "../test-utils/env.js"; import { collectPluginsCodeSafetyFindings } from "./audit-extra.js"; import type { SecurityAuditOptions, SecurityAuditReport } from "./audit.js"; import { runSecurityAudit } from "./audit.js"; @@ -102,19 +103,9 @@ describe("security audit", () => { }; const withStateDir = async (label: string, fn: (tmp: string) => Promise) => { - const prevStateDir = process.env.OPENCLAW_STATE_DIR; const tmp = await makeTmpDir(label); - process.env.OPENCLAW_STATE_DIR = tmp; await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 }); - try { - await fn(tmp); - } finally { - if (prevStateDir == null) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prevStateDir; - } - } + await withEnvAsync({ OPENCLAW_STATE_DIR: tmp }, async () => await fn(tmp)); }; beforeAll(async () => { diff --git a/src/test-utils/env.test.ts b/src/test-utils/env.test.ts index dce4e8946..07c01c097 100644 --- a/src/test-utils/env.test.ts +++ b/src/test-utils/env.test.ts @@ -53,4 +53,14 @@ describe("env test utils", () => { expect(process.env[key]).toBe(prev); }); + + it("withEnvAsync applies values only inside async callback", async () => { + const key = "OPENCLAW_ENV_TEST_ASYNC_OK"; + const prev = process.env[key]; + + const seen = await withEnvAsync({ [key]: "inside" }, async () => process.env[key]); + + expect(seen).toBe("inside"); + expect(process.env[key]).toBe(prev); + }); });