refactor(test): share sessions_spawn e2e harness

This commit is contained in:
Peter Steinberger
2026-02-15 14:38:43 +00:00
parent 893d2fb862
commit dd11a6bcda
4 changed files with 84 additions and 155 deletions

View File

@@ -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<typeof import("../config/config.js")>();
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<typeof import("../config/config.js")>();
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",

View File

@@ -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<typeof import("../config/config.js")>();
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<typeof import("../config/config.js")>();
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 () => {

View File

@@ -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<typeof import("../config/config.js")>();
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<typeof import("../config/config.js")>();
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" } },

View File

@@ -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<typeof import("../config/config.js")>();
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<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => hoisted.state.configOverride,
resolveGatewayPort: () => 18789,
};
});