diff --git a/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts b/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts index ef31491da0..da2bc0f6d2 100644 --- a/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts +++ b/src/web/auto-reply.broadcast-groups.broadcasts-sequentially-configured-order.test.ts @@ -1,98 +1,19 @@ import "./test-helpers.js"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, -})); - +import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { monitorWebChannel } from "./auto-reply.js"; -import { resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; +import { + installWebAutoReplyTestHomeHooks, + installWebAutoReplyUnitTestHooks, + resetLoadConfigMock, + setLoadConfigMock, +} from "./auto-reply.test-harness.js"; -let previousHome: string | undefined; -let tempHome: string | undefined; - -const rmDirWithRetries = async (dir: string): Promise => { - // Some tests can leave async session-store writes in-flight; recursive deletion can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); -}; - -beforeEach(async () => { - resetInboundDedupe(); - previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); - process.env.HOME = tempHome; -}); - -afterEach(async () => { - process.env.HOME = previousHome; - if (tempHome) { - await rmDirWithRetries(tempHome); - tempHome = undefined; - } -}); - -const _makeSessionStore = async ( - entries: Record = {}, -): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); - const storePath = path.join(dir, "sessions.json"); - await fs.writeFile(storePath, JSON.stringify(entries)); - const cleanup = async () => { - // Session store writes can be in-flight when the test finishes (e.g. updateLastRoute - // after a message flush). `fs.rm({ recursive })` can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); - }; - return { - storePath, - cleanup, - }; -}; +installWebAutoReplyTestHomeHooks(); describe("broadcast groups", () => { + installWebAutoReplyUnitTestHooks(); + it("broadcasts sequentially in configured order", async () => { setLoadConfigMock({ channels: { whatsapp: { allowFrom: ["*"] } }, diff --git a/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts b/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts index 75e77272d8..5abe523cf5 100644 --- a/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts +++ b/src/web/auto-reply.broadcast-groups.skips-unknown-broadcast-agent-ids-agents-list.test.ts @@ -1,98 +1,19 @@ import "./test-helpers.js"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, -})); - +import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { monitorWebChannel } from "./auto-reply.js"; -import { resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; +import { + installWebAutoReplyTestHomeHooks, + installWebAutoReplyUnitTestHooks, + resetLoadConfigMock, + setLoadConfigMock, +} from "./auto-reply.test-harness.js"; -let previousHome: string | undefined; -let tempHome: string | undefined; - -const rmDirWithRetries = async (dir: string): Promise => { - // Some tests can leave async session-store writes in-flight; recursive deletion can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); -}; - -beforeEach(async () => { - resetInboundDedupe(); - previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); - process.env.HOME = tempHome; -}); - -afterEach(async () => { - process.env.HOME = previousHome; - if (tempHome) { - await rmDirWithRetries(tempHome); - tempHome = undefined; - } -}); - -const _makeSessionStore = async ( - entries: Record = {}, -): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); - const storePath = path.join(dir, "sessions.json"); - await fs.writeFile(storePath, JSON.stringify(entries)); - const cleanup = async () => { - // Session store writes can be in-flight when the test finishes (e.g. updateLastRoute - // after a message flush). `fs.rm({ recursive })` can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); - }; - return { - storePath, - cleanup, - }; -}; +installWebAutoReplyTestHomeHooks(); describe("broadcast groups", () => { + installWebAutoReplyUnitTestHooks(); + it("skips unknown broadcast agent ids when agents.list is present", async () => { setLoadConfigMock({ channels: { whatsapp: { allowFrom: ["*"] } }, diff --git a/src/web/auto-reply.typing-controller-idle.test.ts b/src/web/auto-reply.typing-controller-idle.test.ts index 52cce40c96..4180420ee3 100644 --- a/src/web/auto-reply.typing-controller-idle.test.ts +++ b/src/web/auto-reply.typing-controller-idle.test.ts @@ -1,98 +1,19 @@ import "./test-helpers.js"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, -})); - +import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; import { monitorWebChannel } from "./auto-reply.js"; -import { resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; +import { + installWebAutoReplyTestHomeHooks, + installWebAutoReplyUnitTestHooks, + resetLoadConfigMock, + setLoadConfigMock, +} from "./auto-reply.test-harness.js"; -let previousHome: string | undefined; -let tempHome: string | undefined; - -const rmDirWithRetries = async (dir: string): Promise => { - // Some tests can leave async session-store writes in-flight; recursive deletion can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); -}; - -beforeEach(async () => { - resetInboundDedupe(); - previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); - process.env.HOME = tempHome; -}); - -afterEach(async () => { - process.env.HOME = previousHome; - if (tempHome) { - await rmDirWithRetries(tempHome); - tempHome = undefined; - } -}); - -const _makeSessionStore = async ( - entries: Record = {}, -): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); - const storePath = path.join(dir, "sessions.json"); - await fs.writeFile(storePath, JSON.stringify(entries)); - const cleanup = async () => { - // Session store writes can be in-flight when the test finishes (e.g. updateLastRoute - // after a message flush). `fs.rm({ recursive })` can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); - }; - return { - storePath, - cleanup, - }; -}; +installWebAutoReplyTestHomeHooks(); describe("typing controller idle", () => { + installWebAutoReplyUnitTestHooks(); + it("marks dispatch idle after replies flush", async () => { const markDispatchIdle = vi.fn(); const typingMock = { diff --git a/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts b/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts index 5fbb76fe60..b93cda1494 100644 --- a/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts +++ b/src/web/auto-reply.web-auto-reply.prefixes-body-same-phone-marker-from.test.ts @@ -1,109 +1,17 @@ import "./test-helpers.js"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, -})); - -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import { resetLogger, setLoggerOverride } from "../logging.js"; +import { describe, expect, it, vi } from "vitest"; import { HEARTBEAT_TOKEN, monitorWebChannel } from "./auto-reply.js"; -import { resetBaileysMocks, resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; +import { + installWebAutoReplyTestHomeHooks, + installWebAutoReplyUnitTestHooks, + resetLoadConfigMock, + setLoadConfigMock, +} from "./auto-reply.test-harness.js"; -let previousHome: string | undefined; -let tempHome: string | undefined; - -const rmDirWithRetries = async (dir: string): Promise => { - // Some tests can leave async session-store writes in-flight; recursive deletion can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); -}; - -beforeEach(async () => { - resetInboundDedupe(); - previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); - process.env.HOME = tempHome; -}); - -afterEach(async () => { - process.env.HOME = previousHome; - if (tempHome) { - await rmDirWithRetries(tempHome); - tempHome = undefined; - } -}); - -const _makeSessionStore = async ( - entries: Record = {}, -): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); - const storePath = path.join(dir, "sessions.json"); - await fs.writeFile(storePath, JSON.stringify(entries)); - const cleanup = async () => { - // Session store writes can be in-flight when the test finishes (e.g. updateLastRoute - // after a message flush). `fs.rm({ recursive })` can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); - }; - return { - storePath, - cleanup, - }; -}; +installWebAutoReplyTestHomeHooks(); describe("web auto-reply", () => { - beforeEach(() => { - vi.clearAllMocks(); - resetBaileysMocks(); - resetLoadConfigMock(); - }); - - afterEach(() => { - resetLogger(); - setLoggerOverride(null); - vi.useRealTimers(); - }); + installWebAutoReplyUnitTestHooks(); it("prefixes body with same-phone marker when from === to", async () => { // Enable messagePrefix for same-phone mode testing diff --git a/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts b/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts index b99c4f6ebb..062bc81860 100644 --- a/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts +++ b/src/web/auto-reply.web-auto-reply.sends-tool-summaries-immediately-responseprefix.test.ts @@ -1,109 +1,17 @@ +import { describe, expect, it, vi } from "vitest"; import "./test-helpers.js"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, -})); - -import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; -import { resetLogger, setLoggerOverride } from "../logging.js"; import { monitorWebChannel } from "./auto-reply.js"; -import { resetBaileysMocks, resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js"; +import { + installWebAutoReplyTestHomeHooks, + installWebAutoReplyUnitTestHooks, + resetLoadConfigMock, + setLoadConfigMock, +} from "./auto-reply.test-harness.js"; -let previousHome: string | undefined; -let tempHome: string | undefined; - -const rmDirWithRetries = async (dir: string): Promise => { - // Some tests can leave async session-store writes in-flight; recursive deletion can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); -}; - -beforeEach(async () => { - resetInboundDedupe(); - previousHome = process.env.HOME; - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-home-")); - process.env.HOME = tempHome; -}); - -afterEach(async () => { - process.env.HOME = previousHome; - if (tempHome) { - await rmDirWithRetries(tempHome); - tempHome = undefined; - } -}); - -const _makeSessionStore = async ( - entries: Record = {}, -): Promise<{ storePath: string; cleanup: () => Promise }> => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-")); - const storePath = path.join(dir, "sessions.json"); - await fs.writeFile(storePath, JSON.stringify(entries)); - const cleanup = async () => { - // Session store writes can be in-flight when the test finishes (e.g. updateLastRoute - // after a message flush). `fs.rm({ recursive })` can race and throw ENOTEMPTY. - for (let attempt = 0; attempt < 10; attempt += 1) { - try { - await fs.rm(dir, { recursive: true, force: true }); - return; - } catch (err) { - const code = - err && typeof err === "object" && "code" in err - ? String((err as { code?: unknown }).code) - : null; - if (code === "ENOTEMPTY" || code === "EBUSY" || code === "EPERM") { - await new Promise((resolve) => setTimeout(resolve, 5)); - continue; - } - throw err; - } - } - - await fs.rm(dir, { recursive: true, force: true }); - }; - return { - storePath, - cleanup, - }; -}; +installWebAutoReplyTestHomeHooks(); describe("web auto-reply", () => { - beforeEach(() => { - vi.clearAllMocks(); - resetBaileysMocks(); - resetLoadConfigMock(); - }); - - afterEach(() => { - resetLogger(); - setLoggerOverride(null); - vi.useRealTimers(); - }); + installWebAutoReplyUnitTestHooks(); it("skips tool summaries and sends final reply with responsePrefix", async () => { setLoadConfigMock(() => ({ diff --git a/src/web/test-helpers.ts b/src/web/test-helpers.ts index c3d2312777..ae87104d15 100644 --- a/src/web/test-helpers.ts +++ b/src/web/test-helpers.ts @@ -44,6 +44,26 @@ vi.mock("../config/config.js", async (importOriginal) => { }; }); +// Some web modules live under `src/web/auto-reply/*` and import config via a different +// relative path (`../../config/config.js`). Mock both specifiers so tests stay stable +// across refactors that move files between folders. +vi.mock("../../config/config.js", async (importOriginal) => { + // `../../config/config.js` is correct for modules under `src/web/auto-reply/*`. + // For typing in this file (which lives in `src/web/*`), refer to the same module + // via the local relative path. + const actual = await importOriginal(); + return { + ...actual, + loadConfig: () => { + const getter = (globalThis as Record)[CONFIG_KEY]; + if (typeof getter === "function") { + return getter(); + } + return DEFAULT_CONFIG; + }, + }; +}); + vi.mock("../media/store.js", () => ({ saveMediaBuffer: vi.fn().mockImplementation(async (_buf: Buffer, contentType?: string) => ({ id: "mid",