mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
* test: stabilize ci and local vitest workers * test: introduce planner-backed test runner * test: address planner review follow-ups * test: derive planner budgets from host capabilities * test: restore planner filter helper import * test: align planner explain output with execution * test: keep low profile as serial alias * test: restrict explicit planner file targets * test: clean planner exits and pnpm launch * test: tighten wrapper flag validation * ci: gate heavy fanout on check * test: key shard assignments by unit identity * ci(bun): shard vitest lanes further * test: restore ci overlap and stabilize planner tests * test: relax planner output worker assertions * test: reset plugin runtime state in optional tools suite * ci: split macos node and swift jobs * test: honor no-isolate top-level concurrency budgets * ci: fix macos swift format lint * test: cap max-profile top-level concurrency * ci: shard macos node checks * ci: use four macos node shards * test: normalize explain targets before classification
188 lines
6.9 KiB
JavaScript
188 lines
6.9 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { channelTestPrefixes } from "../../vitest.channel-paths.mjs";
|
|
import { isUnitConfigTestFile } from "../../vitest.unit-paths.mjs";
|
|
import { dedupeFilesPreserveOrder, loadTestRunnerBehavior } from "../test-runner-manifest.mjs";
|
|
|
|
const baseConfigPrefixes = ["src/agents/", "src/auto-reply/", "src/commands/", "test/", "ui/"];
|
|
|
|
export const normalizeRepoPath = (value) => value.split(path.sep).join("/");
|
|
|
|
const toRepoRelativePath = (value) => {
|
|
const relativePath = normalizeRepoPath(path.relative(process.cwd(), path.resolve(value)));
|
|
return relativePath.startsWith("../") || relativePath === ".." ? null : relativePath;
|
|
};
|
|
|
|
const walkTestFiles = (rootDir) => {
|
|
if (!fs.existsSync(rootDir)) {
|
|
return [];
|
|
}
|
|
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
const files = [];
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(rootDir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...walkTestFiles(fullPath));
|
|
continue;
|
|
}
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
if (
|
|
fullPath.endsWith(".test.ts") ||
|
|
fullPath.endsWith(".live.test.ts") ||
|
|
fullPath.endsWith(".e2e.test.ts")
|
|
) {
|
|
files.push(normalizeRepoPath(fullPath));
|
|
}
|
|
}
|
|
return files;
|
|
};
|
|
|
|
export function loadTestCatalog() {
|
|
const behaviorManifest = loadTestRunnerBehavior();
|
|
const existingFiles = (entries) =>
|
|
entries.map((entry) => entry.file).filter((file) => fs.existsSync(file));
|
|
const existingUnitConfigFiles = (entries) => existingFiles(entries).filter(isUnitConfigTestFile);
|
|
const baseThreadPinnedFiles = existingFiles(behaviorManifest.base?.threadPinned ?? []);
|
|
const channelIsolatedManifestFiles = existingFiles(behaviorManifest.channels?.isolated ?? []);
|
|
const channelIsolatedPrefixes = behaviorManifest.channels?.isolatedPrefixes ?? [];
|
|
const extensionForkIsolatedFiles = existingFiles(behaviorManifest.extensions?.isolated ?? []);
|
|
const unitForkIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.isolated);
|
|
const unitThreadPinnedFiles = existingUnitConfigFiles(behaviorManifest.unit.threadPinned);
|
|
const unitBehaviorOverrideSet = new Set([...unitForkIsolatedFiles, ...unitThreadPinnedFiles]);
|
|
const allKnownTestFiles = [
|
|
...new Set([
|
|
...walkTestFiles("src"),
|
|
...walkTestFiles("extensions"),
|
|
...walkTestFiles("test"),
|
|
...walkTestFiles(path.join("ui", "src", "ui")),
|
|
]),
|
|
];
|
|
const channelIsolatedFiles = dedupeFilesPreserveOrder([
|
|
...channelIsolatedManifestFiles,
|
|
...allKnownTestFiles.filter((file) =>
|
|
channelIsolatedPrefixes.some((prefix) => file.startsWith(prefix)),
|
|
),
|
|
]);
|
|
const channelIsolatedFileSet = new Set(channelIsolatedFiles);
|
|
const extensionForkIsolatedFileSet = new Set(extensionForkIsolatedFiles);
|
|
const baseThreadPinnedFileSet = new Set(baseThreadPinnedFiles);
|
|
const unitThreadPinnedFileSet = new Set(unitThreadPinnedFiles);
|
|
const unitForkIsolatedFileSet = new Set(unitForkIsolatedFiles);
|
|
|
|
const classifyTestFile = (fileFilter, options = {}) => {
|
|
const normalizedFile = normalizeRepoPath(fileFilter);
|
|
const reasons = [];
|
|
const isolated =
|
|
options.unitMemoryIsolatedFiles?.includes(normalizedFile) ||
|
|
unitForkIsolatedFileSet.has(normalizedFile) ||
|
|
extensionForkIsolatedFileSet.has(normalizedFile) ||
|
|
channelIsolatedFileSet.has(normalizedFile);
|
|
if (options.unitMemoryIsolatedFiles?.includes(normalizedFile)) {
|
|
reasons.push("unit-memory-isolated");
|
|
}
|
|
if (unitForkIsolatedFileSet.has(normalizedFile)) {
|
|
reasons.push("unit-isolated-manifest");
|
|
}
|
|
if (extensionForkIsolatedFileSet.has(normalizedFile)) {
|
|
reasons.push("extensions-isolated-manifest");
|
|
}
|
|
if (channelIsolatedFileSet.has(normalizedFile)) {
|
|
reasons.push("channels-isolated-rule");
|
|
}
|
|
|
|
let surface = "base";
|
|
if (isUnitConfigTestFile(normalizedFile)) {
|
|
surface = "unit";
|
|
} else if (normalizedFile.endsWith(".live.test.ts")) {
|
|
surface = "live";
|
|
} else if (normalizedFile.endsWith(".e2e.test.ts")) {
|
|
surface = "e2e";
|
|
} else if (channelTestPrefixes.some((prefix) => normalizedFile.startsWith(prefix))) {
|
|
surface = "channels";
|
|
} else if (normalizedFile.startsWith("extensions/")) {
|
|
surface = "extensions";
|
|
} else if (normalizedFile.startsWith("src/gateway/")) {
|
|
surface = "gateway";
|
|
} else if (baseConfigPrefixes.some((prefix) => normalizedFile.startsWith(prefix))) {
|
|
surface = "base";
|
|
} else if (normalizedFile.startsWith("src/")) {
|
|
surface = "unit";
|
|
}
|
|
if (surface === "unit") {
|
|
reasons.push("unit-surface");
|
|
} else if (surface !== "base") {
|
|
reasons.push(`${surface}-surface`);
|
|
} else {
|
|
reasons.push("base-surface");
|
|
}
|
|
|
|
const legacyBasePinned = baseThreadPinnedFileSet.has(normalizedFile);
|
|
if (legacyBasePinned) {
|
|
reasons.push("base-pinned-manifest");
|
|
}
|
|
if (unitThreadPinnedFileSet.has(normalizedFile)) {
|
|
reasons.push("unit-pinned-manifest");
|
|
}
|
|
|
|
return {
|
|
file: normalizedFile,
|
|
surface,
|
|
isolated,
|
|
legacyBasePinned,
|
|
reasons,
|
|
};
|
|
};
|
|
|
|
const resolveFilterMatches = (fileFilter) => {
|
|
const normalizedFilter = normalizeRepoPath(fileFilter);
|
|
const repoRelativeFilter = toRepoRelativePath(fileFilter);
|
|
if (fs.existsSync(fileFilter)) {
|
|
const stats = fs.statSync(fileFilter);
|
|
if (stats.isFile()) {
|
|
if (repoRelativeFilter && allKnownTestFiles.includes(repoRelativeFilter)) {
|
|
return [repoRelativeFilter];
|
|
}
|
|
throw new Error(`Explicit path ${fileFilter} is not a known test file.`);
|
|
}
|
|
if (stats.isDirectory()) {
|
|
if (!repoRelativeFilter) {
|
|
throw new Error(`Explicit path ${fileFilter} is outside the repo test roots.`);
|
|
}
|
|
const prefix = repoRelativeFilter.endsWith("/")
|
|
? repoRelativeFilter
|
|
: `${repoRelativeFilter}/`;
|
|
const matches = allKnownTestFiles.filter((file) => file.startsWith(prefix));
|
|
if (matches.length === 0) {
|
|
throw new Error(`Explicit path ${fileFilter} does not contain known test files.`);
|
|
}
|
|
return matches;
|
|
}
|
|
}
|
|
if (/[*?[\]{}]/.test(normalizedFilter)) {
|
|
return allKnownTestFiles.filter((file) => path.matchesGlob(file, normalizedFilter));
|
|
}
|
|
return allKnownTestFiles.filter((file) => file.includes(normalizedFilter));
|
|
};
|
|
|
|
return {
|
|
allKnownTestFiles,
|
|
allKnownUnitFiles: allKnownTestFiles.filter((file) => isUnitConfigTestFile(file)),
|
|
baseThreadPinnedFiles,
|
|
channelIsolatedFiles,
|
|
channelIsolatedFileSet,
|
|
channelTestPrefixes,
|
|
extensionForkIsolatedFiles,
|
|
extensionForkIsolatedFileSet,
|
|
unitBehaviorOverrideSet,
|
|
unitForkIsolatedFiles,
|
|
unitThreadPinnedFiles,
|
|
baseThreadPinnedFileSet,
|
|
classifyTestFile,
|
|
resolveFilterMatches,
|
|
};
|
|
}
|
|
|
|
export const testSurfaces = ["unit", "extensions", "channels", "gateway", "live", "e2e", "base"];
|