diff --git a/scripts/changed-lanes.mjs b/scripts/changed-lanes.mjs index a891f4b3f37..f4f2a09b3b1 100644 --- a/scripts/changed-lanes.mjs +++ b/scripts/changed-lanes.mjs @@ -18,8 +18,21 @@ const TOOLING_PATH_RE = const ROOT_GLOBAL_PATH_RE = /^(?:package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsdown\.config\.ts$|vitest\.config\.ts$)/u; const LEGACY_ROOT_ASSET_PATH_RE = /^assets\//u; -const LIVE_DOCKER_TOOLING_PATH_RE = - /^(?:scripts\/test-docker-all\.mjs|scripts\/test-docker-all\.sh|scripts\/lib\/live-docker-auth\.sh|scripts\/test-live-(?:acp-bind|cli-backend|codex-harness|gateway-models|models)-docker\.sh|src\/gateway\/gateway-acp-bind\.live\.test\.ts|src\/gateway\/live-agent-probes\.test\.ts)$/u; +export const LIVE_DOCKER_AUTH_SHELL_TARGETS = [ + "scripts/lib/live-docker-auth.sh", + "scripts/test-live-acp-bind-docker.sh", + "scripts/test-live-cli-backend-docker.sh", + "scripts/test-live-codex-harness-docker.sh", + "scripts/test-live-gateway-models-docker.sh", + "scripts/test-live-models-docker.sh", + "scripts/test-live-subagent-announce-docker.sh", +]; +const LIVE_DOCKER_TOOLING_PATHS = new Set([ + ...LIVE_DOCKER_AUTH_SHELL_TARGETS, + "scripts/test-docker-all.mjs", + "src/gateway/gateway-acp-bind.live.test.ts", + "src/gateway/live-agent-probes.test.ts", +]); const LIVE_DOCKER_PACKAGE_SCRIPT_RE = /^test:docker:live-[\w:-]+$/u; const TEST_PATH_RE = /(?:^|\/)(?:test|__tests__)\/|(?:\.|\/)(?:test|spec|e2e|browser\.test)\.[cm]?[jt]sx?$/u; @@ -145,7 +158,7 @@ export function detectChangedLanes(changedPaths, options = {}) { continue; } - if (LIVE_DOCKER_TOOLING_PATH_RE.test(changedPath)) { + if (LIVE_DOCKER_TOOLING_PATHS.has(changedPath)) { lanes.liveDockerTooling = true; reasons.push(`${changedPath}: live Docker tooling surface`); continue; diff --git a/scripts/check-changed.mjs b/scripts/check-changed.mjs index 6c576be372d..35c44d3f107 100644 --- a/scripts/check-changed.mjs +++ b/scripts/check-changed.mjs @@ -12,6 +12,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { performance } from "node:perf_hooks"; import { + LIVE_DOCKER_AUTH_SHELL_TARGETS, detectChangedLanesForPaths, isChangedLaneTestPath, listChangedPathsFromGit, @@ -29,15 +30,6 @@ import { import { runManagedCommand } from "./lib/managed-child-process.mjs"; import { createSparseTsgoSkipEnv } from "./lib/tsgo-sparse-guard.mjs"; -const LIVE_DOCKER_AUTH_SHELL_TARGETS = [ - "scripts/lib/live-docker-auth.sh", - "scripts/test-live-acp-bind-docker.sh", - "scripts/test-live-cli-backend-docker.sh", - "scripts/test-live-codex-harness-docker.sh", - "scripts/test-live-gateway-models-docker.sh", - "scripts/test-live-models-docker.sh", - "scripts/test-live-subagent-announce-docker.sh", -]; const SHRINKWRAP_POLICY_PATH_RE = /^(?:npm-shrinkwrap\.json|package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|scripts\/generate-npm-shrinkwrap\.mjs|extensions\/[^/]+\/(?:package\.json|npm-shrinkwrap\.json))$/u; const PROMPT_SNAPSHOT_CHECK_PATH_RE = diff --git a/scripts/lib/extension-test-plan.mjs b/scripts/lib/extension-test-plan.mjs index b7412778b5d..957886f5206 100644 --- a/scripts/lib/extension-test-plan.mjs +++ b/scripts/lib/extension-test-plan.mjs @@ -4,6 +4,7 @@ import fs from "node:fs"; 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 { isActiveMemoryExtensionRoot } from "../../test/vitest/vitest.extension-active-memory-paths.mjs"; import { isBrowserExtensionRoot } from "../../test/vitest/vitest.extension-browser-paths.mjs"; import { resolveSplitChannelExtensionShard } from "../../test/vitest/vitest.extension-channel-split-paths.mjs"; import { isCodexExtensionRoot } from "../../test/vitest/vitest.extension-codex-paths.mjs"; @@ -65,6 +66,30 @@ const EXTENSION_TEST_COST_MULTIPLIERS = { // overstates its real wall-clock cost during CI shard planning. "test/vitest/vitest.extensions.config.ts": 1.1, }; +const EXTENSION_TEST_CONFIG_ROUTES = [ + [(root) => channelTestRoots.includes(root), "test/vitest/vitest.extension-channels.config.ts"], + [isActiveMemoryExtensionRoot, "test/vitest/vitest.extension-active-memory.config.ts"], + [isAcpxExtensionRoot, "test/vitest/vitest.extension-acpx.config.ts"], + [isBrowserExtensionRoot, "test/vitest/vitest.extension-browser.config.ts"], + [isCodexExtensionRoot, "test/vitest/vitest.extension-codex.config.ts"], + [isDiffsExtensionRoot, "test/vitest/vitest.extension-diffs.config.ts"], + [isFeishuExtensionRoot, "test/vitest/vitest.extension-feishu.config.ts"], + [isIrcExtensionRoot, "test/vitest/vitest.extension-irc.config.ts"], + [isMattermostExtensionRoot, "test/vitest/vitest.extension-mattermost.config.ts"], + [isMatrixExtensionRoot, "test/vitest/vitest.extension-matrix.config.ts"], + [isMediaExtensionRoot, "test/vitest/vitest.extension-media.config.ts"], + [isMemoryExtensionRoot, "test/vitest/vitest.extension-memory.config.ts"], + [isMessagingExtensionRoot, "test/vitest/vitest.extension-messaging.config.ts"], + [isMiscExtensionRoot, "test/vitest/vitest.extension-misc.config.ts"], + [isMsTeamsExtensionRoot, "test/vitest/vitest.extension-msteams.config.ts"], + [isQaExtensionRoot, "test/vitest/vitest.extension-qa.config.ts"], + [isTelegramExtensionRoot, "test/vitest/vitest.extension-telegram.config.ts"], + [isVoiceCallExtensionRoot, "test/vitest/vitest.extension-voice-call.config.ts"], + [isWhatsAppExtensionRoot, "test/vitest/vitest.extension-whatsapp.config.ts"], + [isZaloExtensionRoot, "test/vitest/vitest.extension-zalo.config.ts"], + [isProviderOpenAiExtensionRoot, "test/vitest/vitest.extension-provider-openai.config.ts"], + [isProviderExtensionRoot, "test/vitest/vitest.extension-providers.config.ts"], +]; function normalizeRelative(inputPath) { return inputPath.split(path.sep).join("/"); @@ -181,6 +206,18 @@ function estimatePlanCost(config, testFileCount) { return Math.max(1, Math.ceil(testFileCount * multiplier)); } +/** Resolve the dedicated Vitest config for an extension root. */ +export function resolveExtensionTestConfig(root) { + const splitChannelShard = resolveSplitChannelExtensionShard(root); + if (splitChannelShard) { + return splitChannelShard.config; + } + return ( + EXTENSION_TEST_CONFIG_ROUTES.find(([matches]) => matches(root))?.[1] ?? + "test/vitest/vitest.extensions.config.ts" + ); +} + function resolveExtensionDirectory(targetArg, cwd = process.cwd()) { if (targetArg) { const asGiven = path.resolve(cwd, targetArg); @@ -228,73 +265,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 usesBrowserConfig = roots.some((root) => isBrowserExtensionRoot(root)); - const usesCodexConfig = roots.some((root) => isCodexExtensionRoot(root)); - const usesDiffsConfig = roots.some((root) => isDiffsExtensionRoot(root)); - const usesFeishuConfig = roots.some((root) => isFeishuExtensionRoot(root)); - const usesIrcConfig = roots.some((root) => isIrcExtensionRoot(root)); - const usesMattermostConfig = roots.some((root) => isMattermostExtensionRoot(root)); - const usesMediaConfig = roots.some((root) => isMediaExtensionRoot(root)); - const usesMiscConfig = roots.some((root) => isMiscExtensionRoot(root)); - const usesTelegramConfig = roots.some((root) => isTelegramExtensionRoot(root)); - const usesVoiceCallConfig = roots.some((root) => isVoiceCallExtensionRoot(root)); - const usesWhatsAppConfig = roots.some((root) => isWhatsAppExtensionRoot(root)); - const usesZaloConfig = roots.some((root) => isZaloExtensionRoot(root)); - const usesMatrixConfig = roots.some((root) => isMatrixExtensionRoot(root)); - const usesQaConfig = roots.some((root) => isQaExtensionRoot(root)); - 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 = splitChannelShard - ? splitChannelShard.config - : usesChannelConfig - ? "test/vitest/vitest.extension-channels.config.ts" - : usesAcpxConfig - ? "test/vitest/vitest.extension-acpx.config.ts" - : usesBrowserConfig - ? "test/vitest/vitest.extension-browser.config.ts" - : usesCodexConfig - ? "test/vitest/vitest.extension-codex.config.ts" - : usesDiffsConfig - ? "test/vitest/vitest.extension-diffs.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" - : usesMediaConfig - ? "test/vitest/vitest.extension-media.config.ts" - : usesMemoryConfig - ? "test/vitest/vitest.extension-memory.config.ts" - : usesMessagingConfig - ? "test/vitest/vitest.extension-messaging.config.ts" - : usesMiscConfig - ? "test/vitest/vitest.extension-misc.config.ts" - : usesMsTeamsConfig - ? "test/vitest/vitest.extension-msteams.config.ts" - : usesQaConfig - ? "test/vitest/vitest.extension-qa.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" - : usesProviderOpenAiConfig - ? "test/vitest/vitest.extension-provider-openai.config.ts" - : usesProviderConfig - ? "test/vitest/vitest.extension-providers.config.ts" - : "test/vitest/vitest.extensions.config.ts"; + const config = resolveExtensionTestConfig(relativeExtensionDir); const testFileCount = roots.reduce( (sum, root) => sum + countTestFiles(path.join(repoRoot, root)), 0, diff --git a/scripts/test-docker-all.sh b/scripts/test-docker-all.sh deleted file mode 100644 index 46211ff5e9f..00000000000 --- a/scripts/test-docker-all.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -exec node "$ROOT_DIR/scripts/test-docker-all.mjs" "$@" diff --git a/scripts/test-live-acp-spawn-defaults-docker.sh b/scripts/test-live-acp-spawn-defaults-docker.sh deleted file mode 100755 index d5e53c32304..00000000000 --- a/scripts/test-live-acp-spawn-defaults-docker.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -if [[ -n "${OPENCLAW_LIVE_ACP_BIND_AGENTS:-}" && "${OPENCLAW_LIVE_ACP_BIND_AGENTS}" != "codex" ]]; then - echo "ERROR: ACP spawn defaults Docker test supports only OPENCLAW_LIVE_ACP_BIND_AGENTS=codex." >&2 - exit 1 -fi - -export OPENCLAW_LIVE_ACP_BIND_AGENTS=codex -export OPENCLAW_LIVE_ACP_BIND_TEST_FILES="${OPENCLAW_LIVE_ACP_BIND_TEST_FILES:-src/gateway/gateway-acp-spawn-defaults.live.test.ts}" -export OPENCLAW_LIVE_ACP_SPAWN_DEFAULTS=1 -export OPENCLAW_LIVE_ACP_SPAWN_DEFAULTS_MODEL="${OPENCLAW_LIVE_ACP_SPAWN_DEFAULTS_MODEL:-openai/gpt-5.5}" -export OPENCLAW_LIVE_ACP_SPAWN_DEFAULTS_THINKING="${OPENCLAW_LIVE_ACP_SPAWN_DEFAULTS_THINKING:-high}" -export OPENCLAW_LIVE_ACP_BIND_CODEX_MODEL="${OPENCLAW_LIVE_ACP_BIND_CODEX_MODEL:-gpt-5.5}" - -exec bash "$SCRIPT_DIR/test-live-acp-bind-docker.sh" diff --git a/scripts/test-live.mjs b/scripts/test-live.mjs index 59f8b2e3449..d3d7bdad193 100644 --- a/scripts/test-live.mjs +++ b/scripts/test-live.mjs @@ -1,5 +1,7 @@ // Runs the full live Vitest suite with live-test env and heartbeat output. +import { terminateManagedChild } from "./lib/managed-child-process.mjs"; import { spawnPnpmRunner } from "./pnpm-runner.mjs"; +import { resolveVitestNoOutputTimeoutMs } from "./run-vitest.mjs"; import { forwardSignalToVitestProcessGroup, installVitestProcessGroupCleanup, @@ -103,6 +105,13 @@ export function resolveTestLiveHeartbeatMs(baseEnv = process.env) { return parsed; } +/** + * Reads the live-test no-output timeout using the shared Vitest watchdog contract. + */ +export function resolveTestLiveNoOutputTimeoutMs(baseEnv = process.env) { + return resolveVitestNoOutputTimeoutMs(baseEnv); +} + /** * Builds pnpm/vitest args for full live test execution. */ @@ -140,8 +149,11 @@ export function main(argv = process.argv.slice(2), baseEnv = process.env) { const env = buildTestLiveEnv(args, baseEnv); const heartbeatMs = resolveTestLiveHeartbeatMs(baseEnv); + const noOutputTimeoutMs = resolveTestLiveNoOutputTimeoutMs(baseEnv); const startedAt = Date.now(); let lastOutputAt = startedAt; + let lastHeartbeatAt = startedAt; + let timedOut = false; const child = spawnPnpmRunner({ pnpmArgs: buildTestLivePnpmArgs(args), @@ -164,6 +176,7 @@ export function main(argv = process.argv.slice(2), baseEnv = process.env) { const noteOutput = () => { lastOutputAt = Date.now(); + lastHeartbeatAt = lastOutputAt; }; child.stdout?.on("data", (chunk) => { @@ -176,18 +189,31 @@ export function main(argv = process.argv.slice(2), baseEnv = process.env) { process.stderr.write(chunk); }); - const heartbeat = setInterval(() => { - const now = Date.now(); - if (now - lastOutputAt < heartbeatMs) { - return; - } - const elapsedSec = Math.max(1, Math.round((now - startedAt) / 1_000)); - const quietSec = Math.max(1, Math.round((now - lastOutputAt) / 1_000)); - process.stderr.write( - `[test:live] still running (${elapsedSec}s elapsed, ${quietSec}s since last output)\n`, - ); - lastOutputAt = now; - }, heartbeatMs); + const heartbeat = setInterval( + () => { + const now = Date.now(); + const quietMs = now - lastOutputAt; + if (noOutputTimeoutMs !== null && quietMs >= noOutputTimeoutMs) { + timedOut = true; + clearInterval(heartbeat); + process.stderr.write( + `[test:live] no output for ${noOutputTimeoutMs}ms; terminating stalled Vitest process group\n`, + ); + terminateManagedChild(child, "SIGKILL"); + return; + } + if (quietMs < heartbeatMs || now - lastHeartbeatAt < heartbeatMs) { + return; + } + const elapsedSec = Math.max(1, Math.round((now - startedAt) / 1_000)); + const quietSec = Math.max(1, Math.round(quietMs / 1_000)); + process.stderr.write( + `[test:live] still running (${elapsedSec}s elapsed, ${quietSec}s since last output)\n`, + ); + lastHeartbeatAt = now; + }, + Math.min(heartbeatMs, noOutputTimeoutMs ?? heartbeatMs), + ); heartbeat.unref?.(); child.on("exit", (code, signal) => { @@ -201,6 +227,10 @@ export function main(argv = process.argv.slice(2), baseEnv = process.env) { process.kill(process.pid, forwardedSignal); return; } + if (timedOut) { + process.exit(1); + return; + } if (signal) { process.stderr.write(`[test:live] vitest exited via signal=${signal}\n`); process.kill(process.pid, signal); diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 502c14eb78a..92b79fe0e01 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -3754,6 +3754,12 @@ function classifyTarget(arg, cwd) { if (isPathAtOrUnder(relative, "src/gateway")) { return "gateway"; } + if ( + isPathAtOrUnder(relative, "packages/gateway-client") || + isPathAtOrUnder(relative, "packages/gateway-protocol") + ) { + return "gatewayClient"; + } if (isPathAtOrUnder(relative, "src/hooks")) { return "hooks"; } diff --git a/test/scripts/changed-lanes.test.ts b/test/scripts/changed-lanes.test.ts index 1ba5dad6d56..37e495257cb 100644 --- a/test/scripts/changed-lanes.test.ts +++ b/test/scripts/changed-lanes.test.ts @@ -547,6 +547,12 @@ describe("scripts/changed-lanes", () => { expect(result.lanes.all).toBe(false); }); + it("routes a subagent-announce-only Docker diff through the live Docker lane", () => { + const result = detectChangedLanes(["scripts/test-live-subagent-announce-docker.sh"]); + + expectLanes(result.lanes, { liveDockerTooling: true }); + }); + it("exposes the shared changed-lane test path classifier", () => { expect(isChangedLaneTestPath("src/shared/string-normalization.test.ts")).toBe(true); expect(isChangedLaneTestPath("packages/foo/__tests__/helper.ts")).toBe(true); @@ -1354,23 +1360,18 @@ describe("scripts/changed-lanes", () => { "config:docs:check", "deps:root-ownership:check", ]); - expect(plan.commands.find((command) => command.args[0] === "release-metadata:check")?.args).toEqual([ - "release-metadata:check", - "--staged", - ]); + expect( + plan.commands.find((command) => command.args[0] === "release-metadata:check")?.args, + ).toEqual(["release-metadata:check", "--staged"]); }); it("passes release metadata base and head refs as options", () => { const result = detectChangedLanes(["CHANGELOG.md"]); const plan = createChangedCheckPlan(result, { base: "main", head: "feature" }); - expect(plan.commands.find((command) => command.args[0] === "release-metadata:check")?.args).toEqual([ - "release-metadata:check", - "--base", - "main", - "--head", - "feature", - ]); + expect( + plan.commands.find((command) => command.args[0] === "release-metadata:check")?.args, + ).toEqual(["release-metadata:check", "--base", "main", "--head", "feature"]); }); it("keeps docs plus changelog entries on the docs-only changed gate", () => { diff --git a/test/scripts/test-extension.test.ts b/test/scripts/test-extension.test.ts index e964a0089c7..07fb70d4b35 100644 --- a/test/scripts/test-extension.test.ts +++ b/test/scripts/test-extension.test.ts @@ -15,6 +15,7 @@ import { DEFAULT_EXTENSION_TEST_SHARD_COUNT, createExtensionTestShards, resolveExtensionBatchPlan, + resolveExtensionTestConfig, resolveExtensionTestPlan, } from "../../scripts/lib/extension-test-plan.mjs"; import { relativizeExtensionVitestArgs } from "../../scripts/lib/extension-vitest-paths.mjs"; @@ -26,6 +27,7 @@ import { runExtensionBatchPlan, } from "../../scripts/test-extension-batch.mjs"; import { expectNoNodeFsScans } from "../../src/test-utils/fs-scan-assertions.js"; +import { extensionCatchAllExcludedTestRoots } from "../vitest/vitest.extensions.config.ts"; const scriptPath = path.join(process.cwd(), "scripts", "test-extension.mjs"); const posixIt = process.platform === "win32" ? it.skip : it; @@ -86,6 +88,13 @@ describe("scripts/test-extension.mjs", () => { ); }); + it.each(extensionCatchAllExcludedTestRoots)( + "routes catch-all-excluded extension root %s to a dedicated config", + (root) => { + expect(resolveExtensionTestConfig(root)).not.toBe("test/vitest/vitest.extensions.config.ts"); + }, + ); + it("resolves split channel extensions onto their own vitest configs", () => { const plan = resolveExtensionTestPlan({ targetArg: "slack", cwd: process.cwd() }); diff --git a/test/scripts/test-live.test.ts b/test/scripts/test-live.test.ts index cee9e53c4b8..36ca01dbfc7 100644 --- a/test/scripts/test-live.test.ts +++ b/test/scripts/test-live.test.ts @@ -134,6 +134,56 @@ describe("scripts/test-live", () => { } }); + posixIt("kills the live pnpm process group after the no-output timeout", async () => { + const root = mkdtempSync(join(tmpdir(), "openclaw-test-live-timeout-")); + const fakePnpmPath = join(root, "pnpm"); + const childPidPath = join(root, "child.pid"); + const descendantPidPath = join(root, "descendant.pid"); + const stderr: Buffer[] = []; + + writeFakePnpm(fakePnpmPath); + const runner = spawn(process.execPath, ["scripts/test-live.mjs", "--", "fake.live.test.ts"], { + env: { + ...process.env, + OPENCLAW_FAKE_PNPM_PID_PATH: childPidPath, + OPENCLAW_FAKE_PNPM_DESCENDANT_PID_PATH: descendantPidPath, + OPENCLAW_LIVE_WRAPPER_HEARTBEAT_MS: "25", + OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "100", + npm_execpath: fakePnpmPath, + }, + stdio: ["ignore", "ignore", "pipe"], + }); + runner.stderr.on("data", (chunk) => stderr.push(chunk)); + let childPid = 0; + let descendantPid = 0; + + try { + await waitFor(() => fileExists(childPidPath), 5_000); + await waitFor(() => fileExists(descendantPidPath), 5_000); + childPid = Number(readFileSync(childPidPath, "utf8")); + descendantPid = Number(readFileSync(descendantPidPath, "utf8")); + + expect(await waitForClose(runner)).toEqual({ code: 1, signal: null }); + expect(Buffer.concat(stderr).toString("utf8")).toContain( + "no output for 100ms; terminating stalled Vitest process group", + ); + expect(Buffer.concat(stderr).toString("utf8")).toContain("[test:live] still running"); + await waitFor(() => !isProcessAlive(childPid), 5_000); + await waitFor(() => !isProcessAlive(descendantPid), 5_000); + } finally { + if (runner.pid && isProcessAlive(runner.pid)) { + process.kill(runner.pid, "SIGKILL"); + } + if (childPid && isProcessAlive(childPid)) { + process.kill(childPid, "SIGKILL"); + } + if (descendantPid && isProcessAlive(descendantPid)) { + process.kill(descendantPid, "SIGKILL"); + } + rmSync(root, { force: true, recursive: true }); + } + }); + it("rejects loose heartbeat intervals instead of parsing prefixes", () => { expect(resolveTestLiveHeartbeatMs({})).toBe(20_000); expect(resolveTestLiveHeartbeatMs({ OPENCLAW_LIVE_WRAPPER_HEARTBEAT_MS: "2500" })).toBe(2500); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index ede1ce7854f..410c0445b9f 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -2314,6 +2314,25 @@ describe("scripts/test-projects changed-target routing", () => { ]); }); + it("routes gateway package targets through the gateway-client lane", () => { + expect( + buildVitestRunPlans([ + "packages/gateway-client/src/timeouts.test.ts", + "packages/gateway-protocol/src/frame-guards.test.ts", + ]), + ).toEqual([ + { + config: "test/vitest/vitest.gateway-client.config.ts", + forwardedArgs: [], + includePatterns: [ + "packages/gateway-client/src/timeouts.test.ts", + "packages/gateway-protocol/src/frame-guards.test.ts", + ], + watchMode: false, + }, + ]); + }); + it("routes explicit imported source files through import-graph tests", () => { let plans: ReturnType = []; withTinyGitRepo( diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index fc04fccd09c..8f191345500 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -97,8 +97,14 @@ describe("projects vitest config", () => { }); it("disables vite env-file loading for vitest lanes", () => { - expect(baseConfig.envFile).toBe(false); - expect(sharedVitestConfig.envFile).toBe(false); + expect(baseConfig.envDir).toBe(false); + expect(sharedVitestConfig.envDir).toBe(false); + }); + + it("uses absolute force-rerun triggers for discovered vitest lane files", () => { + expect(sharedVitestConfig.test.forceRerunTriggers.map(normalizeConfigPath)).toContain( + normalizeConfigPath(`${process.cwd()}/test/vitest/vitest.config.ts`), + ); }); it("keeps root projects on their expected pool defaults", () => { diff --git a/test/vitest-unit-paths.test.ts b/test/vitest-unit-paths.test.ts index 97ff4b75575..6db869e72c7 100644 --- a/test/vitest-unit-paths.test.ts +++ b/test/vitest-unit-paths.test.ts @@ -9,6 +9,8 @@ describe("isUnitConfigTestFile", () => { }); it("rejects files excluded from the unit config", () => { + expect(isUnitConfigTestFile("packages/gateway-client/src/index.test.ts")).toBe(false); + expect(isUnitConfigTestFile("packages/gateway-protocol/src/index.test.ts")).toBe(false); expect( isUnitConfigTestFile( bundledPluginFile("imessage", "src/monitor.shutdown.unhandled-rejection.test.ts"), diff --git a/test/vitest/vitest.extension-clickclack.config.ts b/test/vitest/vitest.extension-clickclack.config.ts deleted file mode 100644 index 6e725ea8e53..00000000000 --- a/test/vitest/vitest.extension-clickclack.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Vitest extension clickclack config wires the extension clickclack test shard. -import { createSingleChannelExtensionVitestConfig } from "./vitest.extension-channel-single-config.ts"; - -function createExtensionClickClackVitestConfig( - env: Record = process.env, -) { - return createSingleChannelExtensionVitestConfig("clickclack", env); -} - -export default createExtensionClickClackVitestConfig(); diff --git a/test/vitest/vitest.extensions.config.ts b/test/vitest/vitest.extensions.config.ts index dc925c35111..dcb84921ef7 100644 --- a/test/vitest/vitest.extensions.config.ts +++ b/test/vitest/vitest.extensions.config.ts @@ -27,6 +27,30 @@ import { zaloExtensionTestRoots } from "./vitest.extension-zalo-paths.mjs"; import { loadPatternListFromEnv } from "./vitest.pattern-file.ts"; import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; +export const extensionCatchAllExcludedTestRoots = [ + activeMemoryExtensionTestRoots, + acpxExtensionTestRoots, + browserExtensionTestRoots, + codexExtensionTestRoots, + diffsExtensionTestRoots, + feishuExtensionTestRoots, + ircExtensionTestRoots, + matrixExtensionTestRoots, + mattermostExtensionTestRoots, + mediaExtensionTestRoots, + memoryExtensionTestRoots, + messagingExtensionTestRoots, + miscExtensionTestRoots, + msTeamsExtensionTestRoots, + providerOpenAiExtensionTestRoots, + providerExtensionTestRoots, + qaExtensionTestRoots, + telegramExtensionTestRoots, + voiceCallExtensionTestRoots, + whatsAppExtensionTestRoots, + zaloExtensionTestRoots, +].flat(); + export function loadIncludePatternsFromEnv( env: Record = process.env, ): string[] | null { @@ -46,27 +70,9 @@ export function createExtensionsVitestConfig( // out of the shared extensions lane. exclude: [ ...extensionExcludedChannelTestGlobs, - ...activeMemoryExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...acpxExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...browserExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...codexExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...diffsExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...feishuExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...ircExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...matrixExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...mattermostExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...mediaExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...memoryExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...messagingExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...miscExtensionTestRoots.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, "")}/**`), - ...qaExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...telegramExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...voiceCallExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...whatsAppExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), - ...zaloExtensionTestRoots.map((root) => `${root.replace(/^extensions\//u, "")}/**`), + ...extensionCatchAllExcludedTestRoots.map( + (root) => `${root.replace(/^extensions\//u, "")}/**`, + ), ], }); } diff --git a/test/vitest/vitest.full-core-unit.config.ts b/test/vitest/vitest.full-core-unit.config.ts deleted file mode 100644 index 3efc5e5b211..00000000000 --- a/test/vitest/vitest.full-core-unit.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Vitest full core unit config wires the full core unit test shard. -import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; -import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; - -export default createProjectShardVitestConfig(fullSuiteVitestShards[0].projects); diff --git a/test/vitest/vitest.shared.config.ts b/test/vitest/vitest.shared.config.ts index 8d8b7ab2f54..a36c4cc7742 100644 --- a/test/vitest/vitest.shared.config.ts +++ b/test/vitest/vitest.shared.config.ts @@ -1,4 +1,5 @@ // Vitest shared config wires the shared test shard. +import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import acpCorePackageJson from "../../packages/acp-core/package.json" with { type: "json" }; @@ -62,6 +63,11 @@ export function resolveDefaultVitestPool( export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); export const nonIsolatedRunnerPath = path.join(repoRoot, "test", "non-isolated-runner.ts"); +const vitestConfigFiles = fs + .readdirSync(path.join(repoRoot, "test", "vitest"), { withFileTypes: true }) + .filter((entry) => entry.isFile() && /\.(?:mjs|ts)$/u.test(entry.name)) + .map((entry) => `test/vitest/${entry.name}`) + .toSorted((left, right) => left.localeCompare(right)); export function resolveRepoRootPath(value: string): string { return path.isAbsolute(value) ? value : path.join(repoRoot, value); } @@ -151,7 +157,7 @@ if (!isCI && localScheduling.throttledBySystem && shouldPrintVitestThrottle(proc export const sharedVitestConfig = { root: repoRoot, - envFile: false, + envDir: false as const, resolve: { alias: [ { @@ -499,110 +505,22 @@ export const sharedVitestConfig = { external: dependencyExternalPatterns, }, }, + // Vitest matches these with picomatch against absolute changed-file paths, so every entry + // must resolve absolute; relative entries silently never match. Explicit lane files keep + // watcher registration working (chokidar v4+ ignores globs in watcher.add) while the glob + // keeps match coverage for files added after config load. forceRerunTriggers: [ "package.json", "pnpm-lock.yaml", + "vitest.config.ts", "test/setup.ts", "test/setup.env.ts", "test/setup.shared.ts", "test/setup.extensions.ts", "test/setup-openclaw-runtime.ts", - "test/vitest/vitest.channel-paths.mjs", - "test/vitest/vitest.agents-paths.mjs", - "test/vitest/vitest.agents-core.config.ts", - "test/vitest/vitest.agents-embedded-agent.config.ts", - "test/vitest/vitest.agents-support.config.ts", - "test/vitest/vitest.agents-tools.config.ts", - "test/vitest/vitest.channels.config.ts", - "test/vitest/vitest.acp.config.ts", - "test/vitest/vitest.boundary.config.ts", - "test/vitest/vitest.bundled.config.ts", - "test/vitest/vitest.cli.config.ts", - "vitest.config.ts", - "test/vitest/vitest.contracts-shared.ts", - "test/vitest/vitest.contracts-channel-surface.config.ts", - "test/vitest/vitest.contracts-channel-config.config.ts", - "test/vitest/vitest.contracts-channel-registry.config.ts", - "test/vitest/vitest.contracts-channel-session.config.ts", - "test/vitest/vitest.contracts-plugin.config.ts", - "test/vitest/vitest.cron.config.ts", - "test/vitest/vitest.daemon.config.ts", - "test/vitest/vitest.e2e.config.ts", - "test/vitest/vitest.extension-acpx-paths.mjs", - "test/vitest/vitest.extension-acpx.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-active-memory-paths.mjs", - "test/vitest/vitest.extension-active-memory.config.ts", - "test/vitest/vitest.extension-codex-paths.mjs", - "test/vitest/vitest.extension-codex.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", - "test/vitest/vitest.extension-matrix.config.ts", - "test/vitest/vitest.extension-memory-paths.mjs", - "test/vitest/vitest.extension-memory.config.ts", - "test/vitest/vitest.extension-messaging-paths.mjs", - "test/vitest/vitest.extension-messaging.config.ts", - "test/vitest/vitest.extension-msteams-paths.mjs", - "test/vitest/vitest.extension-msteams.config.ts", - "test/vitest/vitest.extensions.config.ts", - "test/vitest/vitest.gateway.config.ts", - "test/vitest/vitest.gateway-core.config.ts", - "test/vitest/vitest.gateway-client.config.ts", - "test/vitest/vitest.gateway-methods.config.ts", - "test/vitest/vitest.gateway-server.config.ts", - "test/vitest/vitest.hooks.config.ts", - "test/vitest/vitest.infra.config.ts", - "test/vitest/vitest.live.config.ts", - "test/vitest/vitest.media.config.ts", - "test/vitest/vitest.media-understanding.config.ts", - "test/vitest/vitest.performance-config.ts", - "test/vitest/vitest.unit-fast.config.ts", - "test/vitest/vitest.unit-fast-fake-timers.config.ts", - "test/vitest/vitest.unit-fast-paths.mjs", - "test/vitest/vitest.scoped-config.ts", - "test/vitest/vitest.shared-core.config.ts", - "test/vitest/vitest.shared.config.ts", - "test/vitest/vitest.tooling-isolated.config.ts", - "test/vitest/vitest.tooling.config.ts", - "test/vitest/vitest.tui.config.ts", - "test/vitest/vitest.ui.config.ts", - "test/vitest/vitest.utils.config.ts", - "test/vitest/vitest.unit.config.ts", - "test/vitest/vitest.unit-paths.mjs", - "test/vitest/vitest.runtime-config.config.ts", - "test/vitest/vitest.secrets.config.ts", - "test/vitest/vitest.plugin-sdk.config.ts", - "test/vitest/vitest.plugins.config.ts", - "test/vitest/vitest.extension-telegram-paths.mjs", - "test/vitest/vitest.extension-telegram.config.ts", - "test/vitest/vitest.extension-voice-call-paths.mjs", - "test/vitest/vitest.extension-voice-call.config.ts", - "test/vitest/vitest.extension-whatsapp-paths.mjs", - "test/vitest/vitest.extension-whatsapp.config.ts", - "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", - "test/vitest/vitest.wizard.config.ts", - ], + ...vitestConfigFiles, + "test/vitest/**/*.{ts,mjs}", + ].map(resolveRepoRootPath), include: [ "src/**/*.test.ts", BUNDLED_PLUGIN_TEST_GLOB, diff --git a/test/vitest/vitest.unit-paths.mjs b/test/vitest/vitest.unit-paths.mjs index 4ebb7f562e9..54bc24fb7f4 100644 --- a/test/vitest/vitest.unit-paths.mjs +++ b/test/vitest/vitest.unit-paths.mjs @@ -31,6 +31,8 @@ export const bundledPluginDependentUnitTestFiles = [ export const unitTestAdditionalExcludePatterns = [ "src/gateway/**", + "packages/gateway-client/**", + "packages/gateway-protocol/**", "src/hooks/**", "src/infra/**", `${BUNDLED_PLUGIN_ROOT_DIR}/**`,