From d93686358daf2c7a4e57638f7e081467d116f67f Mon Sep 17 00:00:00 2001 From: Dimitri <35241486+dkdimou@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:50:48 +0100 Subject: [PATCH 1/3] tests: stabilize sessions_spawn mock/import ordering --- ...ons-spawn-applies-thinking-default.test.ts | 10 ++- ...sions-spawn-default-timeout-absent.test.ts | 10 ++- ...nts.sessions-spawn-default-timeout.test.ts | 10 ++- ...agents.sessions-spawn-depth-limits.test.ts | 42 +++++++----- ...subagents.sessions-spawn.allowlist.test.ts | 10 ++- ...subagents.sessions-spawn.cron-note.test.ts | 25 +++++-- ...subagents.sessions-spawn.lifecycle.test.ts | 10 ++- ...ols.subagents.sessions-spawn.model.test.ts | 19 ++++-- src/agents/subagent-spawn.attachments.test.ts | 67 ++++++++++++------- 9 files changed, 136 insertions(+), 67 deletions(-) diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.test.ts index 6dae2be0942..0dbe85bf18e 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn-applies-thinking-default.test.ts @@ -1,10 +1,14 @@ import { beforeEach, describe, expect, it } from "vitest"; import "./test-helpers/fast-core-tools.js"; import * as harness from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; const MAIN_SESSION_KEY = "agent:test:main"; +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} + type ThinkingLevel = "high" | "medium" | "low"; function applyThinkingDefault(thinking: ThinkingLevel) { @@ -60,9 +64,9 @@ async function expectThinkingPropagation(input: { } describe("sessions_spawn thinking defaults", () => { - beforeEach(() => { + beforeEach(async () => { harness.resetSessionsSpawnConfigOverride(); - resetSubagentRegistryForTests(); + await resetRegistry(); harness.getCallGatewayMock().mockClear(); applyThinkingDefault("high"); }); diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout-absent.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout-absent.test.ts index bf3275987fd..4c62720bf20 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout-absent.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout-absent.test.ts @@ -7,10 +7,14 @@ import { setSessionsSpawnConfigOverride, setupSessionsSpawnGatewayMock, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; const MAIN_SESSION_KEY = "agent:test:main"; +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} + function configureDefaultsWithoutTimeout() { setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender" }, @@ -31,9 +35,9 @@ function readSpawnTimeout(calls: Array<{ method?: string; params?: unknown }>): } describe("sessions_spawn default runTimeoutSeconds (config absent)", () => { - beforeEach(() => { + beforeEach(async () => { resetSessionsSpawnConfigOverride(); - resetSubagentRegistryForTests(); + await resetRegistry(); getCallGatewayMock().mockClear(); }); diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout.test.ts index 6066d97ba5c..297e636d158 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn-default-timeout.test.ts @@ -1,10 +1,14 @@ import { beforeEach, describe, expect, it } from "vitest"; import "./test-helpers/fast-core-tools.js"; import * as sessionsHarness from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; const MAIN_SESSION_KEY = "agent:test:main"; +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} + function applySubagentTimeoutDefault(seconds: number) { sessionsHarness.setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender" }, @@ -34,9 +38,9 @@ async function spawnSubagent(callId: string, payload: Record) { } describe("sessions_spawn default runTimeoutSeconds", () => { - beforeEach(() => { + beforeEach(async () => { sessionsHarness.resetSessionsSpawnConfigOverride(); - resetSubagentRegistryForTests(); + await resetRegistry(); sessionsHarness.getCallGatewayMock().mockClear(); }); diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts index 34fcbfbafd4..054ac75fd41 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts @@ -2,9 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { addSubagentRunForTests, resetSubagentRegistryForTests } from "./subagent-registry.js"; import { createPerSenderSessionConfig } from "./test-helpers/session-config.js"; -import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js"; const callGatewayMock = vi.fn(); @@ -25,6 +23,21 @@ vi.mock("../config/config.js", async (importOriginal) => { }; }); +async function createTool(opts: { agentSessionKey?: string }) { + const { createSessionsSpawnTool } = await import("./tools/sessions-spawn-tool.js"); + return createSessionsSpawnTool(opts); +} + +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} + +async function addRegistryRun(run: Parameters<(typeof import("./subagent-registry.js"))["addSubagentRunForTests"]>[0]) { + const { addSubagentRunForTests } = await import("./subagent-registry.js"); + addSubagentRunForTests(run); +} + function writeStore(agentId: string, store: Record) { const storePath = storeTemplatePath.replaceAll("{agentId}", agentId); fs.mkdirSync(path.dirname(storePath), { recursive: true }); @@ -61,8 +74,8 @@ function seedDepthTwoAncestryStore(params?: { sessionIds?: boolean }) { } describe("sessions_spawn depth + child limits", () => { - beforeEach(() => { - resetSubagentRegistryForTests(); + beforeEach(async () => { + await resetRegistry(); callGatewayMock.mockClear(); storeTemplatePath = path.join( os.tmpdir(), @@ -85,10 +98,7 @@ describe("sessions_spawn depth + child limits", () => { }); it("rejects spawning when caller depth reaches maxSpawnDepth", async () => { - const tool = createSessionsSpawnTool({ - agentSessionKey: "agent:main:subagent:parent", - workspaceDir: "/parent/workspace", - }); + const tool = await createTool({ agentSessionKey: "agent:main:subagent:parent" }); const result = await tool.execute("call-depth-reject", { task: "hello" }); expect(result.details).toMatchObject({ @@ -100,7 +110,7 @@ describe("sessions_spawn depth + child limits", () => { it("allows depth-1 callers when maxSpawnDepth is 2", async () => { setSubagentLimits({ maxSpawnDepth: 2 }); - const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:subagent:parent" }); + const tool = await createTool({ agentSessionKey: "agent:main:subagent:parent" }); const result = await tool.execute("call-depth-allow", { task: "hello" }); expect(result.details).toMatchObject({ @@ -140,7 +150,7 @@ describe("sessions_spawn depth + child limits", () => { }, }); - const tool = createSessionsSpawnTool({ agentSessionKey: callerKey }); + const tool = await createTool({ agentSessionKey: callerKey }); const result = await tool.execute("call-depth-2-reject", { task: "hello" }); expect(result.details).toMatchObject({ @@ -153,7 +163,7 @@ describe("sessions_spawn depth + child limits", () => { setSubagentLimits({ maxSpawnDepth: 2 }); const { callerKey } = seedDepthTwoAncestryStore(); - const tool = createSessionsSpawnTool({ agentSessionKey: callerKey }); + const tool = await createTool({ agentSessionKey: callerKey }); const result = await tool.execute("call-depth-ancestry-reject", { task: "hello" }); expect(result.details).toMatchObject({ @@ -166,7 +176,7 @@ describe("sessions_spawn depth + child limits", () => { setSubagentLimits({ maxSpawnDepth: 2 }); seedDepthTwoAncestryStore({ sessionIds: true }); - const tool = createSessionsSpawnTool({ agentSessionKey: "depth-2-session" }); + const tool = await createTool({ agentSessionKey: "depth-2-session" }); const result = await tool.execute("call-depth-sessionid-reject", { task: "hello" }); expect(result.details).toMatchObject({ @@ -188,7 +198,7 @@ describe("sessions_spawn depth + child limits", () => { }, }; - addSubagentRunForTests({ + await addRegistryRun({ runId: "existing-run", childSessionKey: "agent:main:subagent:existing", requesterSessionKey: "agent:main:subagent:parent", @@ -199,7 +209,7 @@ describe("sessions_spawn depth + child limits", () => { startedAt: Date.now(), }); - const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:subagent:parent" }); + const tool = await createTool({ agentSessionKey: "agent:main:subagent:parent" }); const result = await tool.execute("call-max-children", { task: "hello" }); expect(result.details).toMatchObject({ @@ -222,7 +232,7 @@ describe("sessions_spawn depth + child limits", () => { }, }; - const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:subagent:parent" }); + const tool = await createTool({ agentSessionKey: "agent:main:subagent:parent" }); const result = await tool.execute("call-max-concurrent-independent", { task: "hello" }); expect(result.details).toMatchObject({ @@ -247,7 +257,7 @@ describe("sessions_spawn depth + child limits", () => { return {}; }); - const tool = createSessionsSpawnTool({ agentSessionKey: "main" }); + const tool = await createTool({ agentSessionKey: "main" }); const result = await tool.execute("call-model-reject", { task: "hello", model: "bad-model", diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts index d539921653d..c98e5e97448 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts @@ -6,10 +6,14 @@ import { resetSessionsSpawnConfigOverride, setSessionsSpawnConfigOverride, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; const callGatewayMock = getCallGatewayMock(); +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} + describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => { function setAllowAgents(allowAgents: string[]) { setSessionsSpawnConfigOverride({ @@ -125,9 +129,9 @@ describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => { expect(callGatewayMock).not.toHaveBeenCalled(); } - beforeEach(() => { + beforeEach(async () => { resetSessionsSpawnConfigOverride(); - resetSubagentRegistryForTests(); + await resetRegistry(); callGatewayMock.mockClear(); }); diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts index 6e25419cf04..6adb48a57f8 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.cron-note.test.ts @@ -6,17 +6,25 @@ import { resetSessionsSpawnConfigOverride, setupSessionsSpawnGatewayMock, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; -import { SUBAGENT_SPAWN_ACCEPTED_NOTE } from "./subagent-spawn.js"; const callGatewayMock = getCallGatewayMock(); +async function getAcceptedNote() { + const { SUBAGENT_SPAWN_ACCEPTED_NOTE } = await import("./subagent-spawn.js"); + return SUBAGENT_SPAWN_ACCEPTED_NOTE; +} + +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} + type SpawnResult = { status?: string; note?: string }; describe("sessions_spawn: cron isolated session note suppression", () => { - beforeEach(() => { + beforeEach(async () => { callGatewayMock.mockReset(); - resetSubagentRegistryForTests(); + await resetRegistry(); resetSessionsSpawnConfigOverride(); }); @@ -32,17 +40,19 @@ describe("sessions_spawn: cron isolated session note suppression", () => { }); it("preserves ACCEPTED_NOTE for regular sessions (mode=run)", async () => { + const acceptedNote = await getAcceptedNote(); setupSessionsSpawnGatewayMock({}); const tool = await getSessionsSpawnTool({ agentSessionKey: "agent:main:telegram:63448508", }); const result = await tool.execute("call-regular-run", { task: "test task", mode: "run" }); const details = result.details as SpawnResult; - expect(details.note).toBe(SUBAGENT_SPAWN_ACCEPTED_NOTE); + expect(details.note).toBe(acceptedNote); expect(details.status).toBe("accepted"); }); it("does not suppress ACCEPTED_NOTE for non-canonical cron-like keys", async () => { + const acceptedNote = await getAcceptedNote(); setupSessionsSpawnGatewayMock({}); const tool = await getSessionsSpawnTool({ agentSessionKey: "agent:main:slack:cron:job:run:uuid", @@ -51,15 +61,16 @@ describe("sessions_spawn: cron isolated session note suppression", () => { task: "test task", mode: "run", }); - expect((result.details as SpawnResult).note).toBe(SUBAGENT_SPAWN_ACCEPTED_NOTE); + expect((result.details as SpawnResult).note).toBe(acceptedNote); }); it("does not suppress note when agentSessionKey is undefined", async () => { + const acceptedNote = await getAcceptedNote(); setupSessionsSpawnGatewayMock({}); const tool = await getSessionsSpawnTool({ agentSessionKey: undefined, }); const result = await tool.execute("call-no-key", { task: "test task", mode: "run" }); - expect((result.details as SpawnResult).note).toBe(SUBAGENT_SPAWN_ACCEPTED_NOTE); + expect((result.details as SpawnResult).note).toBe(acceptedNote); }); }); diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts index 77b948ea5af..e57f0ad6832 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts @@ -8,7 +8,11 @@ import { setupSessionsSpawnGatewayMock, setSessionsSpawnConfigOverride, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; + +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} const fastModeEnv = vi.hoisted(() => { const previous = process.env.OPENCLAW_TEST_FAST; @@ -103,7 +107,7 @@ async function emitLifecycleEndAndFlush(params: { } describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { - beforeEach(() => { + beforeEach(async () => { resetSessionsSpawnConfigOverride(); setSessionsSpawnConfigOverride({ session: { @@ -116,7 +120,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => { }, }, }); - resetSubagentRegistryForTests(); + await resetRegistry(); callGatewayMock.mockClear(); }); diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts index 042f479d5e4..ea6a7aa5890 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts @@ -7,13 +7,21 @@ import { resetSessionsSpawnConfigOverride, setSessionsSpawnConfigOverride, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; -import { SUBAGENT_SPAWN_ACCEPTED_NOTE } from "./subagent-spawn.js"; const callGatewayMock = getCallGatewayMock(); type GatewayCall = { method?: string; params?: unknown }; type SessionsSpawnConfigOverride = Parameters[0]; +async function getAcceptedNote() { + const { SUBAGENT_SPAWN_ACCEPTED_NOTE } = await import("./subagent-spawn.js"); + return SUBAGENT_SPAWN_ACCEPTED_NOTE; +} + +async function resetRegistry() { + const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + resetSubagentRegistryForTests(); +} + function mockLongRunningSpawnFlow(params: { calls: GatewayCall[]; acceptedAtBase: number; @@ -97,13 +105,14 @@ async function expectSpawnUsesConfiguredModel(params: { } describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => { - beforeEach(() => { + beforeEach(async () => { resetSessionsSpawnConfigOverride(); - resetSubagentRegistryForTests(); + await resetRegistry(); callGatewayMock.mockClear(); }); it("sessions_spawn applies a model to the child session", async () => { + const acceptedNote = await getAcceptedNote(); const calls: GatewayCall[] = []; mockLongRunningSpawnFlow({ calls, acceptedAtBase: 3000 }); @@ -120,7 +129,7 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => { }); expect(result.details).toMatchObject({ status: "accepted", - note: SUBAGENT_SPAWN_ACCEPTED_NOTE, + note: acceptedNote, modelApplied: true, }); diff --git a/src/agents/subagent-spawn.attachments.test.ts b/src/agents/subagent-spawn.attachments.test.ts index 9fe774fa284..30de33e3a75 100644 --- a/src/agents/subagent-spawn.attachments.test.ts +++ b/src/agents/subagent-spawn.attachments.test.ts @@ -5,40 +5,58 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resetSubagentRegistryForTests } from "./subagent-registry.js"; import { decodeStrictBase64, spawnSubagentDirect } from "./subagent-spawn.js"; -const callGatewayMock = vi.fn(); +const hoisted = vi.hoisted(() => { + const defaultConfigOverride: Record = { + session: { + mainKey: "main", + scope: "per-sender", + }, + tools: { + sessions_spawn: { + attachments: { + enabled: true, + maxFiles: 50, + maxFileBytes: 1 * 1024 * 1024, + maxTotalBytes: 5 * 1024 * 1024, + }, + }, + }, + agents: { + defaults: { + workspace: "/tmp", + }, + }, + }; + const state = { + configOverride: defaultConfigOverride, + }; + return { + callGatewayMock: vi.fn(), + defaultConfigOverride, + state, + }; +}); + +const callGatewayMock = hoisted.callGatewayMock; vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: Record = { - session: { - mainKey: "main", - scope: "per-sender", - }, - tools: { - sessions_spawn: { - attachments: { - enabled: true, - maxFiles: 50, - maxFileBytes: 1 * 1024 * 1024, - maxTotalBytes: 5 * 1024 * 1024, - }, - }, - }, - agents: { - defaults: { - workspace: os.tmpdir(), - }, - }, -}; let workspaceDirOverride = ""; - vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - loadConfig: () => configOverride, + loadConfig: () => hoisted.state.configOverride, + }; +}); + +vi.mock("../../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: () => hoisted.state.configOverride, }; }); @@ -144,6 +162,7 @@ describe("decodeStrictBase64", () => { describe("spawnSubagentDirect filename validation", () => { beforeEach(() => { + hoisted.state.configOverride = hoisted.defaultConfigOverride; resetSubagentRegistryForTests(); callGatewayMock.mockClear(); setupGatewayMock(); From a583185e086a593f13547a4df91c440728b79623 Mon Sep 17 00:00:00 2001 From: Dimitri <35241486+dkdimou@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:25:58 +0100 Subject: [PATCH 3/3] tests: format sessions spawn depth limits spec --- ...enclaw-tools.subagents.sessions-spawn-depth-limits.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts index 054ac75fd41..6251d7d5f60 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts @@ -33,7 +33,9 @@ async function resetRegistry() { resetSubagentRegistryForTests(); } -async function addRegistryRun(run: Parameters<(typeof import("./subagent-registry.js"))["addSubagentRunForTests"]>[0]) { +async function addRegistryRun( + run: Parameters<(typeof import("./subagent-registry.js"))["addSubagentRunForTests"]>[0], +) { const { addSubagentRunForTests } = await import("./subagent-registry.js"); addSubagentRunForTests(run); }