diff --git a/extensions/browser/src/browser/chrome-mcp.test.ts b/extensions/browser/src/browser/chrome-mcp.test.ts index 2be77122714d..77312cf09f4f 100644 --- a/extensions/browser/src/browser/chrome-mcp.test.ts +++ b/extensions/browser/src/browser/chrome-mcp.test.ts @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; +import { createOpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ChromeMcpDocumentUnavailableError, @@ -1467,10 +1468,12 @@ describe("chrome MCP page parsing", () => { const user = "browser-user"; const password = "browser-password-1234567890"; // pragma: allowlist secret const cdpUrl = `wss://${user}:${password}@browserless.example/chrome?token=${secretToken}`; - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-mcp-test-")); - const configPath = path.join(tempDir, "openclaw.json"); - await fs.writeFile(configPath, JSON.stringify({ logging: { redactSensitive: "off" } })); - vi.stubEnv("OPENCLAW_CONFIG_PATH", configPath); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-chrome-mcp-test-", + }); + await openClawState.writeConfig({ logging: { redactSensitive: "off" } }); + const tempDir = openClawState.root; const fakeMcpCommand = path.join(tempDir, "fake-mcp.mjs"); await fs.writeFile( fakeMcpCommand, @@ -1505,7 +1508,7 @@ describe("chrome MCP page parsing", () => { } catch (err) { message = err instanceof Error ? err.message : String(err); } finally { - await fs.rm(tempDir, { recursive: true, force: true }); + await openClawState.cleanup(); } expect(message).toContain("Chrome MCP existing-session attach failed"); diff --git a/extensions/browser/src/browser/chrome.internal.test.ts b/extensions/browser/src/browser/chrome.internal.test.ts index 1251510d8cb6..77fb91a8ab27 100644 --- a/extensions/browser/src/browser/chrome.internal.test.ts +++ b/extensions/browser/src/browser/chrome.internal.test.ts @@ -6,6 +6,7 @@ import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import os from "node:os"; import path from "node:path"; +import { createOpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { WebSocketServer } from "ws"; import { rawDataToString } from "../infra/ws.js"; @@ -1738,10 +1739,12 @@ describe("chrome.ts internal", () => { it("buffers stderr chunks when Chrome emits diagnostics while CDP comes up", async () => { // Covers onStderr (appending chunks to the bounded stderr tail) plus the // stderrHint truthy branch on failure. - const configDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-redact-off-")); - const configPath = path.join(configDir, "openclaw.json"); - await fsp.writeFile(configPath, JSON.stringify({ logging: { redactSensitive: "off" } })); - vi.stubEnv("OPENCLAW_CONFIG_PATH", configPath); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-redact-off-", + }); + await openClawState.writeConfig({ logging: { redactSensitive: "off" } }); + const configDir = openClawState.root; const executablePath = path.join(configDir, "chrome-stderr-existing"); await fsp.writeFile(executablePath, ""); vi.spyOn(fs, "existsSync").mockImplementation((p) => { @@ -1787,7 +1790,7 @@ describe("chrome.ts internal", () => { expect(message).toContain("Chrome stderr:"); expect(message).toContain("chrome crash log"); expect(message).not.toContain(secretToken); - await fsp.rm(configDir, { recursive: true, force: true }); + await openClawState.cleanup(); }); it("omits the sandbox hint on non-linux platforms", async () => { diff --git a/extensions/browser/src/browser/config.test.ts b/extensions/browser/src/browser/config.test.ts index eb2e949d8d70..b10f45de8afb 100644 --- a/extensions/browser/src/browser/config.test.ts +++ b/extensions/browser/src/browser/config.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { BrowserConfig } from "../config/config.js"; import { resolveUserPath } from "../utils.js"; @@ -18,18 +19,16 @@ const BROWSER_HEADLESS_ENV_KEY = "OPENCLAW_BROWSER_HEADLESS"; // Isolate the extension relay secret (read from stateDir/credentials) so the // extension-token assertions do not pick up a developer's real secret file. let isolatedStateDir = ""; -const prevStateDir = process.env.OPENCLAW_STATE_DIR; -beforeEach(() => { - isolatedStateDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cfg-"))); - process.env.OPENCLAW_STATE_DIR = isolatedStateDir; +let openClawState: OpenClawTestState; +beforeEach(async () => { + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-cfg-", + }); + isolatedStateDir = openClawState.stateDir; }); -afterEach(() => { - if (prevStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prevStateDir; - } - fs.rmSync(isolatedStateDir, { recursive: true, force: true }); +afterEach(async () => { + await openClawState.cleanup(); }); /** Write a relay secret into the isolated state dir's credentials directory. */ diff --git a/extensions/codex/src/app-server/event-projector.media.test.ts b/extensions/codex/src/app-server/event-projector.media.test.ts index 73fd9f759446..2d13718d4470 100644 --- a/extensions/codex/src/app-server/event-projector.media.test.ts +++ b/extensions/codex/src/app-server/event-projector.media.test.ts @@ -1,16 +1,15 @@ +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; +import { afterEach, beforeEach } from "vitest"; import { describe, registerCodexEventProjectorTestLifecycle, embeddedAgentLog, - withTempDir, expect, it, vi, tinyPngBase64, fs, - os, path, - trackTempDir, createParams, createProjector, buildEmptyToolTelemetry, @@ -21,6 +20,17 @@ import { registerCodexEventProjectorTestLifecycle(); +let openClawState: OpenClawTestState; +beforeEach(async () => { + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-codex-media-state-", + }); +}); +afterEach(async () => { + await openClawState.cleanup(); +}); + describe("CodexAppServerEventProjector media projection", () => { it("attaches native Codex image-generation saved paths as reply media", async () => { const projector = await createProjector(); @@ -51,9 +61,6 @@ describe("CodexAppServerEventProjector media projection", () => { }); it("saves raw Codex image-generation results as reply media", async () => { - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-media-state-")); - trackTempDir(stateDir); - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); const projector = await createProjector(); await projector.handleNotification( @@ -194,9 +201,6 @@ describe("CodexAppServerEventProjector media projection", () => { }); it("dedupes raw and typed Codex image-generation media for the same item", async () => { - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-media-state-")); - trackTempDir(stateDir); - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); const projector = await createProjector(); const savedPath = "/tmp/codex-home/generated_images/session-1/ig_123.png"; @@ -230,50 +234,44 @@ describe("CodexAppServerEventProjector media projection", () => { }); it("prefers gateway-managed image media when the typed event arrives first", async () => { - await withTempDir("openclaw-codex-media-state-", async (stateDir) => { - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); - const projector = await createProjector(); - const savedPath = "/home/dev-user/.codex/generated_images/session-1/ig_123.png"; + const projector = await createProjector(); + const savedPath = "/home/dev-user/.codex/generated_images/session-1/ig_123.png"; - await projector.handleNotification( - forCurrentTurn("item/completed", { - item: { - type: "imageGeneration", - id: "ig_123", - status: "completed", - revisedPrompt: "A tiny blue square", - result: tinyPngBase64, - savedPath, - }, - }), - ); - await projector.handleNotification( - forCurrentTurn("rawResponseItem/completed", { - item: { - type: "image_generation_call", - id: "ig_123", - status: "generating", - result: tinyPngBase64, - }, - }), - ); + await projector.handleNotification( + forCurrentTurn("item/completed", { + item: { + type: "imageGeneration", + id: "ig_123", + status: "completed", + revisedPrompt: "A tiny blue square", + result: tinyPngBase64, + savedPath, + }, + }), + ); + await projector.handleNotification( + forCurrentTurn("rawResponseItem/completed", { + item: { + type: "image_generation_call", + id: "ig_123", + status: "generating", + result: tinyPngBase64, + }, + }), + ); - const result = projector.buildResult(buildEmptyToolTelemetry()); - const mediaUrl = result.toolMediaUrls?.[0]; + const result = projector.buildResult(buildEmptyToolTelemetry()); + const mediaUrl = result.toolMediaUrls?.[0]; - expect(result.toolMediaUrls).toHaveLength(1); - expect(mediaUrl).not.toBe(savedPath); - expect(mediaUrl).toContain(`${path.sep}media${path.sep}tool-image-generation${path.sep}`); - await expect(fs.readFile(mediaUrl ?? "")).resolves.toEqual( - Buffer.from(tinyPngBase64, "base64"), - ); - }); + expect(result.toolMediaUrls).toHaveLength(1); + expect(mediaUrl).not.toBe(savedPath); + expect(mediaUrl).toContain(`${path.sep}media${path.sep}tool-image-generation${path.sep}`); + await expect(fs.readFile(mediaUrl ?? "")).resolves.toEqual( + Buffer.from(tinyPngBase64, "base64"), + ); }); it("preserves distinct raw image-generation items with identical image bytes", async () => { - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-media-state-")); - trackTempDir(stateDir); - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); const projector = await createProjector(); for (const id of ["ig_raw_1", "ig_raw_2"]) { diff --git a/extensions/codex/src/app-server/event-projector.test-harness.ts b/extensions/codex/src/app-server/event-projector.test-harness.ts index 2eba8d527251..877c8b4eabba 100644 --- a/extensions/codex/src/app-server/event-projector.test-harness.ts +++ b/extensions/codex/src/app-server/event-projector.test-harness.ts @@ -20,7 +20,6 @@ import { resetGlobalHookRunner, } from "openclaw/plugin-sdk/hook-runtime"; import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { withTempDir } from "openclaw/plugin-sdk/test-env"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { CodexAppServerEventProjector } from "./event-projector.js"; import { createCodexTestModel, createCodexTestToolTerminalObserver } from "./test-support.js"; @@ -41,11 +40,9 @@ export { initializeGlobalHookRunner, it, onInternalDiagnosticEvent, - os, path, SessionManager, vi, - withTempDir, }; export type { EmbeddedRunAttemptParams, DiagnosticEventPayload }; @@ -108,10 +105,6 @@ export async function createParams(): Promise { } as EmbeddedRunAttemptParams; } -export function trackTempDir(tempDir: string): void { - tempDirs.add(tempDir); -} - export async function createProjector( params?: EmbeddedRunAttemptParams, options?: CodexAppServerEventProjectorOptions, diff --git a/extensions/copilot/src/attempt.test.ts b/extensions/copilot/src/attempt.test.ts index b32c2b07f624..8a291fed682c 100644 --- a/extensions/copilot/src/attempt.test.ts +++ b/extensions/copilot/src/attempt.test.ts @@ -18,6 +18,7 @@ import { resetGlobalHookRunner, } from "openclaw/plugin-sdk/hook-runtime"; import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { createOpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { runCopilotAttempt } from "./attempt.js"; import type { CopilotClientPool } from "./runtime.js"; @@ -916,12 +917,14 @@ describe("runCopilotAttempt", () => { }); it("hydrates offloaded prompt images before creating SDK blob attachments", async () => { - const stateDir = await fsp.mkdtemp(path.join(tmpdir(), "copilot-offloaded-image-")); - const inboundDir = path.join(stateDir, "media", "inbound"); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "copilot-offloaded-image-", + }); + const inboundDir = openClawState.statePath("media", "inbound"); const mediaId = "telegram-photo.png"; await fsp.mkdir(inboundDir, { recursive: true }); await fsp.writeFile(path.join(inboundDir, mediaId), Buffer.from(TINY_PNG_BASE64, "base64")); - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); const sdk = makeFakeSdk(); const pool = makeFakePool(sdk); @@ -960,8 +963,7 @@ describe("runCopilotAttempt", () => { }, ]); } finally { - vi.unstubAllEnvs(); - await fsp.rm(stateDir, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/extensions/imessage/src/monitor.last-route.test.ts b/extensions/imessage/src/monitor.last-route.test.ts index b5f5da392533..720aede00a75 100644 --- a/extensions/imessage/src/monitor.last-route.test.ts +++ b/extensions/imessage/src/monitor.last-route.test.ts @@ -6,6 +6,7 @@ import * as channelInbound from "openclaw/plugin-sdk/channel-inbound"; import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime"; import type { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-runtime"; import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; import type { waitForTransportReady } from "openclaw/plugin-sdk/transport-ready-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { createIMessageRpcClient } from "./client.js"; @@ -139,6 +140,7 @@ async function runChannelInboundEventForLastRouteTest(params: RunChannelInboundE describe("iMessage monitor last-route updates", () => { const tempDirs: string[] = []; + const openClawStates: OpenClawTestState[] = []; beforeEach(() => { vi.spyOn(channelInbound, "runChannelInboundEvent").mockImplementation( @@ -154,9 +156,10 @@ describe("iMessage monitor last-route updates", () => { expireCachedPrivateApiStatus(); }); - afterEach(() => { + afterEach(async () => { vi.restoreAllMocks(); vi.useRealTimers(); + await Promise.all(openClawStates.splice(0).map((state) => state.cleanup())); vi.unstubAllEnvs(); for (const dir of tempDirs.splice(0)) { fs.rmSync(dir, { recursive: true, force: true }); @@ -1673,9 +1676,12 @@ describe("iMessage monitor last-route updates", () => { }); it("repairs anchorless group watch payloads before routing or cursor updates", async () => { - const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-imsg-anchor-repair-")); - tempDirs.push(stateDir); - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); + openClawStates.push( + await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-imsg-anchor-repair-", + }), + ); let onNotification: ((message: { method: string; params: unknown }) => void) | undefined; const client = { diff --git a/extensions/imessage/src/send.test.ts b/extensions/imessage/src/send.test.ts index e916d2a21254..b835a8220fbe 100644 --- a/extensions/imessage/src/send.test.ts +++ b/extensions/imessage/src/send.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { IMessageRpcClient } from "./client.js"; import { loadFreshIMessageReplyCacheForTest } from "./test-support/runtime.js"; @@ -75,15 +76,22 @@ function createApprovalText(id = "approval-123"): string { } describe("sendMessageIMessage receipts", () => { + let openClawState: OpenClawTestState; + beforeEach(async () => { + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-imessage-send-", + }); await loadFreshSendModule(); }); - afterEach(() => { + afterEach(async () => { clearIMessageApprovalReactionTargetsForTest(); vi.restoreAllMocks(); vi.unstubAllEnvs(); vi.useRealTimers(); + await openClawState.cleanup(); }); it("attaches a text receipt for native send ids", async () => { @@ -905,8 +913,6 @@ describe("sendMessageIMessage receipts", () => { }); it("does not persist caption text when the caption follow-up send fails", async () => { - const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-imessage-send-")); - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); const client = createRejectingClient(new Error("caption failed")); const runCliJson = vi.fn().mockResolvedValueOnce({ messageId: "p:0/dm-media-guid" }); diff --git a/extensions/mattermost/src/mattermost/reply-delivery.test.ts b/extensions/mattermost/src/mattermost/reply-delivery.test.ts index 77fbbd2017b5..41c6a9369b09 100644 --- a/extensions/mattermost/src/mattermost/reply-delivery.test.ts +++ b/extensions/mattermost/src/mattermost/reply-delivery.test.ts @@ -1,8 +1,7 @@ // Mattermost tests cover reply delivery plugin behavior. -import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import type { ChunkMode } from "openclaw/plugin-sdk/reply-runtime"; +import { createOpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig, PluginRuntime } from "../../runtime-api.js"; import { @@ -225,9 +224,11 @@ describe("deliverMattermostReplyPayload", () => { }); it("passes agent-scoped mediaLocalRoots when sending media paths", async () => { - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mm-state-")); - process.env.OPENCLAW_STATE_DIR = stateDir; + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-mm-state-", + }); + const stateDir = openClawState.stateDir; try { const sendMessage = vi.fn(async () => undefined); @@ -269,12 +270,7 @@ describe("deliverMattermostReplyPayload", () => { }), ); } finally { - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - await fs.rm(stateDir, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/extensions/microsoft/speech-provider.test.ts b/extensions/microsoft/speech-provider.test.ts index 1f340c5040bf..fd0bae81a4e2 100644 --- a/extensions/microsoft/speech-provider.test.ts +++ b/extensions/microsoft/speech-provider.test.ts @@ -1,6 +1,5 @@ // Microsoft tests cover speech provider plugin behavior. -import { mkdtempSync, writeFileSync } from "node:fs"; -import os from "node:os"; +import { writeFileSync } from "node:fs"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { @@ -8,7 +7,8 @@ import { getDebugProxyCaptureStore, initializeDebugProxyCapture, } from "openclaw/plugin-sdk/proxy-capture"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { installDebugProxyTestResetHooks } from "../test-support/debug-proxy-env-test-helpers.js"; const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn()); @@ -66,6 +66,21 @@ function requireFirstEdgeTtsCall(edgeSpy: ReturnType): { } describe("listMicrosoftVoices", () => { + let openClawState: OpenClawTestState; + + beforeEach(async () => { + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "microsoft-voices-capture-", + }); + }); + + afterEach(async () => { + await openClawState.cleanup(); + }); + + // Install after local teardown so the proxy snapshot is restored before the + // state helper removes its directory and restores the outer environment. const proxyReset = installDebugProxyTestResetHooks(); it("maps Microsoft voice metadata into speech voice options", async () => { @@ -178,10 +193,8 @@ describe("listMicrosoftVoices", () => { }); it("records voice discovery exchanges in debug proxy capture mode", async () => { - const tempDir = mkdtempSync(path.join(os.tmpdir(), "microsoft-voices-capture-")); proxyReset.captureProxyEnv(); process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1"; - process.env.OPENCLAW_STATE_DIR = tempDir; process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID = "ms-voices-session"; globalThis.fetch = vi @@ -217,10 +230,8 @@ describe("listMicrosoftVoices", () => { }); it("does not double-capture voice discovery when the global fetch patch is installed", async () => { - const tempDir = mkdtempSync(path.join(os.tmpdir(), "microsoft-voices-global-")); proxyReset.captureProxyEnv(); process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1"; - process.env.OPENCLAW_STATE_DIR = tempDir; process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID = "ms-voices-global-session"; globalThis.fetch = vi.fn( diff --git a/extensions/openai/tts.test.ts b/extensions/openai/tts.test.ts index 14fc9690fd52..1a84c3bea13b 100644 --- a/extensions/openai/tts.test.ts +++ b/extensions/openai/tts.test.ts @@ -1,13 +1,11 @@ // Openai tests cover tts plugin behavior. -import { mkdtempSync } from "node:fs"; -import os from "node:os"; -import path from "node:path"; import { finalizeDebugProxyCapture, getDebugProxyCaptureStore, initializeDebugProxyCapture, } from "openclaw/plugin-sdk/proxy-capture"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { installDebugProxyTestResetHooks } from "../test-support/debug-proxy-env-test-helpers.js"; import { createStreamingErrorResponse } from "../test-support/streaming-error-response.js"; import { @@ -60,15 +58,27 @@ function firstFetchInit(fetchMock: ReturnType): RequestInit { } describe("openai tts", () => { - const proxyReset = installDebugProxyTestResetHooks(); const originalFetch = globalThis.fetch; + let openClawState: OpenClawTestState; - afterEach(() => { + beforeEach(async () => { + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openai-tts-capture-", + }); + }); + + afterEach(async () => { globalThis.fetch = originalFetch; vi.unstubAllEnvs(); vi.restoreAllMocks(); + await openClawState.cleanup(); }); + // Install after local teardown so the proxy snapshot is restored before the + // state helper removes its directory and restores the outer environment. + const proxyReset = installDebugProxyTestResetHooks(); + describe("isValidOpenAIVoice", () => { it("accepts all valid OpenAI voices including newer additions", () => { for (const voice of OPENAI_TTS_VOICES) { @@ -348,10 +358,8 @@ describe("openai tts", () => { }); it("records TTS exchanges in debug proxy capture mode", async () => { - const tempDir = mkdtempSync(path.join(os.tmpdir(), "openai-tts-capture-")); proxyReset.captureProxyEnv(); process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1"; - process.env.OPENCLAW_STATE_DIR = tempDir; process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID = "tts-session"; globalThis.fetch = vi @@ -391,10 +399,8 @@ describe("openai tts", () => { }); it("does not double-capture TTS exchanges when the global fetch patch is installed", async () => { - const tempDir = mkdtempSync(path.join(os.tmpdir(), "openai-tts-patched-capture-")); proxyReset.captureProxyEnv(); process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1"; - process.env.OPENCLAW_STATE_DIR = tempDir; process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID = "tts-patched-session"; globalThis.fetch = vi diff --git a/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts b/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts index ca2f29671c84..d158aaad1f30 100644 --- a/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts +++ b/extensions/qqbot/src/engine/gateway/outbound-dispatch.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { createOpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { DEFAULT_MEDIA_SEND_ERROR, @@ -710,16 +711,18 @@ describe("dispatchOutbound", () => { }); it("does not expose default sandbox roots through gateway qqmedia replies", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qqbot-agent-root-boundary-")); - const originalStateDir = process.env.OPENCLAW_STATE_DIR; + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "qqbot-agent-root-boundary-", + }); + const tmpRoot = openClawState.root; try { const workspaceDir = path.join(tmpRoot, "workspace"); - const stateSandboxDir = path.join(tmpRoot, "state", "sandboxes", "other-agent"); + const stateSandboxDir = openClawState.statePath("sandboxes", "other-agent"); const stateSandboxFile = path.join(stateSandboxDir, "outside-report.docx"); await fs.mkdir(workspaceDir, { recursive: true }); await fs.mkdir(stateSandboxDir, { recursive: true }); await fs.writeFile(stateSandboxFile, Buffer.from("outside")); - process.env.OPENCLAW_STATE_DIR = path.join(tmpRoot, "state"); const runtime = makeRuntime({ onDeliver: async (deliver) => { await deliver({ text: `${stateSandboxFile}` }, { kind: "block" }); @@ -739,12 +742,7 @@ describe("dispatchOutbound", () => { expect(sendMediaMock).not.toHaveBeenCalled(); } finally { - if (originalStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalStateDir; - } - await fs.rm(tmpRoot, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/extensions/telegram/src/action-runtime.test.ts b/extensions/telegram/src/action-runtime.test.ts index 38f5b907e055..b89a034fe3fc 100644 --- a/extensions/telegram/src/action-runtime.test.ts +++ b/extensions/telegram/src/action-runtime.test.ts @@ -1,10 +1,10 @@ // Telegram tests cover action runtime plugin behavior. -import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; import { captureEnv } from "openclaw/plugin-sdk/test-env"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { handleTelegramAction as handleTelegramActionRuntime, @@ -201,6 +201,7 @@ const createForumTopicTelegram = vi.fn(async () => ({ chatId: "123", })); let envSnapshot: ReturnType; +let openClawState: OpenClawTestState; type TopicNameEntryForTest = { name: string; @@ -329,8 +330,12 @@ describe("handleTelegramAction", () => { expect(options.remove).toBe(false); } - beforeEach(() => { - envSnapshot = captureEnv(["OPENCLAW_STATE_DIR", "TELEGRAM_BOT_TOKEN"]); + beforeEach(async () => { + envSnapshot = captureEnv(["TELEGRAM_BOT_TOKEN"]); + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-telegram-action-", + }); resetTelegramTopicNameCacheForTest(); installTopicNameStoreForTest(); Object.assign(telegramActionRuntime, originalTelegramActionRuntime, { @@ -360,11 +365,12 @@ describe("handleTelegramAction", () => { process.env.TELEGRAM_BOT_TOKEN = "tok"; }); - afterEach(() => { + afterEach(async () => { clearTelegramRuntimeForTest(); resetTelegramTopicNameCacheForTest(); topicNameStoresForTest.clear(); envSnapshot.restore(); + await openClawState.cleanup(); }); it("adds reactions when reactionLevel is minimal", async () => { @@ -880,7 +886,7 @@ describe("handleTelegramAction", () => { }); it("persists sendMessage action deliveries before Telegram platform send", async () => { - const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-action-durable-")); + const stateDir = openClawState.stateDir; const { createOutboundTestPlugin, createTestRegistry, @@ -922,7 +928,6 @@ describe("handleTelegramAction", () => { return { channel: "telegram", messageId: "tg-ok" }; }); - process.env.OPENCLAW_STATE_DIR = stateDir; telegramActionRuntime.sendDurableMessageBatch = originalTelegramActionRuntime.sendDurableMessageBatch; setActivePluginRegistry( @@ -1009,7 +1014,6 @@ describe("handleTelegramAction", () => { }); } finally { setActivePluginRegistry(createTestRegistry([])); - fs.rmSync(stateDir, { recursive: true, force: true }); } }); diff --git a/extensions/telegram/src/telegram-ingress-supersede.test.ts b/extensions/telegram/src/telegram-ingress-supersede.test.ts index 87d1f42f2173..3c3014c57dac 100644 --- a/extensions/telegram/src/telegram-ingress-supersede.test.ts +++ b/extensions/telegram/src/telegram-ingress-supersede.test.ts @@ -1,29 +1,18 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; // Telegram supersede policy for durable ingress (authorization-gated). import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { addChannelAllowFromStoreEntry, closeOpenClawStateDatabaseForTest, } from "openclaw/plugin-sdk/plugin-state-test-runtime"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, describe, expect, it } from "vitest"; -let previousStateDir: string | undefined; -let stateDirTouched = false; +let openClawState: OpenClawTestState | undefined; -afterEach(() => { +afterEach(async () => { closeOpenClawStateDatabaseForTest(); - if (!stateDirTouched) { - return; - } - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - previousStateDir = undefined; - stateDirTouched = false; + await openClawState?.cleanup(); + openClawState = undefined; }); import type { TelegramSpooledUpdatePayload } from "./telegram-ingress-spool.payload.js"; import { @@ -353,11 +342,10 @@ describe("telegram ingress supersede policy", () => { it("authorizes paired DM senders via the pairing store under dmPolicy pairing", async () => { const pairedId = "424242"; - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-supersede-store-")); - // The shared pairing-store loader reads process.env; scoped set + afterEach restore. - previousStateDir = process.env.OPENCLAW_STATE_DIR; - stateDirTouched = true; - process.env.OPENCLAW_STATE_DIR = stateDir; + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-supersede-store-", + }); await addChannelAllowFromStoreEntry({ channel: "telegram", entry: pairedId, diff --git a/extensions/telegram/src/thread-bindings.test.ts b/extensions/telegram/src/thread-bindings.test.ts index 3ac932ba66a1..ea7540676386 100644 --- a/extensions/telegram/src/thread-bindings.test.ts +++ b/extensions/telegram/src/thread-bindings.test.ts @@ -1,7 +1,4 @@ // Telegram tests cover thread bindings plugin behavior. -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime"; import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime"; @@ -10,6 +7,7 @@ import { resetPluginStateStoreForTests, } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; +import { createOpenClawTestState, type OpenClawTestState } from "openclaw/plugin-sdk/test-state"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { setTelegramRuntime } from "./runtime.js"; import { clearTelegramRuntimeForTest } from "./runtime.test-support.js"; @@ -73,8 +71,7 @@ async function flushMicrotasks(): Promise { } describe("telegram thread bindings", () => { - const originalStateDir = process.env.OPENCLAW_STATE_DIR; - let stateDirOverride: string | undefined; + let openClawState: OpenClawTestState; let threadBindingStore: PluginStateSyncKeyedStore; function createThreadBindingStore(): PluginStateSyncKeyedStore { @@ -102,6 +99,10 @@ describe("telegram thread bindings", () => { } beforeEach(async () => { + openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-telegram-bindings-", + }); resetPluginStateStoreForTests({ closeDatabase: false }); installThreadBindingStore(createThreadBindingStore()); threadBindingStore.clear(); @@ -118,15 +119,7 @@ describe("telegram thread bindings", () => { await testing.resetTelegramThreadBindingsForTests(); clearTelegramRuntimeForTest(); resetPluginStateStoreForTests(); - if (stateDirOverride) { - fs.rmSync(stateDirOverride, { recursive: true, force: true }); - stateDirOverride = undefined; - } - if (originalStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalStateDir; - } + await openClawState.cleanup(); }); it("registers a telegram binding adapter and binds current conversations", async () => { @@ -304,8 +297,6 @@ describe("telegram thread bindings", () => { }); it("does not persist lifecycle updates when manager persistence is disabled", async () => { - stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDirOverride; vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z")); @@ -342,9 +333,6 @@ describe("telegram thread bindings", () => { }); it("persists unbinds before restart so removed bindings do not come back", async () => { - stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDirOverride; - createTelegramThreadBindingManager({ accountId: "default", persist: true, @@ -434,9 +422,6 @@ describe("telegram thread bindings", () => { }); it("cleans up stale ACP bindings before restart routing can reuse them", async () => { - stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDirOverride; - createTelegramThreadBindingManager({ accountId: "default", persist: true, @@ -476,9 +461,6 @@ describe("telegram thread bindings", () => { }); it("keeps plugin-owned bindings when ACP cleanup runs on startup", async () => { - stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDirOverride; - createTelegramThreadBindingManager({ accountId: "default", persist: true, @@ -510,9 +492,6 @@ describe("telegram thread bindings", () => { }); it("keeps ACP bindings when the session store cannot be read during startup cleanup", async () => { - stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDirOverride; - createTelegramThreadBindingManager({ accountId: "default", persist: true, @@ -552,8 +531,6 @@ describe("telegram thread bindings", () => { }); it("flushes pending lifecycle update persists before test reset", async () => { - stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDirOverride; vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-06T10:00:00.000Z")); @@ -587,8 +564,6 @@ describe("telegram thread bindings", () => { }); it("does not leak unhandled rejections when a persist write fails", async () => { - stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-")); - process.env.OPENCLAW_STATE_DIR = stateDirOverride; const unhandled: unknown[] = []; const onUnhandledRejection = (reason: unknown) => { unhandled.push(reason); diff --git a/src/commands/health.snapshot.test.ts b/src/commands/health.snapshot.test.ts index 1352cb4a9991..660067a5a7a9 100644 --- a/src/commands/health.snapshot.test.ts +++ b/src/commands/health.snapshot.test.ts @@ -7,6 +7,7 @@ import type { ChannelAccountSnapshot } from "../channels/plugins/types.public.js import type { ChannelPlugin } from "../channels/plugins/types.public.js"; import { createPluginRecord } from "../plugins/status.test-fixtures.js"; import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; +import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import type { HealthSummary } from "./health.js"; let testConfig: Record = {}; @@ -558,9 +559,10 @@ describe("getHealthSnapshot", () => { testConfig = { session: { store: "/tmp/x" } }; testStore = {}; setActivePluginRegistry(createTestRegistry([])); - const tmpStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-health-dq-")); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = tmpStateDir; + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-health-dq-", + }); try { const { moveDeliveryQueueEntryToFailed, upsertDeliveryQueueEntry } = await import("../infra/delivery-queue-sqlite.js"); @@ -592,14 +594,7 @@ describe("getHealthSnapshot", () => { ], }); } finally { - const { closeOpenClawStateDatabaseForTest } = await import("../state/openclaw-state-db.js"); - closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - fs.rmSync(tmpStateDir, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/src/flows/doctor-health-contributions.test.ts b/src/flows/doctor-health-contributions.test.ts index 659df5529264..17b8d63434ef 100644 --- a/src/flows/doctor-health-contributions.test.ts +++ b/src/flows/doctor-health-contributions.test.ts @@ -1,6 +1,5 @@ // Doctor health contribution tests cover plugin-provided health checks. import fs from "node:fs"; -import os from "node:os"; import nodePath from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { DoctorPrompter } from "../commands/doctor-prompter.js"; @@ -8,6 +7,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { LEGACY_SECRETREF_ENV_MARKER_PREFIX } from "../config/types.secrets.js"; import { migrateLegacySecretRefEnvMarkers } from "../secrets/legacy-secretref-env-marker.js"; import { readConfigMachineState } from "../state/config-machine-state.js"; +import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { CORE_HEALTH_CHECKS } from "./doctor-core-checks.js"; import { resolveDoctorContributionHealthChecks } from "./doctor-health-contributions.js"; import { @@ -1919,12 +1919,13 @@ describe("doctor health contributions", () => { }); it("keeps legacy plugin dependency lint opt-in and read-only", async () => { - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - const tempDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "openclaw-legacy-plugin-deps-lint-")); - const stateDir = nodePath.join(tempDir, "state"); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-legacy-plugin-deps-lint-", + }); + const stateDir = openClawState.stateDir; const legacyRuntimeRoot = nodePath.join(stateDir, "plugin-runtime-deps"); fs.mkdirSync(legacyRuntimeRoot, { recursive: true }); - process.env.OPENCLAW_STATE_DIR = stateDir; try { const contributionChecks = await resolveDoctorContributionHealthChecks(); const check = contributionChecks.find( @@ -1961,12 +1962,7 @@ describe("doctor health contributions", () => { }); expect(fs.existsSync(legacyRuntimeRoot)).toBe(true); } finally { - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - fs.rmSync(tempDir, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index ff1deb142fcf..5544b0ddc0eb 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -28,6 +28,7 @@ import { buildSystemRunApprovalEnvBinding, } from "../../infra/system-run-approval-binding.js"; import { resetLogger, setLoggerOverride } from "../../logging.js"; +import { createOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; import { DEFAULT_CHAT_HISTORY_TEXT_MAX_CHARS, augmentChatHistoryWithCanvasBlocks, @@ -5362,9 +5363,10 @@ describe("gateway healthHandlers.health cache freshness", () => { }); it("merges live dead-lettered delivery queue counts into cached health responses", async () => { - const tmpStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-health-cached-dq-")); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = tmpStateDir; + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-health-cached-dq-", + }); try { const { moveDeliveryQueueEntryToFailed, upsertDeliveryQueueEntry } = await import("../../infra/delivery-queue-sqlite.js"); @@ -5422,12 +5424,7 @@ describe("gateway healthHandlers.health cache freshness", () => { expect(typeof payload?.deliveryQueues?.failed?.[0]?.oldestFailedAt).toBe("number"); expect(mockCallArg(respond, 0, 3)).toEqual({ cached: true }); } finally { - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - fs.rmSync(tmpStateDir, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/src/gateway/server.sessions.create.test.ts b/src/gateway/server.sessions.create.test.ts index 3fcfdb94b063..5b8abd8d7aac 100644 --- a/src/gateway/server.sessions.create.test.ts +++ b/src/gateway/server.sessions.create.test.ts @@ -34,6 +34,7 @@ import { resolveIncognitoOpenClawAgentSqlitePath, } from "../state/openclaw-agent-db.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; +import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { resolveGatewaySessionStoreTarget } from "./session-utils.js"; import { @@ -660,12 +661,12 @@ test("sessions.create rejects draft visibility when policy disables drafts", asy }); test("sessions.create provisions and reuses a session worktree for later runs", async () => { - const root = await fs.mkdtemp( - path.join(await fs.realpath(os.tmpdir()), "openclaw-session-worktree-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-session-worktree-", + }); + const root = openClawState.root; const workspace = await initializeGitWorkspace(root); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.join(root, "state"); closeOpenClawStateDatabaseForTest(); testState.agentConfig = { workspace }; await createSessionStoreDir(); @@ -733,20 +734,17 @@ test("sessions.create provisions and reuses a session worktree for later runs", await managedWorktrees.remove({ id: worktreeId, reason: "test-cleanup", force: true }); } closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } testState.agentConfig = undefined; - await fs.rm(root, { recursive: true, force: true }); + await openClawState.cleanup(); } }); test("sessions.create honors worktree name/base ref and persists worktree info", async () => { - const root = await fs.mkdtemp( - path.join(await fs.realpath(os.tmpdir()), "openclaw-session-worktree-target-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-session-worktree-target-", + }); + const root = openClawState.root; const workspace = await initializeGitWorkspace(root); await execFileAsync("git", ["-C", workspace, "checkout", "-b", "base-branch"]); await fs.writeFile(path.join(workspace, "base.txt"), "base\n"); @@ -769,8 +767,6 @@ test("sessions.create honors worktree name/base ref and persists worktree info", "HEAD", ]); await execFileAsync("git", ["-C", workspace, "checkout", "main"]); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.join(root, "state"); closeOpenClawStateDatabaseForTest(); testState.agentConfig = { workspace }; await createSessionStoreDir(); @@ -819,13 +815,8 @@ test("sessions.create honors worktree name/base ref and persists worktree info", await managedWorktrees.remove({ id: worktreeId, reason: "test-cleanup", force: true }); } closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } testState.agentConfig = undefined; - await fs.rm(root, { recursive: true, force: true }); + await openClawState.cleanup(); } }); @@ -956,16 +947,16 @@ test("sessions.create rejects a Gateway worktree targeting a node", async () => }); test("sessions.create provisions a worktree from an admin-selected cwd", async () => { - const configuredRoot = await fs.mkdtemp( - path.join(await fs.realpath(os.tmpdir()), "openclaw-configured-workspace-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-configured-workspace-", + }); + const configuredRoot = openClawState.root; const selectedRoot = await fs.mkdtemp( path.join(await fs.realpath(os.tmpdir()), "openclaw-selected-workspace-"), ); const configuredWorkspace = await initializeGitWorkspace(configuredRoot); const selectedWorkspace = await initializeGitWorkspace(selectedRoot); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.join(configuredRoot, "state"); closeOpenClawStateDatabaseForTest(); testState.agentConfig = { workspace: configuredWorkspace }; await createSessionStoreDir(); @@ -1011,13 +1002,8 @@ test("sessions.create provisions a worktree from an admin-selected cwd", async ( await managedWorktrees.remove({ id: worktreeId, reason: "test-cleanup", force: true }); } closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } testState.agentConfig = undefined; - await fs.rm(configuredRoot, { recursive: true, force: true }); + await openClawState.cleanup(); await fs.rm(selectedRoot, { recursive: true, force: true }); } }); @@ -1085,16 +1071,16 @@ test("sessions.create allows cwd within a sandboxed agent workspace", async () = }); test("sessions.create skips the worktree setup script for non-admin callers", async () => { - const root = await fs.mkdtemp( - path.join(await fs.realpath(os.tmpdir()), "openclaw-worktree-setup-scope-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-worktree-setup-scope-", + }); + const root = openClawState.root; const workspace = await initializeGitWorkspace(root); await fs.mkdir(path.join(workspace, ".openclaw"), { recursive: true }); const setupScript = path.join(workspace, ".openclaw", "worktree-setup.sh"); await fs.writeFile(setupScript, "#!/bin/sh\ntouch setup-marker.txt\n"); await fs.chmod(setupScript, 0o755); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.join(root, "state"); closeOpenClawStateDatabaseForTest(); testState.agentConfig = { workspace }; await createSessionStoreDir(); @@ -1118,27 +1104,22 @@ test("sessions.create skips the worktree setup script for non-admin callers", as await managedWorktrees.remove({ id: worktreeId, reason: "test-cleanup", force: true }); } closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } testState.agentConfig = undefined; - await fs.rm(root, { recursive: true, force: true }); + await openClawState.cleanup(); } }); test("sessions.create preserves a linked-worktree subdirectory", async () => { - const root = await fs.mkdtemp( - path.join(await fs.realpath(os.tmpdir()), "openclaw-subdir-session-worktree-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-subdir-session-worktree-", + }); + const root = openClawState.root; const repoRoot = await initializeGitWorkspace(root); const linkedRoot = path.join(root, "linked"); await execFileAsync("git", ["-C", repoRoot, "worktree", "add", "-b", "linked", linkedRoot]); const workspace = path.join(linkedRoot, "packages", "app"); await fs.mkdir(workspace, { recursive: true }); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.join(root, "state"); closeOpenClawStateDatabaseForTest(); testState.agentConfig = { workspace }; await createSessionStoreDir(); @@ -1167,20 +1148,17 @@ test("sessions.create preserves a linked-worktree subdirectory", async () => { await managedWorktrees.remove({ id: worktreeId, reason: "test-cleanup", force: true }); } closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } testState.agentConfig = undefined; - await fs.rm(root, { recursive: true, force: true }); + await openClawState.cleanup(); } }); test("sessions.create reset-in-place persists the returned worktree cwd", async () => { - const root = await fs.mkdtemp( - path.join(await fs.realpath(os.tmpdir()), "openclaw-reset-session-worktree-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-reset-session-worktree-", + }); + const root = openClawState.root; const workspace = await initializeGitWorkspace(root); // A remote makes the base commit reachable from `--remotes`, so leaving the worktree via a // plain New Chat is lossless and the reset can remove it (the real leave-worktree flow). @@ -1188,8 +1166,6 @@ test("sessions.create reset-in-place persists the returned worktree cwd", async await execFileAsync("git", ["init", "--bare", origin]); await execFileAsync("git", ["-C", workspace, "remote", "add", "origin", origin]); await execFileAsync("git", ["-C", workspace, "push", "-u", "origin", "main"]); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.join(root, "state"); closeOpenClawStateDatabaseForTest(); testState.agentConfig = { workspace, model: { primary: "openai/current-model" } }; testState.sessionConfig = { dmScope: "main" }; @@ -1257,14 +1233,9 @@ test("sessions.create reset-in-place persists the returned worktree cwd", async await managedWorktrees.remove({ id: worktreeId, reason: "test-cleanup", force: true }); } closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } testState.agentConfig = undefined; testState.sessionConfig = undefined; - await fs.rm(root, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/src/gateway/server.sessions.delete-lifecycle.test.ts b/src/gateway/server.sessions.delete-lifecycle.test.ts index 9848b7f454e4..f77a4cd080c6 100644 --- a/src/gateway/server.sessions.delete-lifecycle.test.ts +++ b/src/gateway/server.sessions.delete-lifecycle.test.ts @@ -2,7 +2,6 @@ // active-run cleanup, hooks, thread bindings, and browser/MCP cleanup. import { execFile } from "node:child_process"; import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { afterEach, expect, test, vi } from "vitest"; @@ -23,6 +22,7 @@ import { runExclusiveSessionLifecycleMutation, } from "../sessions/session-lifecycle-admission.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; +import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { embeddedRunMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js"; import { setupGatewaySessionsTestHarness, @@ -119,12 +119,12 @@ function expectThreadBindingsUnbound(targetSessionKey: string) { } test("sessions.delete snapshots and removes session worktrees", async () => { - const root = await fs.mkdtemp( - path.join(await fs.realpath(os.tmpdir()), "openclaw-delete-worktree-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-delete-worktree-", + }); + const root = openClawState.root; const workspace = await initializeRemoteBackedGitWorkspace(root); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = path.join(root, "state"); closeOpenClawStateDatabaseForTest(); testState.agentConfig = { workspace }; await createSessionStoreDir(); @@ -194,13 +194,8 @@ test("sessions.delete snapshots and removes session worktrees", async () => { await managedWorktrees.remove({ id: dirtyWorktreeId, reason: "test-cleanup", force: true }); } closeOpenClawStateDatabaseForTest(); - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } testState.agentConfig = undefined; - await fs.rm(root, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 4c9c72c9ddba..68d51fab69ed 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -1,7 +1,6 @@ // Covers outbound delivery core: hooks, queue cleanup, durable capability // checks, adapter sends, transcript mirroring, and payload outcomes. import fsPromises from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -32,6 +31,7 @@ import { createTestRegistry, } from "../../test-utils/channel-plugins.js"; import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js"; +import { createOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; import { onInternalDiagnosticEvent, resetDiagnosticEventsForTest, @@ -4262,10 +4262,10 @@ describe("deliverOutboundPayloads", () => { const sourceDir = await fsPromises.realpath( await fsPromises.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "deliver-spool-")), ); - const stateDir = await fsPromises.realpath( - await fsPromises.mkdtemp(path.join(os.tmpdir(), "openclaw-deliver-spool-state-")), - ); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-deliver-spool-state-", + }); // Real MPEG-1 Layer III frames: host-local media sends are buffer-verified, // so placeholder text would be rejected before staging is even exercised. const source = path.join(sourceDir, "voice.mp3"); @@ -4281,7 +4281,6 @@ describe("deliverOutboundPayloads", () => { const payload = { mediaUrl: source, audioAsVoice: true }; try { - process.env.OPENCLAW_STATE_DIR = stateDir; await deliverOutboundPayloads({ cfg: matrixChunkConfig, channel: "matrix", @@ -4303,13 +4302,8 @@ describe("deliverOutboundPayloads", () => { expect(payload.mediaUrl).toBe(source); expect(sendMatrix.mock.calls[0]?.[0]?.mediaUrl ?? source).toBe(source); } finally { - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } await fsPromises.rm(sourceDir, { recursive: true, force: true }); - await fsPromises.rm(stateDir, { recursive: true, force: true }); + await openClawState.cleanup(); } }); @@ -4330,10 +4324,11 @@ describe("deliverOutboundPayloads", () => { // Deliberately outside the OpenClaw temp root: that root is itself a default // media root, so a state dir inside it would admit the source by containment // and hide whether the agent-scoped capability is what grants access. - const stateDir = await fsPromises.realpath( - await fsPromises.mkdtemp(path.join(os.tmpdir(), "openclaw-deliver-ws-")), - ); - const previousStateDir = process.env.OPENCLAW_STATE_DIR; + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-deliver-ws-", + }); + const stateDir = openClawState.stateDir; const workspaceDir = path.join(stateDir, "workspace-proofagent"); // Host-local sends are buffer-verified, so the fixture needs real audio. const source = path.join(workspaceDir, "voice.mp3"); @@ -4348,7 +4343,6 @@ describe("deliverOutboundPayloads", () => { const payload = { mediaUrl: source, audioAsVoice: true }; try { - process.env.OPENCLAW_STATE_DIR = stateDir; await fsPromises.mkdir(workspaceDir, { recursive: true }); await fsPromises.writeFile(source, mp3); await deliverOutboundPayloads({ @@ -4375,12 +4369,7 @@ describe("deliverOutboundPayloads", () => { expect(payload.mediaUrl).toBe(source); expect(sendMatrix).toHaveBeenCalled(); } finally { - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - await fsPromises.rm(stateDir, { recursive: true, force: true }); + await openClawState.cleanup(); } }); diff --git a/src/infra/outbound/message-action-params.test.ts b/src/infra/outbound/message-action-params.test.ts index dd351b91feb2..d1395540ae90 100644 --- a/src/infra/outbound/message-action-params.test.ts +++ b/src/infra/outbound/message-action-params.test.ts @@ -6,6 +6,7 @@ import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { MEDIA_MAX_BYTES } from "../../media/store.js"; +import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; const { resolveChannelMessageToolMediaSourceParamKeysMock } = vi.hoisted(() => ({ resolveChannelMessageToolMediaSourceParamKeysMock: vi.fn(() => ["avatarPath", "avatarUrl"]), @@ -29,19 +30,10 @@ const maybeIt = process.platform === "win32" ? it.skip : it; const matrixMediaSourceParamKeys = ["avatarPath", "avatarUrl"] as const; async function withTempOpenClawStateDir(test: (stateDir: string) => Promise): Promise { - const previous = process.env.OPENCLAW_STATE_DIR; - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-params-state-")); - process.env.OPENCLAW_STATE_DIR = stateDir; - try { - return await test(stateDir); - } finally { - if (previous === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previous; - } - await fs.rm(stateDir, { recursive: true, force: true }); - } + return await withOpenClawTestState( + { layout: "state-only", prefix: "msg-params-state-" }, + (state) => test(state.stateDir), + ); } describe("message action media helpers", () => { diff --git a/src/infra/outbound/message-action-runner.media.test.ts b/src/infra/outbound/message-action-runner.media.test.ts index 9cc62f7f519b..c828b5d2ead4 100644 --- a/src/infra/outbound/message-action-runner.media.test.ts +++ b/src/infra/outbound/message-action-runner.media.test.ts @@ -15,6 +15,7 @@ import { createChannelTestPluginBase, createTestRegistry, } from "../../test-utils/channel-plugins.js"; +import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.js"; import { runMessageAction } from "./message-action-runner.js"; @@ -91,19 +92,10 @@ async function withSandbox(test: (sandboxDir: string) => Promise) { } async function withTempOpenClawStateDir(test: (stateDir: string) => Promise): Promise { - const previous = process.env.OPENCLAW_STATE_DIR; - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-runner-state-")); - process.env.OPENCLAW_STATE_DIR = stateDir; - try { - return await test(stateDir); - } finally { - if (previous === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previous; - } - await fs.rm(stateDir, { recursive: true, force: true }); - } + return await withOpenClawTestState( + { layout: "state-only", prefix: "msg-runner-state-" }, + (state) => test(state.stateDir), + ); } const runDrySend = (params: { diff --git a/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts b/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts index 3639e57e5a82..cdca0572045d 100644 --- a/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts +++ b/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts @@ -1,9 +1,9 @@ // Stuck session recovery runtime tests cover recovery inspection and event output. import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { saveCronStore } from "../cron/store.js"; +import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js"; const mocks = vi.hoisted(() => ({ abortEmbeddedAgentRun: vi.fn(), @@ -289,10 +289,12 @@ describe("stuck session recovery", () => { }); it("logs stopped cron context when aborting an active embedded run", async () => { - const previousStateDir = process.env.OPENCLAW_STATE_DIR; - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-recovery-context-")); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-recovery-context-", + }); + const tempDir = openClawState.stateDir; try { - process.env.OPENCLAW_STATE_DIR = tempDir; await saveCronStore(path.join(tempDir, "cron", "jobs.json"), { version: 1, jobs: [ @@ -330,12 +332,7 @@ describe("stuck session recovery", () => { allowActiveAbort: true, }); } finally { - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - fs.rmSync(tempDir, { recursive: true, force: true }); + await openClawState.cleanup(); } expect(warnLogMessages()).toEqual([ diff --git a/src/plugin-sdk/memory-host-event-export.test.ts b/src/plugin-sdk/memory-host-event-export.test.ts index 26914323f4e9..e99b9c372fc6 100644 --- a/src/plugin-sdk/memory-host-event-export.test.ts +++ b/src/plugin-sdk/memory-host-event-export.test.ts @@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { root as createFsSafeRoot } from "../infra/fs-safe.js"; import { resetPluginStateStoreForTests } from "../plugin-state/plugin-state-store.js"; import { clearMemoryPluginState } from "../plugins/memory-state.test-fixtures.js"; +import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { listMemoryHostPublicArtifacts } from "./memory-host-core.js"; import { memoryHostEventExportOwnerContent, @@ -103,9 +104,11 @@ describe("memory host event export recovery", () => { }); it("finishes an inode-owned empty event export after interruption", async () => { - const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-host-inode-owner-")); - const stateDir = path.join(fixtureRoot, "state"); - const workspaceDir = path.join(fixtureRoot, "workspace"); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "memory-host-inode-owner-", + }); + const { stateDir, workspaceDir } = openClawState; const event = { type: "memory.recall.recorded" as const, timestamp: "2026-05-18T12:00:00.000Z", @@ -114,8 +117,6 @@ describe("memory host event export recovery", () => { results: [], }; try { - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); - await fs.mkdir(workspaceDir); await appendMemoryHostEvent(workspaceDir, event); const stateHash = createHash("sha256") .update(await fs.realpath(stateDir)) @@ -165,14 +166,16 @@ describe("memory host event export recovery", () => { }); expect(owner.pendingContentSha256).toBeUndefined(); } finally { - await fs.rm(fixtureRoot, { recursive: true, force: true }); + await openClawState.cleanup(); } }); it("does not claim an empty export after exclusive-create interruption", async () => { - const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-host-empty-export-")); - const stateDir = path.join(fixtureRoot, "state"); - const workspaceDir = path.join(fixtureRoot, "workspace"); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "memory-host-empty-export-", + }); + const { stateDir, workspaceDir } = openClawState; const event = { type: "memory.recall.recorded" as const, timestamp: "2026-05-18T12:00:00.000Z", @@ -181,8 +184,6 @@ describe("memory host event export recovery", () => { results: [], }; try { - vi.stubEnv("OPENCLAW_STATE_DIR", stateDir); - await fs.mkdir(workspaceDir); await appendMemoryHostEvent(workspaceDir, event); const stateHash = createHash("sha256") .update(await fs.realpath(stateDir)) @@ -217,7 +218,7 @@ describe("memory host event export recovery", () => { expect(listed.some((artifact) => artifact.kind === "event-log")).toBe(false); await expect(fs.readFile(exportPath, "utf8")).resolves.toBe(""); } finally { - await fs.rm(fixtureRoot, { recursive: true, force: true }); + await openClawState.cleanup(); } }); }); diff --git a/src/plugins/contracts/run-context-lifecycle.contract.test.ts b/src/plugins/contracts/run-context-lifecycle.contract.test.ts index 1dbd8c29fece..bd9bd148f930 100644 --- a/src/plugins/contracts/run-context-lifecycle.contract.test.ts +++ b/src/plugins/contracts/run-context-lifecycle.contract.test.ts @@ -1,5 +1,4 @@ // Run context lifecycle contract tests cover plugin run context setup and cleanup. -import fs from "node:fs/promises"; import path from "node:path"; import { createPluginRegistryFixture, @@ -8,8 +7,8 @@ import { import { afterEach, describe, expect, it, vi } from "vitest"; import { withTempConfig } from "../../gateway/test-temp-config.js"; import { emitAgentEvent, resetAgentEventsForTest } from "../../infra/agent-events.js"; -import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js"; import { loadSessionStore, updateSessionStore } from "../../plugin-sdk/session-store-runtime.js"; +import { createOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; import { runPluginHostCleanup } from "../host-hook-cleanup.js"; import { clearPluginHostRuntimeState, @@ -686,16 +685,16 @@ describe("plugin run context lifecycle", () => { }, }); - const stateDir = await fs.mkdtemp( - path.join(resolvePreferredOpenClawTmpDir(), "openclaw-run-context-restart-state-"), - ); + const openClawState = await createOpenClawTestState({ + layout: "state-only", + prefix: "openclaw-run-context-restart-state-", + }); + const stateDir = openClawState.stateDir; const storePath = path.join(stateDir, "sessions.json"); const tempConfig = { session: { store: storePath }, }; - const previousStateDir = process.env.OPENCLAW_STATE_DIR; try { - process.env.OPENCLAW_STATE_DIR = stateDir; await withTempConfig({ cfg: tempConfig, run: async () => { @@ -748,12 +747,7 @@ describe("plugin run context lifecycle", () => { }, }); } finally { - if (previousStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = previousStateDir; - } - await fs.rm(stateDir, { recursive: true, force: true }); + await openClawState.cleanup(); } });