test: fix changed-run routing, dedupe gateway package lanes, repair force-rerun triggers (#104287)

* test: fix changed-run routing, dedupe gateway package lanes, repair force-rerun triggers

- route extensions/active-memory changed runs to their dedicated lane; add a
  generic guard that every catch-all-excluded extension root resolves to a
  dedicated config so future split lanes cannot silently skip changed tests
- exclude packages/gateway-client and packages/gateway-protocol from the unit
  lane; explicit targets now route to the gateway-client lane (was double-run)
- generate forceRerunTriggers from the test/vitest directory and resolve all
  entries absolute: Vitest matches triggers against absolute changed-file
  paths, so the old hand-maintained relative list never matched at all
- replace deprecated envFile:false with envDir:false (Vitest 4.1.9 warning)
- give scripts/test-live.mjs stall detection teeth: kill the process group and
  exit non-zero when OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS elapses quietly
- consolidate the live-docker shell target list into changed-lanes.mjs so the
  lane regex and check targets cannot drift; cover subagent-announce edits
- delete orphans: vitest.extension-clickclack.config.ts,
  vitest.full-core-unit.config.ts, scripts/test-docker-all.sh,
  scripts/test-live-acp-spawn-defaults-docker.sh

Closes #104231

* test: keep envDir literal so sharedVitestConfig satisfies Vite UserConfig

Vite 8 types envDir as string | false; the bare object literal widened false to
boolean and broke check:test-types at the defineConfig call sites.
This commit is contained in:
Peter Steinberger
2026-07-11 00:58:14 -07:00
committed by GitHub
parent 3fd8e13c31
commit 8cd092620d
18 changed files with 247 additions and 260 deletions
+16 -3
View File
@@ -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;
+1 -9
View File
@@ -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 =
+38 -67
View File
@@ -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,
-5
View File
@@ -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" "$@"
@@ -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"
+42 -12
View File
@@ -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);
+6
View File
@@ -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";
}
+12 -11
View File
@@ -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", () => {
+9
View File
@@ -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() });
+50
View File
@@ -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);
+19
View File
@@ -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<typeof buildVitestRunPlans> = [];
withTinyGitRepo(
+8 -2
View File
@@ -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", () => {
+2
View File
@@ -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"),
@@ -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<string, string | undefined> = process.env,
) {
return createSingleChannelExtensionVitestConfig("clickclack", env);
}
export default createExtensionClickClackVitestConfig();
+27 -21
View File
@@ -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<string, string | undefined> = 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, "")}/**`,
),
],
});
}
@@ -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);
+15 -97
View File
@@ -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,
+2
View File
@@ -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}/**`,