Files
openclaw/scripts/lib/ci-changed-node-test-plan.mjs
Peter Steinberger 70833dab7f ci: scope PR Node tests to changed targets (#106633)
* ci: scope PR Node tests to changed targets

* ci: bound targeted Node test plans

* test: cover changed path manifest input

* fix(ui): preserve model providers lazy boundary

* ci: cover public SDK re-export consumers

* ci: reject unresolved changed test targets

* ci: cover public SDK wrapper imports

* ci: preserve global checks in targeted plans

* docs(ci): clarify targeted boundary coverage

* test(plugins): declare computed runtime dependencies
2026-07-13 15:26:02 -07:00

158 lines
5.1 KiB
JavaScript

import { existsSync } from "node:fs";
import path from "node:path";
import { detectChangedLanes } from "../changed-lanes.mjs";
import {
buildVitestRunPlans,
findUnmatchedExplicitTestTargets,
hasImportGraphImpactOnTargets,
isTestFileTarget,
resolveChangedTestTargetPlan,
} from "../test-projects.test-support.mjs";
import { createNodeTestShards } from "./ci-node-test-plan.mjs";
import { buildPluginSdkEntrySources, publicPluginSdkEntrypoints } from "./plugin-sdk-entries.mjs";
const DEFAULT_NODE_TEST_RUNNER = "blacksmith-8vcpu-ubuntu-2404";
const DIST_DEPENDENT_CONFIGS = new Set(["test/vitest/vitest.boundary.config.ts"]);
const MAX_CHANGED_NODE_TEST_TARGETS = 20;
const publicPluginSdkEntrySources = Object.values(
buildPluginSdkEntrySources(publicPluginSdkEntrypoints),
);
const fullNodeTestShards = createNodeTestShards({ includeReleaseOnlyPluginShards: false });
const configsRequiringFullSuiteMetadata = new Set(
fullNodeTestShards
.filter((shard) => shard.env || shard.shardName.startsWith("core-tooling"))
.flatMap((shard) => shard.configs),
);
const splitNodeTestConfigs = new Set(
fullNodeTestShards.filter((shard) => shard.includePatterns).flatMap((shard) => shard.configs),
);
/**
* Builds one bounded PR job from precise changed-test targets.
* Null means the caller must fail safe to the compact full-suite plan.
*/
export function createChangedNodeTestShards(changedPaths, options = {}) {
const cwd = options.cwd ?? process.cwd();
if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
return null;
}
// Deletions cannot be reconstructed reliably from the post-merge tree.
if (changedPaths.some((changedPath) => !existsSync(path.join(cwd, changedPath)))) {
return null;
}
// 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/"))) {
return null;
}
// Package-specifier consumers are invisible to the relative import graph.
// Fail safe when a core change reaches a public SDK entrypoint indirectly.
if (
detectChangedLanes(changedPaths).extensionImpactFromCore ||
(changedPaths.some((changedPath) => changedPath.startsWith("src/")) &&
hasImportGraphImpactOnTargets(changedPaths, publicPluginSdkEntrySources, cwd))
) {
return null;
}
const resolveTargetPlan = (paths) =>
resolveChangedTestTargetPlan(paths, {
broad: true,
combineSiblingWithImportGraph: true,
cwd,
forceFullImportGraph: true,
includeExtensionImpact: false,
});
const plan = resolveTargetPlan(changedPaths);
// Aggregate resolution must not let one precise path hide another path that
// contributes no tests. Partial plans silently drop coverage.
if (
changedPaths.some((changedPath) => {
const changedPathPlan = resolveTargetPlan([changedPath]);
return changedPathPlan.mode !== "targets" || changedPathPlan.targets.length === 0;
})
) {
return null;
}
if (
plan.mode !== "targets" ||
plan.targets.length === 0 ||
plan.targets.length > MAX_CHANGED_NODE_TEST_TARGETS ||
plan.targets.some(
(target) =>
/^test\/vitest\/vitest\.full-.*\.config\.ts$/u.test(target) ||
splitNodeTestConfigs.has(target),
)
) {
return null;
}
if (
plan.targets.some(
(target) =>
!isTestFileTarget(target) || findUnmatchedExplicitTestTargets([target], cwd).length > 0,
)
) {
return null;
}
const targetPlans = plan.targets.map((target) => ({
plans: buildVitestRunPlans([target], cwd),
target,
}));
if (
targetPlans.some(
({ plans }) => plans.length === 0 || plans.some((targetPlan) => !targetPlan.includePatterns),
)
) {
return null;
}
// Preserve special shard setup (for example Go and TUI PTY coverage) by using
// the compact plan until targeted jobs can carry per-config prerequisites.
if (
targetPlans.some(({ plans }) =>
plans.some(({ config }) => configsRequiringFullSuiteMetadata.has(config)),
)
) {
return null;
}
const nonDistTargets = targetPlans
.filter(({ plans }) => plans.some(({ config }) => !DIST_DEPENDENT_CONFIGS.has(config)))
.map(({ target }) => target);
const distTargets = targetPlans
.filter(({ plans }) => plans.some(({ config }) => DIST_DEPENDENT_CONFIGS.has(config)))
.map(({ target }) => target);
return [
...(nonDistTargets.length > 0
? [
{
checkName: "checks-node-changed",
configs: [],
requiresDist: false,
runner: DEFAULT_NODE_TEST_RUNNER,
shardName: "changed",
targets: nonDistTargets,
},
]
: []),
...(distTargets.length > 0
? [
{
checkName: "checks-node-changed-dist",
configs: ["test/vitest/vitest.boundary.config.ts"],
requiresDist: true,
runner: DEFAULT_NODE_TEST_RUNNER,
shardName: "changed-dist",
targets: distTargets,
},
]
: []),
];
}