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:
Peter Steinberger
2026-08-10 21:30:21 -07:00
committed by GitHub
parent ee7fae8a90
commit df3e111c91
4 changed files with 109 additions and 7 deletions
+17 -6
View File
@@ -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(
+61 -1
View File
@@ -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([
{
+10
View File
@@ -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({