diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index 554f05b63412..8ac4b692282f 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -1,5 +1,6 @@ // Builds CI node/Vitest shard plans from the full suite configuration. import { relative } from "node:path"; +import { agentsCoreIsolatedTestFiles } from "../../test/vitest/vitest.agents-paths.mjs"; import { commandsLightTestFiles } from "../../test/vitest/vitest.commands-light-paths.mjs"; import { fullSuiteVitestShards } from "../../test/vitest/vitest.test-shards.mjs"; import { toolingIsolatedTestFiles } from "../../test/vitest/vitest.tooling-isolated-paths.mjs"; @@ -280,17 +281,18 @@ function resolveAgentCoreShardName(file) { } function createAgentCoreSplitShards() { + const isolatedTests = new Set(agentsCoreIsolatedTestFiles); const groups = new Map(); for (const file of listTestFiles("src/agents")) { const name = relative("src/agents", file).replaceAll("\\", "/"); - if (name.includes("/")) { + if (name.includes("/") || isolatedTests.has(file)) { continue; } const shardName = resolveAgentCoreShardName(file); groups.set(shardName, [...(groups.get(shardName) ?? []), file]); } - return [ + const sharedShards = [ "agentic-agents-core-auth", "agentic-agents-core-models", "agentic-agents-core-tools", @@ -305,6 +307,16 @@ function createAgentCoreSplitShards() { shardName, })) .filter((shard) => shard.includePatterns.length > 0); + + return [ + ...sharedShards, + { + configs: ["test/vitest/vitest.agents-core-isolated.config.ts"], + includePatterns: agentsCoreIsolatedTestFiles, + requiresDist: false, + shardName: "agentic-agents-core-isolated", + }, + ]; } const GATEWAY_SERVER_BACKED_HTTP_TESTS = new Set([ diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index cbc26018f732..ef1d620279e2 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -6,6 +6,10 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { + agentsCoreIsolatedTestFiles, + isAgentsCoreIsolatedTestFile, +} from "../test/vitest/vitest.agents-paths.mjs"; import { isChannelSurfaceTestFile } from "../test/vitest/vitest.channel-paths.mjs"; import { commandsLightTestFiles, @@ -70,6 +74,7 @@ import { } from "./run-vitest.mjs"; const DEFAULT_VITEST_CONFIG = "test/vitest/vitest.unit.config.ts"; +const AGENTS_CORE_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.agents-core-isolated.config.ts"; const AGENTS_CORE_VITEST_CONFIG = "test/vitest/vitest.agents-core.config.ts"; const AGENTS_EMBEDDED_AGENT_VITEST_CONFIG = "test/vitest/vitest.agents-embedded-agent.config.ts"; const AGENTS_SUPPORT_VITEST_CONFIG = "test/vitest/vitest.agents-support.config.ts"; @@ -314,6 +319,7 @@ const VITEST_CONFIG_BY_KIND = { agentSupport: AGENTS_SUPPORT_VITEST_CONFIG, agentTools: AGENTS_TOOLS_VITEST_CONFIG, agent: AGENTS_VITEST_CONFIG, + agentsCoreIsolated: AGENTS_CORE_ISOLATED_VITEST_CONFIG, agentsCore: AGENTS_CORE_VITEST_CONFIG, agentsSupport: AGENTS_SUPPORT_VITEST_CONFIG, agentsTools: AGENTS_TOOLS_VITEST_CONFIG, @@ -2563,6 +2569,7 @@ function listUnitFastFullSuiteTestTargets() { } function listAgentsCoreFullSuiteTestTargets(cwd) { + const isolatedTests = new Set(agentsCoreIsolatedTestFiles); const agentsDir = path.join(cwd, "src/agents"); if (!fs.existsSync(agentsDir)) { return []; @@ -2571,6 +2578,7 @@ function listAgentsCoreFullSuiteTestTargets(cwd) { .readdirSync(agentsDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts")) .map((entry) => `src/agents/${entry.name}`) + .filter((file) => !isolatedTests.has(file)) .toSorted((left, right) => left.localeCompare(right)); } @@ -3758,6 +3766,9 @@ function classifyTarget(arg, cwd) { if (configTargetKind) { return configTargetKind; } + if (isAgentsCoreIsolatedTestFile(relative)) { + return "agentsCoreIsolated"; + } if (isControlUiE2eTarget(relative)) { return "uiE2e"; } @@ -4205,6 +4216,7 @@ export function buildVitestRunPlans( "agentSupport", "agentTools", "agent", + "agentsCoreIsolated", "agentsCore", "agentsSupport", "agentsTools", diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index d19312dc30a9..078b052064a8 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -184,8 +184,8 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { compact: true, }); - expect(compact).toHaveLength(15); - expect(compact.filter((shard) => !shard.requiresDist)).toHaveLength(14); + expect(compact).toHaveLength(16); + expect(compact.filter((shard) => !shard.requiresDist)).toHaveLength(15); expect(compact.every((shard) => Array.isArray(shard.groups))).toBe(true); expect(compact.some((shard) => shard.requiresDist)).toBe(true); expect( @@ -874,6 +874,14 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { runner: DEFAULT_NODE_TEST_RUNNER, shardName: "agentic-agents-core-runtime", }, + { + checkName: "checks-node-agentic-agents-core-isolated", + configs: ["test/vitest/vitest.agents-core-isolated.config.ts"], + includePatterns: agentShards[6]?.includePatterns, + requiresDist: false, + runner: DEFAULT_NODE_TEST_RUNNER, + shardName: "agentic-agents-core-isolated", + }, { checkName: "checks-node-agentic-agents-embedded", configs: ["test/vitest/vitest.agents-embedded-agent.config.ts"], diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index cf9bd852d60a..7b7e43e97c47 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -31,6 +31,7 @@ import { } from "../../scripts/test-projects.test-support.mjs"; import { captureReaddirSyncCallsDuring } from "../../src/test-utils/fs-scan-assertions.js"; import { toRepoPath } from "../../src/test-utils/repo-files.js"; +import { agentsCoreIsolatedTestFiles } from "../vitest/vitest.agents-paths.mjs"; import { channelConfigContractPatterns, channelRegistryContractPatterns, @@ -2466,6 +2467,34 @@ describe("scripts/test-projects changed-target routing", () => { ]); }); + it.each([ + "test/scripts/check-extension-package-tsc-boundary.test.ts", + "test/scripts/control-ui-i18n.test.ts", + ])("routes process-group test %s to the isolated tooling shard", (testFile) => { + expect(buildVitestRunPlans([testFile])).toEqual([ + { + config: "test/vitest/vitest.tooling-isolated.config.ts", + forwardedArgs: [], + includePatterns: [testFile], + watchMode: false, + }, + ]); + }); + + it.each(agentsCoreIsolatedTestFiles)( + "routes isolated agent test %s to the isolated agents-core shard", + (testFile) => { + expect(buildVitestRunPlans([testFile])).toEqual([ + { + config: "test/vitest/vitest.agents-core-isolated.config.ts", + forwardedArgs: [], + includePatterns: [testFile], + watchMode: false, + }, + ]); + }, + ); + it("routes Docker E2E script targets to their owner tooling tests", () => { const targets = [ "scripts/e2e/kitchen-sink-plugin-docker.sh", @@ -2693,7 +2722,11 @@ describe("scripts/test-projects changed-target routing", () => { { config: "test/vitest/vitest.tooling-isolated.config.ts", forwardedArgs: [], - includePatterns: ["test/scripts/openclaw-e2e-instance.test.ts"], + includePatterns: [ + "test/scripts/check-extension-package-tsc-boundary.test.ts", + "test/scripts/control-ui-i18n.test.ts", + "test/scripts/openclaw-e2e-instance.test.ts", + ], watchMode: false, }, ]); @@ -2837,7 +2870,11 @@ describe("scripts/test-projects changed-target routing", () => { { config: "test/vitest/vitest.tooling-isolated.config.ts", forwardedArgs: [], - includePatterns: ["test/scripts/openclaw-e2e-instance.test.ts"], + includePatterns: [ + "test/scripts/check-extension-package-tsc-boundary.test.ts", + "test/scripts/control-ui-i18n.test.ts", + "test/scripts/openclaw-e2e-instance.test.ts", + ], watchMode: false, }, ]); @@ -3404,7 +3441,7 @@ describe("scripts/test-projects changed-target routing", () => { ]), ).toEqual([ { - config: "test/vitest/vitest.tooling.config.ts", + config: "test/vitest/vitest.tooling-isolated.config.ts", forwardedArgs: [], includePatterns: ["test/scripts/control-ui-i18n.test.ts"], watchMode: false, @@ -4461,6 +4498,7 @@ describe("scripts/test-projects full-suite sharding", () => { "test/vitest/vitest.cli.config.ts", "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts", + "test/vitest/vitest.agents-core-isolated.config.ts", ...agentsCorePlans.map(() => agentsCoreConfig), "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index 6a09800a458c..1cf667606872 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -7,6 +7,9 @@ import { describe, expect, it } from "vitest"; import { cleanupTempDirs, makeTempDir } from "./helpers/temp-dir.js"; import { normalizeConfigPath, normalizeConfigPaths } from "./helpers/vitest-config-paths.js"; import { createAcpVitestConfig } from "./vitest/vitest.acp.config.ts"; +import { createAgentsCoreIsolatedVitestConfig } from "./vitest/vitest.agents-core-isolated.config.ts"; +import { createAgentsCoreVitestConfig } from "./vitest/vitest.agents-core.config.ts"; +import { agentsCoreIsolatedTestFiles } from "./vitest/vitest.agents-paths.mjs"; import { createAgentsVitestConfig } from "./vitest/vitest.agents.config.ts"; import { createAutoReplyCoreVitestConfig } from "./vitest/vitest.auto-reply-core.config.ts"; import { createAutoReplyReplyVitestConfig } from "./vitest/vitest.auto-reply-reply.config.ts"; @@ -566,6 +569,8 @@ describe("scoped vitest configs", () => { const defaultAutoReplyTopLevelConfig = createAutoReplyTopLevelVitestConfig({}); const defaultAutoReplyReplyConfig = createAutoReplyReplyVitestConfig({}); const defaultAgentsConfig = createAgentsVitestConfig({}); + const defaultAgentsCoreConfig = createAgentsCoreVitestConfig({}); + const defaultAgentsCoreIsolatedConfig = createAgentsCoreIsolatedVitestConfig({}); const defaultPluginsConfig = createPluginsVitestConfig({}); const defaultProcessConfig = createProcessVitestConfig({}); const defaultToolingDockerConfig = createToolingDockerVitestConfig({}); @@ -638,6 +643,19 @@ describe("scoped vitest configs", () => { ); }); + it("isolates media task status suites from conflicting task-runtime mocks", () => { + const sharedConfig = requireTestConfig(defaultAgentsCoreConfig); + const isolatedConfig = requireTestConfig(defaultAgentsCoreIsolatedConfig); + + const scopedIsolatedFiles = agentsCoreIsolatedTestFiles.map((file) => + file.replace("src/agents/", ""), + ); + expect(sharedConfig.exclude).toEqual(expect.arrayContaining(scopedIsolatedFiles)); + expect(isolatedConfig.include).toEqual(scopedIsolatedFiles); + expect(isolatedConfig.isolate).toBe(true); + expect(isolatedConfig.runner).toBeUndefined(); + }); + it("keeps selected plugin-sdk and commands light lanes off the openclaw runtime setup", () => { expect(normalizeConfigPaths(requireTestConfig(defaultPluginSdkLightConfig).setupFiles)).toEqual( ["test/setup.ts"], diff --git a/test/vitest/vitest.agents-core-isolated.config.ts b/test/vitest/vitest.agents-core-isolated.config.ts new file mode 100644 index 000000000000..bd4a82f6d114 --- /dev/null +++ b/test/vitest/vitest.agents-core-isolated.config.ts @@ -0,0 +1,16 @@ +// Vitest agents core isolated config separates suites with conflicting module mocks. +import { agentsCoreIsolatedTestFiles } from "./vitest.agents-paths.mjs"; +import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; + +export function createAgentsCoreIsolatedVitestConfig(env?: Record) { + return createScopedVitestConfig(agentsCoreIsolatedTestFiles, { + dir: "src/agents", + env, + isolate: true, + name: "agents-core-isolated", + passWithNoTests: true, + useNonIsolatedRunner: false, + }); +} + +export default createAgentsCoreIsolatedVitestConfig(); diff --git a/test/vitest/vitest.agents-core.config.ts b/test/vitest/vitest.agents-core.config.ts index 464ab687914e..64cc69afb780 100644 --- a/test/vitest/vitest.agents-core.config.ts +++ b/test/vitest/vitest.agents-core.config.ts @@ -1,11 +1,12 @@ // Vitest agents core config wires the agents core test shard. -import { agentsCoreTestPatterns } from "./vitest.agents-paths.mjs"; +import { agentsCoreIsolatedTestFiles, agentsCoreTestPatterns } from "./vitest.agents-paths.mjs"; import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; export function createAgentsCoreVitestConfig(env?: Record) { return createScopedVitestConfig(agentsCoreTestPatterns, { dir: "src/agents", env, + exclude: agentsCoreIsolatedTestFiles, fileParallelism: false, name: "agents-core", }); diff --git a/test/vitest/vitest.agents-paths.d.mts b/test/vitest/vitest.agents-paths.d.mts index 6de0033a249b..6baed749ec2f 100644 --- a/test/vitest/vitest.agents-paths.d.mts +++ b/test/vitest/vitest.agents-paths.d.mts @@ -1,4 +1,6 @@ export const agentsAllTestPatterns: string[]; +export const agentsCoreIsolatedTestFiles: string[]; +export function isAgentsCoreIsolatedTestFile(file: string): boolean; export const agentsCoreTestPatterns: string[]; export const agentsEmbeddedTestPatterns: string[]; export const agentsToolsTestPatterns: string[]; diff --git a/test/vitest/vitest.agents-paths.mjs b/test/vitest/vitest.agents-paths.mjs index cc9bd295e45d..379f59b598f6 100644 --- a/test/vitest/vitest.agents-paths.mjs +++ b/test/vitest/vitest.agents-paths.mjs @@ -1,6 +1,20 @@ // Test routing globs for agent core, embedded-agent, tool, and support suites. export const agentsAllTestPatterns = ["src/agents/**/*.test.ts"]; +// These suites install different mocks for the same task-runtime module. Keep +// their module graphs separate from the shared agents-core worker. +export const agentsCoreIsolatedTestFiles = [ + "src/agents/image-generation-task-status.test.ts", + "src/agents/media-generation-task-status-shared.test.ts", + "src/agents/video-generation-task-status.test.ts", +]; + +const agentsCoreIsolatedTestFileSet = new Set(agentsCoreIsolatedTestFiles); + +export function isAgentsCoreIsolatedTestFile(value) { + return agentsCoreIsolatedTestFileSet.has(value.replaceAll("\\", "/")); +} + export const agentsCoreTestPatterns = ["src/agents/*.test.ts"]; export const agentsEmbeddedTestPatterns = ["src/agents/embedded-agent-runner/**/*.test.ts"]; diff --git a/test/vitest/vitest.config.ts b/test/vitest/vitest.config.ts index f77e7e468bc5..c3201f03928a 100644 --- a/test/vitest/vitest.config.ts +++ b/test/vitest/vitest.config.ts @@ -32,6 +32,7 @@ export const rootVitestProjects = [ "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts", "test/vitest/vitest.auto-reply.config.ts", + "test/vitest/vitest.agents-core-isolated.config.ts", "test/vitest/vitest.agents-core.config.ts", "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", diff --git a/test/vitest/vitest.test-shards.mjs b/test/vitest/vitest.test-shards.mjs index 0cdfc6bb73fd..646e50bea1c2 100644 --- a/test/vitest/vitest.test-shards.mjs +++ b/test/vitest/vitest.test-shards.mjs @@ -98,6 +98,7 @@ export const fullSuiteVitestShards = [ "test/vitest/vitest.cli.config.ts", "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts", + "test/vitest/vitest.agents-core-isolated.config.ts", "test/vitest/vitest.agents-core.config.ts", "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", diff --git a/test/vitest/vitest.tooling-isolated-paths.mjs b/test/vitest/vitest.tooling-isolated-paths.mjs index 8123f05638e0..366f304d630b 100644 --- a/test/vitest/vitest.tooling-isolated-paths.mjs +++ b/test/vitest/vitest.tooling-isolated-paths.mjs @@ -1,6 +1,8 @@ // Tooling tests that need fresh module or process state instead of the shared serial worker. export const toolingIsolatedTestFiles = [ "test/plugins/bundled-provider-auth-literal-parity.test.ts", + "test/scripts/check-extension-package-tsc-boundary.test.ts", + "test/scripts/control-ui-i18n.test.ts", "test/scripts/openclaw-e2e-instance.test.ts", ];