mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
Keep direct runtime consumers, expanding to depth two only below 20 direct importers. Select adjacent tests only in directories with at most 30 tests; larger directories use name prefixes and explicit ownership. Keep package consumers direct so mixed import styles cannot bypass the hub cutoff. Use config-loading and plugin-registry smoke within two Node rows. Preserve the source-inventory guards, full-selection kill switch, and full hourly plans. A controlled 48-diff replay drops from 2,193 to 1,083 rows, with 44 of 48 at or below 40. Historical failure replay has 29 hits, one process-fixture miss retained by hourly main, and three unavailable historical test files.
463 lines
18 KiB
TypeScript
463 lines
18 KiB
TypeScript
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { expect, it, vi } from "vitest";
|
|
import {
|
|
createChangedNodeTestShards,
|
|
hasControlUiPerformanceAffectingChange,
|
|
resolveChangedNodeTestTargets,
|
|
} from "../../scripts/lib/ci-changed-node-test-plan.mts";
|
|
import {
|
|
createNodeTestShardBundles,
|
|
createSelectedNodeTestShardBundles,
|
|
type CompactNodeTestShard,
|
|
} from "../../scripts/lib/ci-node-test-plan.mts";
|
|
import {
|
|
isReleaseOnlyRuntimeTestFile,
|
|
listPrExemptRuntimeTestFiles,
|
|
PR_PROTECTED_RUNTIME_TEST_FILES,
|
|
} from "../../scripts/lib/ci-proof-test-inventory.mts";
|
|
import * as testTimings from "../../scripts/lib/ci-test-timings.mts";
|
|
|
|
// Real-checkout compositions share the planner's process-scoped import-graph cache.
|
|
// Small synthetic graphs and canonical process selection remain in the unit file.
|
|
function fallbackGroups(shards: NonNullable<ReturnType<typeof createChangedNodeTestShards>>) {
|
|
return shards.flatMap((shard) => shard.groups ?? [{ ...shard, shard_name: shard.shardName }]);
|
|
}
|
|
|
|
function selectedFiles(shards: ReturnType<typeof createChangedNodeTestShards>) {
|
|
return (shards ?? []).flatMap((shard) =>
|
|
(shard.targets ?? []).concat(
|
|
shard.includePatterns ?? [],
|
|
shard.groups?.flatMap((group) => group.includePatterns ?? []) ?? [],
|
|
),
|
|
);
|
|
}
|
|
|
|
it("keeps the aggressive fixed smoke within two Node rows", () => {
|
|
let smoke: string[] = [];
|
|
resolveChangedNodeTestTargets(["src/infra/new-unlisted-module.ts"], {
|
|
selectionMode: "aggressive",
|
|
onSelection: ({ rule, targets }) => {
|
|
if (rule === "fixed-smoke") {
|
|
smoke = targets;
|
|
}
|
|
},
|
|
});
|
|
expect(smoke).toEqual([
|
|
"src/config/io.load-async.test.ts",
|
|
"src/plugins/loader.runtime-registry.test.ts",
|
|
]);
|
|
const rows = createChangedNodeTestShards(["src/infra/new-unlisted-module.ts"], {
|
|
selectedTestTargets: smoke,
|
|
selectionMode: "aggressive",
|
|
runnerBackend: "hybrid",
|
|
dedicatedBuildArtifacts: false,
|
|
dedicatedUiTests: true,
|
|
dedicatedUiE2e: true,
|
|
});
|
|
expect(selectedFiles(rows).toSorted()).toEqual(smoke.toSorted());
|
|
expect(rows?.filter((row) => !row.requiresDist).length).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
it("keeps the hybrid hourly plan within the main-tier cap", () => {
|
|
const hourly = createNodeTestShardBundles({
|
|
runnerBackend: "hybrid",
|
|
compactMode: "pull-request",
|
|
compactNodeJobCap: 77,
|
|
includeProofTests: true,
|
|
includeReleaseOnlyToolingShards: true,
|
|
includePrExemptRuntimeTests: true,
|
|
includeReleaseOnlyRuntimeTests: false,
|
|
includeReleaseOnlyPluginShards: false,
|
|
});
|
|
expect(hourly.filter((job) => !job.requiresDist).length).toBeLessThanOrEqual(77);
|
|
expect(hourly.length).toBeLessThanOrEqual(79);
|
|
});
|
|
|
|
it.each(
|
|
[
|
|
{
|
|
target: "test/scripts/bench-gateway-installed.test.ts",
|
|
sources: ["scripts/bench-gateway-startup.ts"],
|
|
preciseSubjects: true,
|
|
},
|
|
{
|
|
target: "src/commands/doctor-lint.native-capture.test.ts",
|
|
sources: [
|
|
"src/commands/doctor-lint.native-capture.test-support.ts",
|
|
"src/cli/run-main-plugin-cache.ts",
|
|
],
|
|
preciseSubjects: false,
|
|
},
|
|
].flatMap(({ target, sources, preciseSubjects }) =>
|
|
[target, ...sources].map((changedPath) => ({ target, changedPath, preciseSubjects })),
|
|
),
|
|
)(
|
|
"opts in $target for $changedPath beside hub inputs",
|
|
({ target, changedPath, preciseSubjects }) => {
|
|
expect(listPrExemptRuntimeTestFiles()).toContain(target);
|
|
const options = {
|
|
runnerBackend: "github",
|
|
includeReleaseOnlyRuntimeTests: false,
|
|
includePrExemptRuntimeTests: false,
|
|
includeReleaseOnlyToolingShards: false,
|
|
};
|
|
const fallbackFiles = (changedPaths: string[]) =>
|
|
createNodeTestShardBundles({
|
|
...options,
|
|
compactMode: "pull-request",
|
|
changedPaths,
|
|
}).flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []));
|
|
const precise = createChangedNodeTestShards([changedPath], options);
|
|
if (preciseSubjects || changedPath === target) {
|
|
expect(precise, changedPath).not.toBeNull();
|
|
}
|
|
expect(precise ? selectedFiles(precise) : fallbackFiles([changedPath]), changedPath).toContain(
|
|
target,
|
|
);
|
|
const withHub = createChangedNodeTestShards(["tsconfig.json", changedPath], options);
|
|
if (preciseSubjects) {
|
|
expect(withHub, changedPath).not.toBeNull();
|
|
}
|
|
const withHubFiles = withHub
|
|
? selectedFiles(withHub)
|
|
: fallbackFiles(["tsconfig.json", changedPath]);
|
|
expect(withHubFiles, changedPath).toContain(target);
|
|
expect(withHubFiles).not.toContain(
|
|
"extensions/acpx/src/runtime-advertised-model.process.test.ts",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("keeps precise first-signin targets under exclusive Gateway admission", () => {
|
|
const target = "src/gateway/setup-inference.first-signin.integration.test.ts";
|
|
const jobs = createChangedNodeTestShards([target], { runnerBackend: "hybrid" });
|
|
expect(jobs).not.toBeNull();
|
|
const owner = jobs?.find((job) =>
|
|
job.groups?.some((group) => group.includePatterns?.includes(target)),
|
|
);
|
|
expect(owner).toMatchObject({ planConcurrency: 1 });
|
|
expect(jobs?.flatMap((job) => job.targets ?? [])).not.toContain(target);
|
|
});
|
|
|
|
it("keeps boundary coverage when only a deferred proof helper changes", () => {
|
|
const helper = "test/helpers/sqlite-sessions-transcripts-flip-proof-assertions.ts";
|
|
const shards = createChangedNodeTestShards([helper]);
|
|
expect(shards).toContainEqual(
|
|
expect.objectContaining({
|
|
checkName: "checks-node-changed-boundary",
|
|
configs: ["test/vitest/vitest.boundary.config.ts"],
|
|
}),
|
|
);
|
|
const withDeleted = createChangedNodeTestShards([helper, "src/deleted-unowned-source.ts"]);
|
|
expect(withDeleted).not.toBeNull();
|
|
expect(selectedFiles(withDeleted)).toEqual(
|
|
expect.arrayContaining(["src/gateway/client-callsites.guard.test.ts"]),
|
|
);
|
|
expect(selectedFiles(withDeleted)).not.toContain(
|
|
"extensions/acpx/src/runtime-advertised-model.process.test.ts",
|
|
);
|
|
});
|
|
|
|
it("retains package and plugin consumers together in a mixed diff", () => {
|
|
const changedPaths = [
|
|
"packages/gateway-protocol/src/frame-guards.ts",
|
|
"extensions/codex/src/session-upstream-marker.ts",
|
|
];
|
|
|
|
const fallbackReasons: string[] = [];
|
|
const shards = createChangedNodeTestShards(changedPaths, {
|
|
onFallback: (reason) => fallbackReasons.push(reason),
|
|
});
|
|
expect(shards, fallbackReasons.join("\n")).not.toBeNull();
|
|
expect(selectedFiles(shards)).toEqual(
|
|
expect.arrayContaining([
|
|
"packages/gateway-protocol/src/frame-guards.test.ts",
|
|
"extensions/codex/src/session-upstream-marker.test.ts",
|
|
]),
|
|
);
|
|
const extensionGroups = fallbackGroups(shards ?? []).filter((group) =>
|
|
group.configs.some((config) => config.includes("vitest.extension")),
|
|
);
|
|
expect(extensionGroups.length).toBeGreaterThan(0);
|
|
expect(extensionGroups.every((group) => (group.includePatterns?.length ?? 0) > 0)).toBe(true);
|
|
});
|
|
|
|
it("keeps UI and core changes with exact owners and direct consumers", () => {
|
|
const paths = [
|
|
"ui/src/components/markdown-file-links.ts",
|
|
"src/agents/live-provider-owner.ts",
|
|
"ui/config/control-ui-boot-modules.json",
|
|
];
|
|
const options = {
|
|
runnerBackend: "hybrid",
|
|
dedicatedUiE2e: true,
|
|
includeReleaseOnlyToolingShards: false,
|
|
includeReleaseOnlyRuntimeTests: false,
|
|
};
|
|
const shards = createChangedNodeTestShards(paths, options);
|
|
expect(shards).not.toBeNull();
|
|
expect(hasControlUiPerformanceAffectingChange([paths[2]!])).toBe(true);
|
|
const targets = selectedFiles(shards);
|
|
expect(targets).toEqual(
|
|
expect.arrayContaining([
|
|
"ui/src/components/markdown-file-links.test.ts",
|
|
"ui/src/app/control-ui-chunking.test.ts",
|
|
"ui/src/app/vite-config.node.test.ts",
|
|
"src/agents/live-model-filter.test.ts",
|
|
"src/agents/live-model-dynamic-candidates.test.ts",
|
|
"src/agents/live-target-matcher.test.ts",
|
|
"src/agents/model-compat.test.ts",
|
|
"test/scripts/pr-worktree-provision.test.ts",
|
|
]),
|
|
);
|
|
// These whole-UI and transitive consumers belonged to the old broad fallback.
|
|
for (const unrelated of [
|
|
"test/ui.presenter-next-run.test.ts",
|
|
"test/talk-browser-defaults.test.ts",
|
|
"src/audit/execution-decision-facts.test.ts",
|
|
"src/auto-reply/reply/commands-export-session.test.ts",
|
|
"src/gateway/server-methods/session-change-event.fallback.test.ts",
|
|
"test/scripts/pr-merge-recovery.test.ts",
|
|
"test/scripts/mobile-release-ci.test.ts",
|
|
]) {
|
|
expect(targets, unrelated).not.toContain(unrelated);
|
|
}
|
|
expect(new Set(targets).size).toBe(targets.length);
|
|
expect(new Set(shards?.map((shard) => shard.checkName)).size).toBe(shards?.length);
|
|
expect(
|
|
targets
|
|
.filter(isReleaseOnlyRuntimeTestFile)
|
|
.every((file) => PR_PROTECTED_RUNTIME_TEST_FILES.includes(file)),
|
|
).toBe(true);
|
|
expect(shards?.some((shard) => shard.requiresDist)).toBe(false);
|
|
const placement = vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
|
let canonical: CompactNodeTestShard[];
|
|
let selectedCanonical: CompactNodeTestShard[];
|
|
try {
|
|
canonical = createNodeTestShardBundles({
|
|
compactMode: "pull-request",
|
|
runnerBackend: "hybrid",
|
|
includeReleaseOnlyPluginShards: false,
|
|
includeReleaseOnlyToolingShards: true,
|
|
includeProofTests: false,
|
|
includeReleaseOnlyRuntimeTests: true,
|
|
// Match the focused selector's admitted owner inventory before comparing resources.
|
|
includePrExemptRuntimeTests: true,
|
|
});
|
|
selectedCanonical = expectDefined(
|
|
createSelectedNodeTestShardBundles(
|
|
(shards ?? []).flatMap(
|
|
(job) => job.groups?.flatMap((group) => group.includePatterns ?? []) ?? [],
|
|
),
|
|
{
|
|
runnerBackend: options.runnerBackend,
|
|
includeReleaseOnlyRuntimeTests: true,
|
|
includePrExemptRuntimeTests: true,
|
|
},
|
|
),
|
|
"canonical selected UI consumer owners",
|
|
);
|
|
} finally {
|
|
placement.mockRestore();
|
|
}
|
|
// Precise tooling is repacked without unrelated compiler fixtures whose full
|
|
// inventory promotes their shared job. Compare against the same selected owner.
|
|
const toolingTargets = (shards ?? []).flatMap((job) =>
|
|
(job.groups ?? []).flatMap((group) =>
|
|
group.configs.includes("test/vitest/vitest.tooling.config.ts")
|
|
? (group.includePatterns ?? [])
|
|
: [],
|
|
),
|
|
);
|
|
const canonicalTooling = expectDefined(
|
|
createSelectedNodeTestShardBundles(toolingTargets, {
|
|
runnerBackend: "hybrid",
|
|
includeReleaseOnlyRuntimeTests: true,
|
|
includePrExemptRuntimeTests: true,
|
|
}),
|
|
"canonical selected tooling owners",
|
|
);
|
|
for (const job of shards ?? []) {
|
|
for (const group of job.groups ?? []) {
|
|
const owners = group.configs.includes("test/vitest/vitest.tooling.config.ts")
|
|
? canonicalTooling
|
|
: canonical;
|
|
const ownerJob = expectDefined(
|
|
owners.find((candidate) =>
|
|
candidate.groups.some((owner) => owner.shard_name === group.shard_name),
|
|
),
|
|
`canonical UI consumer job for ${group.shard_name}`,
|
|
);
|
|
const owner = expectDefined(
|
|
ownerJob.groups.find((candidate) => candidate.shard_name === group.shard_name),
|
|
"canonical UI consumer group",
|
|
);
|
|
if (group.includePatterns) {
|
|
expect(group.includePatterns.length).toBeGreaterThan(0);
|
|
} else {
|
|
expect(group).toEqual(owner);
|
|
}
|
|
expect(group.configs.every((config) => owner.configs.includes(config))).toBe(true);
|
|
// Tooling capacity follows selected files; an excluded compiler can require a larger full job.
|
|
const selectedJob = expectDefined(
|
|
selectedCanonical.find((candidate) =>
|
|
candidate.groups.some((selected) => selected.shard_name === group.shard_name),
|
|
),
|
|
`selected UI consumer job for ${group.shard_name}`,
|
|
);
|
|
const selectedGroup = expectDefined(
|
|
selectedJob.groups.find((selected) => selected.shard_name === group.shard_name),
|
|
"selected UI consumer group",
|
|
);
|
|
for (const key of [
|
|
"configs",
|
|
"env",
|
|
"runner",
|
|
"fallbackMaxWorkers",
|
|
"minTotalMemoryBytes",
|
|
"pretestBuildMode",
|
|
"requiresDist",
|
|
] as const) {
|
|
expect(group[key], `${group.shard_name} group ${key}`).toEqual(selectedGroup[key]);
|
|
}
|
|
for (const key of [
|
|
"env",
|
|
"runner",
|
|
"planConcurrency",
|
|
"pretestBuildMode",
|
|
"requiresDist",
|
|
"timeoutMinutes",
|
|
] as const) {
|
|
expect(job[key], `${group.shard_name} job ${key}`).toEqual(selectedJob[key]);
|
|
}
|
|
}
|
|
}
|
|
expect(createChangedNodeTestShards([paths[1]!, "ui/src/AGENTS.md"], options)).toEqual(
|
|
createChangedNodeTestShards([paths[1]!], options),
|
|
);
|
|
for (const hub of ["package.json", "tsconfig.json"]) {
|
|
const withHub = createChangedNodeTestShards([...paths, hub], options);
|
|
expect(withHub).not.toBeNull();
|
|
expect(selectedFiles(withHub)).toEqual(expect.arrayContaining(targets));
|
|
expect(selectedFiles(withHub)).not.toContain(
|
|
"extensions/acpx/src/runtime-advertised-model.process.test.ts",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("adds the fixed smoke once to narrow, hub, and directly edited smoke plans", () => {
|
|
const smoke = [
|
|
"test/gateway-rpc-exporters.test.ts",
|
|
"src/config/io.load-async.test.ts",
|
|
"src/config/io.compat.test.ts",
|
|
"src/config/utility-model-separation-migration.io.test.ts",
|
|
"src/plugins/loader.runtime-registry.test.ts",
|
|
"test/qa-channel-message-tool-delivery.test.ts",
|
|
];
|
|
for (const changedPaths of [
|
|
["src/infra/retry.test.ts"],
|
|
["tsconfig.json"],
|
|
["test/gateway-rpc-exporters.test.ts"],
|
|
]) {
|
|
const shards = createChangedNodeTestShards(changedPaths, {
|
|
includePrExemptRuntimeTests: false,
|
|
includeReleaseOnlyRuntimeTests: false,
|
|
dedicatedBuildArtifacts: false,
|
|
});
|
|
expect(shards).not.toBeNull();
|
|
const files = selectedFiles(shards);
|
|
for (const target of smoke) {
|
|
expect(
|
|
files.filter((file) => file === target),
|
|
target,
|
|
).toHaveLength(1);
|
|
}
|
|
expect(files).not.toContain("extensions/acpx/src/runtime-advertised-model.process.test.ts");
|
|
expect(shards?.every((shard) => (shard.predictedSeconds ?? 0) <= 300)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("keeps new-plugin, core, and manifest changes within the complete PR matrix cap", () => {
|
|
// Exact changed set from PR #159879, whose preflight originally emitted 134 rows.
|
|
const changedPaths = [
|
|
".github/labeler.yml",
|
|
"docs/.generated/config-baseline.counts.json",
|
|
"docs/.generated/config-baseline.sha256",
|
|
"docs/.i18n/glossary.zh-CN.json",
|
|
"docs/channels/slack.md",
|
|
"docs/cli/transcripts.md",
|
|
"docs/docs.json",
|
|
"docs/plugins/meeting-plugins.md",
|
|
"docs/plugins/plugin-inventory.md",
|
|
"docs/plugins/reference.md",
|
|
"docs/plugins/reference/slack-huddles.md",
|
|
"docs/plugins/sdk-runtime.md",
|
|
"docs/plugins/slack-huddles.md",
|
|
"extensions/slack-huddles/README.md",
|
|
"extensions/slack-huddles/cli-metadata.ts",
|
|
"extensions/slack-huddles/index.ts",
|
|
"extensions/slack-huddles/openclaw.plugin.json",
|
|
"extensions/slack-huddles/package.json",
|
|
"extensions/slack-huddles/src/cli-output-mode.ts",
|
|
"extensions/slack-huddles/src/cli.ts",
|
|
"extensions/slack-huddles/src/config.test.ts",
|
|
"extensions/slack-huddles/src/config.ts",
|
|
"extensions/slack-huddles/src/errors.ts",
|
|
"extensions/slack-huddles/src/node-host.ts",
|
|
"extensions/slack-huddles/src/node-invoke-policy.test.ts",
|
|
"extensions/slack-huddles/src/node-invoke-policy.ts",
|
|
"extensions/slack-huddles/src/runtime-probes.ts",
|
|
"extensions/slack-huddles/src/runtime-setup.ts",
|
|
"extensions/slack-huddles/src/runtime.ts",
|
|
"extensions/slack-huddles/src/transports/chrome.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-page-scripts.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-platform-adapter.audio.test.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-platform-adapter.test-helpers.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-platform-adapter.test.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-platform-adapter.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-selectors.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-status-call-source.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-status-prejoin-source.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-urls.test.ts",
|
|
"extensions/slack-huddles/src/transports/slack-huddles-urls.ts",
|
|
"extensions/slack-huddles/src/transports/types.ts",
|
|
"extensions/slack-huddles/tsconfig.json",
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"scripts/generate-plugin-inventory-doc.mts",
|
|
"scripts/lib/official-external-plugin-catalog.json",
|
|
"src/meeting-bot/status-call-ownership-source.ts",
|
|
"src/meeting-bot/status-call-source.test.ts",
|
|
"src/meeting-bot/status-call-source.ts",
|
|
"src/plugins/bundled-plugin-metadata.test.ts",
|
|
"src/plugins/official-external-meeting-catalog.test.ts",
|
|
"src/plugins/official-external-plugin-catalog.test.ts",
|
|
"test/scripts/bundled-plugin-build-entries.test.ts",
|
|
];
|
|
const shards = expectDefined(
|
|
createChangedNodeTestShards(changedPaths, {
|
|
runnerBackend: "hybrid",
|
|
compactNodeJobCap: 130,
|
|
dedicatedCoreTypeChecks: true,
|
|
dedicatedBuildArtifacts: false,
|
|
includeReleaseOnlyToolingShards: false,
|
|
includeReleaseOnlyRuntimeTests: false,
|
|
includePrExemptRuntimeTests: false,
|
|
dedicatedUiE2e: true,
|
|
dedicatedUiTests: true,
|
|
}),
|
|
"new plugin and global-input owner plan",
|
|
);
|
|
expect(shards.filter((shard) => !shard.requiresDist).length).toBeLessThanOrEqual(130);
|
|
expect(new Set(shards.map((shard) => shard.checkName)).size).toBe(shards.length);
|
|
const files = selectedFiles(shards);
|
|
expect(files).toContain("src/plugins/official-external-plugin-catalog.test.ts");
|
|
expect(files).toContain("src/plugins/bundled-plugin-metadata.test.ts");
|
|
expect(files).toContain("test/scripts/bundled-plugin-build-entries.test.ts");
|
|
expect(shards.some((shard) => shard.checkName.startsWith("checks-node-changed-extensions"))).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
shards.some((shard) => shard.configs.includes("test/vitest/vitest.boundary.config.ts")),
|
|
).toBe(true);
|
|
});
|