From dd11a6bcdaa4673e0bc292b81858f71de1601cbd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 14:38:43 +0000 Subject: [PATCH] refactor(test): share sessions_spawn e2e harness --- ...gents.sessions-spawn.allowlist.e2e.test.ts | 62 +++--------------- ...gents.sessions-spawn.lifecycle.e2e.test.ts | 55 ++-------------- ...subagents.sessions-spawn.model.e2e.test.ts | 64 +++---------------- ...s.subagents.sessions-spawn.test-harness.ts | 58 +++++++++++++++++ 4 files changed, 84 insertions(+), 155 deletions(-) create mode 100644 src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.e2e.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.e2e.test.ts index 65ca30175..b1c697064 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.e2e.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.e2e.test.ts @@ -1,62 +1,18 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { createOpenClawTools } from "./openclaw-tools.js"; import "./test-helpers/fast-core-tools.js"; +import { + getCallGatewayMock, + resetSessionsSpawnConfigOverride, + setSessionsSpawnConfigOverride, +} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; import { resetSubagentRegistryForTests } from "./subagent-registry.js"; -type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>; - -const hoisted = vi.hoisted(() => { - const callGatewayMock = vi.fn(); - const defaultConfigOverride = { - session: { - mainKey: "main", - scope: "per-sender", - }, - } as SessionsSpawnTestConfig; - const state = { configOverride: defaultConfigOverride }; - return { callGatewayMock, defaultConfigOverride, state }; -}); - -const callGatewayMock = hoisted.callGatewayMock; - -function resetConfigOverride() { - hoisted.state.configOverride = hoisted.defaultConfigOverride; -} - -function setConfigOverride(next: SessionsSpawnTestConfig) { - hoisted.state.configOverride = next; -} - -vi.mock("../gateway/call.js", () => ({ - callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), -})); -// Some tools import callGateway via "../../gateway/call.js" (from nested folders). Mock that too. -vi.mock("../../gateway/call.js", () => ({ - callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => hoisted.state.configOverride, - resolveGatewayPort: () => 18789, - }; -}); - -// Same module, different specifier (used by tools under src/agents/tools/*). -vi.mock("../../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => hoisted.state.configOverride, - resolveGatewayPort: () => 18789, - }; -}); +const callGatewayMock = getCallGatewayMock(); describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => { beforeEach(() => { - resetConfigOverride(); + resetSessionsSpawnConfigOverride(); }); it("sessions_spawn only allows same-agent by default", async () => { @@ -84,7 +40,7 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => { it("sessions_spawn forbids cross-agent spawning when not allowed", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); - setConfigOverride({ + setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender", diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.e2e.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.e2e.test.ts index 946814973..002683386 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.e2e.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.e2e.test.ts @@ -1,60 +1,19 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { emitAgentEvent } from "../infra/agent-events.js"; import { sleep } from "../utils.js"; import { createOpenClawTools } from "./openclaw-tools.js"; import "./test-helpers/fast-core-tools.js"; +import { + getCallGatewayMock, + resetSessionsSpawnConfigOverride, +} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; import { resetSubagentRegistryForTests } from "./subagent-registry.js"; -type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>; - -const hoisted = vi.hoisted(() => { - const callGatewayMock = vi.fn(); - const defaultConfigOverride = { - session: { - mainKey: "main", - scope: "per-sender", - }, - } as SessionsSpawnTestConfig; - const state = { configOverride: defaultConfigOverride }; - return { callGatewayMock, defaultConfigOverride, state }; -}); - -const callGatewayMock = hoisted.callGatewayMock; - -function resetConfigOverride() { - hoisted.state.configOverride = hoisted.defaultConfigOverride; -} - -vi.mock("../gateway/call.js", () => ({ - callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), -})); -// Some tools import callGateway via "../../gateway/call.js" (from nested folders). Mock that too. -vi.mock("../../gateway/call.js", () => ({ - callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => hoisted.state.configOverride, - resolveGatewayPort: () => 18789, - }; -}); - -// Same module, different specifier (used by tools under src/agents/tools/*). -vi.mock("../../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => hoisted.state.configOverride, - resolveGatewayPort: () => 18789, - }; -}); +const callGatewayMock = getCallGatewayMock(); describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { beforeEach(() => { - resetConfigOverride(); + resetSessionsSpawnConfigOverride(); }); it("sessions_spawn runs cleanup flow after subagent completion", async () => { diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.model.e2e.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.model.e2e.test.ts index 4955a9bcc..7d3cd00d6 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.model.e2e.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.model.e2e.test.ts @@ -1,63 +1,19 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; import "./test-helpers/fast-core-tools.js"; import { createOpenClawTools } from "./openclaw-tools.js"; +import { + getCallGatewayMock, + resetSessionsSpawnConfigOverride, + setSessionsSpawnConfigOverride, +} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; import { resetSubagentRegistryForTests } from "./subagent-registry.js"; -type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>; - -const hoisted = vi.hoisted(() => { - const callGatewayMock = vi.fn(); - const defaultConfigOverride = { - session: { - mainKey: "main", - scope: "per-sender", - }, - } as SessionsSpawnTestConfig; - const state = { configOverride: defaultConfigOverride }; - return { callGatewayMock, defaultConfigOverride, state }; -}); - -const callGatewayMock = hoisted.callGatewayMock; - -function resetConfigOverride() { - hoisted.state.configOverride = hoisted.defaultConfigOverride; -} - -function setConfigOverride(next: SessionsSpawnTestConfig) { - hoisted.state.configOverride = next; -} - -vi.mock("../gateway/call.js", () => ({ - callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), -})); -// Some tools import callGateway via "../../gateway/call.js" (from nested folders). Mock that too. -vi.mock("../../gateway/call.js", () => ({ - callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), -})); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => hoisted.state.configOverride, - resolveGatewayPort: () => 18789, - }; -}); - -// Same module, different specifier (used by tools under src/agents/tools/*). -vi.mock("../../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => hoisted.state.configOverride, - resolveGatewayPort: () => 18789, - }; -}); +const callGatewayMock = getCallGatewayMock(); describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => { beforeEach(() => { - resetConfigOverride(); + resetSessionsSpawnConfigOverride(); }); it("sessions_spawn applies a model to the child session", async () => { @@ -192,7 +148,7 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => { it("sessions_spawn applies default subagent model from defaults config", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); - setConfigOverride({ + setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender" }, agents: { defaults: { subagents: { model: "minimax/MiniMax-M2.1" } } }, }); @@ -278,7 +234,7 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => { it("sessions_spawn prefers per-agent subagent model over defaults", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); - setConfigOverride({ + setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender" }, agents: { defaults: { subagents: { model: "minimax/MiniMax-M2.1" } }, diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts new file mode 100644 index 000000000..8aec6bb87 --- /dev/null +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts @@ -0,0 +1,58 @@ +import { vi } from "vitest"; + +type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>; + +// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit). +// oxlint-disable-next-line typescript/no-explicit-any +type AnyMock = any; + +const hoisted = vi.hoisted(() => { + const callGatewayMock = vi.fn(); + const defaultConfigOverride = { + session: { + mainKey: "main", + scope: "per-sender", + }, + } as SessionsSpawnTestConfig; + const state = { configOverride: defaultConfigOverride }; + return { callGatewayMock, defaultConfigOverride, state }; +}); + +export function getCallGatewayMock(): AnyMock { + return hoisted.callGatewayMock; +} + +export function resetSessionsSpawnConfigOverride(): void { + hoisted.state.configOverride = hoisted.defaultConfigOverride; +} + +export function setSessionsSpawnConfigOverride(next: SessionsSpawnTestConfig): void { + hoisted.state.configOverride = next; +} + +vi.mock("../gateway/call.js", () => ({ + callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), +})); +// Some tools import callGateway via "../../gateway/call.js" (from nested folders). Mock that too. +vi.mock("../../gateway/call.js", () => ({ + callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), +})); + +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: () => hoisted.state.configOverride, + resolveGatewayPort: () => 18789, + }; +}); + +// Same module, different specifier (used by tools under src/agents/tools/*). +vi.mock("../../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: () => hoisted.state.configOverride, + resolveGatewayPort: () => 18789, + }; +});