diff --git a/AGENTS.md b/AGENTS.md index 290528191c8..f6a03f44a04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,11 +59,13 @@ Scoped guides: - Install: `pnpm install` (Bun supported; keep lockfiles/patches aligned if touched). - Dev CLI: `pnpm openclaw ...` or `pnpm dev`. - Build: `pnpm build` -- Smart local gate: `pnpm check:changed` +- Smart local gate: `pnpm check:changed` (scoped typecheck/lint/guards + relevant tests) - Explain smart gate: `pnpm changed:lanes --json` - Pre-commit view: `pnpm check:changed --staged` -- Normal full prod sweep: `pnpm check` +- Normal full prod sweep: `pnpm check` (prod typecheck/lint/guards, no tests) - Full tests: `pnpm test` +- Changed tests only: `pnpm test:changed` +- Extension tests: `pnpm test:extensions` or `pnpm test extensions` = all extension shards; `pnpm test extensions/` = one extension lane. Heavy channels/OpenAI have dedicated shards. - Targeted tests: `pnpm test [vitest args...]`; do not call raw `vitest`. - Coverage: `pnpm test:coverage` - Format check/fix: `pnpm format:check` / `pnpm format` @@ -92,7 +94,7 @@ Scoped guides: - extension tests => extension test typecheck/tests only - public SDK/plugin contract => extension prod/test validation too - unknown root/config => all lanes -- Local loop: prefer `pnpm check:changed`; use `pnpm check` for full prod TS/lint sweep. +- Local loop: prefer `pnpm check:changed`; use `pnpm test:changed` for tests only; use `pnpm check` for full prod TS/lint sweep without tests. - Landing on `main`: verify touched surface near landing; default bar is `pnpm check` + `pnpm test` when feasible. - Hard build gate: run/pass `pnpm build` before push if build output, packaging, lazy/module boundaries, or published surfaces can change. - Do not land related failing format/lint/type/build/tests. If failures are unrelated on latest `origin/main`, say so and give scoped proof. diff --git a/docs/reference/test.md b/docs/reference/test.md index 5c78b81db0b..8df68c4109e 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -15,14 +15,13 @@ title: "Tests" - `pnpm test:changed`: expands changed git paths into scoped Vitest lanes when the diff only touches routable source/test files. Config/setup changes still fall back to the native root projects run so wiring edits rerun broadly when needed. - `pnpm changed:lanes`: shows the architectural lanes triggered by the diff against `origin/main`. - `pnpm check:changed`: runs the smart changed gate for the diff against `origin/main`. It runs core work with core test lanes, extension work with extension test lanes, test-only work with test typecheck/tests only, and expands public Plugin SDK or plugin-contract changes to extension validation. -- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute eleven sequential shard configs (`vitest.full-core-unit-src.config.ts`, `vitest.full-core-unit-security.config.ts`, `vitest.full-core-unit-ui.config.ts`, `vitest.full-core-unit-support.config.ts`, `vitest.full-core-support-boundary.config.ts`, `vitest.full-core-contracts.config.ts`, `vitest.full-core-bundled.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process. +- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs use fixed shard groups and expand to leaf configs for local parallel execution; the extension group always expands to the per-extension shard configs instead of one giant root-project process. - Selected `plugin-sdk` and `commands` test files now route through dedicated light lanes that keep only `test/setup.ts`, leaving runtime-heavy cases on their existing lanes. - Selected `plugin-sdk` and `commands` helper source files also map `pnpm test:changed` to explicit sibling tests in those light lanes, so small helper edits avoid rerunning the heavy runtime-backed suites. - `auto-reply` now also splits into three dedicated configs (`core`, `top-level`, `reply`) so the reply harness does not dominate the lighter top-level status/token/helper tests. - Base Vitest config now defaults to `pool: "threads"` and `isolate: false`, with the shared non-isolated runner enabled across the repo configs. - `pnpm test:channels` runs `vitest.channels.config.ts`. -- `pnpm test:extensions` runs `vitest.extensions.config.ts`. -- `pnpm test:extensions`: runs extension/plugin suites. +- `pnpm test:extensions` and `pnpm test extensions` run all extension/plugin shards. Heavy channel extensions and OpenAI run as dedicated shards; other extension groups stay batched. Use `pnpm test extensions/` for one bundled plugin lane. - `pnpm test:perf:imports`: enables Vitest import-duration + import-breakdown reporting, while still using scoped lane routing for explicit file/directory targets. - `pnpm test:perf:imports:changed`: same import profiling, but only for files changed since `origin/main`. - `pnpm test:perf:changed:bench -- --ref ` benchmarks the routed changed-mode path against the native root-project run for the same committed git diff. diff --git a/package.json b/package.json index 171c8a045af..1e3faca8a1f 100644 --- a/package.json +++ b/package.json @@ -1437,7 +1437,7 @@ "test:e2e": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts", "test:e2e:openshell": "OPENCLAW_E2E_OPENSHELL=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/openshell/src/backend.e2e.test.ts", "test:extension": "node scripts/test-extension.mjs", - "test:extensions": "node scripts/run-vitest.mjs run --config test/vitest/vitest.extensions.config.ts", + "test:extensions": "node scripts/test-projects.mjs extensions", "test:extensions:batch": "node scripts/test-extension-batch.mjs", "test:extensions:memory": "node scripts/profile-extension-memory.mjs", "test:extensions:package-boundary": "node scripts/check-extension-package-tsc-boundary.mjs", diff --git a/scripts/lib/extension-test-plan.mjs b/scripts/lib/extension-test-plan.mjs index 1e9498e76b2..edeaad7ce57 100644 --- a/scripts/lib/extension-test-plan.mjs +++ b/scripts/lib/extension-test-plan.mjs @@ -3,6 +3,7 @@ import path from "node:path"; import { channelTestRoots } from "../../test/vitest/vitest.channel-paths.mjs"; import { isAcpxExtensionRoot } from "../../test/vitest/vitest.extension-acpx-paths.mjs"; import { isBlueBubblesExtensionRoot } from "../../test/vitest/vitest.extension-bluebubbles-paths.mjs"; +import { resolveSplitChannelExtensionShard } from "../../test/vitest/vitest.extension-channel-split-paths.mjs"; import { isDiffsExtensionRoot } from "../../test/vitest/vitest.extension-diffs-paths.mjs"; import { isFeishuExtensionRoot } from "../../test/vitest/vitest.extension-feishu-paths.mjs"; import { isIrcExtensionRoot } from "../../test/vitest/vitest.extension-irc-paths.mjs"; @@ -11,7 +12,10 @@ import { isMattermostExtensionRoot } from "../../test/vitest/vitest.extension-ma import { isMemoryExtensionRoot } from "../../test/vitest/vitest.extension-memory-paths.mjs"; import { isMessagingExtensionRoot } from "../../test/vitest/vitest.extension-messaging-paths.mjs"; import { isMsTeamsExtensionRoot } from "../../test/vitest/vitest.extension-msteams-paths.mjs"; -import { isProviderExtensionRoot } from "../../test/vitest/vitest.extension-provider-paths.mjs"; +import { + isProviderExtensionRoot, + isProviderOpenAiExtensionRoot, +} from "../../test/vitest/vitest.extension-provider-paths.mjs"; import { isTelegramExtensionRoot } from "../../test/vitest/vitest.extension-telegram-paths.mjs"; import { isVoiceCallExtensionRoot } from "../../test/vitest/vitest.extension-voice-call-paths.mjs"; import { isWhatsAppExtensionRoot } from "../../test/vitest/vitest.extension-whatsapp-paths.mjs"; @@ -111,6 +115,7 @@ export function resolveExtensionTestPlan(params = {}) { const roots = [relativeExtensionDir]; + const splitChannelShard = resolveSplitChannelExtensionShard(relativeExtensionDir); const usesChannelConfig = roots.some((root) => channelTestRoots.includes(root)); const usesAcpxConfig = roots.some((root) => isAcpxExtensionRoot(root)); const usesDiffsConfig = roots.some((root) => isDiffsExtensionRoot(root)); @@ -126,40 +131,45 @@ export function resolveExtensionTestPlan(params = {}) { const usesMemoryConfig = roots.some((root) => isMemoryExtensionRoot(root)); const usesMsTeamsConfig = roots.some((root) => isMsTeamsExtensionRoot(root)); const usesMessagingConfig = roots.some((root) => isMessagingExtensionRoot(root)); + const usesProviderOpenAiConfig = roots.some((root) => isProviderOpenAiExtensionRoot(root)); const usesProviderConfig = roots.some((root) => isProviderExtensionRoot(root)); - const config = usesChannelConfig - ? "test/vitest/vitest.extension-channels.config.ts" - : usesAcpxConfig - ? "test/vitest/vitest.extension-acpx.config.ts" - : usesDiffsConfig - ? "test/vitest/vitest.extension-diffs.config.ts" - : usesBlueBubblesConfig - ? "test/vitest/vitest.extension-bluebubbles.config.ts" - : usesFeishuConfig - ? "test/vitest/vitest.extension-feishu.config.ts" - : usesIrcConfig - ? "test/vitest/vitest.extension-irc.config.ts" - : usesMattermostConfig - ? "test/vitest/vitest.extension-mattermost.config.ts" - : usesMatrixConfig - ? "test/vitest/vitest.extension-matrix.config.ts" - : usesTelegramConfig - ? "test/vitest/vitest.extension-telegram.config.ts" - : usesVoiceCallConfig - ? "test/vitest/vitest.extension-voice-call.config.ts" - : usesWhatsAppConfig - ? "test/vitest/vitest.extension-whatsapp.config.ts" - : usesZaloConfig - ? "test/vitest/vitest.extension-zalo.config.ts" - : usesMemoryConfig - ? "test/vitest/vitest.extension-memory.config.ts" - : usesMsTeamsConfig - ? "test/vitest/vitest.extension-msteams.config.ts" - : usesMessagingConfig - ? "test/vitest/vitest.extension-messaging.config.ts" - : usesProviderConfig - ? "test/vitest/vitest.extension-providers.config.ts" - : "test/vitest/vitest.extensions.config.ts"; + const config = splitChannelShard + ? splitChannelShard.config + : usesChannelConfig + ? "test/vitest/vitest.extension-channels.config.ts" + : usesAcpxConfig + ? "test/vitest/vitest.extension-acpx.config.ts" + : usesDiffsConfig + ? "test/vitest/vitest.extension-diffs.config.ts" + : usesBlueBubblesConfig + ? "test/vitest/vitest.extension-bluebubbles.config.ts" + : usesFeishuConfig + ? "test/vitest/vitest.extension-feishu.config.ts" + : usesIrcConfig + ? "test/vitest/vitest.extension-irc.config.ts" + : usesMattermostConfig + ? "test/vitest/vitest.extension-mattermost.config.ts" + : usesMatrixConfig + ? "test/vitest/vitest.extension-matrix.config.ts" + : usesTelegramConfig + ? "test/vitest/vitest.extension-telegram.config.ts" + : usesVoiceCallConfig + ? "test/vitest/vitest.extension-voice-call.config.ts" + : usesWhatsAppConfig + ? "test/vitest/vitest.extension-whatsapp.config.ts" + : usesZaloConfig + ? "test/vitest/vitest.extension-zalo.config.ts" + : usesMemoryConfig + ? "test/vitest/vitest.extension-memory.config.ts" + : usesMsTeamsConfig + ? "test/vitest/vitest.extension-msteams.config.ts" + : usesMessagingConfig + ? "test/vitest/vitest.extension-messaging.config.ts" + : usesProviderOpenAiConfig + ? "test/vitest/vitest.extension-provider-openai.config.ts" + : usesProviderConfig + ? "test/vitest/vitest.extension-providers.config.ts" + : "test/vitest/vitest.extensions.config.ts"; const testFileCount = roots.reduce( (sum, root) => sum + countTestFiles(path.join(repoRoot, root)), 0, diff --git a/scripts/test-projects.mjs b/scripts/test-projects.mjs index 15b6c4a3cdd..00864fccfea 100644 --- a/scripts/test-projects.mjs +++ b/scripts/test-projects.mjs @@ -11,6 +11,7 @@ import { applyParallelVitestCachePaths, buildFullSuiteVitestRunPlans, createVitestRunSpecs, + listFullExtensionVitestProjectConfigs, parseTestProjectsArgs, resolveParallelFullSuiteConcurrency, resolveChangedTargetArgs, @@ -33,7 +34,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([ ["test/vitest/vitest.agents.config.ts", 170], ["test/vitest/vitest.extension-voice-call.config.ts", 169], ["test/vitest/vitest.extensions.config.ts", 168], - ["test/vitest/vitest.extension-channels.config.ts", 167], + ["test/vitest/vitest.extension-provider-openai.config.ts", 167], ["test/vitest/vitest.runtime-config.config.ts", 166], ["test/vitest/vitest.contracts.config.ts", 165], ["test/vitest/vitest.tasks.config.ts", 165], @@ -46,6 +47,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([ ["test/vitest/vitest.wizard.config.ts", 130], ["test/vitest/vitest.unit-src.config.ts", 125], ["test/vitest/vitest.extension-matrix.config.ts", 100], + ["test/vitest/vitest.extension-discord.config.ts", 98], ["test/vitest/vitest.extension-providers.config.ts", 96], ["test/vitest/vitest.extension-telegram.config.ts", 94], ["test/vitest/vitest.extension-whatsapp.config.ts", 92], @@ -54,6 +56,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([ ["test/vitest/vitest.media.config.ts", 84], ["test/vitest/vitest.plugins.config.ts", 82], ["test/vitest/vitest.bundled.config.ts", 80], + ["test/vitest/vitest.extension-slack.config.ts", 78], ["test/vitest/vitest.commands-light.config.ts", 48], ["test/vitest/vitest.plugin-sdk.config.ts", 46], ["test/vitest/vitest.auto-reply-top-level.config.ts", 45], @@ -70,6 +73,9 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([ ["test/vitest/vitest.extension-feishu.config.ts", 18], ["test/vitest/vitest.extension-mattermost.config.ts", 16], ["test/vitest/vitest.extension-messaging.config.ts", 14], + ["test/vitest/vitest.extension-imessage.config.ts", 13], + ["test/vitest/vitest.extension-line.config.ts", 12], + ["test/vitest/vitest.extension-signal.config.ts", 11], ["test/vitest/vitest.extension-acpx.config.ts", 10], ["test/vitest/vitest.extension-diffs.config.ts", 8], ["test/vitest/vitest.extension-memory.config.ts", 6], @@ -160,6 +166,19 @@ function orderFullSuiteSpecsForParallelRun(specs) { }); } +function isFullExtensionsProjectRun(specs) { + const fullExtensionProjectConfigs = new Set(listFullExtensionVitestProjectConfigs()); + return ( + specs.length > 1 && + specs.every( + (spec) => + spec.watchMode === false && + spec.includePatterns === null && + fullExtensionProjectConfigs.has(spec.config), + ) + ); +} + async function runVitestSpecsParallel(specs, concurrency) { let nextIndex = 0; let exitCode = 0; @@ -216,6 +235,11 @@ async function main() { cwd: process.cwd(), }); + if (runSpecs.length === 0) { + console.error("[test] no changed test targets; skipping Vitest."); + return; + } + releaseLock = shouldAcquireLocalHeavyCheckLock(runSpecs, process.env) ? acquireLocalHeavyCheckLockSync({ cwd: process.cwd(), @@ -228,7 +252,8 @@ async function main() { targetArgs.length === 0 && changedTargetArgs === null && !runSpecs.some((spec) => spec.watchMode); - if (isFullSuiteRun) { + const isParallelShardRun = isFullSuiteRun || isFullExtensionsProjectRun(runSpecs); + if (isParallelShardRun) { const concurrency = resolveParallelFullSuiteConcurrency(runSpecs.length, process.env); if (concurrency > 1) { const localFullSuiteProfile = resolveLocalFullSuiteProfile(process.env); diff --git a/scripts/test-projects.test-support.d.mts b/scripts/test-projects.test-support.d.mts index d7445974733..04fa5b5fe2d 100644 --- a/scripts/test-projects.test-support.d.mts +++ b/scripts/test-projects.test-support.d.mts @@ -35,6 +35,8 @@ export function resolveChangedTargetArgs( listChangedPaths?: (baseRef: string, cwd: string) => string[], ): string[] | null; +export function listFullExtensionVitestProjectConfigs(): string[]; + export function createVitestRunSpecs( args: string[], params?: { diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 4da20a6fc4d..7173a2aa9fd 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -9,6 +9,7 @@ import { } from "../test/vitest/vitest.commands-light-paths.mjs"; import { isAcpxExtensionRoot } from "../test/vitest/vitest.extension-acpx-paths.mjs"; import { isBlueBubblesExtensionRoot } from "../test/vitest/vitest.extension-bluebubbles-paths.mjs"; +import { resolveSplitChannelExtensionShard } from "../test/vitest/vitest.extension-channel-split-paths.mjs"; import { isDiffsExtensionRoot } from "../test/vitest/vitest.extension-diffs-paths.mjs"; import { isFeishuExtensionRoot } from "../test/vitest/vitest.extension-feishu-paths.mjs"; import { isIrcExtensionRoot } from "../test/vitest/vitest.extension-irc-paths.mjs"; @@ -17,7 +18,10 @@ import { isMattermostExtensionRoot } from "../test/vitest/vitest.extension-matte import { isMemoryExtensionRoot } from "../test/vitest/vitest.extension-memory-paths.mjs"; import { isMessagingExtensionRoot } from "../test/vitest/vitest.extension-messaging-paths.mjs"; import { isMsTeamsExtensionRoot } from "../test/vitest/vitest.extension-msteams-paths.mjs"; -import { isProviderExtensionRoot } from "../test/vitest/vitest.extension-provider-paths.mjs"; +import { + isProviderExtensionRoot, + isProviderOpenAiExtensionRoot, +} from "../test/vitest/vitest.extension-provider-paths.mjs"; import { isTelegramExtensionRoot } from "../test/vitest/vitest.extension-telegram-paths.mjs"; import { isVoiceCallExtensionRoot } from "../test/vitest/vitest.extension-voice-call-paths.mjs"; import { isWhatsAppExtensionRoot } from "../test/vitest/vitest.extension-whatsapp-paths.mjs"; @@ -32,6 +36,7 @@ import { isBoundaryTestFile, isBundledPluginDependentUnitTestFile, } from "../test/vitest/vitest.unit-paths.mjs"; +import { detectChangedLanes } from "./changed-lanes.mjs"; import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-scheduling.mjs"; import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs"; @@ -53,14 +58,21 @@ const EXTENSION_ACPX_VITEST_CONFIG = "test/vitest/vitest.extension-acpx.config.t const EXTENSION_BLUEBUBBLES_VITEST_CONFIG = "test/vitest/vitest.extension-bluebubbles.config.ts"; const EXTENSION_CHANNELS_VITEST_CONFIG = "test/vitest/vitest.extension-channels.config.ts"; const EXTENSION_DIFFS_VITEST_CONFIG = "test/vitest/vitest.extension-diffs.config.ts"; +const EXTENSION_DISCORD_VITEST_CONFIG = "test/vitest/vitest.extension-discord.config.ts"; const EXTENSION_FEISHU_VITEST_CONFIG = "test/vitest/vitest.extension-feishu.config.ts"; +const EXTENSION_IMESSAGE_VITEST_CONFIG = "test/vitest/vitest.extension-imessage.config.ts"; const EXTENSION_IRC_VITEST_CONFIG = "test/vitest/vitest.extension-irc.config.ts"; +const EXTENSION_LINE_VITEST_CONFIG = "test/vitest/vitest.extension-line.config.ts"; const EXTENSION_MATTERMOST_VITEST_CONFIG = "test/vitest/vitest.extension-mattermost.config.ts"; const EXTENSION_MATRIX_VITEST_CONFIG = "test/vitest/vitest.extension-matrix.config.ts"; const EXTENSION_MEMORY_VITEST_CONFIG = "test/vitest/vitest.extension-memory.config.ts"; const EXTENSION_MSTEAMS_VITEST_CONFIG = "test/vitest/vitest.extension-msteams.config.ts"; const EXTENSION_MESSAGING_VITEST_CONFIG = "test/vitest/vitest.extension-messaging.config.ts"; +const EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG = + "test/vitest/vitest.extension-provider-openai.config.ts"; const EXTENSION_PROVIDERS_VITEST_CONFIG = "test/vitest/vitest.extension-providers.config.ts"; +const EXTENSION_SIGNAL_VITEST_CONFIG = "test/vitest/vitest.extension-signal.config.ts"; +const EXTENSION_SLACK_VITEST_CONFIG = "test/vitest/vitest.extension-slack.config.ts"; const EXTENSION_TELEGRAM_VITEST_CONFIG = "test/vitest/vitest.extension-telegram.config.ts"; const EXTENSION_VOICE_CALL_VITEST_CONFIG = "test/vitest/vitest.extension-voice-call.config.ts"; const EXTENSION_WHATSAPP_VITEST_CONFIG = "test/vitest/vitest.extension-whatsapp.config.ts"; @@ -105,18 +117,25 @@ const VITEST_CONFIG_BY_KIND = { daemon: DAEMON_VITEST_CONFIG, e2e: E2E_VITEST_CONFIG, extension: EXTENSIONS_VITEST_CONFIG, + extensionFull: FULL_EXTENSIONS_VITEST_CONFIG, extensionAcpx: EXTENSION_ACPX_VITEST_CONFIG, extensionBlueBubbles: EXTENSION_BLUEBUBBLES_VITEST_CONFIG, extensionChannel: EXTENSION_CHANNELS_VITEST_CONFIG, extensionDiffs: EXTENSION_DIFFS_VITEST_CONFIG, + extensionDiscord: EXTENSION_DISCORD_VITEST_CONFIG, extensionFeishu: EXTENSION_FEISHU_VITEST_CONFIG, + extensionImessage: EXTENSION_IMESSAGE_VITEST_CONFIG, extensionIrc: EXTENSION_IRC_VITEST_CONFIG, + extensionLine: EXTENSION_LINE_VITEST_CONFIG, extensionMatrix: EXTENSION_MATRIX_VITEST_CONFIG, extensionMattermost: EXTENSION_MATTERMOST_VITEST_CONFIG, extensionMemory: EXTENSION_MEMORY_VITEST_CONFIG, extensionMessaging: EXTENSION_MESSAGING_VITEST_CONFIG, extensionMsTeams: EXTENSION_MSTEAMS_VITEST_CONFIG, + extensionProviderOpenAi: EXTENSION_PROVIDER_OPENAI_VITEST_CONFIG, extensionProvider: EXTENSION_PROVIDERS_VITEST_CONFIG, + extensionSignal: EXTENSION_SIGNAL_VITEST_CONFIG, + extensionSlack: EXTENSION_SLACK_VITEST_CONFIG, extensionTelegram: EXTENSION_TELEGRAM_VITEST_CONFIG, extensionVoiceCall: EXTENSION_VOICE_CALL_VITEST_CONFIG, extensionWhatsApp: EXTENSION_WHATSAPP_VITEST_CONFIG, @@ -148,6 +167,7 @@ const BROAD_CHANGED_RERUN_PATTERNS = [ /^test\/setup(?:\.shared|\.extensions|-openclaw-runtime)?\.ts$/u, /^vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u, /^test\/vitest\/vitest(?:\..+)?\.(?:config\.ts|paths\.mjs)$/u, + /^test\/helpers\//u, /^scripts\/run-vitest\.mjs$/u, /^scripts\/test-projects(?:\.test-support)?\.mjs$/u, ]; @@ -260,7 +280,14 @@ function shouldKeepBroadChangedRun(changedPaths) { } function isRoutableChangedTarget(changedPath) { - return /^(?:src|test|extensions|ui|packages|apps)(?:\/|$)/u.test(changedPath); + return /^(?:src|test|extensions|ui|packages)(?:\/|$)/u.test(changedPath); +} + +export function listFullExtensionVitestProjectConfigs() { + return ( + fullSuiteVitestShards.find((shard) => shard.config === FULL_EXTENSIONS_VITEST_CONFIG) + ?.projects ?? [] + ); } export function resolveChangedTargetArgs( @@ -276,8 +303,16 @@ export function resolveChangedTargetArgs( if (changedPaths.length === 0 || shouldKeepBroadChangedRun(changedPaths)) { return null; } + const changedLanes = detectChangedLanes(changedPaths); + if (changedLanes.lanes.all) { + return null; + } const routablePaths = changedPaths.filter(isRoutableChangedTarget); - return routablePaths.length > 0 ? [...new Set(routablePaths)] : null; + const targets = [...routablePaths]; + if (changedLanes.extensionImpactFromCore) { + targets.push("extensions"); + } + return [...new Set(targets)]; } function classifyTarget(arg, cwd) { @@ -295,8 +330,18 @@ function classifyTarget(arg, cwd) { ) { return "e2e"; } + if (relative === "extensions") { + return "extensionFull"; + } if (relative.startsWith("extensions/")) { const extensionRoot = relative.split("/").slice(0, 2).join("/"); + const splitChannelShard = resolveSplitChannelExtensionShard(extensionRoot); + if (splitChannelShard) { + return splitChannelShard.kind; + } + if (isProviderOpenAiExtensionRoot(extensionRoot)) { + return "extensionProviderOpenAi"; + } if (isChannelSurfaceTestFile(relative)) { return "extensionChannel"; } @@ -506,7 +551,11 @@ export function buildVitestRunPlans( const changedTargetArgs = targetArgs.length === 0 ? resolveChangedTargetArgs(args, cwd, listChangedPaths) : null; const activeTargetArgs = changedTargetArgs ?? targetArgs; - const activeForwardedArgs = changedTargetArgs ? stripChangedArgs(forwardedArgs) : forwardedArgs; + const activeForwardedArgs = + changedTargetArgs !== null ? stripChangedArgs(forwardedArgs) : forwardedArgs; + if (changedTargetArgs !== null && activeTargetArgs.length === 0) { + return []; + } if (activeTargetArgs.length === 0) { return [ { @@ -570,8 +619,11 @@ export function buildVitestRunPlans( "extensionAcpx", "extensionDiffs", "extensionBlueBubbles", + "extensionDiscord", "extensionFeishu", + "extensionImessage", "extensionIrc", + "extensionLine", "extensionMattermost", "extensionChannel", "extensionTelegram", @@ -582,7 +634,11 @@ export function buildVitestRunPlans( "extensionMemory", "extensionMsTeams", "extensionMessaging", + "extensionProviderOpenAi", "extensionProvider", + "extensionSignal", + "extensionSlack", + "extensionFull", "channel", "extension", ]; @@ -592,6 +648,20 @@ export function buildVitestRunPlans( if (!grouped || grouped.length === 0) { continue; } + if (kind === "extensionFull") { + const configs = watchMode + ? [FULL_EXTENSIONS_VITEST_CONFIG] + : listFullExtensionVitestProjectConfigs(); + for (const config of configs) { + plans.push({ + config, + forwardedArgs: nonTargetArgs, + includePatterns: null, + watchMode, + }); + } + continue; + } const config = VITEST_CONFIG_BY_KIND[kind] ?? DEFAULT_VITEST_CONFIG; const useCliTargetArgs = kind === "e2e" || diff --git a/src/scripts/test-projects.test.ts b/src/scripts/test-projects.test.ts index a5b53c0b082..c24f9ab3351 100644 --- a/src/scripts/test-projects.test.ts +++ b/src/scripts/test-projects.test.ts @@ -785,12 +785,12 @@ describe("test-projects args", () => { ]); }); - it("routes direct channel extension file targets to the channels config", () => { + it("routes direct Discord extension file targets to the Discord config", () => { expect( buildVitestRunPlans(["extensions/discord/src/monitor/message-handler.preflight.test.ts"]), ).toEqual([ { - config: "test/vitest/vitest.extension-channels.config.ts", + config: "test/vitest/vitest.extension-discord.config.ts", forwardedArgs: [], includePatterns: ["extensions/discord/src/monitor/message-handler.preflight.test.ts"], watchMode: false, @@ -809,10 +809,10 @@ describe("test-projects args", () => { ]); }); - it("routes line extension targets to the extension channel config", () => { + it("routes line extension targets to the line config", () => { expect(buildVitestRunPlans(["extensions/line/src/send.test.ts"])).toEqual([ { - config: "test/vitest/vitest.extension-channels.config.ts", + config: "test/vitest/vitest.extension-line.config.ts", forwardedArgs: [], includePatterns: ["extensions/line/src/send.test.ts"], watchMode: false, @@ -831,10 +831,10 @@ describe("test-projects args", () => { ]); }); - it("routes direct provider extension file targets to the extension providers config", () => { + it("routes direct OpenAI provider extension file targets to the OpenAI provider config", () => { expect(buildVitestRunPlans(["extensions/openai/openai-codex-provider.test.ts"])).toEqual([ { - config: "test/vitest/vitest.extension-providers.config.ts", + config: "test/vitest/vitest.extension-provider-openai.config.ts", forwardedArgs: [], includePatterns: ["extensions/openai/openai-codex-provider.test.ts"], watchMode: false, @@ -869,7 +869,7 @@ describe("test-projects args", () => { watchMode: false, }, { - config: "test/vitest/vitest.extension-channels.config.ts", + config: "test/vitest/vitest.extension-discord.config.ts", forwardedArgs: ["-t", "mention"], includePatterns: ["extensions/discord/src/monitor/message-handler.preflight.test.ts"], watchMode: false, @@ -886,7 +886,7 @@ describe("test-projects args", () => { ...VITEST_NODE_PREFIX, "run", "--config", - "test/vitest/vitest.extension-channels.config.ts", + "test/vitest/vitest.extension-discord.config.ts", ]); expect(spec?.includePatterns).toEqual([ "extensions/discord/src/monitor/message-handler.preflight.test.ts", diff --git a/test/scripts/test-extension.test.ts b/test/scripts/test-extension.test.ts index f4c24330182..7d52487173f 100644 --- a/test/scripts/test-extension.test.ts +++ b/test/scripts/test-extension.test.ts @@ -33,12 +33,12 @@ function findExtensionWithoutTests() { } describe("scripts/test-extension.mjs", () => { - it("resolves channel-root extensions onto the channel vitest config", () => { + it("resolves split channel extensions onto their own vitest configs", () => { const plan = resolveExtensionTestPlan({ targetArg: "slack", cwd: process.cwd() }); expect(plan.extensionId).toBe("slack"); expect(plan.extensionDir).toBe(bundledPluginRoot("slack")); - expect(plan.config).toBe("test/vitest/vitest.extension-channels.config.ts"); + expect(plan.config).toBe("test/vitest/vitest.extension-slack.config.ts"); expect(plan.roots).toContain(bundledPluginRoot("slack")); expect(plan.hasTests).toBe(true); }); @@ -79,11 +79,11 @@ describe("scripts/test-extension.mjs", () => { expect(plan.hasTests).toBe(true); }); - it("resolves provider extensions onto the provider vitest config", () => { + it("resolves OpenAI onto its own provider vitest config", () => { const plan = resolveExtensionTestPlan({ targetArg: "openai", cwd: process.cwd() }); expect(plan.extensionId).toBe("openai"); - expect(plan.config).toBe("test/vitest/vitest.extension-providers.config.ts"); + expect(plan.config).toBe("test/vitest/vitest.extension-provider-openai.config.ts"); expect(plan.roots).toContain(bundledPluginRoot("openai")); expect(plan.hasTests).toBe(true); }); @@ -183,7 +183,7 @@ describe("scripts/test-extension.mjs", () => { expect(plan.roots).toContain(bundledPluginRoot("line")); expect(plan.roots).not.toContain("src/line"); - expect(plan.config).toBe("test/vitest/vitest.extension-channels.config.ts"); + expect(plan.config).toBe("test/vitest/vitest.extension-line.config.ts"); expect(plan.hasTests).toBe(true); }); @@ -294,13 +294,6 @@ describe("scripts/test-extension.mjs", () => { roots: [bundledPluginRoot("bluebubbles")], testFileCount: expect.any(Number), }, - { - config: "test/vitest/vitest.extension-channels.config.ts", - estimatedCost: expect.any(Number), - extensionIds: ["line", "slack"], - roots: [bundledPluginRoot("slack"), bundledPluginRoot("line")], - testFileCount: expect.any(Number), - }, { config: "test/vitest/vitest.extension-diffs.config.ts", estimatedCost: expect.any(Number), @@ -322,6 +315,13 @@ describe("scripts/test-extension.mjs", () => { roots: [bundledPluginRoot("irc")], testFileCount: expect.any(Number), }, + { + config: "test/vitest/vitest.extension-line.config.ts", + estimatedCost: expect.any(Number), + extensionIds: ["line"], + roots: [bundledPluginRoot("line")], + testFileCount: expect.any(Number), + }, { config: "test/vitest/vitest.extension-matrix.config.ts", estimatedCost: expect.any(Number), @@ -351,12 +351,19 @@ describe("scripts/test-extension.mjs", () => { testFileCount: expect.any(Number), }, { - config: "test/vitest/vitest.extension-providers.config.ts", + config: "test/vitest/vitest.extension-provider-openai.config.ts", estimatedCost: expect.any(Number), extensionIds: ["openai"], roots: [bundledPluginRoot("openai")], testFileCount: expect.any(Number), }, + { + config: "test/vitest/vitest.extension-slack.config.ts", + estimatedCost: expect.any(Number), + extensionIds: ["slack"], + roots: [bundledPluginRoot("slack")], + testFileCount: expect.any(Number), + }, { config: "test/vitest/vitest.extension-telegram.config.ts", estimatedCost: expect.any(Number), diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index ff1794ec285..57c72291865 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -4,6 +4,7 @@ import { applyParallelVitestCachePaths, buildFullSuiteVitestRunPlans, buildVitestRunPlans, + listFullExtensionVitestProjectConfigs, shouldAcquireLocalHeavyCheckLock, resolveChangedTargetArgs, resolveParallelFullSuiteConcurrency, @@ -28,12 +29,89 @@ describe("scripts/test-projects changed-target routing", () => { ).toBeNull(); }); - it("ignores changed files that cannot map to test lanes", () => { + it("keeps the broad changed run for shared test helpers", () => { + expect( + resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [ + "test/helpers/channels/plugin.ts", + ]), + ).toBeNull(); + }); + + it("keeps the broad changed run for unknown root surfaces", () => { + expect( + resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [ + "unknown/file.txt", + ]), + ).toBeNull(); + }); + + it("skips changed docs files that cannot map to test lanes", () => { expect( resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [ "docs/help/testing.md", ]), - ).toBeNull(); + ).toEqual([]); + }); + + it("skips root agent guidance changes instead of broad-running tests", () => { + expect( + buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => ["AGENTS.md"]), + ).toEqual([]); + }); + + it("skips app-only changes because app tests are separate from Vitest lanes", () => { + expect( + buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ + "apps/macos/OpenClaw/AppDelegate.swift", + ]), + ).toEqual([]); + }); + + it("adds extension tests for public plugin SDK changes", () => { + const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ + "src/plugin-sdk/provider-entry.ts", + ]); + + expect(plans).toEqual([ + { + config: "test/vitest/vitest.unit-fast.config.ts", + forwardedArgs: [], + includePatterns: ["src/plugin-sdk/provider-entry.test.ts"], + watchMode: false, + }, + ...listFullExtensionVitestProjectConfigs().map((config) => ({ + config, + forwardedArgs: [], + includePatterns: null, + watchMode: false, + })), + ]); + }); + + it("routes LM Studio changes to the provider extension lane", () => { + const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ + "extensions/lmstudio/src/runtime.ts", + ]); + + expect(plans).toEqual([ + { + config: "test/vitest/vitest.extension-providers.config.ts", + forwardedArgs: [], + includePatterns: ["extensions/lmstudio/src/**/*.test.ts"], + watchMode: false, + }, + ]); + }); + + it("routes the top-level extensions target to every extension shard", () => { + expect(buildVitestRunPlans(["extensions"], process.cwd())).toEqual( + listFullExtensionVitestProjectConfigs().map((config) => ({ + config, + forwardedArgs: [], + includePatterns: null, + watchMode: false, + })), + ); }); it("narrows default-lane changed source files to include globs", () => { @@ -115,7 +193,7 @@ describe("scripts/test-projects changed-target routing", () => { ]); }); - it("routes changed plugin-sdk source allowlist files to sibling light tests", () => { + it("keeps changed plugin-sdk allowlist files on sibling light tests plus extension tests", () => { const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ "src/plugin-sdk/provider-entry.ts", ]); @@ -127,6 +205,12 @@ describe("scripts/test-projects changed-target routing", () => { includePatterns: ["src/plugin-sdk/provider-entry.test.ts"], watchMode: false, }, + ...listFullExtensionVitestProjectConfigs().map((config) => ({ + config, + forwardedArgs: [], + includePatterns: null, + watchMode: false, + })), ]); }); @@ -149,7 +233,7 @@ describe("scripts/test-projects changed-target routing", () => { ]); }); - it("keeps non-allowlisted plugin-sdk source files on the heavy lane", () => { + it("keeps non-allowlisted plugin-sdk source files on the heavy lane plus extension tests", () => { const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [ "src/plugin-sdk/facade-runtime.ts", ]); @@ -161,6 +245,12 @@ describe("scripts/test-projects changed-target routing", () => { includePatterns: ["src/plugin-sdk/**/*.test.ts"], watchMode: false, }, + ...listFullExtensionVitestProjectConfigs().map((config) => ({ + config, + forwardedArgs: [], + includePatterns: null, + watchMode: false, + })), ]); }); @@ -316,16 +406,21 @@ describe("scripts/test-projects full-suite sharding", () => { "test/vitest/vitest.full-auto-reply.config.ts", "test/vitest/vitest.extension-acpx.config.ts", "test/vitest/vitest.extension-bluebubbles.config.ts", - "test/vitest/vitest.extension-channels.config.ts", "test/vitest/vitest.extension-diffs.config.ts", + "test/vitest/vitest.extension-discord.config.ts", "test/vitest/vitest.extension-feishu.config.ts", + "test/vitest/vitest.extension-imessage.config.ts", "test/vitest/vitest.extension-irc.config.ts", + "test/vitest/vitest.extension-line.config.ts", "test/vitest/vitest.extension-mattermost.config.ts", "test/vitest/vitest.extension-matrix.config.ts", "test/vitest/vitest.extension-memory.config.ts", "test/vitest/vitest.extension-messaging.config.ts", "test/vitest/vitest.extension-msteams.config.ts", + "test/vitest/vitest.extension-provider-openai.config.ts", "test/vitest/vitest.extension-providers.config.ts", + "test/vitest/vitest.extension-signal.config.ts", + "test/vitest/vitest.extension-slack.config.ts", "test/vitest/vitest.extension-telegram.config.ts", "test/vitest/vitest.extension-voice-call.config.ts", "test/vitest/vitest.extension-whatsapp.config.ts", @@ -499,16 +594,21 @@ describe("scripts/test-projects full-suite sharding", () => { "test/vitest/vitest.auto-reply-reply.config.ts", "test/vitest/vitest.extension-acpx.config.ts", "test/vitest/vitest.extension-bluebubbles.config.ts", - "test/vitest/vitest.extension-channels.config.ts", "test/vitest/vitest.extension-diffs.config.ts", + "test/vitest/vitest.extension-discord.config.ts", "test/vitest/vitest.extension-feishu.config.ts", + "test/vitest/vitest.extension-imessage.config.ts", "test/vitest/vitest.extension-irc.config.ts", + "test/vitest/vitest.extension-line.config.ts", "test/vitest/vitest.extension-mattermost.config.ts", "test/vitest/vitest.extension-matrix.config.ts", "test/vitest/vitest.extension-memory.config.ts", "test/vitest/vitest.extension-messaging.config.ts", "test/vitest/vitest.extension-msteams.config.ts", + "test/vitest/vitest.extension-provider-openai.config.ts", "test/vitest/vitest.extension-providers.config.ts", + "test/vitest/vitest.extension-signal.config.ts", + "test/vitest/vitest.extension-slack.config.ts", "test/vitest/vitest.extension-telegram.config.ts", "test/vitest/vitest.extension-voice-call.config.ts", "test/vitest/vitest.extension-whatsapp.config.ts", diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index 8101931fad2..707d1841201 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -22,14 +22,20 @@ import { createExtensionAcpxVitestConfig } from "./vitest/vitest.extension-acpx. import { createExtensionBlueBubblesVitestConfig } from "./vitest/vitest.extension-bluebubbles.config.ts"; import { createExtensionChannelsVitestConfig } from "./vitest/vitest.extension-channels.config.ts"; import { createExtensionDiffsVitestConfig } from "./vitest/vitest.extension-diffs.config.ts"; +import { createExtensionDiscordVitestConfig } from "./vitest/vitest.extension-discord.config.ts"; import { createExtensionFeishuVitestConfig } from "./vitest/vitest.extension-feishu.config.ts"; +import { createExtensionImessageVitestConfig } from "./vitest/vitest.extension-imessage.config.ts"; import { createExtensionIrcVitestConfig } from "./vitest/vitest.extension-irc.config.ts"; +import { createExtensionLineVitestConfig } from "./vitest/vitest.extension-line.config.ts"; import { createExtensionMatrixVitestConfig } from "./vitest/vitest.extension-matrix.config.ts"; import { createExtensionMattermostVitestConfig } from "./vitest/vitest.extension-mattermost.config.ts"; import { createExtensionMemoryVitestConfig } from "./vitest/vitest.extension-memory.config.ts"; import { createExtensionMessagingVitestConfig } from "./vitest/vitest.extension-messaging.config.ts"; import { createExtensionMsTeamsVitestConfig } from "./vitest/vitest.extension-msteams.config.ts"; +import { createExtensionProviderOpenAiVitestConfig } from "./vitest/vitest.extension-provider-openai.config.ts"; import { createExtensionProvidersVitestConfig } from "./vitest/vitest.extension-providers.config.ts"; +import { createExtensionSignalVitestConfig } from "./vitest/vitest.extension-signal.config.ts"; +import { createExtensionSlackVitestConfig } from "./vitest/vitest.extension-slack.config.ts"; import { createExtensionTelegramVitestConfig } from "./vitest/vitest.extension-telegram.config.ts"; import { createExtensionVoiceCallVitestConfig } from "./vitest/vitest.extension-voice-call.config.ts"; import { createExtensionWhatsAppVitestConfig } from "./vitest/vitest.extension-whatsapp.config.ts"; @@ -183,14 +189,20 @@ describe("scoped vitest configs", () => { const defaultExtensionBlueBubblesConfig = createExtensionBlueBubblesVitestConfig({}); const defaultExtensionChannelsConfig = createExtensionChannelsVitestConfig({}); const defaultExtensionDiffsConfig = createExtensionDiffsVitestConfig({}); + const defaultExtensionDiscordConfig = createExtensionDiscordVitestConfig({}); const defaultExtensionFeishuConfig = createExtensionFeishuVitestConfig({}); + const defaultExtensionImessageConfig = createExtensionImessageVitestConfig({}); const defaultExtensionIrcConfig = createExtensionIrcVitestConfig({}); + const defaultExtensionLineConfig = createExtensionLineVitestConfig({}); const defaultExtensionMatrixConfig = createExtensionMatrixVitestConfig({}); const defaultExtensionMattermostConfig = createExtensionMattermostVitestConfig({}); const defaultExtensionMemoryConfig = createExtensionMemoryVitestConfig({}); const defaultExtensionMsTeamsConfig = createExtensionMsTeamsVitestConfig({}); const defaultExtensionMessagingConfig = createExtensionMessagingVitestConfig({}); + const defaultExtensionProviderOpenAiConfig = createExtensionProviderOpenAiVitestConfig({}); const defaultExtensionProvidersConfig = createExtensionProvidersVitestConfig({}); + const defaultExtensionSignalConfig = createExtensionSignalVitestConfig({}); + const defaultExtensionSlackConfig = createExtensionSlackVitestConfig({}); const defaultExtensionTelegramConfig = createExtensionTelegramVitestConfig({}); const defaultExtensionVoiceCallConfig = createExtensionVoiceCallVitestConfig({}); const defaultExtensionWhatsAppConfig = createExtensionWhatsAppVitestConfig({}); @@ -230,7 +242,13 @@ describe("scoped vitest configs", () => { defaultAcpConfig, defaultExtensionsConfig, defaultExtensionChannelsConfig, + defaultExtensionDiscordConfig, + defaultExtensionImessageConfig, + defaultExtensionLineConfig, + defaultExtensionProviderOpenAiConfig, defaultExtensionProvidersConfig, + defaultExtensionSignalConfig, + defaultExtensionSlackConfig, defaultInfraConfig, defaultAutoReplyConfig, defaultAutoReplyCoreConfig, @@ -337,17 +355,17 @@ describe("scoped vitest configs", () => { ); }); - it("normalizes extension channel include patterns relative to the scoped dir", () => { - expect(defaultExtensionChannelsConfig.test?.dir).toBe(path.join(process.cwd(), "extensions")); - expect(defaultExtensionChannelsConfig.test?.include).toEqual( - expect.arrayContaining([ - "discord/**/*.test.ts", - "line/**/*.test.ts", - "slack/**/*.test.ts", - "signal/**/*.test.ts", - "imessage/**/*.test.ts", - ]), - ); + it("normalizes split extension channel include patterns relative to the scoped dir", () => { + for (const [config, include] of [ + [defaultExtensionDiscordConfig, "discord/**/*.test.ts"], + [defaultExtensionLineConfig, "line/**/*.test.ts"], + [defaultExtensionSlackConfig, "slack/**/*.test.ts"], + [defaultExtensionSignalConfig, "signal/**/*.test.ts"], + [defaultExtensionImessageConfig, "imessage/**/*.test.ts"], + ] as const) { + expect(config.test?.dir).toBe(path.join(process.cwd(), "extensions")); + expect(config.test?.include).toEqual([include]); + } }); it("normalizes bluebubbles extension include patterns relative to the scoped dir", () => { @@ -385,8 +403,13 @@ describe("scoped vitest configs", () => { it("normalizes extension provider include patterns relative to the scoped dir", () => { expect(defaultExtensionProvidersConfig.test?.dir).toBe(path.join(process.cwd(), "extensions")); expect(defaultExtensionProvidersConfig.test?.include).toEqual( - expect.arrayContaining(["openai/**/*.test.ts", "xai/**/*.test.ts", "google/**/*.test.ts"]), + expect.arrayContaining(["xai/**/*.test.ts", "google/**/*.test.ts"]), ); + expect(defaultExtensionProvidersConfig.test?.include).not.toContain("openai/**/*.test.ts"); + expect(defaultExtensionProviderOpenAiConfig.test?.dir).toBe( + path.join(process.cwd(), "extensions"), + ); + expect(defaultExtensionProviderOpenAiConfig.test?.include).toEqual(["openai/**/*.test.ts"]); }); it("normalizes extension messaging include patterns relative to the scoped dir", () => { diff --git a/test/vitest/vitest.channel-paths.mjs b/test/vitest/vitest.channel-paths.mjs index b3336d14808..48912561873 100644 --- a/test/vitest/vitest.channel-paths.mjs +++ b/test/vitest/vitest.channel-paths.mjs @@ -1,8 +1,6 @@ import path from "node:path"; -import { - BUNDLED_PLUGIN_PATH_PREFIX, - bundledPluginRoot, -} from "../../scripts/lib/bundled-plugin-paths.mjs"; +import { BUNDLED_PLUGIN_PATH_PREFIX } from "../../scripts/lib/bundled-plugin-paths.mjs"; +import { splitChannelExtensionTestRoots } from "./vitest.extension-channel-split-paths.mjs"; const normalizeRepoPath = (value) => value.split(path.sep).join("/"); @@ -10,17 +8,13 @@ export const extensionRoutedChannelTestFiles = []; const extensionRoutedChannelTestFileSet = new Set(extensionRoutedChannelTestFiles); -export const channelTestRoots = [ - "src/channels", - bundledPluginRoot("discord"), - bundledPluginRoot("slack"), - bundledPluginRoot("signal"), - bundledPluginRoot("imessage"), - bundledPluginRoot("line"), -]; +export const channelTestRoots = ["src/channels", ...splitChannelExtensionTestRoots]; -export const extensionChannelTestRoots = channelTestRoots.filter((root) => - root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX), +const splitChannelExtensionTestRootSet = new Set(splitChannelExtensionTestRoots); + +export const extensionChannelTestRoots = channelTestRoots.filter( + (root) => + root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX) && !splitChannelExtensionTestRootSet.has(root), ); export const coreChannelTestRoots = channelTestRoots.filter( (root) => !root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX), diff --git a/test/vitest/vitest.config.ts b/test/vitest/vitest.config.ts index f3ec799ded9..3cc33fdd1b2 100644 --- a/test/vitest/vitest.config.ts +++ b/test/vitest/vitest.config.ts @@ -48,16 +48,21 @@ export const rootVitestProjects = [ "test/vitest/vitest.channels.config.ts", "test/vitest/vitest.extension-acpx.config.ts", "test/vitest/vitest.extension-bluebubbles.config.ts", - "test/vitest/vitest.extension-channels.config.ts", "test/vitest/vitest.extension-diffs.config.ts", + "test/vitest/vitest.extension-discord.config.ts", "test/vitest/vitest.extension-feishu.config.ts", + "test/vitest/vitest.extension-imessage.config.ts", "test/vitest/vitest.extension-irc.config.ts", + "test/vitest/vitest.extension-line.config.ts", "test/vitest/vitest.extension-mattermost.config.ts", "test/vitest/vitest.extension-matrix.config.ts", "test/vitest/vitest.extension-memory.config.ts", "test/vitest/vitest.extension-msteams.config.ts", "test/vitest/vitest.extension-messaging.config.ts", + "test/vitest/vitest.extension-provider-openai.config.ts", "test/vitest/vitest.extension-providers.config.ts", + "test/vitest/vitest.extension-signal.config.ts", + "test/vitest/vitest.extension-slack.config.ts", "test/vitest/vitest.extension-telegram.config.ts", "test/vitest/vitest.extension-voice-call.config.ts", "test/vitest/vitest.extension-whatsapp.config.ts", diff --git a/test/vitest/vitest.extension-channel-single-config.ts b/test/vitest/vitest.extension-channel-single-config.ts new file mode 100644 index 00000000000..0c4963a2668 --- /dev/null +++ b/test/vitest/vitest.extension-channel-single-config.ts @@ -0,0 +1,14 @@ +import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; + +export function createSingleChannelExtensionVitestConfig( + extensionId: string, + env: Record = process.env, +) { + return createScopedVitestConfig([`${extensionId}/**/*.test.ts`], { + dir: "extensions", + env, + name: `extension-${extensionId}`, + passWithNoTests: true, + setupFiles: ["test/setup.extensions.ts"], + }); +} diff --git a/test/vitest/vitest.extension-channel-split-paths.mjs b/test/vitest/vitest.extension-channel-split-paths.mjs new file mode 100644 index 00000000000..205927eeb99 --- /dev/null +++ b/test/vitest/vitest.extension-channel-split-paths.mjs @@ -0,0 +1,44 @@ +import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; + +export const splitChannelExtensionShardSpecs = [ + { + id: "discord", + kind: "extensionDiscord", + config: "test/vitest/vitest.extension-discord.config.ts", + }, + { + id: "slack", + kind: "extensionSlack", + config: "test/vitest/vitest.extension-slack.config.ts", + }, + { + id: "signal", + kind: "extensionSignal", + config: "test/vitest/vitest.extension-signal.config.ts", + }, + { + id: "imessage", + kind: "extensionImessage", + config: "test/vitest/vitest.extension-imessage.config.ts", + }, + { + id: "line", + kind: "extensionLine", + config: "test/vitest/vitest.extension-line.config.ts", + }, +]; + +export const splitChannelExtensionTestRoots = splitChannelExtensionShardSpecs.map((spec) => + bundledPluginRoot(spec.id), +); + +export function resolveSplitChannelExtensionShard(root) { + const normalizedRoot = root.replaceAll("\\", "/"); + return splitChannelExtensionShardSpecs.find( + (spec) => bundledPluginRoot(spec.id) === normalizedRoot, + ); +} + +export function isSplitChannelExtensionRoot(root) { + return Boolean(resolveSplitChannelExtensionShard(root)); +} diff --git a/test/vitest/vitest.extension-discord.config.ts b/test/vitest/vitest.extension-discord.config.ts new file mode 100644 index 00000000000..645d5bc3bc6 --- /dev/null +++ b/test/vitest/vitest.extension-discord.config.ts @@ -0,0 +1,9 @@ +import { createSingleChannelExtensionVitestConfig } from "./vitest.extension-channel-single-config.ts"; + +export function createExtensionDiscordVitestConfig( + env: Record = process.env, +) { + return createSingleChannelExtensionVitestConfig("discord", env); +} + +export default createExtensionDiscordVitestConfig(); diff --git a/test/vitest/vitest.extension-imessage.config.ts b/test/vitest/vitest.extension-imessage.config.ts new file mode 100644 index 00000000000..f5caccfd9f0 --- /dev/null +++ b/test/vitest/vitest.extension-imessage.config.ts @@ -0,0 +1,9 @@ +import { createSingleChannelExtensionVitestConfig } from "./vitest.extension-channel-single-config.ts"; + +export function createExtensionImessageVitestConfig( + env: Record = process.env, +) { + return createSingleChannelExtensionVitestConfig("imessage", env); +} + +export default createExtensionImessageVitestConfig(); diff --git a/test/vitest/vitest.extension-line.config.ts b/test/vitest/vitest.extension-line.config.ts new file mode 100644 index 00000000000..b09bf41c11e --- /dev/null +++ b/test/vitest/vitest.extension-line.config.ts @@ -0,0 +1,9 @@ +import { createSingleChannelExtensionVitestConfig } from "./vitest.extension-channel-single-config.ts"; + +export function createExtensionLineVitestConfig( + env: Record = process.env, +) { + return createSingleChannelExtensionVitestConfig("line", env); +} + +export default createExtensionLineVitestConfig(); diff --git a/test/vitest/vitest.extension-provider-openai.config.ts b/test/vitest/vitest.extension-provider-openai.config.ts new file mode 100644 index 00000000000..635ee045a04 --- /dev/null +++ b/test/vitest/vitest.extension-provider-openai.config.ts @@ -0,0 +1,27 @@ +import { providerOpenAiExtensionTestRoots } from "./vitest.extension-provider-paths.mjs"; +import { loadPatternListFromEnv } from "./vitest.pattern-file.ts"; +import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; + +export function loadIncludePatternsFromEnv( + env: Record = process.env, +): string[] | null { + return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env); +} + +export function createExtensionProviderOpenAiVitestConfig( + env: Record = process.env, +) { + return createScopedVitestConfig( + loadIncludePatternsFromEnv(env) ?? + providerOpenAiExtensionTestRoots.map((root) => `${root}/**/*.test.ts`), + { + dir: "extensions", + env, + name: "extension-provider-openai", + passWithNoTests: true, + setupFiles: ["test/setup.extensions.ts"], + }, + ); +} + +export default createExtensionProviderOpenAiVitestConfig(); diff --git a/test/vitest/vitest.extension-provider-paths.mjs b/test/vitest/vitest.extension-provider-paths.mjs index 2e60e311ad2..b06cccb8767 100644 --- a/test/vitest/vitest.extension-provider-paths.mjs +++ b/test/vitest/vitest.extension-provider-paths.mjs @@ -14,6 +14,7 @@ export const providerExtensionIds = [ "groq", "huggingface", "kimi-coding", + "lmstudio", "microsoft", "microsoft-foundry", "minimax", @@ -22,7 +23,6 @@ export const providerExtensionIds = [ "moonshot", "nvidia", "ollama", - "openai", "openrouter", "qianfan", "stepfun", @@ -33,8 +33,17 @@ export const providerExtensionIds = [ "zai", ]; +export const providerOpenAiExtensionIds = ["openai"]; + export const providerExtensionTestRoots = providerExtensionIds.map((id) => bundledPluginRoot(id)); +export const providerOpenAiExtensionTestRoots = providerOpenAiExtensionIds.map((id) => + bundledPluginRoot(id), +); export function isProviderExtensionRoot(root) { return providerExtensionTestRoots.includes(root); } + +export function isProviderOpenAiExtensionRoot(root) { + return providerOpenAiExtensionTestRoots.includes(root); +} diff --git a/test/vitest/vitest.extension-signal.config.ts b/test/vitest/vitest.extension-signal.config.ts new file mode 100644 index 00000000000..68231494e11 --- /dev/null +++ b/test/vitest/vitest.extension-signal.config.ts @@ -0,0 +1,9 @@ +import { createSingleChannelExtensionVitestConfig } from "./vitest.extension-channel-single-config.ts"; + +export function createExtensionSignalVitestConfig( + env: Record = process.env, +) { + return createSingleChannelExtensionVitestConfig("signal", env); +} + +export default createExtensionSignalVitestConfig(); diff --git a/test/vitest/vitest.extension-slack.config.ts b/test/vitest/vitest.extension-slack.config.ts new file mode 100644 index 00000000000..3245a859eb6 --- /dev/null +++ b/test/vitest/vitest.extension-slack.config.ts @@ -0,0 +1,9 @@ +import { createSingleChannelExtensionVitestConfig } from "./vitest.extension-channel-single-config.ts"; + +export function createExtensionSlackVitestConfig( + env: Record = process.env, +) { + return createSingleChannelExtensionVitestConfig("slack", env); +} + +export default createExtensionSlackVitestConfig(); diff --git a/test/vitest/vitest.extensions.config.ts b/test/vitest/vitest.extensions.config.ts index 876cb5b37ed..e65efab1b54 100644 --- a/test/vitest/vitest.extensions.config.ts +++ b/test/vitest/vitest.extensions.config.ts @@ -10,7 +10,10 @@ import { mattermostExtensionTestRoots } from "./vitest.extension-mattermost-path import { memoryExtensionTestRoots } from "./vitest.extension-memory-paths.mjs"; import { messagingExtensionTestRoots } from "./vitest.extension-messaging-paths.mjs"; import { msTeamsExtensionTestRoots } from "./vitest.extension-msteams-paths.mjs"; -import { providerExtensionTestRoots } from "./vitest.extension-provider-paths.mjs"; +import { + providerExtensionTestRoots, + providerOpenAiExtensionTestRoots, +} from "./vitest.extension-provider-paths.mjs"; import { telegramExtensionTestRoots } from "./vitest.extension-telegram-paths.mjs"; import { voiceCallExtensionTestRoots } from "./vitest.extension-voice-call-paths.mjs"; import { whatsAppExtensionTestRoots } from "./vitest.extension-whatsapp-paths.mjs"; @@ -47,6 +50,7 @@ export function createExtensionsVitestConfig( ...memoryExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), ...messagingExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), ...msTeamsExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), + ...providerOpenAiExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), ...providerExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), ...telegramExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), ...voiceCallExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), diff --git a/test/vitest/vitest.scoped-config.ts b/test/vitest/vitest.scoped-config.ts index 8575196b27d..342a4d096cc 100644 --- a/test/vitest/vitest.scoped-config.ts +++ b/test/vitest/vitest.scoped-config.ts @@ -61,14 +61,20 @@ const SCOPED_PROJECT_GROUP_ORDER_BY_NAME = new Map( "extension-bluebubbles", "extension-channels", "extension-diffs", + "extension-discord", "extension-feishu", + "extension-imessage", "extension-irc", + "extension-line", "extension-mattermost", "extension-matrix", "extension-memory", "extension-messaging", "extension-msteams", + "extension-provider-openai", "extension-providers", + "extension-signal", + "extension-slack", "extension-telegram", "extension-voice-call", "extension-whatsapp", diff --git a/test/vitest/vitest.shared.config.ts b/test/vitest/vitest.shared.config.ts index 398e022d5b5..86d3a050ee7 100644 --- a/test/vitest/vitest.shared.config.ts +++ b/test/vitest/vitest.shared.config.ts @@ -142,13 +142,18 @@ export const sharedVitestConfig = { "test/vitest/vitest.extension-acpx.config.ts", "test/vitest/vitest.extension-bluebubbles-paths.mjs", "test/vitest/vitest.extension-bluebubbles.config.ts", + "test/vitest/vitest.extension-channel-single-config.ts", + "test/vitest/vitest.extension-channel-split-paths.mjs", "test/vitest/vitest.extension-channels.config.ts", "test/vitest/vitest.extension-diffs-paths.mjs", "test/vitest/vitest.extension-diffs.config.ts", + "test/vitest/vitest.extension-discord.config.ts", "test/vitest/vitest.extension-feishu-paths.mjs", "test/vitest/vitest.extension-feishu.config.ts", + "test/vitest/vitest.extension-imessage.config.ts", "test/vitest/vitest.extension-irc-paths.mjs", "test/vitest/vitest.extension-irc.config.ts", + "test/vitest/vitest.extension-line.config.ts", "test/vitest/vitest.extension-mattermost-paths.mjs", "test/vitest/vitest.extension-mattermost.config.ts", "test/vitest/vitest.extension-matrix-paths.mjs", @@ -195,7 +200,10 @@ export const sharedVitestConfig = { "test/vitest/vitest.extension-zalo-paths.mjs", "test/vitest/vitest.extension-zalo.config.ts", "test/vitest/vitest.extension-provider-paths.mjs", + "test/vitest/vitest.extension-provider-openai.config.ts", "test/vitest/vitest.extension-providers.config.ts", + "test/vitest/vitest.extension-signal.config.ts", + "test/vitest/vitest.extension-slack.config.ts", "test/vitest/vitest.logging.config.ts", "test/vitest/vitest.process.config.ts", "test/vitest/vitest.tasks.config.ts", diff --git a/test/vitest/vitest.test-shards.mjs b/test/vitest/vitest.test-shards.mjs index 87c3ff7a489..260e3e55907 100644 --- a/test/vitest/vitest.test-shards.mjs +++ b/test/vitest/vitest.test-shards.mjs @@ -103,16 +103,21 @@ export const fullSuiteVitestShards = [ projects: [ "test/vitest/vitest.extension-acpx.config.ts", "test/vitest/vitest.extension-bluebubbles.config.ts", - "test/vitest/vitest.extension-channels.config.ts", "test/vitest/vitest.extension-diffs.config.ts", + "test/vitest/vitest.extension-discord.config.ts", "test/vitest/vitest.extension-feishu.config.ts", + "test/vitest/vitest.extension-imessage.config.ts", "test/vitest/vitest.extension-irc.config.ts", + "test/vitest/vitest.extension-line.config.ts", "test/vitest/vitest.extension-mattermost.config.ts", "test/vitest/vitest.extension-matrix.config.ts", "test/vitest/vitest.extension-memory.config.ts", "test/vitest/vitest.extension-messaging.config.ts", "test/vitest/vitest.extension-msteams.config.ts", + "test/vitest/vitest.extension-provider-openai.config.ts", "test/vitest/vitest.extension-providers.config.ts", + "test/vitest/vitest.extension-signal.config.ts", + "test/vitest/vitest.extension-slack.config.ts", "test/vitest/vitest.extension-telegram.config.ts", "test/vitest/vitest.extension-voice-call.config.ts", "test/vitest/vitest.extension-whatsapp.config.ts",