mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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() });
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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}/**`,
|
||||
|
||||
Reference in New Issue
Block a user