mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ci): run policy tests for watched source changes (#121841)
* fix(ci): route source policy tests by watched paths * chore(ci): keep policy watch table module-private
This commit is contained in:
committed by
GitHub
parent
ee7fae8a90
commit
df3e111c91
@@ -8,7 +8,11 @@ import {
|
||||
isTestFileTarget,
|
||||
resolveChangedTestTargetPlan,
|
||||
} from "../test-projects.test-support.mts";
|
||||
import { createNodeTestShards } from "./ci-node-test-plan.mts";
|
||||
import {
|
||||
createNodeTestShards,
|
||||
isPolicyTestOwnedPath,
|
||||
resolvePolicyTestTargets,
|
||||
} from "./ci-node-test-plan.mts";
|
||||
import { buildPluginSdkEntrySources, publicPluginSdkEntrypoints } from "./plugin-sdk-entries.mts";
|
||||
|
||||
type ChangedNodeTestShard = {
|
||||
@@ -183,6 +187,11 @@ export function createChangedNodeTestShards(
|
||||
return null;
|
||||
}
|
||||
|
||||
const policyTargetsByPath = new Map(
|
||||
livePaths.map((changedPath) => [changedPath, resolvePolicyTestTargets([changedPath])]),
|
||||
);
|
||||
const regularLivePaths = livePaths.filter((changedPath) => !isPolicyTestOwnedPath(changedPath));
|
||||
|
||||
// Workspace package consumers often use package specifiers, which the
|
||||
// relative import graph cannot connect back to the changed package source.
|
||||
if (changedPaths.some((changedPath) => changedPath.startsWith("packages/"))) {
|
||||
@@ -193,8 +202,8 @@ export function createChangedNodeTestShards(
|
||||
// Fail safe when a core change reaches a public SDK entrypoint indirectly.
|
||||
if (
|
||||
detectChangedLanes(changedPaths).extensionImpactFromCore ||
|
||||
(livePaths.some((changedPath) => changedPath.startsWith("src/")) &&
|
||||
hasImportGraphImpactOnTargets(livePaths, publicPluginSdkEntrySources, cwd))
|
||||
(regularLivePaths.some((changedPath) => changedPath.startsWith("src/")) &&
|
||||
hasImportGraphImpactOnTargets(regularLivePaths, publicPluginSdkEntrySources, cwd))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -208,11 +217,13 @@ export function createChangedNodeTestShards(
|
||||
includeExtensionImpact: false,
|
||||
});
|
||||
const plan =
|
||||
livePaths.length > 0 ? resolveTargetPlan(livePaths) : { mode: "targets", targets: [] };
|
||||
regularLivePaths.length > 0
|
||||
? resolveTargetPlan(regularLivePaths)
|
||||
: { mode: "targets" as const, targets: [] };
|
||||
// Aggregate resolution must not let one precise path hide another path that
|
||||
// contributes no tests. Partial plans silently drop coverage.
|
||||
if (
|
||||
livePaths.some((changedPath) => {
|
||||
regularLivePaths.some((changedPath) => {
|
||||
const changedPathPlan = resolveTargetPlan([changedPath]);
|
||||
return changedPathPlan.mode !== "targets" || changedPathPlan.targets.length === 0;
|
||||
})
|
||||
@@ -222,7 +233,7 @@ export function createChangedNodeTestShards(
|
||||
if (plan.mode !== "targets") {
|
||||
return null;
|
||||
}
|
||||
const targets = [...new Set(plan.targets)];
|
||||
const targets = [...new Set([...plan.targets, ...[...policyTargetsByPath.values()].flat()])];
|
||||
if (
|
||||
targets.length > MAX_CHANGED_NODE_TEST_TARGETS ||
|
||||
targets.some(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Builds CI node/Vitest shard plans from the full suite configuration.
|
||||
import { relative } from "node:path";
|
||||
import { matchesGlob, relative } from "node:path";
|
||||
import {
|
||||
agentVitestProjectOwners,
|
||||
embeddedAgentVitestProjectOwners,
|
||||
@@ -50,6 +50,66 @@ type NodeTestPlanOptions = {
|
||||
compactWholeGroupCount?: number;
|
||||
};
|
||||
|
||||
type PolicyTestWatch = {
|
||||
ownerGlobs?: readonly string[];
|
||||
testFile: string;
|
||||
watchGlobs: readonly string[];
|
||||
};
|
||||
|
||||
// These tests read source trees instead of importing every file whose policy
|
||||
// they enforce. Boundary and contract suites have dedicated always-on lanes;
|
||||
// this inventory covers the remaining tests that changed targeting cannot
|
||||
// discover from imports alone.
|
||||
const policyTestWatches = [
|
||||
{
|
||||
testFile: "ui/src/components/web-awesome-migration.node.test.ts",
|
||||
watchGlobs: ["ui/src/**/*.ts"],
|
||||
},
|
||||
{
|
||||
testFile: "ui/src/styles/base-theme-tokens.node.test.ts",
|
||||
ownerGlobs: ["ui/src/**/*.css"],
|
||||
watchGlobs: ["ui/src/**/*.css", "ui/src/**/*.ts"],
|
||||
},
|
||||
{
|
||||
testFile: "ui/src/styles/cursor-policy.node.test.ts",
|
||||
ownerGlobs: ["ui/index.html", "ui/src/**/*.css"],
|
||||
watchGlobs: ["ui/index.html", "ui/src/**/*.css", "ui/src/**/*.ts"],
|
||||
},
|
||||
...[
|
||||
"src/cron/service.stream-trigger.test.ts",
|
||||
"src/cron/service.stream-validation.test.ts",
|
||||
"src/cron/service/timer.timeout-watchdog.test.ts",
|
||||
].map((testFile) => ({
|
||||
testFile,
|
||||
ownerGlobs: ["src/cron/failure-notification-text.ts"],
|
||||
watchGlobs: ["src/cron/failure-notification-text.ts"],
|
||||
})),
|
||||
] satisfies readonly PolicyTestWatch[];
|
||||
|
||||
function normalizeChangedPath(changedPath: string): string {
|
||||
return changedPath.replaceAll("\\", "/").replace(/^\.\//u, "");
|
||||
}
|
||||
|
||||
/** Resolve policy tests whose scanned source surface intersects this diff. */
|
||||
export function resolvePolicyTestTargets(changedPaths: readonly string[]): string[] {
|
||||
const normalizedPaths = changedPaths.map(normalizeChangedPath);
|
||||
return policyTestWatches
|
||||
.filter(({ watchGlobs }) =>
|
||||
normalizedPaths.some((changedPath) =>
|
||||
watchGlobs.some((watchGlob) => matchesGlob(changedPath, watchGlob)),
|
||||
),
|
||||
)
|
||||
.map(({ testFile }) => testFile);
|
||||
}
|
||||
|
||||
/** True when the policy tests are the complete bounded owner for this path. */
|
||||
export function isPolicyTestOwnedPath(changedPath: string): boolean {
|
||||
const normalizedPath = normalizeChangedPath(changedPath);
|
||||
return policyTestWatches.some(({ ownerGlobs }) =>
|
||||
ownerGlobs?.some((ownerGlob) => matchesGlob(normalizedPath, ownerGlob)),
|
||||
);
|
||||
}
|
||||
|
||||
type CompactNodeTestShard = Omit<NodeTestShard, "configs" | "groups"> & {
|
||||
groups: NodeTestShardGroup[];
|
||||
};
|
||||
|
||||
@@ -13,6 +13,27 @@ import { listGitTrackedFiles } from "../../src/test-utils/repo-files.js";
|
||||
import { isGatewayServerTestFile } from "../vitest/vitest.gateway-server-paths.mjs";
|
||||
|
||||
describe("CI changed Node test plan", () => {
|
||||
it("routes Control UI style changes through source-scanning policy tests", () => {
|
||||
const shards = createChangedNodeTestShards(["ui/src/styles/chat/layout.css"]);
|
||||
const targets = shards?.flatMap((shard) => shard.targets ?? []) ?? [];
|
||||
|
||||
expect(targets).toEqual([
|
||||
"ui/src/styles/base-theme-tokens.node.test.ts",
|
||||
"ui/src/styles/cursor-policy.node.test.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("routes cron alert sanitization changes through alert policy suites", () => {
|
||||
const shards = createChangedNodeTestShards(["src/cron/failure-notification-text.ts"]);
|
||||
const targets = shards?.flatMap((shard) => shard.targets ?? []) ?? [];
|
||||
|
||||
expect(targets).toEqual([
|
||||
"src/cron/service.stream-trigger.test.ts",
|
||||
"src/cron/service.stream-validation.test.ts",
|
||||
"src/cron/service/timer.timeout-watchdog.test.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("routes a focused source change into one targeted job", () => {
|
||||
expect(createChangedNodeTestShards(["src/agents/live-model-filter.ts"])).toEqual([
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
assignVitestFsCacheWriter,
|
||||
createNodeTestShardBundles,
|
||||
createNodeTestShards,
|
||||
resolvePolicyTestTargets,
|
||||
type NodeTestShard,
|
||||
} from "../../scripts/lib/ci-node-test-plan.mts";
|
||||
import { expectNoNodeFsScans } from "../../src/test-utils/fs-scan-assertions.js";
|
||||
@@ -91,6 +92,15 @@ function listAllToolingTestFiles(): string[] {
|
||||
}
|
||||
|
||||
describe("scripts/lib/ci-node-test-plan.mts", () => {
|
||||
it("inventories source-scanning Control UI policy tests", () => {
|
||||
expect(resolvePolicyTestTargets(["ui/src/pages/chat/view.ts"])).toEqual([
|
||||
"ui/src/components/web-awesome-migration.node.test.ts",
|
||||
"ui/src/styles/base-theme-tokens.node.test.ts",
|
||||
"ui/src/styles/cursor-policy.node.test.ts",
|
||||
]);
|
||||
expect(resolvePolicyTestTargets(["docs/web/control-ui.md"])).toEqual([]);
|
||||
});
|
||||
|
||||
it("assigns one semantic Vitest cache writer without changing shard order", () => {
|
||||
const full = createNodeTestShardBundles({ includeReleaseOnlyPluginShards: false });
|
||||
const compact = createNodeTestShardBundles({
|
||||
|
||||
Reference in New Issue
Block a user