mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
test: speed up channel contract CI
This commit is contained in:
@@ -23,16 +23,16 @@ const CONTRACT_FILE_WEIGHTS = new Map([
|
||||
function resolveContractFileWeight(file) {
|
||||
const name = file.replaceAll("\\", "/").split("/").pop();
|
||||
if (name.startsWith("plugin.registry-backed-shard-")) {
|
||||
return 5;
|
||||
return 40;
|
||||
}
|
||||
if (name.startsWith("surfaces-only.registry-backed-shard-")) {
|
||||
return 5;
|
||||
return 40;
|
||||
}
|
||||
if (name.startsWith("directory.registry-backed-shard-")) {
|
||||
return 4;
|
||||
return 24;
|
||||
}
|
||||
if (name.startsWith("threading.registry-backed-shard-")) {
|
||||
return 4;
|
||||
return 18;
|
||||
}
|
||||
return CONTRACT_FILE_WEIGHTS.get(name) ?? 8;
|
||||
}
|
||||
|
||||
@@ -208,6 +208,49 @@ const GENERATED_CHANGED_TEST_TARGETS = new Set([
|
||||
const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map(
|
||||
Object.entries(VITEST_CONFIG_BY_KIND).map(([kind, config]) => [config, kind]),
|
||||
);
|
||||
const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map([
|
||||
[
|
||||
CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
|
||||
[
|
||||
"src/channels/plugins/contracts/channel-catalog.contract.test.ts",
|
||||
"src/channels/plugins/contracts/channel-import-guardrails.test.ts",
|
||||
"src/channels/plugins/contracts/group-policy.fallback.contract.test.ts",
|
||||
"src/channels/plugins/contracts/outbound-payload.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-a.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-e.contract.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
|
||||
[
|
||||
"src/channels/plugins/contracts/plugins-core.authorize-config-write.policy.contract.test.ts",
|
||||
"src/channels/plugins/contracts/plugins-core.authorize-config-write.targets.contract.test.ts",
|
||||
"src/channels/plugins/contracts/plugins-core.catalog.entries.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-b.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-f.contract.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
|
||||
[
|
||||
"src/channels/plugins/contracts/plugins-core.catalog.paths.contract.test.ts",
|
||||
"src/channels/plugins/contracts/plugins-core.loader.contract.test.ts",
|
||||
"src/channels/plugins/contracts/plugins-core.registry.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-c.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-g.contract.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
|
||||
[
|
||||
"src/channels/plugins/contracts/plugins-core.resolve-config-writes.contract.test.ts",
|
||||
"src/channels/plugins/contracts/registry.contract.test.ts",
|
||||
"src/channels/plugins/contracts/session-binding.registry-backed.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-d.contract.test.ts",
|
||||
"src/channels/plugins/contracts/*-shard-h.contract.test.ts",
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
function normalizePathPattern(value) {
|
||||
return value.replaceAll("\\", "/");
|
||||
@@ -960,7 +1003,8 @@ export function applyParallelVitestCachePaths(specs, params = {}) {
|
||||
|
||||
export function createVitestRunSpecs(args, params = {}) {
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const plans = buildVitestRunPlans(args, cwd);
|
||||
const baseEnv = params.baseEnv ?? process.env;
|
||||
const plans = filterPlansForContractIncludeFile(buildVitestRunPlans(args, cwd), baseEnv);
|
||||
return plans.map((plan, index) => {
|
||||
const includeFilePath = plan.includePatterns
|
||||
? path.join(
|
||||
@@ -972,10 +1016,10 @@ export function createVitestRunSpecs(args, params = {}) {
|
||||
config: plan.config,
|
||||
env: includeFilePath
|
||||
? {
|
||||
...(params.baseEnv ?? process.env),
|
||||
...baseEnv,
|
||||
[INCLUDE_FILE_ENV_KEY]: includeFilePath,
|
||||
}
|
||||
: (params.baseEnv ?? process.env),
|
||||
: baseEnv,
|
||||
includeFilePath,
|
||||
includePatterns: plan.includePatterns,
|
||||
pnpmArgs: createVitestArgs(plan),
|
||||
@@ -984,6 +1028,40 @@ export function createVitestRunSpecs(args, params = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function loadIncludePatternsForSpecFilter(env) {
|
||||
const filePath = env[INCLUDE_FILE_ENV_KEY]?.trim();
|
||||
if (!filePath) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.filter((value) => typeof value === "string" && value.length > 0);
|
||||
}
|
||||
|
||||
function includePatternMatchesConfig(candidate, configPatterns) {
|
||||
return configPatterns.some(
|
||||
(pattern) => path.matchesGlob(candidate, pattern) || path.matchesGlob(pattern, candidate),
|
||||
);
|
||||
}
|
||||
|
||||
function filterPlansForContractIncludeFile(plans, env) {
|
||||
const includePatterns = loadIncludePatternsForSpecFilter(env);
|
||||
if (!includePatterns) {
|
||||
return plans;
|
||||
}
|
||||
return plans.filter((plan) => {
|
||||
const configPatterns = CHANNEL_CONTRACT_CONFIG_PATTERNS.get(plan.config);
|
||||
if (!configPatterns) {
|
||||
return true;
|
||||
}
|
||||
return includePatterns.some((candidate) =>
|
||||
includePatternMatchesConfig(candidate, configPatterns),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldAcquireLocalHeavyCheckLock(runSpecs, env = process.env) {
|
||||
if (env.OPENCLAW_TEST_PROJECTS_FORCE_LOCK === "1") {
|
||||
return true;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const {
|
||||
@@ -975,6 +978,40 @@ describe("test-projects args", () => {
|
||||
expect(spec?.env.OPENCLAW_VITEST_INCLUDE_FILE).toBe(spec?.includeFilePath);
|
||||
});
|
||||
|
||||
it("skips channel contract configs with no matching external include patterns", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-contract-include-"));
|
||||
try {
|
||||
const includeFile = path.join(tempDir, "include.json");
|
||||
fs.writeFileSync(
|
||||
includeFile,
|
||||
JSON.stringify([
|
||||
"src/channels/plugins/contracts/surfaces-only.registry-backed-shard-b.contract.test.ts",
|
||||
]),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const specs = createVitestRunSpecs(
|
||||
[
|
||||
"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",
|
||||
],
|
||||
{
|
||||
baseEnv: {
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||||
} as NodeJS.ProcessEnv,
|
||||
},
|
||||
);
|
||||
|
||||
expect(specs.map((spec) => spec.config)).toEqual([
|
||||
"test/vitest/vitest.contracts-channel-config.config.ts",
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects watch mode when a command spans multiple suites", () => {
|
||||
expect(() =>
|
||||
buildVitestRunPlans([
|
||||
|
||||
@@ -43,6 +43,36 @@ const surfaceContractEntryCache = new Map<ChannelId, SurfaceContractEntry | null
|
||||
let threadingContractRegistryCache: ThreadingContractEntry[] | undefined;
|
||||
let directoryContractRegistryCache: DirectoryContractEntry[] | undefined;
|
||||
|
||||
const threadingContractPluginIds = new Set<ChannelId>([
|
||||
"bluebubbles",
|
||||
"discord",
|
||||
"googlechat",
|
||||
"matrix",
|
||||
"mattermost",
|
||||
"msteams",
|
||||
"slack",
|
||||
"telegram",
|
||||
"zalo",
|
||||
"zalouser",
|
||||
]);
|
||||
|
||||
const directoryContractPluginIds = new Set<ChannelId>([
|
||||
"discord",
|
||||
"feishu",
|
||||
"googlechat",
|
||||
"irc",
|
||||
"line",
|
||||
"matrix",
|
||||
"mattermost",
|
||||
"msteams",
|
||||
"slack",
|
||||
"synology-chat",
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"zalo",
|
||||
"zalouser",
|
||||
]);
|
||||
|
||||
function toSurfaceContractEntry(plugin: ChannelPlugin): SurfaceContractEntry {
|
||||
return {
|
||||
id: plugin.id,
|
||||
@@ -86,12 +116,19 @@ export function getSurfaceContractRegistryShard(params: {
|
||||
}
|
||||
|
||||
export function getThreadingContractRegistry(): ThreadingContractEntry[] {
|
||||
threadingContractRegistryCache ??= getSurfaceContractRegistry()
|
||||
.filter((entry) => entry.surfaces.includes("threading"))
|
||||
.map((entry) => ({
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
}));
|
||||
threadingContractRegistryCache ??= listBundledChannelPluginIds()
|
||||
.filter((id) => threadingContractPluginIds.has(id))
|
||||
.flatMap((id) => {
|
||||
const entry = getSurfaceContractEntry(id);
|
||||
return entry && entry.surfaces.includes("threading")
|
||||
? [
|
||||
{
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
return threadingContractRegistryCache;
|
||||
}
|
||||
|
||||
@@ -99,24 +136,38 @@ export function getThreadingContractRegistryShard(params: {
|
||||
shardIndex: number;
|
||||
shardCount: number;
|
||||
}): ThreadingContractEntry[] {
|
||||
return getSurfaceContractRegistryShard(params)
|
||||
.filter((entry) => entry.surfaces.includes("threading"))
|
||||
.map((entry) => ({
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
}));
|
||||
return getBundledChannelPluginIdsForShard(params)
|
||||
.filter((id) => threadingContractPluginIds.has(id))
|
||||
.flatMap((id) => {
|
||||
const entry = getSurfaceContractEntry(id);
|
||||
return entry && entry.surfaces.includes("threading")
|
||||
? [
|
||||
{
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
const directoryPresenceOnlyIds = new Set(["whatsapp", "zalouser"]);
|
||||
|
||||
export function getDirectoryContractRegistry(): DirectoryContractEntry[] {
|
||||
directoryContractRegistryCache ??= getSurfaceContractRegistry()
|
||||
.filter((entry) => entry.surfaces.includes("directory"))
|
||||
.map((entry) => ({
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
coverage: directoryPresenceOnlyIds.has(entry.id) ? "presence" : "lookups",
|
||||
}));
|
||||
directoryContractRegistryCache ??= listBundledChannelPluginIds()
|
||||
.filter((id) => directoryContractPluginIds.has(id))
|
||||
.flatMap((id) => {
|
||||
const entry = getSurfaceContractEntry(id);
|
||||
return entry && entry.surfaces.includes("directory")
|
||||
? [
|
||||
{
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
coverage: directoryPresenceOnlyIds.has(entry.id) ? "presence" : "lookups",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
return directoryContractRegistryCache;
|
||||
}
|
||||
|
||||
@@ -124,11 +175,18 @@ export function getDirectoryContractRegistryShard(params: {
|
||||
shardIndex: number;
|
||||
shardCount: number;
|
||||
}): DirectoryContractEntry[] {
|
||||
return getSurfaceContractRegistryShard(params)
|
||||
.filter((entry) => entry.surfaces.includes("directory"))
|
||||
.map((entry) => ({
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
coverage: directoryPresenceOnlyIds.has(entry.id) ? "presence" : "lookups",
|
||||
}));
|
||||
return getBundledChannelPluginIdsForShard(params)
|
||||
.filter((id) => directoryContractPluginIds.has(id))
|
||||
.flatMap((id) => {
|
||||
const entry = getSurfaceContractEntry(id);
|
||||
return entry && entry.surfaces.includes("directory")
|
||||
? [
|
||||
{
|
||||
id: entry.id,
|
||||
plugin: entry.plugin,
|
||||
coverage: directoryPresenceOnlyIds.has(entry.id) ? "presence" : "lookups",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,4 +43,15 @@ describe("scripts/lib/channel-contract-test-plan.mjs", () => {
|
||||
expect(actual).toEqual(listContractTests());
|
||||
expect(new Set(actual).size).toBe(actual.length);
|
||||
});
|
||||
|
||||
it("keeps registry-backed surface shards spread across checks", () => {
|
||||
for (const shard of createChannelContractTestShards().filter((entry) =>
|
||||
entry.checkName.includes("-registry-"),
|
||||
)) {
|
||||
const surfaceRegistryFiles = shard.includePatterns.filter((pattern) =>
|
||||
pattern.includes("/surfaces-only.registry-backed-shard-"),
|
||||
);
|
||||
expect(surfaceRegistryFiles.length).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createPatternFileHelper } from "./helpers/pattern-file.js";
|
||||
import { normalizeConfigPath, normalizeConfigPaths } from "./helpers/vitest-config-paths.js";
|
||||
import { createAgentsVitestConfig } from "./vitest/vitest.agents.config.ts";
|
||||
import bundledConfig from "./vitest/vitest.bundled.config.ts";
|
||||
@@ -17,6 +18,12 @@ import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts"
|
||||
import unitUiConfig from "./vitest/vitest.unit-ui.config.ts";
|
||||
import { createUnitVitestConfig } from "./vitest/vitest.unit.config.ts";
|
||||
|
||||
const patternFiles = createPatternFileHelper("openclaw-vitest-projects-config-");
|
||||
|
||||
afterEach(() => {
|
||||
patternFiles.cleanup();
|
||||
});
|
||||
|
||||
describe("projects vitest config", () => {
|
||||
it("defines the native root project list for all non-live Vitest lanes", () => {
|
||||
expect(baseConfig.test?.projects).toEqual([...rootVitestProjects]);
|
||||
@@ -57,6 +64,25 @@ describe("projects vitest config", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("intersects contract include-file shards with the config family", () => {
|
||||
const includeFile = patternFiles.writePatternFile("include.json", [
|
||||
"src/channels/plugins/contracts/surfaces-only.registry-backed-shard-b.contract.test.ts",
|
||||
"src/channels/plugins/contracts/surfaces-only.registry-backed-shard-d.contract.test.ts",
|
||||
"src/channels/plugins/contracts/directory.registry-backed-shard-a.contract.test.ts",
|
||||
]);
|
||||
|
||||
const config = createContractsVitestConfig(
|
||||
["src/channels/plugins/contracts/*-shard-a.contract.test.ts"],
|
||||
{
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||||
},
|
||||
);
|
||||
|
||||
expect(config.test.include).toEqual([
|
||||
"src/channels/plugins/contracts/directory.registry-backed-shard-a.contract.test.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the root ui lane aligned with the isolated jsdom setup", () => {
|
||||
const config = createUiVitestConfig();
|
||||
expect(config.test.environment).toBe("jsdom");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
|
||||
import { nonIsolatedRunnerPath, sharedVitestConfig } from "./vitest.shared.config.ts";
|
||||
@@ -46,12 +47,35 @@ export function loadContractsIncludePatternsFromEnv(
|
||||
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
|
||||
}
|
||||
|
||||
function narrowContractIncludePatterns(
|
||||
includePatterns: string[],
|
||||
candidatePatterns: string[] | null,
|
||||
): string[] | null {
|
||||
if (!candidatePatterns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
candidatePatterns.filter((candidate) =>
|
||||
includePatterns.some(
|
||||
(pattern) => path.matchesGlob(candidate, pattern) || path.matchesGlob(pattern, candidate),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function createContractsVitestConfig(
|
||||
includePatterns: string[],
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
argv: string[] = process.argv,
|
||||
) {
|
||||
const cliIncludePatterns = narrowIncludePatternsForCli(includePatterns, argv);
|
||||
const envIncludePatterns = narrowContractIncludePatterns(
|
||||
includePatterns,
|
||||
loadContractsIncludePatternsFromEnv(env),
|
||||
);
|
||||
return defineConfig({
|
||||
...base,
|
||||
test: {
|
||||
@@ -62,7 +86,7 @@ export function createContractsVitestConfig(
|
||||
pool: "forks",
|
||||
runner: nonIsolatedRunnerPath,
|
||||
setupFiles: baseTest.setupFiles ?? [],
|
||||
include: loadContractsIncludePatternsFromEnv(env) ?? cliIncludePatterns ?? includePatterns,
|
||||
include: envIncludePatterns ?? cliIncludePatterns ?? includePatterns,
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user