mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
Retain complete worker-qualified timing identities, split oversized families by file, and enforce the ordered-slot 300-second wall budget across broad and selected plans. Direct Blacksmith observations keep their native price. Use the qualified four-worker Gateway policy with its memory admission and two-worker fallback, preserve hosted hourly budgets, and expose test wall separately from aggregate work. Coverage and exclusive-group policies remain owned by the existing planner.
7819 lines
330 KiB
TypeScript
7819 lines
330 KiB
TypeScript
// Ci Node Test Plan tests cover ci node test plan script behavior.
|
||
import { existsSync, globSync, readFileSync, writeFileSync } from "node:fs";
|
||
import { syncBuiltinESMExports } from "node:module";
|
||
import os from "node:os";
|
||
import { join, matchesGlob, relative } from "node:path";
|
||
import { isDeepStrictEqual } from "node:util";
|
||
import { expectDefined } from "@openclaw/normalization-core";
|
||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||
import {
|
||
createChangedExtensionFallbackShards,
|
||
createChangedNodeTestShards,
|
||
} from "../../scripts/lib/ci-changed-node-test-plan.mts";
|
||
import { rebalanceMeasuredSerialJobs } from "../../scripts/lib/ci-measured-compact-packing.mts";
|
||
import {
|
||
type CompactNodeTestShard,
|
||
createNodeTestShardBundles,
|
||
createNodeTestShards,
|
||
createSelectedNodeTestShardBundles,
|
||
createUiRealGatewayTestShards,
|
||
createUiTestShardGroups,
|
||
createVitestCacheWarmGroups,
|
||
hasCompleteStartupCorpusCoverage,
|
||
isExclusiveCompactShardName,
|
||
packNodeTestGroups,
|
||
resolveStartupCorpusTestFiles,
|
||
} from "../../scripts/lib/ci-node-test-plan.mts";
|
||
import {
|
||
isPolicyTestOwnedPath,
|
||
resolvePolicyTestTargets,
|
||
} from "../../scripts/lib/ci-policy-test-watch.mts";
|
||
import {
|
||
isCiProofTestFile,
|
||
isReleaseOnlyRuntimeTestFile,
|
||
} from "../../scripts/lib/ci-proof-test-inventory.mts";
|
||
import * as proofTestInventory from "../../scripts/lib/ci-proof-test-inventory.mts";
|
||
import { isRuntimePlacementIncludePatterns } from "../../scripts/lib/ci-test-timings-schema.mts";
|
||
import * as testTimings from "../../scripts/lib/ci-test-timings.mts";
|
||
import { isExclusiveCiTestConfig } from "../../scripts/lib/local-check-runtime.mts";
|
||
import * as buildPrerequisites from "../../scripts/lib/vitest-build-prerequisites.mts";
|
||
import { listVitestRuntimeConsumerFiles } from "../../scripts/lib/vitest-build-prerequisites.mts";
|
||
import * as shardMetadata from "../../scripts/lib/vitest-shard-metadata.mts";
|
||
import {
|
||
createCompactSplitTimingGeneration,
|
||
parseCompactSplitTimingKey,
|
||
} from "../../scripts/lib/vitest-shard-metadata.mts";
|
||
import { createVitestRunSpecs } from "../../scripts/test-projects.test-support.mts";
|
||
import { expectNoNodeFsScans } from "../../src/test-utils/fs-scan-assertions.js";
|
||
import { spawnNodeEvalSync } from "../../src/test-utils/node-process.js";
|
||
import { toRepoPath } from "../../src/test-utils/repo-files.js";
|
||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||
import { createAgentsCoreIsolatedVitestConfig } from "../vitest/vitest.agents-core-isolated.config.ts";
|
||
import { createAgentsCoreVitestConfig } from "../vitest/vitest.agents-core.config.ts";
|
||
import {
|
||
agentVitestProjectOwners,
|
||
embeddedAgentVitestProjectOwners,
|
||
} from "../vitest/vitest.agents-paths.mjs";
|
||
import { createAgentsSupportVitestConfig } from "../vitest/vitest.agents-support.config.ts";
|
||
import { createAgentsToolsVitestConfig } from "../vitest/vitest.agents-tools.config.ts";
|
||
import { createAgentsVitestConfig } from "../vitest/vitest.agents.config.ts";
|
||
import { cliProcessTestFiles } from "../vitest/vitest.cli-process-paths.mjs";
|
||
import { createCliProcessVitestConfig } from "../vitest/vitest.cli-process.config.ts";
|
||
import { createCliVitestConfig } from "../vitest/vitest.cli.config.ts";
|
||
import { createCommandsVitestConfig } from "../vitest/vitest.commands.config.ts";
|
||
import { databaseWorkerCoreTestFiles } from "../vitest/vitest.database-worker-core-paths.mjs";
|
||
import { diagnosticForksPool } from "../vitest/vitest.forks-pool.ts";
|
||
import { createGatewayClientVitestConfig } from "../vitest/vitest.gateway-client.config.ts";
|
||
import { createGatewayCoreVitestConfig } from "../vitest/vitest.gateway-core.config.ts";
|
||
import { createGatewayDatabaseWorkersVitestConfig } from "../vitest/vitest.gateway-database-workers.config.ts";
|
||
import { createGatewayMethodsIsolatedVitestConfig } from "../vitest/vitest.gateway-methods-isolated.config.ts";
|
||
import { createGatewayMethodsVitestConfig } from "../vitest/vitest.gateway-methods.config.ts";
|
||
import { createGatewayServerIsolatedVitestConfig } from "../vitest/vitest.gateway-server-isolated.config.ts";
|
||
import {
|
||
gatewayDatabaseWorkerTestFiles,
|
||
gatewayServerIsolatedTestFiles,
|
||
gatewayServerSerialTestFiles,
|
||
isGatewayServerTestFile,
|
||
} from "../vitest/vitest.gateway-server-paths.mjs";
|
||
import { createGatewayServerVitestConfig } from "../vitest/vitest.gateway-server.config.ts";
|
||
import { createGatewayVitestConfig } from "../vitest/vitest.gateway.config.ts";
|
||
import { createInfraVitestConfig } from "../vitest/vitest.infra.config.ts";
|
||
import { createLoggingVitestConfig } from "../vitest/vitest.logging.config.ts";
|
||
import { createMediaUnderstandingVitestConfig } from "../vitest/vitest.media-understanding.config.ts";
|
||
import { createMediaVitestConfig } from "../vitest/vitest.media.config.ts";
|
||
import { createPluginSdkLightVitestConfig } from "../vitest/vitest.plugin-sdk-light.config.ts";
|
||
import { createPluginSdkVitestConfig } from "../vitest/vitest.plugin-sdk.config.ts";
|
||
import { createPluginsVitestConfig } from "../vitest/vitest.plugins.config.ts";
|
||
import { createProcessVitestConfig } from "../vitest/vitest.process.config.ts";
|
||
import { createRuntimeConfigVitestConfig } from "../vitest/vitest.runtime-config.config.ts";
|
||
import { sharedVitestConfig } from "../vitest/vitest.shared.config.ts";
|
||
import { startupCorpusTestFiles } from "../vitest/vitest.startup-corpus-paths.mjs";
|
||
import { fullSuiteVitestShards } from "../vitest/vitest.test-shards.mjs";
|
||
import { createToolingIsolatedVitestConfig } from "../vitest/vitest.tooling-isolated.config.ts";
|
||
import { createToolingVitestConfig } from "../vitest/vitest.tooling.config.ts";
|
||
import { createTuiVitestConfig } from "../vitest/vitest.tui.config.ts";
|
||
import { createUiIsolatedVitestConfig } from "../vitest/vitest.ui-isolated.config.ts";
|
||
import { uiE2eRealGatewayTestFiles, uiTimingTestFiles } from "../vitest/vitest.ui-paths.mjs";
|
||
import { createUiTimingVitestConfig } from "../vitest/vitest.ui-timing.config.ts";
|
||
import { createUiVitestConfig } from "../vitest/vitest.ui.config.ts";
|
||
import {
|
||
getUnitFastTestFilesForIncludePatterns,
|
||
getUnitFastIsolatedTestFiles,
|
||
getUnitFastTimerTestFiles,
|
||
} from "../vitest/vitest.unit-fast-paths.mjs";
|
||
import { createUnitFastVitestConfig } from "../vitest/vitest.unit-fast.config.ts";
|
||
import { createUnitVitestConfigWithOptions } from "../vitest/vitest.unit.config.ts";
|
||
import { createWizardVitestConfig } from "../vitest/vitest.wizard.config.ts";
|
||
import {
|
||
expectRuntimeReleaseInventory,
|
||
listMatchedTestFiles,
|
||
listTestFiles,
|
||
} from "./ci-node-test-plan.test-support.js";
|
||
|
||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||
|
||
describe("Control UI release-only inventories", () => {
|
||
const sidebar = "ui/src/components/app-sidebar.stress.browser.test.ts";
|
||
const embed = "ui/src/e2e/native-embed-settings.e2e.test.ts";
|
||
const entry = "ui/src/e2e/chat-session-entry.e2e.test.ts";
|
||
const automationManagement =
|
||
"extensions/qa-lab/src/control-ui-automation-management.real-gateway.e2e.test.ts";
|
||
const releaseOnlyRealGateway = new Set([
|
||
"ui/src/e2e/cron-duration-save.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/desktop-resize.real-gateway.e2e.test.ts",
|
||
automationManagement,
|
||
"ui/src/e2e/quota-reset-status.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/session-pr-reader-lifetime.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/chat-collaborator-scroll.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/mcp-app-conformance.e2e.test.ts",
|
||
"ui/src/e2e/usage-sessions-owner-attribution.e2e.test.ts",
|
||
"extensions/qa-lab/src/control-ui-openclaw-delegation.real-gateway.e2e.test.ts",
|
||
"extensions/qa-lab/src/control-ui-media-transcript.real-gateway.e2e.test.ts",
|
||
"extensions/qa-lab/src/session-host-command-state.real-gateway.e2e.test.ts",
|
||
]);
|
||
const tours = [
|
||
"ui/src/e2e/board-fixture.e2e.test.ts",
|
||
"ui/src/e2e/chat-attachment-menu.e2e.test.ts",
|
||
"ui/src/e2e/chat-mobile-bubble-margin.e2e.test.ts",
|
||
"ui/src/e2e/github-link-hovercard.e2e.test.ts",
|
||
"ui/src/e2e/settings-layout.e2e.test.ts",
|
||
"ui/src/e2e/theme-muted-contrast.e2e.test.ts",
|
||
];
|
||
|
||
function expectRealGatewayCoverage(
|
||
e2eGroups: Parameters<typeof createUiRealGatewayTestShards>[0],
|
||
expectedFiles: readonly string[],
|
||
) {
|
||
const desktop = "ui/src/e2e/desktop-resize.real-gateway.e2e.test.ts";
|
||
const shards = createUiRealGatewayTestShards(e2eGroups);
|
||
expect(
|
||
shards.map(({ shard, shard_count, run_desktop }) => ({
|
||
shard,
|
||
shard_count,
|
||
run_desktop,
|
||
})),
|
||
).toEqual([
|
||
{ shard: 1, shard_count: 2, run_desktop: expectedFiles.includes(desktop) },
|
||
{ shard: 2, shard_count: 2, run_desktop: false },
|
||
]);
|
||
const files = shards.flatMap((shard) => [
|
||
...shard.groups.flatMap((group) => {
|
||
expect(group.configs).toEqual(["test/vitest/vitest.ui-e2e-prebuilt.config.ts"]);
|
||
expect(group.includePatterns.length).toBeGreaterThan(0);
|
||
expect(group.includePatterns).not.toContain(desktop);
|
||
return group.includePatterns;
|
||
}),
|
||
...(shard.run_desktop ? [desktop] : []),
|
||
]);
|
||
// Array equality also catches duplicate ownership between jobs or desktop proof.
|
||
expect(files.toSorted()).toEqual(expectedFiles.toSorted());
|
||
for (const file of [
|
||
"ui/src/e2e/provider-browser-login.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/chat-loading-performance.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/chat-project-media.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/chat-widget-sandbox.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/command-palette-catalog.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/model-api-keys.real-gateway.e2e.test.ts",
|
||
"ui/src/e2e/model-catalog-partial-refresh.real-gateway.e2e.test.ts",
|
||
]) {
|
||
expect(shards[0]?.groups[0]?.includePatterns).toContain(file);
|
||
}
|
||
expect(shards[1]?.groups[0]?.includePatterns).toContain(
|
||
"ui/src/e2e/chat-flow.catalog-bootstrap.e2e.test.ts",
|
||
);
|
||
}
|
||
|
||
it("omits only the named exhaustive matrices from ordinary UI owners", () => {
|
||
const groups = createUiTestShardGroups({ includeReleaseOnlyTests: false });
|
||
expect(groups.ui[0]?.includePatterns).not.toContain(sidebar);
|
||
expect(groups.e2e[0]?.includePatterns).not.toContain(embed);
|
||
expect(groups.e2e[0]?.includePatterns).not.toContain(entry);
|
||
expect(
|
||
uiE2eRealGatewayTestFiles.filter((file) => groups.e2e[0]?.includePatterns?.includes(file)),
|
||
).toEqual(uiE2eRealGatewayTestFiles.filter((file) => !releaseOnlyRealGateway.has(file)));
|
||
for (const file of tours) {
|
||
expect(groups.e2e[0]?.includePatterns).not.toContain(file);
|
||
}
|
||
for (const file of [
|
||
"ui/src/e2e/board-mcp-app.e2e.test.ts",
|
||
"ui/src/e2e/chat-composer-redesign.e2e.test.ts",
|
||
"ui/src/e2e/chat-position-rail-layout.e2e.test.ts",
|
||
]) {
|
||
expect(groups.e2e[0]?.includePatterns).toContain(file);
|
||
}
|
||
expect(groups.ui[0]?.includePatterns).toContain(
|
||
"ui/src/components/app-sidebar-row-identity.browser.test.ts",
|
||
);
|
||
expect(groups.e2e[0]?.includePatterns).toContain(
|
||
"ui/src/e2e/chat-flow.navigation-presentation.e2e.test.ts",
|
||
);
|
||
expect(groups.ui[0]?.includePatterns?.some((file) => file.endsWith(".e2e.test.ts"))).toBe(
|
||
false,
|
||
);
|
||
expectRealGatewayCoverage(
|
||
groups.e2e,
|
||
uiE2eRealGatewayTestFiles.filter((file) => !releaseOnlyRealGateway.has(file)),
|
||
);
|
||
});
|
||
|
||
it("retains directly edited matrices without widening from their source owner", () => {
|
||
const options = {
|
||
includeReleaseOnlyTests: false,
|
||
changedPaths: [
|
||
entry,
|
||
...tours,
|
||
...releaseOnlyRealGateway,
|
||
"ui/src/components/app-sidebar.ts",
|
||
"ui/src/e2e",
|
||
],
|
||
};
|
||
const groups = createUiTestShardGroups(options);
|
||
expect(groups.e2e[0]?.includePatterns).toContain(entry);
|
||
expect(
|
||
groups.e2e[0]?.includePatterns?.filter((file) => releaseOnlyRealGateway.has(file)).toSorted(),
|
||
).toEqual(
|
||
uiE2eRealGatewayTestFiles.filter((file) => releaseOnlyRealGateway.has(file)).toSorted(),
|
||
);
|
||
expect(groups.e2e[0]?.includePatterns).toEqual(expect.arrayContaining(tours));
|
||
expect(groups.e2e[0]?.includePatterns).not.toContain(embed);
|
||
expect(groups.ui[0]?.includePatterns).not.toContain(sidebar);
|
||
expectRealGatewayCoverage(groups.e2e, uiE2eRealGatewayTestFiles);
|
||
expectRealGatewayCoverage(
|
||
createUiTestShardGroups({
|
||
includeReleaseOnlyTests: false,
|
||
changedPaths: [automationManagement, "ui/src/e2e", "ui/src/pages/usage/usage-page.ts"],
|
||
}).e2e,
|
||
uiE2eRealGatewayTestFiles.filter(
|
||
(file) => !releaseOnlyRealGateway.has(file) || file === automationManagement,
|
||
),
|
||
);
|
||
});
|
||
|
||
it("leaves the complete canonical config inventories in full release validation", () => {
|
||
const groups = createUiTestShardGroups();
|
||
expect(groups).toEqual({
|
||
ui: [{ configs: ["ui/vitest.config.ts"], shard_name: "ui/vitest.config.ts" }],
|
||
e2e: [
|
||
{
|
||
configs: ["test/vitest/vitest.ui-e2e.config.ts"],
|
||
shard_name: "test/vitest/vitest.ui-e2e.config.ts",
|
||
},
|
||
],
|
||
});
|
||
expectRealGatewayCoverage(groups.e2e, uiE2eRealGatewayTestFiles);
|
||
});
|
||
});
|
||
|
||
describe("startup corpus coverage", () => {
|
||
const files = startupCorpusTestFiles;
|
||
const group = {
|
||
shard_name: "core-runtime-config",
|
||
configs: ["test/vitest/vitest.runtime-config.config.ts"],
|
||
requiresDist: false,
|
||
runner: "ubuntu-24.04",
|
||
includePatterns: files,
|
||
env: { OPENCLAW_VITEST_MAX_WORKERS: "2" },
|
||
};
|
||
it("retains complete ownership across separate file groups", () => {
|
||
const groups = files.map((file) => ({ ...group, includePatterns: [file] }));
|
||
expect(hasCompleteStartupCorpusCoverage([{ requiresDist: false, groups }])).toBe(true);
|
||
expect(listMatchedTestFiles(createRuntimeConfigVitestConfig({}))).toEqual(
|
||
expect.arrayContaining(files),
|
||
);
|
||
});
|
||
it("certifies only the selected tier and directly edited startup cells", () => {
|
||
const regular = "src/config/config-startup-corpus.test.ts";
|
||
const changed = "src/config/state-startup-corpus.part-2.test.ts";
|
||
const options = { includeReleaseOnlyRuntimeTests: false };
|
||
expect(resolveStartupCorpusTestFiles()).toEqual(files);
|
||
expect(resolveStartupCorpusTestFiles(options)).toEqual([regular]);
|
||
const selected = resolveStartupCorpusTestFiles({
|
||
...options,
|
||
changedPaths: [changed, "src/config/state-startup-corpus.test-support.ts"],
|
||
});
|
||
expect(selected).toEqual([regular, changed]);
|
||
const shards = [{ requiresDist: false, groups: [{ ...group, includePatterns: selected }] }];
|
||
expect(hasCompleteStartupCorpusCoverage(shards, selected)).toBe(true);
|
||
expect(hasCompleteStartupCorpusCoverage(shards)).toBe(false);
|
||
expect(hasCompleteStartupCorpusCoverage(shards, [])).toBe(false);
|
||
expect(
|
||
hasCompleteStartupCorpusCoverage(
|
||
[{ requiresDist: false, groups: [{ ...group, includePatterns: [regular] }] }],
|
||
selected,
|
||
),
|
||
).toBe(false);
|
||
});
|
||
it.each<
|
||
{ label: string } & Partial<Parameters<typeof hasCompleteStartupCorpusCoverage>[0][number]>
|
||
>([
|
||
...files.map((missingFile) => ({
|
||
label: `missing ${missingFile}`,
|
||
groups: [{ ...group, includePatterns: files.filter((file) => file !== missingFile) }],
|
||
})),
|
||
{ label: "unknown full config", groups: [{ ...group, includePatterns: undefined }] },
|
||
{
|
||
label: "glob instead of complete files",
|
||
groups: [{ ...group, includePatterns: ["src/config/**"] }],
|
||
},
|
||
{ label: "different config", groups: [{ ...group, configs: ["vitest.config.ts"] }] },
|
||
{
|
||
label: "native shard",
|
||
groups: [{ ...group, env: { OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--shard=1/2"]' } }],
|
||
},
|
||
{
|
||
label: "name filter",
|
||
groups: [{ ...group, env: { OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["-t","one"]' } }],
|
||
},
|
||
{ label: "target precedence", groups: [group], targets: files.slice(0, 1) },
|
||
{ label: "non-admitted dist row", groups: [group], requiresDist: true },
|
||
{ label: "no groups", groups: [] },
|
||
])("does not certify $label", ({ label: _label, ...shard }) => {
|
||
expect(hasCompleteStartupCorpusCoverage([{ requiresDist: false, ...shard }])).toBe(false);
|
||
});
|
||
});
|
||
|
||
const PLUGIN_PRERELEASE_NPM_SPEC_TEST = "src/plugins/install.npm-spec.test.ts";
|
||
const RELEASE_REPORT_OWNER_TEST = "test/scripts/vitest-report-owner.test.ts";
|
||
const PRIVATE_QA_TOOLING_TEST = "test/e2e/qa-lab/runtime/gateway-codex-delivery-cache.test.ts";
|
||
const MEASURED_STORAGE_RECOVERY_TEST =
|
||
"src/agents/main-session-recovery/main-session-restart-recovery.test.ts";
|
||
const DEFAULT_NODE_TEST_RUNNER = "blacksmith-8vcpu-ubuntu-2404";
|
||
const BUNDLED_NODE_TEST_RUNNER = "blacksmith-4vcpu-ubuntu-2404";
|
||
const EXTRA_LARGE_NODE_TEST_RUNNER = "blacksmith-32vcpu-ubuntu-2404";
|
||
// Original process-proof allocation inputs from runs 33360253877 and 33388762505.
|
||
const historicalProcessProofSeconds = {
|
||
"test/scripts/ci-git-owner.test.ts": 367,
|
||
"test/scripts/managed-child-process.test.ts": 52,
|
||
"test/scripts/vitest-worker-artifacts.test.ts": 181,
|
||
"test/scripts/vitest-worker-artifacts.transforms.test.ts": 70,
|
||
"test/scripts/openclaw-performance-git-lifecycle.test.ts": 305,
|
||
"test/scripts/ci-linux-git.test.ts": 204,
|
||
"test/scripts/pr-merge-outcome.test.ts": 159,
|
||
};
|
||
const fileParallelAgentGroupNames = new Set([
|
||
"agentic-agents-embedded-base-1",
|
||
"agentic-agents-embedded-base-2",
|
||
"agentic-agents-embedded-base-3",
|
||
"agentic-agents-embedded-run",
|
||
"agentic-agents-tools",
|
||
]);
|
||
const plannerHosts = [
|
||
{ label: "hosted-2cpu", logicalCpuCount: 2, totalMemoryBytes: 16 * 1024 ** 3 },
|
||
{ label: "large-32cpu", logicalCpuCount: 32, totalMemoryBytes: 512 * 1024 ** 3 },
|
||
] as const;
|
||
type PlannerHost = (typeof plannerHosts)[number];
|
||
function usesParallelPacking(job: CompactNodeTestShard | undefined) {
|
||
return (
|
||
job?.planConcurrency === 2 ||
|
||
job?.env?.OPENCLAW_VITEST_MAX_WORKERS === "2" ||
|
||
(job?.planConcurrency === 1 &&
|
||
job.runner === EXTRA_LARGE_NODE_TEST_RUNNER &&
|
||
job.groups.length > 1 &&
|
||
job.groups.some(
|
||
(group) =>
|
||
(group.fallbackMaxWorkers === 2 &&
|
||
(group.configs.some(isExclusiveCiTestConfig) ||
|
||
fileParallelAgentGroupNames.has(group.shard_name.replace(/-hosted-\d+$/u, "")))) ||
|
||
group.configs.includes("test/vitest/vitest.commands.config.ts"),
|
||
))
|
||
);
|
||
}
|
||
function isNumberedToolingGroup(group: { shard_name: string }) {
|
||
return /^core-tooling-\d+(?:-hosted-\d+)?$/u.test(group.shard_name);
|
||
}
|
||
function nonToolingPlacement(plan: CompactNodeTestShard[]) {
|
||
return plan
|
||
.flatMap((job) => {
|
||
const groups = job.groups
|
||
.filter((group) => !isNumberedToolingGroup(group))
|
||
.map((group) => group.shard_name)
|
||
.toSorted();
|
||
return groups.length === 0
|
||
? []
|
||
: [
|
||
{
|
||
groups,
|
||
planConcurrency: job.planConcurrency,
|
||
pretestBuildMode: job.pretestBuildMode,
|
||
requiresDist: job.requiresDist,
|
||
runner: job.runner,
|
||
},
|
||
];
|
||
})
|
||
.toSorted((a, b) => a.groups.join("\0").localeCompare(b.groups.join("\0")));
|
||
}
|
||
function isCombinedUnbuiltCliJob(job: CompactNodeTestShard) {
|
||
return (
|
||
job.groups.length > 1 &&
|
||
!job.requiresDist &&
|
||
!job.pretestBuildMode &&
|
||
job.groups.every((group) =>
|
||
group.configs.every((config) =>
|
||
["test/vitest/vitest.cli.config.ts", "test/vitest/vitest.cli-process.config.ts"].includes(
|
||
config,
|
||
),
|
||
),
|
||
)
|
||
);
|
||
}
|
||
const STORE_ALIAS_CHANGED_PATHS = [
|
||
"docs/gateway/secrets.md",
|
||
"src/agents/auth-profiles/read-only-availability.test.ts",
|
||
"src/agents/auth-profiles/read-only-availability.ts",
|
||
"src/agents/model-auth-availability.test.ts",
|
||
"src/plugins/manifest-tool-availability.test.ts",
|
||
"src/plugins/manifest-tool-availability.ts",
|
||
"src/plugins/tools.optional.test.ts",
|
||
];
|
||
function listAllToolingTestFiles(): string[] {
|
||
const originalArgv = process.argv;
|
||
try {
|
||
process.argv = originalArgv.slice(0, 2);
|
||
return listMatchedTestFiles(
|
||
createToolingVitestConfig({
|
||
...process.env,
|
||
OPENCLAW_VITEST_INCLUDE_FILE: undefined,
|
||
}),
|
||
);
|
||
} finally {
|
||
process.argv = originalArgv;
|
||
}
|
||
}
|
||
|
||
describe("scripts/lib/ci-node-test-plan.mts", () => {
|
||
it("packs ordinary work more densely while retaining the serial Gateway budget", () => {
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue({
|
||
"agentic-gateway-server-isolated": 200,
|
||
"agentic-agents-core-models": 160,
|
||
"agentic-agents-core-runtime": 160,
|
||
"ordinary-a": 150,
|
||
"ordinary-b": 150,
|
||
"ordinary-c": 150,
|
||
"ordinary-d": 150,
|
||
"agentic-agents-core-auth": 150,
|
||
"agentic-agents-core-tools": 150,
|
||
});
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
vi.spyOn(buildPrerequisites, "resolveVitestPretestBuildMode").mockReturnValue(undefined);
|
||
const original = fullSuiteVitestShards.slice();
|
||
try {
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...[
|
||
["agentic-gateway-server-isolated", "gateway-server-isolated"],
|
||
["agentic-agents-core-models", "unit-support"],
|
||
["agentic-agents-core-runtime", "unit-fast-isolated"],
|
||
["ordinary-a", "hooks"],
|
||
["ordinary-b", "secrets"],
|
||
["ordinary-c", "logging"],
|
||
["ordinary-d", "unit-support"],
|
||
["agentic-agents-core-auth", "hooks"],
|
||
["agentic-agents-core-tools", "secrets"],
|
||
].map(([name, config]) => ({
|
||
name: name!,
|
||
config: `fixture-${name}.config.ts`,
|
||
projects: [`test/vitest/vitest.${config}.config.ts`],
|
||
})),
|
||
);
|
||
const jobs = createNodeTestShardBundles({
|
||
compactMode: "push",
|
||
runnerBackend: "blacksmith",
|
||
includeReleaseOnlyPluginShards: false,
|
||
});
|
||
const gateway = expectDefined(
|
||
jobs.find((job) =>
|
||
job.groups.some((group) =>
|
||
group.configs.includes("test/vitest/vitest.gateway-server-isolated.config.ts"),
|
||
),
|
||
),
|
||
"Gateway job",
|
||
);
|
||
expect(gateway.planConcurrency).toBe(1);
|
||
expect(gateway.groups.map((group) => group.shard_name).toSorted()).toEqual([
|
||
"agentic-gateway-server-isolated",
|
||
]);
|
||
expect(gateway.env).toBeUndefined();
|
||
expect(
|
||
gateway.groups
|
||
.map((group): [string, string | undefined] => [
|
||
group.shard_name,
|
||
group.env?.OPENCLAW_VITEST_MAX_WORKERS,
|
||
])
|
||
.toSorted(([left], [right]) => left.localeCompare(right)),
|
||
).toEqual([["agentic-gateway-server-isolated", "8"]]);
|
||
expect(gateway.predictedSeconds).toBe(200);
|
||
expect(gateway.predictedSeconds).toBeLessThanOrEqual(300);
|
||
expect(gateway.predictedTestSeconds).toBe(200);
|
||
const ordinary = expectDefined(
|
||
jobs.find((job) => job.groups.some((group) => group.shard_name === "ordinary-a")),
|
||
"ordinary job",
|
||
);
|
||
expect(ordinary.planConcurrency).toBe(2);
|
||
expect(ordinary.groups.map((group) => group.shard_name).toSorted()).toEqual([
|
||
"agentic-agents-core-auth",
|
||
"agentic-agents-core-tools",
|
||
"ordinary-a",
|
||
]);
|
||
expect(new Set(ordinary.groups.map((group) => group.runner))).toEqual(
|
||
new Set([BUNDLED_NODE_TEST_RUNNER, DEFAULT_NODE_TEST_RUNNER]),
|
||
);
|
||
expect(ordinary.predictedSeconds).toBe(450);
|
||
// Three 150s children require two waves even though half the work is 225s.
|
||
expect(ordinary.predictedTestSeconds).toBe(300);
|
||
expect(
|
||
jobs.find((job) => job.groups.some((group) => group.shard_name === "ordinary-d")),
|
||
).toMatchObject({
|
||
planConcurrency: 2,
|
||
runner: EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
predictedSeconds: 450,
|
||
predictedTestSeconds: 300,
|
||
});
|
||
expect(jobs).toHaveLength(4);
|
||
expect(jobs.flatMap((job) => job.groups.map((group) => group.shard_name)).toSorted()).toEqual(
|
||
fullSuiteVitestShards.map((shard) => shard.name).toSorted(),
|
||
);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
});
|
||
|
||
it("rejects parallel bins whose ordered queue exceeds the test budget", () => {
|
||
const entries = [170, 170, 160, 160, 150, 150].map(
|
||
(seconds, index) => [`ordinary-${index}`, seconds] as const,
|
||
);
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(Object.fromEntries(entries));
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
vi.spyOn(buildPrerequisites, "resolveVitestPretestBuildMode").mockReturnValue(undefined);
|
||
const original = fullSuiteVitestShards.slice();
|
||
try {
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...entries.map(([name]) => ({
|
||
name,
|
||
config: `fixture-${name}.config.ts`,
|
||
projects: ["test/vitest/vitest.hooks.config.ts"],
|
||
})),
|
||
);
|
||
const jobs = createNodeTestShardBundles({
|
||
compactMode: "push",
|
||
runnerBackend: "blacksmith",
|
||
includeReleaseOnlyPluginShards: false,
|
||
});
|
||
// The tempting 170 + 170 + 160 bin fits 500 aggregate seconds but takes 330 on two slots.
|
||
expect(jobs.every((job) => job.predictedTestSeconds! <= 300)).toBe(true);
|
||
expect(jobs.map((job) => job.planConcurrency)).toEqual([2, 2, 2]);
|
||
expect(jobs.reduce((sum, job) => sum + job.predictedSeconds!, 0)).toBe(960);
|
||
expect(jobs.flatMap((job) => job.groups.map((group) => group.shard_name)).toSorted()).toEqual(
|
||
entries.map(([name]) => name).toSorted(),
|
||
);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
});
|
||
|
||
it("retains the two-worker ceiling when compaction leaves a singleton", () => {
|
||
const entries = [
|
||
["core-runtime-media-ui-11", 200],
|
||
["agentic-agents-embedded-base-11", 180],
|
||
["core-unit-src-security-11", 180],
|
||
["agentic-agents-embedded-base-12", 170],
|
||
["core-runtime-media-ui-12", 170],
|
||
["agentic-agents-embedded-base-13", 150],
|
||
["core-unit-src-security-12", 150],
|
||
["agentic-agents-embedded-base-14", 110],
|
||
["core-runtime-media-ui-13", 110],
|
||
["core-unit-src-security-13", 50],
|
||
] as const;
|
||
// These synthetic costs already describe parallel embedded invocations.
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(
|
||
Object.fromEntries(
|
||
entries.map(([name, seconds]) => [
|
||
name.startsWith("agentic-agents-embedded-base-") ? `${name}#file-parallel` : name,
|
||
seconds,
|
||
]),
|
||
),
|
||
);
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
vi.spyOn(buildPrerequisites, "resolveVitestPretestBuildMode").mockReturnValue(undefined);
|
||
const original = fullSuiteVitestShards.slice();
|
||
try {
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...entries.map(([name]) => ({
|
||
name,
|
||
config: `fixture-${name}.config.ts`,
|
||
projects: ["test/vitest/vitest.hooks.config.ts"],
|
||
})),
|
||
);
|
||
const jobs = createNodeTestShardBundles({
|
||
compactMode: "push",
|
||
runnerBackend: "blacksmith",
|
||
includeReleaseOnlyPluginShards: false,
|
||
});
|
||
expect(jobs).toHaveLength(4);
|
||
const singleton = jobs.find((job) => job.groups.length === 1);
|
||
expect(singleton).toMatchObject({
|
||
runner: EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
planConcurrency: 1,
|
||
predictedSeconds: 110,
|
||
predictedTestSeconds: 110,
|
||
env: { OPENCLAW_VITEST_MAX_WORKERS: "2" },
|
||
});
|
||
expect(singleton?.groups[0]?.shard_name).toBe("agentic-agents-embedded-base-14");
|
||
expect(jobs.flatMap((job) => job.groups.map((group) => group.shard_name)).toSorted()).toEqual(
|
||
entries.map(([name]) => name).toSorted(),
|
||
);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
});
|
||
|
||
it("freezes settled serial Gateway rows when compacting neighboring parallel jobs", () => {
|
||
const entries = [
|
||
["agentic-agents-core-auth", "unit-support", 200],
|
||
["agentic-agents-core-models", "hooks", 160],
|
||
["agentic-agents-core-runtime", "secrets", 140],
|
||
["agentic-gateway-server-isolated", "gateway-server-isolated", 130],
|
||
["agentic-agents-core-subagents", "logging", 90],
|
||
["agentic-agents-core-tools", "unit-fast-isolated", 80],
|
||
["agentic-agents-core-runner-commands", "unit-support", 80],
|
||
["agentic-agents-core-runner-embedded", "hooks", 80],
|
||
["agentic-agents-core-runner-sessions", "secrets", 80],
|
||
["core-unit-fast-1", "logging", 80],
|
||
["core-unit-fast-2", "unit-fast-isolated", 80],
|
||
] as const;
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(
|
||
Object.fromEntries(entries.map(([name, , seconds]) => [name, seconds])),
|
||
);
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
vi.spyOn(buildPrerequisites, "resolveVitestPretestBuildMode").mockReturnValue(undefined);
|
||
const original = fullSuiteVitestShards.slice();
|
||
try {
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...entries.map(([name, config]) => ({
|
||
name,
|
||
config: `fixture-${name}.config.ts`,
|
||
projects: [`test/vitest/vitest.${config}.config.ts`],
|
||
})),
|
||
);
|
||
const jobs = createNodeTestShardBundles({
|
||
compactMode: "push",
|
||
runnerBackend: "blacksmith",
|
||
includeReleaseOnlyPluginShards: false,
|
||
});
|
||
const gateway = jobs.find((job) =>
|
||
job.groups.some((group) => group.shard_name === "agentic-gateway-server-isolated"),
|
||
);
|
||
expect(gateway).toMatchObject({
|
||
checkName: "checks-node-compact-large-2",
|
||
shardName: "compact-large-2",
|
||
runner: EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
planConcurrency: 1,
|
||
predictedSeconds: 270,
|
||
env: undefined,
|
||
});
|
||
expect(gateway?.predictedSeconds).toBeLessThanOrEqual(300);
|
||
expect(gateway?.groups.map((group) => group.shard_name)).toEqual([
|
||
"agentic-agents-core-runtime",
|
||
"agentic-gateway-server-isolated",
|
||
]);
|
||
expect(gateway?.groups.map((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS)).toEqual([
|
||
"2",
|
||
"8",
|
||
]);
|
||
const parallelJobs = jobs.filter((job) => job !== gateway);
|
||
expect(parallelJobs.map((job) => job.planConcurrency)).toEqual([2, 2]);
|
||
for (const job of parallelJobs) {
|
||
expect(job.predictedSeconds).toBeLessThanOrEqual(500);
|
||
expect(
|
||
job.groups.every((group) =>
|
||
[undefined, "2"].includes(group.env?.OPENCLAW_VITEST_MAX_WORKERS),
|
||
),
|
||
).toBe(true);
|
||
}
|
||
expect(jobs.flatMap((job) => job.groups.map((group) => group.shard_name)).toSorted()).toEqual(
|
||
entries.map(([name]) => name).toSorted(),
|
||
);
|
||
expect(jobs.reduce((sum, job) => sum + job.predictedSeconds!, 0)).toBe(
|
||
entries.reduce((sum, entry) => sum + entry[2], 0),
|
||
);
|
||
expect(jobs).toHaveLength(3);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
});
|
||
|
||
// Read-only cases share this baseline; inventory and timing mutations build fresh plans.
|
||
let defaultShards: ReturnType<typeof createNodeTestShards>;
|
||
|
||
// Only unchanged committed inputs share snapshots; every caller receives its own graph.
|
||
const committedCompactPlans = new Map<string, CompactNodeTestShard[]>();
|
||
let plannerHostPinned = false;
|
||
function pinPlannerHost(host: PlannerHost) {
|
||
plannerHostPinned = true;
|
||
committedCompactPlans.clear();
|
||
vi.spyOn(os, "availableParallelism").mockReturnValue(host.logicalCpuCount);
|
||
vi.spyOn(os, "cpus").mockReturnValue(
|
||
Array.from({ length: host.logicalCpuCount }, () => ({
|
||
model: "fixture",
|
||
speed: 0,
|
||
times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 },
|
||
})),
|
||
);
|
||
vi.spyOn(os, "totalmem").mockReturnValue(host.totalMemoryBytes);
|
||
syncBuiltinESMExports();
|
||
vi.stubEnv("CI", "true");
|
||
vi.stubEnv("OPENCLAW_CI_TEST_TIMINGS", "1");
|
||
expect(Object.keys(testTimings.readCompactGroupTimings("blacksmith")).length).toBeGreaterThan(
|
||
0,
|
||
);
|
||
expect(testTimings.readRuntimePlacementTimings("blacksmith").length).toBeGreaterThan(0);
|
||
}
|
||
function getCommittedCompactPlan(
|
||
compactMode: "push" | "pull-request",
|
||
runnerBackend?: string,
|
||
): CompactNodeTestShard[] {
|
||
const key = JSON.stringify([compactMode, runnerBackend]);
|
||
let snapshot = committedCompactPlans.get(key);
|
||
if (!snapshot) {
|
||
snapshot = structuredClone(
|
||
createNodeTestShardBundles({
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode,
|
||
...(runnerBackend === undefined ? {} : { runnerBackend }),
|
||
}),
|
||
);
|
||
committedCompactPlans.set(key, snapshot);
|
||
}
|
||
return structuredClone(snapshot);
|
||
}
|
||
|
||
beforeAll(() => {
|
||
defaultShards = createNodeTestShards();
|
||
});
|
||
|
||
it.each(["push", "pull-request"] as const)(
|
||
"retains child policies while routing RunsOn from measured hybrid %s plans",
|
||
(compactMode) => {
|
||
const hybrid = getCommittedCompactPlan(compactMode, "hybrid");
|
||
const runson = getCommittedCompactPlan(compactMode, "runson");
|
||
const routed = runson.filter((job) => job.runner === "runson-c8i-8xlarge");
|
||
expect(routed).toHaveLength(1);
|
||
expect(routed[0]).toMatchObject({
|
||
planConcurrency: 1,
|
||
requiresDist: false,
|
||
env: { OPENCLAW_VITEST_MAX_WORKERS: "2" },
|
||
});
|
||
expect(routed[0]!.pretestBuildMode).toBeUndefined();
|
||
const cronGroups = routed[0]!.groups;
|
||
expect(cronGroups.map((group) => group.shard_name).toSorted()).toEqual([
|
||
"core-runtime-cron-parallel-core",
|
||
"core-runtime-cron-parallel-isolated-agent",
|
||
"core-runtime-cron-parallel-service",
|
||
]);
|
||
const cronNames = new Set(cronGroups.map((group) => group.shard_name));
|
||
const previousCronJobs = hybrid.filter((job) =>
|
||
job.groups.some((group) => cronNames.has(group.shard_name)),
|
||
);
|
||
expect(routed[0]!.timeoutMinutes).toBe(
|
||
Math.min(...previousCronJobs.map((job) => job.timeoutMinutes ?? 60)),
|
||
);
|
||
const orderedGroups = (jobs: CompactNodeTestShard[]) =>
|
||
jobs
|
||
.flatMap((job) => job.groups)
|
||
.toSorted((a, b) => a.shard_name.localeCompare(b.shard_name));
|
||
// Coverage and the complete executor contract survive the provider move.
|
||
expect(orderedGroups(runson)).toEqual(orderedGroups(hybrid));
|
||
expect(runson.filter((job) => job.runner !== "runson-c8i-8xlarge")).toEqual(
|
||
hybrid
|
||
.flatMap((job) => {
|
||
const groups = job.groups.filter((group) => !cronNames.has(group.shard_name));
|
||
return groups.length ? [{ ...job, groups }] : [];
|
||
})
|
||
.toSorted((a, b) => a.checkName.localeCompare(b.checkName)),
|
||
);
|
||
expect(runson.length).toBeLessThanOrEqual(90);
|
||
expect(
|
||
createNodeTestShardBundles({
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode,
|
||
runnerBackend: "hybrid",
|
||
}),
|
||
).toEqual(hybrid);
|
||
},
|
||
);
|
||
|
||
// Frozen executor inputs keep measurement regression tests independent of
|
||
// unrelated inventory additions. Only a new native observation updates them.
|
||
const measuredCompactFixture = JSON.parse(
|
||
readFileSync(new URL("./fixtures/ci-measured-compact-jobs.json", import.meta.url), "utf8"),
|
||
) as {
|
||
toolingJobs: CompactNodeTestShard[];
|
||
cliTailJob: CompactNodeTestShard;
|
||
cliChildJobWallSeconds: number[];
|
||
toolingTailJobs: CompactNodeTestShard[];
|
||
};
|
||
|
||
function measuredToolingFixture(): CompactNodeTestShard[] {
|
||
return structuredClone(measuredCompactFixture.toolingJobs);
|
||
}
|
||
|
||
const measuredPackingOptions = {
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
estimateGroup: () => ({ seconds: 0, complete: false }),
|
||
canShare: (groups: CompactNodeTestShard["groups"]) => {
|
||
const families = groups.map((group) => group.shard_name.replace(/-hosted-\d+$/u, ""));
|
||
return groups.length <= 10 && new Set(families).size === families.length;
|
||
},
|
||
};
|
||
const sortedMeasuredGroups = (jobs: CompactNodeTestShard[]) =>
|
||
jobs.flatMap((job) => job.groups).toSorted((a, b) => a.shard_name.localeCompare(b.shard_name));
|
||
|
||
it("packs within 300 test seconds while retaining indivisible native-wall outliers", () => {
|
||
const before = measuredToolingFixture();
|
||
const after = rebalanceMeasuredSerialJobs(before, measuredPackingOptions);
|
||
expect(after).toHaveLength(10);
|
||
expect(sortedMeasuredGroups(after)).toEqual(sortedMeasuredGroups(before));
|
||
expect(Math.max(...after.map((job) => job.predictedSeconds!))).toBe(351);
|
||
expect(
|
||
after.filter((job) => job.groups.length > 1).every((job) => job.predictedSeconds! <= 300),
|
||
).toBe(true);
|
||
expect(
|
||
after.filter((job) => job.predictedSeconds! > 300).every((job) => job.groups.length === 1),
|
||
).toBe(true);
|
||
expect(after.every((job) => job.planConcurrency === 1 && job.timeoutMinutes === 20)).toBe(true);
|
||
expect(
|
||
after.every(
|
||
(job) =>
|
||
job.env?.OPENCLAW_VITEST_MAX_WORKERS === "2" ||
|
||
job.groups.every((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === "2"),
|
||
),
|
||
).toBe(true);
|
||
});
|
||
|
||
it("packs hosted hourly tooling only with complete supplied prices and ignores native tails", () => {
|
||
const tooling = measuredToolingFixture()
|
||
.slice(0, 2)
|
||
.map((job) => Object.assign({}, job, { predictedSeconds: 220 }));
|
||
const cli = structuredClone(measuredCompactFixture.cliTailJob);
|
||
const options = {
|
||
...measuredPackingOptions,
|
||
profile: "hosted-hourly" as const,
|
||
estimateGroup: () => ({ seconds: 220, complete: false }),
|
||
};
|
||
const unmeasured = rebalanceMeasuredSerialJobs([...tooling, cli], options);
|
||
expect(unmeasured).toHaveLength(3);
|
||
expect(unmeasured).toContainEqual(cli);
|
||
const packed = rebalanceMeasuredSerialJobs([...tooling, cli], {
|
||
...options,
|
||
estimateGroup: () => ({ seconds: 220, complete: true }),
|
||
});
|
||
expect(packed).toHaveLength(2);
|
||
expect(packed).toContainEqual(cli);
|
||
expect(sortedMeasuredGroups(packed)).toEqual(sortedMeasuredGroups([...tooling, cli]));
|
||
expect(packed.every((job) => job.planConcurrency === 1)).toBe(true);
|
||
expect(packed.every((job) => job.timeoutMinutes === 20)).toBe(true);
|
||
expect(packed.find((job) => job !== cli)?.predictedSeconds).toBe(440);
|
||
const native = rebalanceMeasuredSerialJobs(tooling, {
|
||
...options,
|
||
profile: "native",
|
||
estimateGroup: () => ({ seconds: 220, complete: true }),
|
||
});
|
||
expect(native).toHaveLength(2);
|
||
expect(native.every((job) => job.predictedSeconds === 220)).toBe(true);
|
||
});
|
||
|
||
it("splits the observed CLI pair with its measured wall floors and complete child contracts", () => {
|
||
const before = structuredClone(measuredCompactFixture.cliTailJob);
|
||
const after = rebalanceMeasuredSerialJobs([before], measuredPackingOptions);
|
||
expect(after).toHaveLength(2);
|
||
expect(after.flatMap((job) => job.groups)).toEqual(before.groups);
|
||
expect(new Set(after.map((job) => job.checkName)).size).toBe(2);
|
||
for (const [index, job] of after.entries()) {
|
||
expect(job).toMatchObject({
|
||
runner: before.runner,
|
||
planConcurrency: before.planConcurrency,
|
||
requiresDist: before.requiresDist,
|
||
timeoutMinutes: before.timeoutMinutes,
|
||
});
|
||
expect(job.env).toEqual(before.env);
|
||
expect(job.pretestBuildMode).toBeUndefined();
|
||
// The fixture records complete job walls, including 60s of setup.
|
||
expect(job.predictedSeconds).toBe(measuredCompactFixture.cliChildJobWallSeconds[index]! - 60);
|
||
}
|
||
});
|
||
|
||
it.each(measuredCompactFixture.toolingTailJobs)(
|
||
"splits observed tooling pair $shardName without transferring runtime preparation",
|
||
(fixture) => {
|
||
const before = structuredClone(fixture);
|
||
const after = rebalanceMeasuredSerialJobs([before], measuredPackingOptions);
|
||
expect(after).toHaveLength(2);
|
||
expect(after.flatMap((job) => job.groups)).toEqual(before.groups);
|
||
for (const [index, job] of after.entries()) {
|
||
expect(job).toMatchObject({
|
||
runner: before.runner,
|
||
planConcurrency: before.planConcurrency,
|
||
requiresDist: before.requiresDist,
|
||
});
|
||
expect(job.env).toEqual(before.env);
|
||
expect(job.timeoutMinutes).toBe(before.timeoutMinutes);
|
||
expect(job.pretestBuildMode).toBe(before.groups[index]!.pretestBuildMode);
|
||
expect(job.predictedSeconds).toBeGreaterThanOrEqual(before.predictedSeconds!);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("expires a serial tail observation when the executed selectors change", () => {
|
||
const before = structuredClone(measuredCompactFixture.cliTailJob);
|
||
before.groups[0]!.includePatterns!.push("src/cli/unmeasured-fixture.test.ts");
|
||
const { timingKeys } = createCompactSplitTimingGeneration({
|
||
parentShardName: "agentic-cli-process",
|
||
configs: before.groups[0]!.configs,
|
||
env: before.groups[0]!.env,
|
||
stripes: before.groups.map((group) => group.includePatterns!),
|
||
});
|
||
before.groups.forEach((group, index) => {
|
||
group.timing_key = timingKeys[index]!;
|
||
});
|
||
expect(rebalanceMeasuredSerialJobs([before], measuredPackingOptions)).toEqual([before]);
|
||
});
|
||
|
||
it("retains observations when only a sibling's timing generation changes", () => {
|
||
const before = measuredToolingFixture();
|
||
const renamed = before.map((job) => ({
|
||
...job,
|
||
groups: job.groups.map((group) => ({
|
||
...group,
|
||
timing_key: `${group.timing_key ?? group.shard_name}#changed-sibling`,
|
||
})),
|
||
}));
|
||
const after = rebalanceMeasuredSerialJobs(renamed, measuredPackingOptions);
|
||
expect(after.map((job) => job.predictedSeconds)).toEqual(
|
||
rebalanceMeasuredSerialJobs(before, measuredPackingOptions).map(
|
||
(job) => job.predictedSeconds,
|
||
),
|
||
);
|
||
expect(sortedMeasuredGroups(after)).toEqual(sortedMeasuredGroups(renamed));
|
||
});
|
||
|
||
it.each(["runner", "workers", "concurrency"] as const)(
|
||
"does not spend serial tooling observations after the %s contract changes",
|
||
(change) => {
|
||
const before = measuredToolingFixture();
|
||
if (change === "runner") {
|
||
before.forEach((job) => {
|
||
job.runner = EXTRA_LARGE_NODE_TEST_RUNNER;
|
||
});
|
||
} else if (change === "workers") {
|
||
before.forEach((job) => {
|
||
job.env = { OPENCLAW_VITEST_MAX_WORKERS: "1" };
|
||
});
|
||
} else if (change === "concurrency") {
|
||
before.forEach((job) => {
|
||
job.planConcurrency = 2;
|
||
});
|
||
}
|
||
expect(rebalanceMeasuredSerialJobs(before, measuredPackingOptions)).toEqual(before);
|
||
},
|
||
);
|
||
|
||
it("reprices changed selectors without spending their expired native observation", () => {
|
||
const observed = measuredToolingFixture().find((job) =>
|
||
job.groups.some((group) => group.shard_name === "core-tooling-7-hosted-1"),
|
||
)!;
|
||
const options = {
|
||
...measuredPackingOptions,
|
||
estimateGroup: () => ({ seconds: 200, complete: true }),
|
||
};
|
||
expect(rebalanceMeasuredSerialJobs([observed], options)[0]!.predictedSeconds).toBe(276);
|
||
const changed = structuredClone(observed);
|
||
changed.groups[0]!.includePatterns!.push("test/scripts/unmeasured-fixture.test.ts");
|
||
const after = rebalanceMeasuredSerialJobs([changed], options);
|
||
expect(after[0]!.predictedSeconds).toBe(200);
|
||
expect(after[0]!.groups).toEqual(changed.groups);
|
||
});
|
||
|
||
it("splits observed pairs above 300 test seconds without discounting their canonical prices", () => {
|
||
const before = measuredToolingFixture()[7]!;
|
||
const after = rebalanceMeasuredSerialJobs([before], {
|
||
...measuredPackingOptions,
|
||
estimateGroup: (group) => ({
|
||
seconds: group.shard_name === "core-tooling-12-hosted-1" ? 218 : 351,
|
||
complete: true,
|
||
}),
|
||
});
|
||
expect(after).toHaveLength(2);
|
||
expect(after.flatMap((job) => job.groups)).toEqual(before.groups);
|
||
expect(after.map((job) => job.predictedSeconds)).toEqual([218, 351]);
|
||
});
|
||
|
||
it("splits newly expensive tooling pairs after their historical timing identities expire", () => {
|
||
const before = structuredClone(measuredCompactFixture.toolingTailJobs[1]!);
|
||
before.groups.forEach((group, index) => {
|
||
group.timing_key = `unmeasured-child-${index}`;
|
||
group.includePatterns!.push(`test/scripts/unmeasured-fixture-${index}.test.ts`);
|
||
});
|
||
const after = rebalanceMeasuredSerialJobs([before], {
|
||
...measuredPackingOptions,
|
||
estimateGroup: () => ({ seconds: 320, complete: true }),
|
||
});
|
||
expect(after).toHaveLength(2);
|
||
expect(after.flatMap((job) => job.groups)).toEqual(before.groups);
|
||
expect(after.map((job) => job.predictedSeconds)).toEqual([320, 320]);
|
||
});
|
||
|
||
it("does not pack an unmeasured file using the canonical fallback as a wall observation", () => {
|
||
const before = measuredToolingFixture().slice(0, 2);
|
||
before.forEach((job, index) => {
|
||
job.groups[0]!.includePatterns!.push(`test/scripts/unmeasured-fixture-${index}.test.ts`);
|
||
job.predictedSeconds = 80;
|
||
});
|
||
const estimateGroup = () => ({ seconds: 80, complete: false });
|
||
const after = rebalanceMeasuredSerialJobs(before, { ...measuredPackingOptions, estimateGroup });
|
||
expect(after).toHaveLength(2);
|
||
expect(after.map((job) => job.groups)).toEqual(before.map((job) => job.groups));
|
||
expect(after.map((job) => job.predictedSeconds)).toEqual([80, 80]);
|
||
expect(
|
||
rebalanceMeasuredSerialJobs(before, {
|
||
...measuredPackingOptions,
|
||
estimateGroup: () => ({ seconds: 80, complete: true }),
|
||
}),
|
||
).toHaveLength(1);
|
||
});
|
||
|
||
it("preserves distinct job deadlines when considering measured tooling packing", () => {
|
||
const before = measuredToolingFixture();
|
||
before.forEach((job, index) => {
|
||
job.timeoutMinutes = 14 + index;
|
||
});
|
||
const after = rebalanceMeasuredSerialJobs(before, measuredPackingOptions);
|
||
expect(after).toHaveLength(10);
|
||
const deadlines = (jobs: CompactNodeTestShard[]) =>
|
||
jobs
|
||
.flatMap((job) => job.groups.map((group) => ({ group, timeout: job.timeoutMinutes })))
|
||
.toSorted((a, b) => a.group.shard_name.localeCompare(b.group.shard_name));
|
||
expect(deadlines(after)).toEqual(deadlines(before));
|
||
});
|
||
|
||
it.each(["job", "file"] as const)(
|
||
"does not replace a higher %s price with a faster measured tooling wall",
|
||
(source) => {
|
||
const before = measuredToolingFixture();
|
||
if (source === "job") {
|
||
before[0]!.predictedSeconds = 900;
|
||
}
|
||
const after = rebalanceMeasuredSerialJobs(before, {
|
||
...measuredPackingOptions,
|
||
estimateGroup: (group) => ({
|
||
seconds: source === "file" && group.shard_name === "core-tooling-1" ? 900 : 0,
|
||
complete: source === "file" && group.shard_name === "core-tooling-1",
|
||
}),
|
||
});
|
||
const expensive = expectDefined(
|
||
after.find((job) => job.checkName === before[0]!.checkName),
|
||
"expensive owner",
|
||
);
|
||
expect(expensive.groups).toEqual(before[0]!.groups);
|
||
expect(expensive.predictedSeconds).toBe(900);
|
||
expect(sortedMeasuredGroups(after)).toEqual(sortedMeasuredGroups(before));
|
||
},
|
||
);
|
||
|
||
it("counts every placement stage against the compact cap", () => {
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "pull-request" as const,
|
||
runnerBackend: "runson",
|
||
};
|
||
const stages = ["hybrid", "runson"].map((profile) =>
|
||
getCommittedCompactPlan(options.compactMode, profile),
|
||
);
|
||
const compactNodeJobCap = Math.max(
|
||
...stages.map((jobs) => jobs.filter((job) => !job.requiresDist).length),
|
||
);
|
||
expect(createNodeTestShardBundles({ ...options, compactNodeJobCap })).toEqual(stages[1]);
|
||
expect(() =>
|
||
createNodeTestShardBundles({ ...options, compactNodeJobCap: compactNodeJobCap - 1 }),
|
||
).toThrow(/compact (?:hybrid|runson) node test plan exceeds/u);
|
||
});
|
||
|
||
it("keeps precise RunsOn targets and their canonical child policies", () => {
|
||
const cronTarget = "src/cron/validate-timestamp.test.ts";
|
||
const siblingTarget = "src/cli/update-dry-run-state.process.test.ts";
|
||
const targets = [cronTarget, siblingTarget];
|
||
const hybrid = expectDefined(
|
||
createSelectedNodeTestShardBundles(targets, { runnerBackend: "hybrid" }),
|
||
"precise hybrid plan",
|
||
);
|
||
const runson = expectDefined(
|
||
createSelectedNodeTestShardBundles(targets, { runnerBackend: "runson" }),
|
||
"precise RunsOn plan",
|
||
);
|
||
const orderedGroups = (jobs: CompactNodeTestShard[]) =>
|
||
jobs
|
||
.flatMap((job) => job.groups)
|
||
.toSorted((a, b) => a.shard_name.localeCompare(b.shard_name));
|
||
expect(orderedGroups(runson)).toEqual(orderedGroups(hybrid));
|
||
expect(
|
||
orderedGroups(runson)
|
||
.flatMap((group) => group.includePatterns ?? [])
|
||
.toSorted(),
|
||
).toEqual(targets.toSorted());
|
||
expect(runson.filter((job) => job.runner === "runson-c8i-8xlarge")).toMatchObject([
|
||
{ groups: [{ includePatterns: [cronTarget] }], planConcurrency: 1 },
|
||
]);
|
||
expect(
|
||
runson.find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(siblingTarget)),
|
||
)?.runner,
|
||
).toBe(
|
||
hybrid.find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(siblingTarget)),
|
||
)?.runner,
|
||
);
|
||
});
|
||
|
||
it("discovers only tooling files for a cold precise tooling plan", () => {
|
||
const result = spawnNodeEvalSync(`
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { syncBuiltinESMExports } from "node:module";
|
||
let sourceReads = 0;
|
||
const readFileSync = fs.readFileSync;
|
||
fs.readFileSync = function(file, ...args) {
|
||
const relative = path.relative(process.cwd(), String(file)).replaceAll("\\\\", "/");
|
||
if (relative.startsWith("src/") && relative.endsWith(".test.ts")) sourceReads += 1;
|
||
return readFileSync.call(this, file, ...args);
|
||
};
|
||
syncBuiltinESMExports();
|
||
const { createChangedNodeTestShards } = await import("./scripts/lib/ci-changed-node-test-plan.mts");
|
||
const importReads = sourceReads;
|
||
const target = "test/scripts/managed-child-process.test.ts";
|
||
const plan = createChangedNodeTestShards([target], { runnerBackend: "github" });
|
||
console.log(JSON.stringify({
|
||
importReads,
|
||
sourceReads,
|
||
distOwners: plan?.filter((shard) => shard.requiresDist).map((shard) => shard.groups.flatMap((group) => group.configs)).flat().sort(),
|
||
selected: plan?.filter((shard) => !shard.requiresDist).flatMap((shard) => shard.groups?.flatMap((group) => group.includePatterns ?? []) ?? []).sort(),
|
||
}));
|
||
`);
|
||
expect(result.status, result.stderr).toBe(0);
|
||
expect(JSON.parse(result.stdout)).toEqual({
|
||
importReads: 0,
|
||
sourceReads: 0,
|
||
distOwners: [],
|
||
selected: [
|
||
"test/scripts/managed-child-process.test.ts",
|
||
"test/scripts/test-projects.test.ts",
|
||
],
|
||
});
|
||
});
|
||
|
||
it.each([
|
||
{ runnerBackend: "blacksmith", serialEstimate: 60, parallelEstimate: 24 },
|
||
{ runnerBackend: "blacksmith", serialEstimate: 53, parallelEstimate: 24, indivisible: true },
|
||
{ runnerBackend: "hybrid", serialEstimate: 60, parallelEstimate: 24 },
|
||
{ runnerBackend: "github", serialEstimate: 80, parallelEstimate: 24 },
|
||
])(
|
||
"prices parallel agent files once across $runnerBackend timing refits ($serialEstimate s fallback)",
|
||
({ runnerBackend, serialEstimate, parallelEstimate, indivisible = false }) => {
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((config) =>
|
||
indivisible
|
||
? embeddedAgentVitestProjectOwners.some((owner) => owner.config === config)
|
||
: config === agentVitestProjectOwners.tools.config,
|
||
),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0),
|
||
);
|
||
try {
|
||
const owner = "agentic-agents-tools";
|
||
const timings: Record<"blacksmith" | "github", Record<string, number>> = {
|
||
blacksmith: indivisible
|
||
? {
|
||
"agentic-agents-embedded-base-1": 66,
|
||
"agentic-agents-embedded-base-2": 66,
|
||
"agentic-agents-embedded-base-3": 66,
|
||
// Keep non-base owners out of the indivisible file's admission row.
|
||
"agentic-agents-embedded-incomplete-turn": 1_000,
|
||
"agentic-agents-embedded-overflow-compaction": 1_000,
|
||
"agentic-agents-embedded-run": 2_000,
|
||
}
|
||
: { [owner]: 120 },
|
||
github: { [owner]: 160 },
|
||
};
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockImplementation(
|
||
(profile) => timings[profile],
|
||
);
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
const options = { compactMode: "push" as const, runnerBackend };
|
||
const selectOwnerJob = (plan: CompactNodeTestShard[]) =>
|
||
expectDefined(
|
||
plan.find((job) =>
|
||
job.groups.some((group) =>
|
||
indivisible
|
||
? group.includePatterns?.includes(
|
||
"src/agents/embedded-agent-runner/run.compaction-runtime.test.ts",
|
||
)
|
||
: group.shard_name === owner,
|
||
),
|
||
),
|
||
"priced agent owner",
|
||
);
|
||
const initial = selectOwnerJob(createNodeTestShardBundles(options));
|
||
expect(initial.predictedSeconds).toBe(serialEstimate);
|
||
expect(initial.groups).toHaveLength(1);
|
||
const group = initial.groups[0]!;
|
||
const key = expectDefined(group.timing_key, "parallel timing identity");
|
||
expect(key).toBe(`${group.shard_name}#file-parallel`);
|
||
timings.blacksmith[key] = 24;
|
||
timings.github[key] = 24;
|
||
const refitted = selectOwnerJob(createNodeTestShardBundles(options));
|
||
// New wall samples override serial/file floors and must not be divided again.
|
||
expect(refitted.predictedSeconds).toBe(parallelEstimate);
|
||
expect(refitted.groups).toEqual(initial.groups);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("binds split timing identity to exact complete-file membership", () => {
|
||
const common = {
|
||
configs: ["test/vitest/vitest.gateway-server.config.ts"],
|
||
env: { OPENCLAW_GATEWAY_TEST_WORKERS: "2" },
|
||
parentShardName: "agentic-control-plane-agent-chat",
|
||
};
|
||
const original = createCompactSplitTimingGeneration({
|
||
...common,
|
||
stripes: [["src/gateway/a.test.ts"], ["src/gateway/b.test.ts"]],
|
||
});
|
||
expect(
|
||
createCompactSplitTimingGeneration({
|
||
...common,
|
||
stripes: [["src/gateway/a.test.ts"], ["src/gateway/b.test.ts"]],
|
||
}),
|
||
).toEqual(original);
|
||
expect(
|
||
createCompactSplitTimingGeneration({
|
||
...common,
|
||
stripes: [["src/gateway/a.test.ts"], ["src/gateway/c.test.ts"]],
|
||
}),
|
||
).not.toEqual(original);
|
||
expect(
|
||
createCompactSplitTimingGeneration({
|
||
...common,
|
||
stripes: [["src/gateway/b.test.ts"], ["src/gateway/a.test.ts"]],
|
||
}),
|
||
).not.toEqual(original);
|
||
});
|
||
|
||
it.each(["blacksmith", "hybrid", "github"])(
|
||
"prices agents-core serial history with two file workers on %s without discounting parallel samples",
|
||
async (runnerBackend) => {
|
||
const config = agentVitestProjectOwners.core.config;
|
||
const models = "agentic-agents-core-models";
|
||
const singleton = "agentic-agents-core-auth";
|
||
const commands = "agentic-agents-core-runner-commands";
|
||
const heavyModel = "src/agents/model-heavy.test.ts";
|
||
const fastModel = "src/agents/model-fast.test.ts";
|
||
const e2eModel = "src/agents/model-fixture.e2e.test.ts";
|
||
const runtimeFile = "src/agents/agent-command-runtime.test.ts";
|
||
const commandPeers = [
|
||
"src/agents/agent-command-a.test.ts",
|
||
"src/agents/agent-command-b.test.ts",
|
||
];
|
||
const files = [
|
||
heavyModel,
|
||
"src/agents/model-light.test.ts",
|
||
fastModel,
|
||
e2eModel,
|
||
"src/agents/auth-fixture.test.ts",
|
||
runtimeFile,
|
||
...commandPeers,
|
||
].toSorted();
|
||
const seconds: Record<string, number> = { [models]: 200, [singleton]: 40, [commands]: 0 };
|
||
const fileWeights = vi.fn((_file: string) => 1);
|
||
const buildMode = vi.fn<typeof buildPrerequisites.resolveVitestPretestBuildMode>(
|
||
() => undefined,
|
||
);
|
||
const unitFastPaths = await vi.importActual<
|
||
typeof import("../vitest/vitest.unit-fast-paths.mjs")
|
||
>("../vitest/vitest.unit-fast-paths.mjs");
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.unit-fast-paths.mjs", () => ({
|
||
...unitFastPaths,
|
||
getUnitFastTestFiles: () => [fastModel],
|
||
getUnitFastIsolatedTestFiles: () => [],
|
||
getUnitFastTimerTestFiles: () => [],
|
||
getUnitFastTestFilesForIncludePatterns: () => [fastModel],
|
||
}));
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [
|
||
{
|
||
config: "test/vitest/vitest.full-agentic.config.ts",
|
||
name: "agentic",
|
||
projects: [config],
|
||
},
|
||
],
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (root: string) => (root === "src/agents" ? files : []),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => seconds,
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
vi.doMock("../../scripts/lib/vitest-shard-metadata.mts", () => ({
|
||
...shardMetadata,
|
||
estimateVitestTestFileSeconds: fileWeights,
|
||
}));
|
||
vi.doMock("../../scripts/lib/vitest-build-prerequisites.mts", () => ({
|
||
...buildPrerequisites,
|
||
resolveVitestPretestBuildMode: buildMode,
|
||
listVitestRuntimeConsumerFiles: () => [runtimeFile],
|
||
}));
|
||
try {
|
||
const { createNodeTestShardBundles: createPlan } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const options = {
|
||
compactMode: "push" as const,
|
||
runnerBackend,
|
||
includeReleaseOnlyPluginShards: false,
|
||
};
|
||
const predicted = (jobs: CompactNodeTestShard[]) =>
|
||
jobs.reduce((sum, job) => sum + (job.predictedSeconds ?? 0), 0);
|
||
const baseline = createPlan(options);
|
||
// The authentication fixture is indivisible; only the two models share work.
|
||
expect(predicted(baseline)).toBe(runnerBackend === "hybrid" ? 122 : 140);
|
||
expect(
|
||
baseline
|
||
.flatMap((job) => job.groups)
|
||
.every((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === "2"),
|
||
).toBe(true);
|
||
for (const excluded of [e2eModel, fastModel]) {
|
||
fileWeights.mockImplementation((file) => (file === excluded ? 1_000_000 : 1));
|
||
expect(predicted(createPlan(options))).toBe(predicted(baseline));
|
||
}
|
||
// One model owns 75% of the serial work; two workers cannot halve it.
|
||
fileWeights.mockImplementation((file) => (file === heavyModel ? 3 : 1));
|
||
expect(predicted(createPlan(options))).toBe(runnerBackend === "hybrid" ? 166 : 190);
|
||
fileWeights.mockReturnValue(1);
|
||
seconds[`${models}-parallel`] = 200;
|
||
const measured = createPlan(options);
|
||
// Hosted splitting turns two concurrent 200s files into two 200s children.
|
||
expect(predicted(measured)).toBe(
|
||
runnerBackend === "blacksmith" ? 240 : runnerBackend === "hybrid" ? 435 : 440,
|
||
);
|
||
expect(
|
||
measured
|
||
.flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
).toEqual(files);
|
||
expect(
|
||
measured
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name.startsWith(models))
|
||
.every((group) => group.timing_key?.startsWith(`${models}-parallel`)),
|
||
).toBe(true);
|
||
if (runnerBackend !== "blacksmith") {
|
||
const modelGroups = measured
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name.startsWith(models));
|
||
expect(modelGroups).toHaveLength(2);
|
||
expect(
|
||
modelGroups.map(
|
||
(group) =>
|
||
group.includePatterns?.filter((file) => file !== e2eModel && file !== fastModel)
|
||
.length,
|
||
),
|
||
).toEqual([1, 1]);
|
||
modelGroups.forEach((group, index) => {
|
||
seconds[group.timing_key!] = index === 0 ? 180 : 190;
|
||
});
|
||
const childMeasured = createPlan(options);
|
||
expect(predicted(childMeasured)).toBe(runnerBackend === "hybrid" ? 405 : 410);
|
||
expect(
|
||
childMeasured
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name.startsWith(models))
|
||
.map((group) => expectDefined(group.timing_key, "parallel model timing key"))
|
||
.toSorted(),
|
||
).toEqual(
|
||
modelGroups
|
||
.map((group) => expectDefined(group.timing_key, "parallel model timing key"))
|
||
.toSorted(),
|
||
);
|
||
for (const group of modelGroups) {
|
||
delete seconds[group.timing_key!];
|
||
}
|
||
}
|
||
|
||
seconds[models] = 0;
|
||
seconds[`${models}-parallel`] = 0;
|
||
seconds[singleton] = 0;
|
||
seconds[commands] = 74;
|
||
const legacy = createCompactSplitTimingGeneration({
|
||
configs: [config],
|
||
parentShardName: commands,
|
||
stripes: [[runtimeFile], [commandPeers[0]!], [commandPeers[1]!]],
|
||
});
|
||
Object.assign(
|
||
seconds,
|
||
Object.fromEntries(legacy.timingKeys.map((key, index) => [key, index === 0 ? 28 : 23])),
|
||
);
|
||
fileWeights.mockImplementation((file) => (commandPeers.includes(file) ? 4 : 1));
|
||
buildMode.mockImplementation((plans) =>
|
||
plans.some((plan) => plan.matchesFile?.(runtimeFile, false, plan.includePatterns))
|
||
? "runtime"
|
||
: undefined,
|
||
);
|
||
const migrated = createPlan(options);
|
||
const runtimeJob = migrated.find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(runtimeFile)),
|
||
);
|
||
// Merging the two sibling stripes preserves the singleton's 28s measurement.
|
||
expect(runtimeJob?.predictedSeconds).toBe(
|
||
runnerBackend === "github" ? 124 : runnerBackend === "hybrid" ? 85 : 88,
|
||
);
|
||
seconds[`${commands}-parallel`] = 200;
|
||
const projectedRuntimeJob = createPlan(options).find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(runtimeFile)),
|
||
);
|
||
expect(projectedRuntimeJob?.predictedSeconds).toBe(runnerBackend === "github" ? 141 : 105);
|
||
const runtimeGroup = projectedRuntimeJob!.groups.find((group) =>
|
||
group.includePatterns?.includes(runtimeFile),
|
||
)!;
|
||
expect(runtimeGroup.includePatterns).toEqual([runtimeFile]);
|
||
seconds[runtimeGroup.timing_key!] = 10;
|
||
const refitted = createPlan(options).find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(runtimeFile)),
|
||
);
|
||
// An exact parallel sample replaces the migrated serial observation.
|
||
expect(refitted?.predictedSeconds).toBe(runnerBackend === "github" ? 106 : 70);
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/vitest-build-prerequisites.mts");
|
||
vi.doUnmock("../../scripts/lib/vitest-shard-metadata.mts");
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.doUnmock("../vitest/vitest.unit-fast-paths.mjs");
|
||
vi.resetModules();
|
||
}
|
||
},
|
||
);
|
||
|
||
it("retains isolated Gateway timing history recorded under its former job cap", () => {
|
||
const owner = "agentic-gateway-server-isolated";
|
||
const configs = [
|
||
"test/vitest/vitest.gateway-server-isolated.config.ts",
|
||
"test/vitest/vitest.gateway-database-workers.config.ts",
|
||
];
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
const fixtureShards = originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((config) => configs.includes(config)),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0);
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...fixtureShards);
|
||
try {
|
||
const previous: Record<string, number> = { [owner]: 100 };
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockImplementation((profile) =>
|
||
profile === "blacksmith" ? previous : { [owner]: 100 },
|
||
);
|
||
const options = { compactMode: "push" as const, runnerBackend: "hybrid" };
|
||
const initial = createNodeTestShardBundles(options).flatMap((job) => job.groups);
|
||
expect(initial).toHaveLength(2);
|
||
const legacy = createCompactSplitTimingGeneration({
|
||
configs,
|
||
parentShardName: owner,
|
||
stripes: initial.map((group) => group.includePatterns!),
|
||
});
|
||
for (const [index, key] of legacy.timingKeys.entries()) {
|
||
previous[key] = 247 + index;
|
||
}
|
||
const expanded = createNodeTestShardBundles(options);
|
||
expect(
|
||
expanded.reduce(
|
||
(sum, job) => sum + expectDefined(job.predictedSeconds, "compact job prediction"),
|
||
0,
|
||
),
|
||
).toBeGreaterThanOrEqual(495);
|
||
expect(
|
||
expanded.flatMap((job) => job.groups.flatMap((group) => group.includePatterns!)).toSorted(),
|
||
).toEqual(initial.flatMap((group) => group.includePatterns!).toSorted());
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
}
|
||
});
|
||
|
||
it.each(["blacksmith", "hybrid", "github"])(
|
||
"gates Gateway method workers without changing complete coverage (%s)",
|
||
(runnerBackend) => {
|
||
const owner = "agentic-gateway-methods";
|
||
const configs = new Set([
|
||
"test/vitest/vitest.gateway-methods.config.ts",
|
||
"test/vitest/vitest.gateway-methods-isolated.config.ts",
|
||
]);
|
||
const original = fullSuiteVitestShards.slice();
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...original
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((config) => configs.has(config)),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0),
|
||
);
|
||
const timings: Record<string, number> = { [owner]: 120 };
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(timings);
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
try {
|
||
const jobs = createNodeTestShardBundles({
|
||
compactMode: "push",
|
||
runnerBackend,
|
||
includeReleaseOnlyPluginShards: false,
|
||
includeReleaseOnlyRuntimeTests: true,
|
||
});
|
||
const groups = jobs.flatMap((job) => job.groups);
|
||
expect(groups.length).toBeGreaterThan(0);
|
||
expect(groups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
[
|
||
...listMatchedTestFiles(createGatewayMethodsVitestConfig({})),
|
||
...listMatchedTestFiles(createGatewayMethodsIsolatedVitestConfig({})),
|
||
].toSorted(),
|
||
);
|
||
for (const job of jobs) {
|
||
expect(job.planConcurrency).toBe(1);
|
||
if (runnerBackend !== "github") {
|
||
expect(job.runner).toBe(EXTRA_LARGE_NODE_TEST_RUNNER);
|
||
expect(job.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
}
|
||
for (const group of job.groups) {
|
||
const measured = runnerBackend !== "github";
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe(measured ? "4" : "2");
|
||
expect(group.minTotalMemoryBytes).toBe(measured ? 28 * 1024 ** 3 : undefined);
|
||
expect(group.fallbackMaxWorkers).toBe(measured ? 2 : undefined);
|
||
expect(group.timing_key?.includes("#workers-4") ?? false).toBe(measured);
|
||
}
|
||
}
|
||
if (runnerBackend !== "github") {
|
||
const fullFiles = groups.flatMap((group) => group.includePatterns ?? []);
|
||
const excluded = expectDefined(fullFiles[0], "Gateway method release-only fixture");
|
||
vi.spyOn(proofTestInventory, "isReleaseOnlyRuntimeTestFile").mockImplementation(
|
||
(file) => file === excluded,
|
||
);
|
||
vi.spyOn(buildPrerequisites, "resolveVitestPretestBuildMode").mockReturnValue(undefined);
|
||
for (const [key, seconds] of [
|
||
[owner, 1023],
|
||
[`changed-${owner}`, 800],
|
||
[`${owner}#workers-4`, 600],
|
||
[`changed-${owner}#workers-4`, 400],
|
||
] as const) {
|
||
timings[key] = seconds;
|
||
const reduced = createNodeTestShardBundles({
|
||
compactMode: "pull-request",
|
||
runnerBackend,
|
||
includeProofTests: true,
|
||
includeReleaseOnlyPluginShards: false,
|
||
includeReleaseOnlyRuntimeTests: false,
|
||
});
|
||
const predicted = reduced.reduce(
|
||
(sum, job) => sum + expectDefined(job.predictedSeconds, "Gateway prediction"),
|
||
0,
|
||
);
|
||
expect(predicted, key).toBeGreaterThanOrEqual(seconds);
|
||
expect(predicted, key).toBeLessThan(seconds + reduced.length);
|
||
expect(
|
||
reduced
|
||
.flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
).toEqual(fullFiles.filter((file) => file !== excluded).toSorted());
|
||
}
|
||
}
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
},
|
||
);
|
||
|
||
it.each([
|
||
{
|
||
owner: "agentic-agents-support",
|
||
config: agentVitestProjectOwners.support.config,
|
||
previousWorkers: undefined,
|
||
},
|
||
{
|
||
owner: "agentic-gateway-core-2",
|
||
config: "test/vitest/vitest.gateway-core.config.ts",
|
||
previousWorkers: "2",
|
||
},
|
||
])(
|
||
"retains complete $owner timing floors and ignores partial generations",
|
||
({ owner, config, previousWorkers }) => {
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
const fixtureShards = originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((candidate) => candidate === config),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0);
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...fixtureShards);
|
||
try {
|
||
let overlays: Record<"blacksmith" | "github", Readonly<Record<string, number>>> = {
|
||
blacksmith: { [owner]: 165 },
|
||
github: { [owner]: 253 },
|
||
};
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockImplementation(
|
||
(profile) => overlays[profile],
|
||
);
|
||
const options = {
|
||
compactMode: "pull-request" as const,
|
||
includeReleaseOnlyPluginShards: false,
|
||
runnerBackend: "hybrid",
|
||
};
|
||
const initialPlan = createNodeTestShardBundles(options);
|
||
const ownerGroups = (plan: typeof initialPlan) =>
|
||
plan
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name.replace(/-hosted-\d+$/u, "") === owner)
|
||
.toSorted((left, right) => left.shard_name.localeCompare(right.shard_name));
|
||
const initial = ownerGroups(initialPlan);
|
||
expect(initial).toHaveLength(2);
|
||
const originalFiles = initial.flatMap((group) => group.includePatterns!).toSorted();
|
||
const previousGeneration = createCompactSplitTimingGeneration({
|
||
configs: initial[0]!.configs,
|
||
env: previousWorkers
|
||
? { ...initial[0]!.env, OPENCLAW_VITEST_MAX_WORKERS: previousWorkers }
|
||
: initial[0]!.env,
|
||
parentShardName: owner,
|
||
stripes: initial.map((group) => group.includePatterns!),
|
||
});
|
||
overlays.blacksmith = {
|
||
...overlays.blacksmith,
|
||
...Object.fromEntries(
|
||
previousGeneration.timingKeys.map((key, index) => [key, 247 + index]),
|
||
),
|
||
};
|
||
|
||
const expanded = ownerGroups(createNodeTestShardBundles(options));
|
||
expect(expanded).toHaveLength(4);
|
||
expect(expanded.flatMap((group) => group.includePatterns!).toSorted()).toEqual(
|
||
originalFiles,
|
||
);
|
||
expect(
|
||
expanded.every((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === undefined),
|
||
).toBe(true);
|
||
const stripes = expanded.map((group) => group.includePatterns!);
|
||
const changedStripesA = stripes.map((patterns) => patterns.slice());
|
||
const first = changedStripesA[0]!.shift()!;
|
||
const second = changedStripesA[1]!.shift()!;
|
||
changedStripesA[0]!.push(second);
|
||
changedStripesA[1]!.push(first);
|
||
const partialA = createCompactSplitTimingGeneration({
|
||
configs: expanded[0]!.configs,
|
||
env: expanded[0]!.env,
|
||
parentShardName: owner,
|
||
stripes: changedStripesA,
|
||
});
|
||
const changedStripesB = stripes.map((patterns) => patterns.slice());
|
||
const third = changedStripesB[2]!.shift()!;
|
||
const fourth = changedStripesB[3]!.shift()!;
|
||
changedStripesB[2]!.push(fourth);
|
||
changedStripesB[3]!.push(third);
|
||
const partialB = createCompactSplitTimingGeneration({
|
||
configs: expanded[0]!.configs,
|
||
env: expanded[0]!.env,
|
||
parentShardName: owner,
|
||
stripes: changedStripesB,
|
||
});
|
||
overlays = {
|
||
github: { [owner]: 100 },
|
||
blacksmith: {
|
||
[owner]: 100,
|
||
[partialA.timingKeys[0]!]: 1_000,
|
||
[partialA.timingKeys[1]!]: 1_000,
|
||
[partialB.timingKeys[2]!]: 1_000,
|
||
[partialB.timingKeys[3]!]: 1_000,
|
||
},
|
||
};
|
||
const incomplete = createNodeTestShardBundles(options).flatMap((job) => job.groups);
|
||
expect(incomplete.filter((group) => group.shard_name === owner)).toHaveLength(1);
|
||
expect(
|
||
incomplete.filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)),
|
||
).toHaveLength(0);
|
||
|
||
overlays.blacksmith = {
|
||
...overlays.blacksmith,
|
||
...Object.fromEntries(expanded.map((group) => [group.timing_key!, 124])),
|
||
};
|
||
|
||
const stable = ownerGroups(createNodeTestShardBundles(options));
|
||
expect(stable).toHaveLength(4);
|
||
expect(stable.map((group) => group.timing_key)).toEqual(
|
||
expanded.map((group) => group.timing_key),
|
||
);
|
||
expect(stable.flatMap((group) => group.includePatterns!).toSorted()).toEqual(originalFiles);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
}
|
||
},
|
||
);
|
||
|
||
it.each([
|
||
{ profile: "blacksmith", owner: "auto-reply-reply-state-routing", fallback: 60, measured: 99 },
|
||
{ profile: "hybrid", owner: "auto-reply-reply-state-routing", fallback: 52, measured: 99 },
|
||
{ profile: "github", owner: "auto-reply-reply-state-routing", fallback: 60, measured: 99 },
|
||
{ profile: "blacksmith", owner: "auto-reply-reply-dispatch-core", fallback: 120, measured: 99 },
|
||
])(
|
||
"prices $owner on $profile at two workers without discounting new measurements",
|
||
({ profile, owner, fallback, measured }) => {
|
||
const original = fullSuiteVitestShards.slice();
|
||
const config = "test/vitest/vitest.auto-reply-reply.config.ts";
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...original
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((candidate) => candidate === config),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0),
|
||
);
|
||
try {
|
||
const owners = createNodeTestShards();
|
||
const timings = Object.fromEntries(owners.map((shard) => [shard.shardName, 0]));
|
||
timings[owner] = 120;
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(timings);
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "pull-request" as const,
|
||
runnerBackend: profile,
|
||
};
|
||
const before = createNodeTestShardBundles(options);
|
||
expect(
|
||
before.reduce(
|
||
(sum, job) => sum + expectDefined(job.predictedSeconds, "predicted compact seconds"),
|
||
0,
|
||
),
|
||
).toBe(fallback);
|
||
expect(
|
||
before
|
||
.flatMap((job) => job.groups)
|
||
.every((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === "2"),
|
||
).toBe(true);
|
||
const current = expectDefined(
|
||
before.flatMap((job) => job.groups).find((group) => group.shard_name === owner),
|
||
"parallel owner",
|
||
);
|
||
expect(current.timing_key).not.toBe(owner);
|
||
const files = expectDefined(current.includePatterns, "complete parallel owner inventory");
|
||
if (files.length > 1) {
|
||
const legacy = createCompactSplitTimingGeneration({
|
||
configs: current.configs,
|
||
parentShardName: owner,
|
||
stripes: [files.slice(0, 1), files.slice(1)],
|
||
});
|
||
const mismatched = createCompactSplitTimingGeneration({
|
||
configs: current.configs,
|
||
parentShardName: owner,
|
||
stripes: [files.slice(1)],
|
||
});
|
||
const complete = Object.fromEntries(legacy.timingKeys.map((key) => [key, 120]));
|
||
for (const scenario of [
|
||
{ observations: { [legacy.timingKeys[0]!]: 240 }, expected: fallback },
|
||
{ observations: { [mismatched.timingKeys[0]!]: 240 }, expected: fallback },
|
||
{ observations: complete, expected: profile === "hybrid" ? 104 : 120 },
|
||
]) {
|
||
Object.assign(timings, scenario.observations);
|
||
const plan = createNodeTestShardBundles(options);
|
||
expect(
|
||
plan.reduce(
|
||
(sum, job) =>
|
||
sum + expectDefined(job.predictedSeconds, "predicted compact seconds"),
|
||
0,
|
||
),
|
||
).toBe(scenario.expected);
|
||
for (const key of Object.keys(scenario.observations)) {
|
||
delete timings[key];
|
||
}
|
||
}
|
||
Object.assign(timings, complete);
|
||
}
|
||
timings[current.timing_key!] = 99;
|
||
const after = createNodeTestShardBundles(options);
|
||
expect(
|
||
after.reduce(
|
||
(sum, job) => sum + expectDefined(job.predictedSeconds, "predicted compact seconds"),
|
||
0,
|
||
),
|
||
).toBe(measured);
|
||
expect(
|
||
after.flatMap((job) => job.groups.flatMap((group) => group.includePatterns!)).toSorted(),
|
||
).toEqual(owners.flatMap((shard) => shard.includePatterns!).toSorted());
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("retains each singleton auto-reply stripe's full cost when splitting a parallel parent", () => {
|
||
const original = fullSuiteVitestShards.slice();
|
||
const config = "test/vitest/vitest.auto-reply-reply.config.ts";
|
||
const owner = "auto-reply-reply-state-routing";
|
||
const files = expectDefined(
|
||
defaultShards.find((shard) => shard.shardName === owner)?.includePatterns,
|
||
"mutable planner inventory fixture",
|
||
);
|
||
const originalFiles = files.slice();
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...original
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((candidate) => candidate === config),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0),
|
||
);
|
||
try {
|
||
files.splice(
|
||
0,
|
||
files.length,
|
||
"src/auto-reply/reply/parallel-fixture-a.test.ts",
|
||
"src/auto-reply/reply/parallel-fixture-b.test.ts",
|
||
);
|
||
const timings = {
|
||
...Object.fromEntries(createNodeTestShards().map((shard) => [shard.shardName, 0])),
|
||
[owner]: 400,
|
||
};
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(timings);
|
||
const options = {
|
||
compactMode: "pull-request" as const,
|
||
includeReleaseOnlyPluginShards: false,
|
||
runnerBackend: "github",
|
||
};
|
||
const plan = createNodeTestShardBundles(options);
|
||
const children = plan.flatMap((job) =>
|
||
job.groups.filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)),
|
||
);
|
||
expect(children).toHaveLength(2);
|
||
expect(children.map((group) => group.includePatterns?.length)).toEqual([1, 1]);
|
||
expect(children.flatMap((group) => group.includePatterns!).toSorted()).toEqual(files);
|
||
expect(
|
||
plan.reduce(
|
||
(sum, job) => sum + expectDefined(job.predictedSeconds, "predicted compact seconds"),
|
||
0,
|
||
),
|
||
).toBe(400);
|
||
Object.assign(timings, Object.fromEntries(children.map((group) => [group.timing_key!, 200])));
|
||
const measured = createNodeTestShardBundles(options);
|
||
expect(
|
||
measured.reduce(
|
||
(sum, job) => sum + expectDefined(job.predictedSeconds, "predicted compact seconds"),
|
||
0,
|
||
),
|
||
).toBe(400);
|
||
expect(
|
||
measured.flatMap((job) =>
|
||
job.groups.filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)),
|
||
),
|
||
).toEqual(children);
|
||
} finally {
|
||
files.splice(0, files.length, ...originalFiles);
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
});
|
||
|
||
it.each(["blacksmith", "github", "hybrid"])(
|
||
"prices serial Gateway server measurements at two workers and preserves parallel samples (%s)",
|
||
(runnerBackend) => {
|
||
const config = "test/vitest/vitest.gateway-server.config.ts";
|
||
const owner = "agentic-control-plane-agent-chat";
|
||
const original = fullSuiteVitestShards.slice();
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...original
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((candidate) => candidate === config),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0),
|
||
);
|
||
try {
|
||
const timings: Record<string, number> = { [owner]: 540 };
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(timings);
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
const options = { compactMode: "pull-request" as const, runnerBackend };
|
||
const select = (includeReleaseOnlyRuntimeTests = true) =>
|
||
createNodeTestShardBundles({ ...options, includeReleaseOnlyRuntimeTests })
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name.replace(/-hosted-\d+$/u, "") === owner);
|
||
const inherited = select();
|
||
expect(inherited).toHaveLength(2);
|
||
expect(inherited.every((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === "2")).toBe(
|
||
true,
|
||
);
|
||
const parallelParent = expectDefined(
|
||
parseCompactSplitTimingKey(inherited[0]!.timing_key!)?.parentShardName,
|
||
"parallel measurement parent",
|
||
);
|
||
expect(parallelParent).not.toBe(owner);
|
||
const { OPENCLAW_VITEST_MAX_WORKERS: _workers, ...serialEnv } = inherited[0]!.env!;
|
||
const serialGeneration = createCompactSplitTimingGeneration({
|
||
configs: [config],
|
||
env: serialEnv,
|
||
parentShardName: owner,
|
||
stripes: inherited.map((group) => group.includePatterns!),
|
||
});
|
||
timings[serialGeneration.timingKeys[0]!] = 500;
|
||
expect(select()).toHaveLength(2);
|
||
timings[serialGeneration.timingKeys[1]!] = 500;
|
||
expect(select()).toHaveLength(4);
|
||
timings[`${owner}-parallel-native-serial`] = 440;
|
||
timings[parallelParent] = 440;
|
||
const measured = select();
|
||
expect(measured).toHaveLength(3);
|
||
expect(measured.flatMap((group) => group.includePatterns!).toSorted()).toEqual(
|
||
inherited.flatMap((group) => group.includePatterns!).toSorted(),
|
||
);
|
||
const reduced = select(false);
|
||
expect(reduced).toHaveLength(measured.length);
|
||
expect(reduced.flatMap((group) => group.includePatterns!).toSorted()).toEqual(
|
||
measured
|
||
.flatMap((group) => group.includePatterns!)
|
||
.filter((file) => !isReleaseOnlyRuntimeTestFile(file))
|
||
.toSorted(),
|
||
);
|
||
expect(
|
||
reduced.map((group) => parseCompactSplitTimingKey(group.timing_key!)?.parentShardName),
|
||
).toEqual(reduced.map(() => `changed-${parallelParent}`));
|
||
timings[`changed-${owner}-parallel-native-serial`] = 180;
|
||
expect(select(false).length).toBeLessThan(reduced.length);
|
||
expect(select()).toEqual(measured);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("charges native fixture spans before discounting concurrent Gateway work", async () => {
|
||
const config = "test/vitest/vitest.gateway-server.config.ts";
|
||
const owner = "agentic-control-plane-agent-chat";
|
||
const files = [
|
||
gatewayServerSerialTestFiles[0]!,
|
||
"src/gateway/server.chat.fixture-a.test.ts",
|
||
"src/gateway/server.chat.fixture-b.test.ts",
|
||
];
|
||
const timings: Record<string, number> = { [owner]: 230 };
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [{ name: "agentic", config, projects: [config] }],
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (root: string) => (root === "src/gateway" ? files : []),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => timings,
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
try {
|
||
const { createNodeTestShardBundles: createPlan } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const options = { compactMode: "push" as const, runnerBackend: "blacksmith" };
|
||
const initial = createPlan(options);
|
||
expect(initial).toHaveLength(1);
|
||
expect(initial[0]?.predictedSeconds).toBe(130);
|
||
const parentKey = expectDefined(initial[0]?.groups[0]?.timing_key, "mixed phase timing key");
|
||
expect(parentKey).toContain("native-serial");
|
||
timings[parentKey] = 230;
|
||
const split = createPlan(options);
|
||
expect(split.map((job) => job.predictedSeconds).toSorted((a, b) => a! - b!)).toEqual([
|
||
30, 200,
|
||
]);
|
||
expect(
|
||
split
|
||
.flatMap((job) => job.groups)
|
||
.flatMap((group) => group.includePatterns!)
|
||
.toSorted(),
|
||
).toEqual(files.toSorted());
|
||
const native = expectDefined(
|
||
split.find((job) => job.groups.some((group) => group.includePatterns?.includes(files[0]!))),
|
||
"native singleton",
|
||
);
|
||
expect(native.groups.flatMap((group) => group.includePatterns!)).toEqual([files[0]]);
|
||
expect(native.predictedSeconds).toBe(30);
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.resetModules();
|
||
}
|
||
});
|
||
|
||
it("keeps parallel singleton measurements separate from the legacy serial floor", async () => {
|
||
const config = "test/vitest/vitest.gateway-server.config.ts";
|
||
const owner = "agentic-control-plane-runtime-server";
|
||
const files = [
|
||
"src/gateway/server-sidecar-retention.test.ts",
|
||
"src/gateway/server-file-fixtures.test.ts",
|
||
];
|
||
const timings: Record<string, number> = { [owner]: 200 };
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [{ name: "agentic", config, projects: [config] }],
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (root: string) => (root === "src/gateway" ? files : []),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => timings,
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
try {
|
||
const { createNodeTestShardBundles: createPlan } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const options = { compactMode: "push" as const, runnerBackend: "blacksmith" };
|
||
const initial = createPlan(options);
|
||
const groups = initial.flatMap((job) => job.groups);
|
||
expect(groups.map((group) => group.includePatterns?.length)).toEqual([1, 1]);
|
||
expect(groups.flatMap((group) => group.includePatterns!).toSorted()).toEqual(
|
||
files.toSorted(),
|
||
);
|
||
const keys = groups.map((group) => expectDefined(group.timing_key, "singleton timing key"));
|
||
const parent = expectDefined(
|
||
parseCompactSplitTimingKey(keys[0]!),
|
||
"parallel family",
|
||
).parentShardName;
|
||
const seconds = () =>
|
||
createPlan(options).reduce((sum, job) => sum + (job.predictedSeconds ?? 0), 0);
|
||
// One runtime preparation costs 60s; both singleton files retain their serial floor.
|
||
expect(seconds()).toBe(260);
|
||
// Two 300s files still cost 300s each when a 300s parallel parent is split.
|
||
timings[`${owner}-parallel`] = 300;
|
||
expect(seconds()).toBe(660);
|
||
delete timings[`${owner}-parallel`];
|
||
for (const key of keys) {
|
||
timings[key] = 300;
|
||
}
|
||
expect(seconds()).toBe(660);
|
||
timings[parent] = 600;
|
||
expect(seconds()).toBe(660);
|
||
for (const key of keys) {
|
||
delete timings[key];
|
||
}
|
||
expect(seconds()).toBe(660);
|
||
for (const key of keys) {
|
||
timings[key] = 300;
|
||
}
|
||
timings[parent] = 400;
|
||
expect(seconds()).toBe(660);
|
||
delete timings[parent];
|
||
for (const key of keys) {
|
||
timings[key] = 30;
|
||
}
|
||
expect(seconds()).toBe(120);
|
||
timings[parent] = 700;
|
||
expect(seconds()).toBe(120);
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.resetModules();
|
||
}
|
||
});
|
||
|
||
it("keeps Chromium files in the UI CI owner and Node-driven Playwright files in Node stripes", () => {
|
||
const shards = defaultShards;
|
||
const uiStripes = shards.filter((shard) =>
|
||
shard.shardName.startsWith("core-runtime-media-ui-"),
|
||
);
|
||
const files = uiStripes.flatMap((shard) => shard.includePatterns ?? []);
|
||
expect(files).toContain("ui/src/components/form-controls.browser.test.ts");
|
||
expect(files).not.toContain("ui/src/components/markdown-mermaid.runtime.browser.test.ts");
|
||
expect(shards.flatMap((shard) => shard.configs)).not.toContain(
|
||
"test/vitest/vitest.ui-browser.config.ts",
|
||
);
|
||
});
|
||
it.each(["github", "hybrid"])("keeps oversized sparse groups nonempty on %s", (runnerBackend) => {
|
||
const native = createNodeTestShards({ includeReleaseOnlyPluginShards: false });
|
||
const targets = [1, 2].map((count) =>
|
||
native.find(
|
||
(shard) =>
|
||
shard.includePatterns?.length === count && !shard.shardName.startsWith("core-tooling"),
|
||
)!,
|
||
);
|
||
expect(targets.every(Boolean)).toBe(true);
|
||
const original = testTimings.readCompactGroupTimings;
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockImplementation((profile) => ({
|
||
...original(profile),
|
||
...Object.fromEntries(targets.map((target) => [target.shardName, 1_000])),
|
||
}));
|
||
const plan = createNodeTestShardBundles({
|
||
compactMode: "push",
|
||
runnerBackend,
|
||
includeReleaseOnlyPluginShards: false,
|
||
});
|
||
for (const target of targets) {
|
||
const groups = plan
|
||
.flatMap((job) => job.groups)
|
||
.filter(
|
||
(group) =>
|
||
group.shard_name === target.shardName ||
|
||
group.shard_name.startsWith(`${target.shardName}-hosted-`),
|
||
);
|
||
expect(groups.every((group) => (group.includePatterns?.length ?? 0) > 0)).toBe(true);
|
||
expect(groups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
target.includePatterns!.toSorted(),
|
||
);
|
||
expect(groups).toHaveLength(target.includePatterns!.length);
|
||
}
|
||
});
|
||
|
||
it.each([
|
||
{ runnerBackend: "github", slowerProfile: "github" },
|
||
{ runnerBackend: "hybrid", slowerProfile: "github" },
|
||
{ runnerBackend: "hybrid", slowerProfile: "blacksmith" },
|
||
])(
|
||
"bounds $runnerBackend child groups by the slower $slowerProfile path",
|
||
({ runnerBackend, slowerProfile }) => {
|
||
// This lane still uses profile-specific group spans; tooling now prices
|
||
// current per-file costs and has separate worker/longest-file coverage.
|
||
const target = {
|
||
...createNodeTestShards().find((shard) => shard.shardName === "core-runtime-config")!,
|
||
includePatterns: listMatchedTestFiles(createRuntimeConfigVitestConfig({})),
|
||
};
|
||
const runtimeFiles = new Set(listVitestRuntimeConsumerFiles(target.configs));
|
||
const runtimeConsumers = target.includePatterns.filter((file) => runtimeFiles.has(file));
|
||
const buildMode = "runtime";
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
// Exercise this owner's split without consuming unrelated suite families' job budget.
|
||
const fixtureShards = originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((config) => target.configs.includes(config)),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0);
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...fixtureShards);
|
||
try {
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockImplementation((profile) => ({
|
||
[target.shardName]: profile === slowerProfile ? 400 : 100,
|
||
}));
|
||
const plan = createNodeTestShardBundles({
|
||
compactMode: "pull-request",
|
||
runnerBackend,
|
||
includeReleaseOnlyPluginShards: false,
|
||
});
|
||
const groups = plan
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name.startsWith(`${target.shardName}-hosted-`));
|
||
// Runtime consumers share one isolated build child; its fixed build may
|
||
// exceed the cap. Remaining work needs three stripes on both profiles.
|
||
expect(groups).toHaveLength(4);
|
||
if (runnerBackend === "github") {
|
||
expect(
|
||
plan
|
||
.filter((job) => job.predictedSeconds! > 150)
|
||
.every((job) => {
|
||
if (job.groups.length > 1 && job.pretestBuildMode) {
|
||
return (
|
||
job.predictedSeconds! <= 210 &&
|
||
job.planConcurrency === 1 &&
|
||
!job.requiresDist &&
|
||
job.groups.every((group) => group.pretestBuildMode)
|
||
);
|
||
}
|
||
return (
|
||
job.groups.length === 1 &&
|
||
(job.groups[0]!.includePatterns?.length === 1 ||
|
||
(job.pretestBuildMode === buildMode &&
|
||
job.groups[0]!.includePatterns?.length === runtimeConsumers.length &&
|
||
job.groups[0]!.includePatterns?.every((file) =>
|
||
runtimeConsumers.some((runtimeConsumer) => runtimeConsumer === file),
|
||
)))
|
||
);
|
||
}),
|
||
).toBe(true);
|
||
}
|
||
expect(
|
||
groups
|
||
.filter((group) => group.pretestBuildMode !== undefined)
|
||
.map(({ pretestBuildMode, includePatterns }) => ({
|
||
pretestBuildMode,
|
||
includePatterns,
|
||
})),
|
||
).toEqual([{ pretestBuildMode: buildMode, includePatterns: runtimeConsumers }]);
|
||
for (const job of plan) {
|
||
if (
|
||
job.groups.some(
|
||
(group) => groups.includes(group) && group.pretestBuildMode === undefined,
|
||
)
|
||
) {
|
||
expect(job.predictedSeconds, `${runnerBackend}/${slowerProfile}`).toBeLessThanOrEqual(
|
||
150,
|
||
);
|
||
}
|
||
}
|
||
expect(groups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
target.includePatterns!.toSorted(),
|
||
);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
}
|
||
},
|
||
);
|
||
afterEach(() => {
|
||
vi.restoreAllMocks();
|
||
if (plannerHostPinned) {
|
||
vi.unstubAllEnvs();
|
||
syncBuiltinESMExports();
|
||
committedCompactPlans.clear();
|
||
plannerHostPinned = false;
|
||
}
|
||
});
|
||
|
||
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([]);
|
||
expect(resolvePolicyTestTargets(["extensions/anthropic/openclaw.plugin.json"])).toEqual([
|
||
"src/agents/model-ref-shared.test.ts",
|
||
]);
|
||
});
|
||
|
||
it("selects the core root budget guard alongside changed messaging test owners", () => {
|
||
const guard = "test/scripts/tsgo-core-test-shards.test.ts";
|
||
const changed = "src/infra/outbound/outbound-send-service.accepted-outcomes.test.ts";
|
||
const shards = expectDefined(createChangedNodeTestShards([changed]), "core test plan");
|
||
const targets = shards.flatMap((shard) =>
|
||
(shard.targets ?? []).concat(
|
||
(shard.groups ?? []).flatMap((group) => group.includePatterns ?? []),
|
||
),
|
||
);
|
||
expect(targets).toContain(changed);
|
||
expect(targets.filter((target) => target === guard)).toEqual([guard]);
|
||
for (const changedPath of [
|
||
"src/auto-reply/reply/new.test.tsx",
|
||
"src/infra/outbound/new.test.ts",
|
||
"test/tsconfig/tsconfig.core.test.messaging.json",
|
||
]) {
|
||
expect(resolvePolicyTestTargets([changedPath]), changedPath).toContain(guard);
|
||
expect(isPolicyTestOwnedPath(changedPath), changedPath).toBe(false);
|
||
}
|
||
for (const changedPath of [
|
||
"src/infra/outbound/message.ts",
|
||
"src/infra/outbound/message.test-support.ts",
|
||
"src/agents/new.test.tsx",
|
||
"ui/src/pages/new.test.ts",
|
||
"packages/example/new.test.tsx",
|
||
"extensions/example/new.test.ts",
|
||
]) {
|
||
expect(resolvePolicyTestTargets([changedPath]), changedPath).not.toContain(guard);
|
||
}
|
||
});
|
||
|
||
it("selects provisioning and closure guards without replacing source test owners", () => {
|
||
const guards = [
|
||
"test/scripts/pr-worktree-provision.test.ts",
|
||
"test/scripts/eager-import-closure.test.ts",
|
||
];
|
||
const manifest = "scripts/pr-lib/wrapper-components.txt";
|
||
for (const changedPath of [
|
||
"scripts/pr",
|
||
"scripts/pr-lib/worktree.sh",
|
||
"src/plugins/discovery.ts",
|
||
"src/plugins/discovery-availability.ts",
|
||
]) {
|
||
const targets = resolvePolicyTestTargets([changedPath]);
|
||
for (const guard of guards) {
|
||
expect(targets, changedPath).toContain(guard);
|
||
}
|
||
expect(isPolicyTestOwnedPath(changedPath), changedPath).toBe(false);
|
||
}
|
||
const unrelatedTargets = resolvePolicyTestTargets(["src/plugins/unrelated-new-plugin.ts"]);
|
||
for (const guard of guards) {
|
||
expect(unrelatedTargets).not.toContain(guard);
|
||
}
|
||
expect(isPolicyTestOwnedPath(manifest)).toBe(true);
|
||
const shards = expectDefined(createChangedNodeTestShards([manifest]), "manifest test plan");
|
||
for (const guard of guards) {
|
||
const owners = shards
|
||
.flatMap((shard) => shard.groups ?? [])
|
||
.filter((group) => group.includePatterns?.includes(guard));
|
||
expect(owners).toHaveLength(1);
|
||
expect(owners[0]?.configs).toEqual(["test/vitest/vitest.tooling.config.ts"]);
|
||
}
|
||
});
|
||
|
||
it("matches policy owners only for exact changed paths", () => {
|
||
const changedPath = "ui/src/styles/base.css";
|
||
expect(isPolicyTestOwnedPath(changedPath)).toBe(true);
|
||
expect(resolvePolicyTestTargets([changedPath])).not.toEqual([]);
|
||
for (const lookalike of [` ${changedPath}`, String.raw`ui\src\pages\chat\view.ts`]) {
|
||
expect(isPolicyTestOwnedPath(lookalike), lookalike).toBe(false);
|
||
expect(resolvePolicyTestTargets([lookalike]), lookalike).toEqual([]);
|
||
}
|
||
});
|
||
|
||
it("projects cache-warm groups from the owned node test plan", () => {
|
||
const groups = createVitestCacheWarmGroups();
|
||
expect(groups).toHaveLength(12);
|
||
expect(groups.every((group) => group.configs.length === 1)).toBe(true);
|
||
expect(new Set(groups.flatMap((group) => group.configs))).toHaveProperty("size", 11);
|
||
expect(new Set(groups.map((group) => group.shard_name))).toHaveProperty("size", groups.length);
|
||
|
||
const coreStripeGroups = groups.filter(
|
||
(group) => group.configs[0] === "test/vitest/vitest.unit-fast.config.ts",
|
||
);
|
||
expect(coreStripeGroups).toHaveLength(2);
|
||
expect(coreStripeGroups.every((group) => (group.includePatterns?.length ?? 0) > 0)).toBe(true);
|
||
const coreStripePatterns = coreStripeGroups.flatMap((group) => group.includePatterns ?? []);
|
||
expect(new Set(coreStripePatterns).size).toBe(coreStripePatterns.length);
|
||
|
||
const isolatedGroups = groups.filter(
|
||
(group) =>
|
||
group.shard_name.startsWith("cache-warm:core-unit-fast-isolated:") ||
|
||
group.shard_name.startsWith("cache-warm:core-unit-fast-fake-timers:"),
|
||
);
|
||
expect(isolatedGroups).toHaveLength(2);
|
||
expect(isolatedGroups.every((group) => group.includePatterns === undefined)).toBe(true);
|
||
expect(isolatedGroups.every((group) => group.env === undefined)).toBe(true);
|
||
|
||
const embeddedGroups = groups.filter((group) =>
|
||
group.shard_name.startsWith("cache-warm:agentic-agents-embedded:"),
|
||
);
|
||
expect(embeddedGroups).toHaveLength(4);
|
||
expect(
|
||
embeddedGroups.every((group) => group.env?.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS === "660000"),
|
||
).toBe(true);
|
||
|
||
const gatewayGroups = groups.filter((group) =>
|
||
group.shard_name.startsWith("cache-warm:agentic-gateway-methods:"),
|
||
);
|
||
expect(
|
||
gatewayGroups.flatMap((group) => group.configs).toSorted((a, b) => a.localeCompare(b)),
|
||
).toEqual([
|
||
"test/vitest/vitest.gateway-methods-isolated.config.ts",
|
||
"test/vitest/vitest.gateway-methods.config.ts",
|
||
]);
|
||
expect(gatewayGroups.every((group) => group.includePatterns === undefined)).toBe(true);
|
||
expect(gatewayGroups.every((group) => group.env === undefined)).toBe(true);
|
||
|
||
const autoReplyGroups = groups.filter((group) =>
|
||
group.shard_name.startsWith("cache-warm:auto-reply-reply-commands-3:"),
|
||
);
|
||
expect(autoReplyGroups).toHaveLength(1);
|
||
const autoReplyShard = expectDefined(
|
||
defaultShards.find((shard) => shard.shardName === "auto-reply-reply-commands-3"),
|
||
"auto-reply command shard",
|
||
);
|
||
expect(autoReplyGroups[0]?.includePatterns).toEqual(autoReplyShard.includePatterns);
|
||
expect(autoReplyGroups[0]?.env).toBeUndefined();
|
||
|
||
expect(groups.find((group) => group.shard_name === "cache-warm:ui-package")).toEqual({
|
||
configs: ["ui/vitest.config.ts"],
|
||
env: { OPENCLAW_VITEST_MAX_WORKERS: "1" },
|
||
includePatterns: [
|
||
"ui/src/components/app-sidebar.catalog.test.ts",
|
||
"ui/src/components/app-sidebar.interactions.test.ts",
|
||
"ui/src/components/app-sidebar.people.test.ts",
|
||
"ui/src/components/app-sidebar.sessions.test.ts",
|
||
"ui/src/pages/chat/chat-view.test.ts",
|
||
"ui/src/pages/chat/chat-pane-lifecycle.test.ts",
|
||
"ui/src/pages/usage/metrics.node.test.ts",
|
||
],
|
||
shard_name: "cache-warm:ui-package",
|
||
});
|
||
});
|
||
|
||
it("bounds the hybrid hosted seed to real consumer configs without runtime builds", () => {
|
||
const groups = createVitestCacheWarmGroups("hybrid-hosted");
|
||
expect(groups).toHaveLength(7);
|
||
|
||
expect(groups.every((group) => (group.includePatterns?.length ?? 0) > 0)).toBe(true);
|
||
const files = groups.flatMap((group) => group.includePatterns ?? []);
|
||
expect(files).toHaveLength(17);
|
||
expect(files.every((file) => existsSync(file))).toBe(true);
|
||
expect(buildPrerequisites.resolveVitestPretestBuildMode(groups)).toBeUndefined();
|
||
const tooling = expectDefined(
|
||
groups.find((group) => group.shard_name === "cache-warm:hosted-tooling"),
|
||
"hosted tooling seed",
|
||
);
|
||
expect(
|
||
createVitestRunSpecs(expectDefined(tooling.includePatterns, "hosted tooling files"), {
|
||
baseEnv: { CI: "true", OPENCLAW_TEST_PROJECTS_PARALLEL: "3" },
|
||
}).map((spec) => spec.config),
|
||
).toEqual(tooling.configs);
|
||
const configs = groups.flatMap((group) => group.configs);
|
||
expect(configs).toContain("test/vitest/vitest.tooling.config.ts");
|
||
expect(configs).toContain("ui/vitest.config.ts");
|
||
expect(configs.filter((config) => config.includes("vitest.contracts-"))).toHaveLength(5);
|
||
expect(groups.find((group) => group.configs[0] === "ui/vitest.config.ts")).toEqual(
|
||
createVitestCacheWarmGroups().find((group) => group.configs[0] === "ui/vitest.config.ts"),
|
||
);
|
||
});
|
||
|
||
it("creates split shards without walking test roots", () => {
|
||
const payload = expectNoNodeFsScans<{
|
||
includePatterns: number;
|
||
shards: number;
|
||
}>(`
|
||
const { createNodeTestShards } = await import("./scripts/lib/ci-node-test-plan.mts");
|
||
const shards = createNodeTestShards();
|
||
return {
|
||
includePatterns: shards.reduce(
|
||
(total, shard) => total + (shard.includePatterns?.length ?? 0),
|
||
0,
|
||
),
|
||
shards: shards.length,
|
||
};
|
||
`);
|
||
expect(payload.shards).toBeGreaterThan(0);
|
||
expect(payload.includePatterns).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("keeps reduced Gateway coverage under distinct complete timing parts", () => {
|
||
const isReleaseOnlyRuntime = proofTestInventory.isReleaseOnlyRuntimeTestFile;
|
||
vi.spyOn(proofTestInventory, "isReleaseOnlyRuntimeTestFile").mockImplementation(
|
||
(file) => file === "src/gateway/server.chat-cli-auth.test.ts" || isReleaseOnlyRuntime(file),
|
||
);
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
includeReleaseOnlyRuntimeTests: false,
|
||
};
|
||
const owner = expectDefined(
|
||
createNodeTestShards(options).find(
|
||
(shard) => shard.shardName === "agentic-gateway-server-isolated",
|
||
),
|
||
"reduced Gateway owner",
|
||
);
|
||
const stripes = createNodeTestShardBundles(options).filter((shard) =>
|
||
shard.shardName.startsWith("agentic-gateway-server-isolated-"),
|
||
);
|
||
expect(stripes.length).toBeGreaterThan(1);
|
||
const timingKeys = stripes.map((stripe) =>
|
||
expectDefined(stripe.timing_key, "reduced Gateway stripe timing"),
|
||
);
|
||
expect(new Set(timingKeys).size).toBe(stripes.length);
|
||
expect(timingKeys).not.toContain(owner.timing_key);
|
||
expect(stripes.flatMap((stripe) => stripe.includePatterns ?? []).toSorted()).toEqual(
|
||
owner.includePatterns?.toSorted(),
|
||
);
|
||
const timingParts = timingKeys.map((key) =>
|
||
expectDefined(parseCompactSplitTimingKey(key), "reduced Gateway timing part"),
|
||
);
|
||
expect(timingParts.map((part) => part.parentShardName)).toEqual(
|
||
stripes.map(() => "changed-agentic-gateway-server-isolated"),
|
||
);
|
||
expect(new Set(timingParts.map((part) => part.generationKey)).size).toBe(1);
|
||
expect(timingParts.map((part) => part.expectedParts)).toEqual(
|
||
stripes.map(() => stripes.length),
|
||
);
|
||
expect(timingParts.map((part) => part.part).toSorted((a, b) => a - b)).toEqual(
|
||
stripes.map((_, index) => index + 1),
|
||
);
|
||
});
|
||
|
||
it("bundles split shards with deterministic unique identities and unchanged coverage", () => {
|
||
const base = createNodeTestShards({ includeReleaseOnlyPluginShards: false });
|
||
const bundled = createNodeTestShardBundles({ includeReleaseOnlyPluginShards: false });
|
||
expect(
|
||
bundled.some((shard) => {
|
||
if (!shard.shardName.startsWith("bundle-")) {
|
||
return false;
|
||
}
|
||
const patterns = new Set(shard.includePatterns);
|
||
return (
|
||
base.filter((owner) => owner.includePatterns?.some((pattern) => patterns.has(pattern)))
|
||
.length > 1
|
||
);
|
||
}),
|
||
).toBe(true);
|
||
const gatewayOwner = expectDefined(
|
||
base.find((shard) => shard.shardName === "agentic-gateway-server-isolated"),
|
||
"full Gateway owner",
|
||
);
|
||
const gatewayStripes = bundled.filter((shard) =>
|
||
shard.shardName.startsWith("agentic-gateway-server-isolated-"),
|
||
);
|
||
expect(gatewayStripes.length).toBeGreaterThan(1);
|
||
for (const stripe of gatewayStripes) {
|
||
expect(stripe.includePatterns!.length).toBeGreaterThan(0);
|
||
expect(stripe.includePatterns!.length).toBeLessThanOrEqual(64);
|
||
expect(stripe.configs).toEqual(gatewayOwner.configs);
|
||
expect(stripe.env).toEqual(gatewayOwner.env);
|
||
expect(stripe.pretestBuildMode).toBe(gatewayOwner.pretestBuildMode);
|
||
expect(stripe.runner).toBe(DEFAULT_NODE_TEST_RUNNER);
|
||
expect(stripe.timeoutMinutes).toBe(gatewayOwner.timeoutMinutes);
|
||
expect(stripe.planConcurrency).toBe(gatewayOwner.planConcurrency);
|
||
}
|
||
const gatewayPatterns = [...gatewayServerIsolatedTestFiles, ...gatewayDatabaseWorkerTestFiles];
|
||
const basePatterns = base
|
||
.flatMap((shard) => shard.includePatterns ?? (shard === gatewayOwner ? gatewayPatterns : []))
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
const bundledPatterns = bundled
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
|
||
const outputBundles = bundled.filter((shard) => shard.shardName.startsWith("bundle-"));
|
||
const bundledConfigs = new Set(outputBundles.flatMap((shard) => shard.configs));
|
||
// Required 64-file splits can offset bundling savings in the total owner count.
|
||
const inputChunks = base
|
||
.filter((shard) => shard.configs.every((config) => bundledConfigs.has(config)))
|
||
.reduce((count, shard) => count + Math.ceil((shard.includePatterns?.length ?? 0) / 64), 0);
|
||
expect(outputBundles.length).toBeLessThan(inputChunks);
|
||
expect(new Set(bundled.map((shard) => shard.checkName)).size).toBe(bundled.length);
|
||
expect(bundledPatterns).toEqual(basePatterns);
|
||
expect(
|
||
bundled
|
||
.filter((shard) => shard.shardName.startsWith("bundle-"))
|
||
.every((shard) => (shard.includePatterns?.length ?? 0) <= 64),
|
||
).toBe(true);
|
||
expect(bundled.every((shard) => shard.runner?.startsWith("blacksmith-"))).toBe(true);
|
||
expect(bundled).toEqual(createNodeTestShardBundles({ includeReleaseOnlyPluginShards: false }));
|
||
expect(bundled.slice(0, 7).map((shard) => shard.shardName)).toEqual([
|
||
"core-unit-fast-1",
|
||
"core-unit-fast-2",
|
||
"core-tooling-1",
|
||
"core-tooling-10",
|
||
"core-tooling-11",
|
||
"core-tooling-12",
|
||
"core-tooling-13",
|
||
]);
|
||
expect(bundled.find((shard) => shard.shardName === "core-unit-fast-1")?.runner).toBe(
|
||
DEFAULT_NODE_TEST_RUNNER,
|
||
);
|
||
expect(bundled.find((shard) => shard.shardName === "core-unit-fast-2")?.runner).toBe(
|
||
DEFAULT_NODE_TEST_RUNNER,
|
||
);
|
||
expect(
|
||
bundled.find((shard) => shard.shardName === "agentic-control-plane-startup-health-runtime")
|
||
?.env,
|
||
).toEqual({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "60000" });
|
||
expect(
|
||
bundled.find((shard) => shard.shardName === "agentic-control-plane-startup-core")?.runner,
|
||
).toBe(DEFAULT_NODE_TEST_RUNNER);
|
||
expect(bundled.find((shard) => shard.shardName === "bundle-infra-small-1")?.runner).toBe(
|
||
"blacksmith-4vcpu-ubuntu-2404",
|
||
);
|
||
expect(
|
||
new Set(
|
||
bundled
|
||
.filter((shard) => shard.shardName.startsWith("bundle-"))
|
||
.flatMap((shard) => shard.configs),
|
||
),
|
||
).toEqual(new Set(["test/vitest/vitest.infra.config.ts"]));
|
||
expect(bundled.some((shard) => shard.shardName.startsWith("bundle-commands-"))).toBe(false);
|
||
expect(bundled.some((shard) => shard.shardName.startsWith("bundle-cron-"))).toBe(false);
|
||
expect(bundled.some((shard) => shard.shardName.startsWith("bundle-agents-core-"))).toBe(false);
|
||
expect(bundled.some((shard) => shard.shardName.startsWith("bundle-gateway-server-"))).toBe(
|
||
false,
|
||
);
|
||
});
|
||
|
||
it.each([
|
||
{ profile: "blacksmith", legacy: 116, measured: 310, defaultSeconds: 25 },
|
||
{ profile: "github", legacy: 186, measured: 370, defaultSeconds: 40 },
|
||
{ profile: "hybrid", legacy: 101, measured: 310, defaultSeconds: 22 },
|
||
])(
|
||
"prefers $profile measurements while retaining unmeasured hints and defaults",
|
||
({ profile, legacy, measured, defaultSeconds }) => {
|
||
const timings = vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue({});
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "pull-request" as const,
|
||
runnerBackend: profile,
|
||
};
|
||
const fallback = createNodeTestShardBundles(options);
|
||
const tuiJob = (plan: typeof fallback) =>
|
||
plan.find((shard) =>
|
||
shard.groups.some((group) => group.shard_name === "core-runtime-tui-pty"),
|
||
);
|
||
expect(tuiJob(fallback)?.predictedSeconds).toBe(legacy);
|
||
timings.mockImplementation((runner) => ({
|
||
"core-runtime-tui-pty": runner === "blacksmith" ? 310 : 370,
|
||
"removed-test-group": 999,
|
||
}));
|
||
const updated = createNodeTestShardBundles(options);
|
||
expect(tuiJob(updated)?.groups.map((group) => group.shard_name)).toEqual([
|
||
"core-runtime-tui-pty",
|
||
]);
|
||
expect(tuiJob(updated)?.predictedSeconds).toBe(measured);
|
||
expect(
|
||
updated.find((shard) =>
|
||
shard.groups.some((group) => group.shard_name === "core-support-boundary"),
|
||
)?.predictedSeconds,
|
||
).toBe(defaultSeconds);
|
||
const groupNames = (plan: typeof fallback) =>
|
||
plan.flatMap((shard) => shard.groups.map((group) => group.shard_name)).toSorted();
|
||
expect(groupNames(updated)).toEqual(groupNames(fallback));
|
||
|
||
// Two complete, compatible configs share setup without changing either
|
||
// process envelope. Blacksmith placements request capacity for overlapping plans.
|
||
const fixtureConfigs = new Set([
|
||
"test/vitest/vitest.hooks.config.ts",
|
||
"test/vitest/vitest.secrets.config.ts",
|
||
]);
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
try {
|
||
const fixtureShards = originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((config) => fixtureConfigs.has(config)),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0);
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...fixtureShards);
|
||
const base = createNodeTestShards(options);
|
||
expect(base).toHaveLength(2);
|
||
const groupSeconds = profile === "github" ? 70 : 170;
|
||
timings.mockReturnValue(
|
||
Object.fromEntries(base.map((shard) => [shard.shardName, groupSeconds])),
|
||
);
|
||
const packed = createNodeTestShardBundles(options);
|
||
expect(packed).toHaveLength(1);
|
||
expect(packed[0]?.groups).toEqual(
|
||
base.map(({ checkName: _checkName, shardName, ...group }) => ({
|
||
...group,
|
||
shard_name: shardName,
|
||
})),
|
||
);
|
||
expect(packed[0]?.planConcurrency).toBe(profile === "github" ? 1 : 2);
|
||
expect(packed[0]?.runner).toBe(
|
||
profile === "github" ? base[0]?.runner : EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
);
|
||
expect(packed[0]?.predictedSeconds).toBe(groupSeconds * 2);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
}
|
||
|
||
// Cheap envelopes use the time budget instead of creating extra
|
||
// runners at ten groups; serial placements keep their existing count limit.
|
||
timings.mockReturnValue(
|
||
Object.fromEntries(createNodeTestShards(options).map((shard) => [shard.shardName, 1])),
|
||
);
|
||
const dense = createNodeTestShardBundles(options);
|
||
expect(dense.some((shard) => shard.groups.length > 10)).toBe(profile !== "github");
|
||
for (const shard of dense.filter((entry) => entry.groups.length > 10)) {
|
||
expect(shard).toMatchObject({
|
||
planConcurrency: shard.groups.some((group) => group.configs.some(isExclusiveCiTestConfig))
|
||
? 1
|
||
: 2,
|
||
requiresDist: false,
|
||
runner: EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
});
|
||
expect(shard.pretestBuildMode).toBeUndefined();
|
||
expect(shard.predictedSeconds).toBeLessThanOrEqual(500);
|
||
}
|
||
},
|
||
);
|
||
|
||
function measuredCliWallFloor(job: CompactNodeTestShard): number | undefined {
|
||
const observed = measuredCompactFixture.cliTailJob;
|
||
if (
|
||
job.groups.length !== 1 ||
|
||
job.runner !== observed.runner ||
|
||
job.planConcurrency !== observed.planConcurrency ||
|
||
job.requiresDist !== observed.requiresDist ||
|
||
job.pretestBuildMode !== observed.pretestBuildMode ||
|
||
!isDeepStrictEqual(job.env, observed.env)
|
||
) {
|
||
return undefined;
|
||
}
|
||
// Parent timing labels and logical group runners do not change the child
|
||
// executed on this already-checked job capacity.
|
||
const execution = (group: CompactNodeTestShard["groups"][number]) =>
|
||
Object.fromEntries(
|
||
Object.entries(group).filter(
|
||
([key, value]) => key !== "timing_key" && key !== "runner" && value !== undefined,
|
||
),
|
||
);
|
||
const child = execution(job.groups[0]!);
|
||
const index = observed.groups.findIndex((group) => isDeepStrictEqual(child, execution(group)));
|
||
return index < 0 ? undefined : measuredCompactFixture.cliChildJobWallSeconds[index]! - 60;
|
||
}
|
||
|
||
it("keeps hybrid fallback bounds when other measurements change", () => {
|
||
vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
const timings = vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue({});
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "push" as const,
|
||
runnerBackend: "hybrid",
|
||
};
|
||
const fallback = createNodeTestShardBundles(options);
|
||
const commandRuntimeJobs = fallback.filter((job) =>
|
||
job.groups.some((group) => group.shard_name === "agentic-commands-runtime"),
|
||
);
|
||
expect(commandRuntimeJobs).toHaveLength(1);
|
||
const commandRuntimeJob = expectDefined(commandRuntimeJobs[0], "command runtime job");
|
||
// The refusal file alone costs ceil(194.3 × 0.87 + 60) = 230s with its runtime build.
|
||
expect(commandRuntimeJob).toMatchObject({
|
||
planConcurrency: 1,
|
||
predictedSeconds: 230,
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
});
|
||
expect(commandRuntimeJob.groups).toHaveLength(1);
|
||
const commandRuntimeGroup = expectDefined(commandRuntimeJob.groups[0], "command runtime group");
|
||
expect(commandRuntimeGroup).toMatchObject({
|
||
configs: ["test/vitest/vitest.commands.config.ts"],
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
shard_name: "agentic-commands-runtime",
|
||
});
|
||
expect(commandRuntimeGroup.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
expect(commandRuntimeGroup.fallbackMaxWorkers).toBe(2);
|
||
expect(commandRuntimeGroup.includePatterns?.toSorted()).toEqual([
|
||
"src/commands/doctor-agent-database-order.process.test.ts",
|
||
"src/commands/doctor-config-flow.legacy-composition.test.ts",
|
||
"src/commands/doctor-config-preflight.process.test.ts",
|
||
"src/commands/doctor-config-preflight.refusal.process.test.ts",
|
||
"src/commands/doctor-config-preflight.test.ts",
|
||
"src/commands/doctor-config-preflight.v17-atomicity.process.test.ts",
|
||
"src/commands/doctor-plugin-install-config.process.test.ts",
|
||
]);
|
||
// Retain native floors only while the executed child contract still matches.
|
||
// The immutable pair above exercises the positive path when live selectors drift.
|
||
const measuredCliJobs = fallback.filter((job) => measuredCliWallFloor(job) !== undefined);
|
||
for (const job of measuredCliJobs) {
|
||
expect(job).toMatchObject({ runner: DEFAULT_NODE_TEST_RUNNER, planConcurrency: 1 });
|
||
expect(job.pretestBuildMode).toBeUndefined();
|
||
expect(job.predictedSeconds).toBeGreaterThanOrEqual(measuredCliWallFloor(job)!);
|
||
}
|
||
const measuredStorageJobs = fallback.filter((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(MEASURED_STORAGE_RECOVERY_TEST)),
|
||
);
|
||
expect(measuredStorageJobs).toHaveLength(1);
|
||
for (const job of measuredStorageJobs) {
|
||
expect(job).toMatchObject({ runner: EXTRA_LARGE_NODE_TEST_RUNNER, planConcurrency: 1 });
|
||
expect(job.groups).toHaveLength(1);
|
||
expect(job.groups[0]?.configs).toEqual(["test/vitest/vitest.infra.config.ts"]);
|
||
expect(job.pretestBuildMode).toBeUndefined();
|
||
expect(job.predictedTestSeconds).toBeGreaterThanOrEqual(279.963 + 20);
|
||
expect(job.predictedSeconds).toBeLessThanOrEqual(300);
|
||
}
|
||
expect(
|
||
fallback
|
||
.filter(
|
||
(shard) =>
|
||
!shard.requiresDist &&
|
||
shard !== commandRuntimeJob &&
|
||
!measuredCliJobs.includes(shard) &&
|
||
!measuredStorageJobs.includes(shard),
|
||
)
|
||
.every(
|
||
(shard) =>
|
||
(shard.predictedSeconds ?? Infinity) <=
|
||
(usesParallelPacking(shard) ? 500 : isCombinedUnbuiltCliJob(shard) ? 250 : 210),
|
||
),
|
||
).toBe(true);
|
||
// Slow process files retain singleton envelopes without inheriting the
|
||
// separate runtime-consumer group's build.
|
||
expect(
|
||
fallback
|
||
.filter((shard) => !shard.requiresDist && !measuredCliJobs.includes(shard))
|
||
.filter((shard) =>
|
||
shard.groups.some((group) => isExclusiveCompactShardName(group.shard_name)),
|
||
)
|
||
.filter((shard) => !((shard.predictedSeconds ?? Infinity) <= 150))
|
||
.filter((shard) => !isCombinedUnbuiltCliJob(shard))
|
||
.map((shard) => ({
|
||
groups: shard.groups.map((group) => ({
|
||
configs: group.configs,
|
||
includePatterns: group.includePatterns,
|
||
pretestBuildMode: group.pretestBuildMode,
|
||
})),
|
||
planConcurrency: shard.planConcurrency,
|
||
pretestBuildMode: shard.pretestBuildMode,
|
||
predictedSeconds: shard.predictedSeconds,
|
||
}))
|
||
.toSorted((a, b) =>
|
||
(a.groups[0]!.includePatterns ?? a.groups[0]!.configs)
|
||
.join(",")
|
||
.localeCompare((b.groups[0]!.includePatterns ?? b.groups[0]!.configs).join(",")),
|
||
),
|
||
).toEqual([
|
||
{
|
||
groups: [
|
||
{
|
||
configs: ["test/vitest/vitest.cli-process.config.ts"],
|
||
includePatterns: ["src/cli/gateway-backed-exit-health.process.test.ts"],
|
||
pretestBuildMode: undefined,
|
||
},
|
||
],
|
||
planConcurrency: 1,
|
||
pretestBuildMode: undefined,
|
||
predictedSeconds: 200,
|
||
},
|
||
{
|
||
groups: [
|
||
{
|
||
configs: ["test/vitest/vitest.cli-process.config.ts"],
|
||
includePatterns: ["src/cli/gateway-backed-exit.process.test.ts"],
|
||
pretestBuildMode: undefined,
|
||
},
|
||
],
|
||
planConcurrency: 1,
|
||
pretestBuildMode: undefined,
|
||
predictedSeconds: 200,
|
||
},
|
||
]);
|
||
timings.mockImplementation((runner): Readonly<Record<string, number>> =>
|
||
runner === "blacksmith"
|
||
? {
|
||
"agentic-agents-core-models": 123,
|
||
"core-unit-fast-1": 100,
|
||
"core-runtime-hooks": 80,
|
||
}
|
||
: {},
|
||
);
|
||
const updated = createNodeTestShardBundles(options);
|
||
const tail = updated.find((shard) =>
|
||
shard.groups.some((group) => group.shard_name === "agentic-gateway-core-3"),
|
||
);
|
||
expect(tail?.predictedSeconds).toBeGreaterThanOrEqual(140);
|
||
expect(tail?.predictedSeconds).toBeLessThanOrEqual(usesParallelPacking(tail) ? 500 : 210);
|
||
});
|
||
|
||
it("preserves unmeasured Gateway stripes when another cohort's measurement changes", async () => {
|
||
const config = "test/vitest/vitest.gateway-server.config.ts";
|
||
const files = [
|
||
"src/gateway/server.chat.fixture-a.test.ts",
|
||
"src/gateway/server.chat.fixture-b.test.ts",
|
||
];
|
||
const timings: Record<string, number> = {
|
||
"agentic-control-plane-agent-chat-parallel": 300,
|
||
"core-runtime-hooks": 40,
|
||
};
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [
|
||
{ name: "agentic", config, projects: [config] },
|
||
{ name: "core-runtime", config, projects: ["test/vitest/vitest.hooks.config.ts"] },
|
||
],
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (root: string) => (root === "src/gateway" ? files : []),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => timings,
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
try {
|
||
const { createNodeTestShardBundles: createPlan } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const options = { compactMode: "push" as const, runnerBackend: "hybrid" };
|
||
const stripes = (plan: readonly CompactNodeTestShard[]) =>
|
||
plan
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.configs.includes(config))
|
||
.toSorted((a, b) => a.shard_name.localeCompare(b.shard_name));
|
||
const before = createPlan(options);
|
||
const originalStripes = stripes(before);
|
||
expect(originalStripes).toHaveLength(2);
|
||
expect(originalStripes.map((group) => group.includePatterns?.length)).toEqual([1, 1]);
|
||
expect(originalStripes.flatMap((group) => group.includePatterns!).toSorted()).toEqual(files);
|
||
for (const group of originalStripes) {
|
||
expect(group.timing_key).toBeDefined();
|
||
expect(timings[group.timing_key!]).toBeUndefined();
|
||
}
|
||
timings["core-runtime-hooks"] = 120;
|
||
const after = createPlan(options);
|
||
expect(stripes(after)).toEqual(originalStripes);
|
||
const seconds = (plan: typeof before) =>
|
||
plan.reduce((sum, job) => sum + (job.predictedSeconds ?? 0), 0);
|
||
expect(seconds(before)).toBe(640);
|
||
expect(seconds(after) - seconds(before)).toBe(80);
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.resetModules();
|
||
}
|
||
});
|
||
|
||
it.each([
|
||
{ profile: "github", timingProfile: "github", addedSeconds: 40 },
|
||
{ profile: "hybrid", timingProfile: "blacksmith", addedSeconds: 40 },
|
||
] as const)(
|
||
"uses exact $profile child timings across sibling generations without changing partitions",
|
||
({ profile, timingProfile, addedSeconds }) => {
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
const fixtureShards = originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter(
|
||
(config) => config === agentVitestProjectOwners.support.config,
|
||
),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0);
|
||
// Other families can change packing and job-level rounding without changing these timings.
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...fixtureShards);
|
||
try {
|
||
const shardName = "agentic-agents-support-hosted-2";
|
||
let directTimings: Readonly<Record<string, number>> = {};
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockImplementation(
|
||
(runner): Readonly<Record<string, number>> => ({
|
||
"agentic-agents-support": runner === "blacksmith" ? 165 : 253,
|
||
...(runner === timingProfile ? directTimings : {}),
|
||
}),
|
||
);
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "push" as const,
|
||
runnerBackend: profile,
|
||
};
|
||
const unmeasured = createNodeTestShardBundles(options);
|
||
const initialGroup = unmeasured
|
||
.flatMap((shard) => shard.groups)
|
||
.find((group) => group.shard_name === shardName);
|
||
expect(initialGroup?.timing_key).toMatch(
|
||
/^agentic-agents-support#selector-.+#generation-.+#part-2-of-2#include-.+$/u,
|
||
);
|
||
const timingKey = initialGroup!.timing_key!;
|
||
directTimings = { [timingKey]: 1 };
|
||
const belowFloor = createNodeTestShardBundles(options);
|
||
// Exact child observations supersede a stale parent projection in either direction.
|
||
directTimings = { [timingKey]: 200 };
|
||
const baseline = createNodeTestShardBundles(options);
|
||
directTimings = { [timingKey]: 240 };
|
||
const updated = createNodeTestShardBundles(options);
|
||
const totalSeconds = (plan: typeof baseline) =>
|
||
plan.reduce((sum, shard) => sum + (shard.predictedSeconds ?? 0), 0);
|
||
const testPartition = (plan: typeof baseline) =>
|
||
plan
|
||
.flatMap((shard) => shard.groups)
|
||
.map((group) => ({
|
||
name: group.shard_name,
|
||
configs: group.configs,
|
||
includePatterns: group.includePatterns,
|
||
runner: group.runner,
|
||
}))
|
||
.toSorted((a, b) => a.name.localeCompare(b.name));
|
||
|
||
expect(totalSeconds(belowFloor)).toBeLessThan(totalSeconds(unmeasured));
|
||
expect(totalSeconds(baseline) - totalSeconds(belowFloor)).toBe(199);
|
||
expect(totalSeconds(updated) - totalSeconds(baseline)).toBe(addedSeconds);
|
||
expect(testPartition(baseline)).toEqual(testPartition(unmeasured));
|
||
expect(testPartition(updated)).toEqual(testPartition(baseline));
|
||
expect(
|
||
updated.flatMap((shard) => shard.groups).find((group) => group.shard_name === shardName)
|
||
?.timing_key,
|
||
).toBe(timingKey);
|
||
|
||
const previousGeneration = timingKey.replace(
|
||
/#generation-[^#]+/u,
|
||
"#generation-000000000000",
|
||
);
|
||
directTimings = { [previousGeneration]: 240 };
|
||
const reused = createNodeTestShardBundles(options);
|
||
expect(totalSeconds(reused)).toBe(totalSeconds(updated));
|
||
expect(testPartition(reused)).toEqual(testPartition(updated));
|
||
for (const incompatible of [
|
||
previousGeneration.replace(
|
||
/(#include-\d+-)[a-f0-9]+$/u,
|
||
(_match, prefix) => `${prefix}000000000000`,
|
||
),
|
||
previousGeneration.replace(
|
||
/(#selector-\d+-)[a-f0-9]+/u,
|
||
(_match, prefix) => `${prefix}000000000000`,
|
||
),
|
||
]) {
|
||
directTimings = { [incompatible]: 1_000 };
|
||
expect(totalSeconds(createNodeTestShardBundles(options))).toBe(totalSeconds(unmeasured));
|
||
}
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("preserves scoped project inventories when plans project file selections", () => {
|
||
const originalArgv = process.argv;
|
||
process.argv = originalArgv.slice(0, 2);
|
||
try {
|
||
const env = { ...process.env, OPENCLAW_VITEST_INCLUDE_FILE: undefined };
|
||
const includeFile = join(tempDirs.make("openclaw-plugin-plan-"), "include.json");
|
||
const plan = createNodeTestShardBundles({
|
||
compactMode: "push",
|
||
runnerBackend: "github",
|
||
includeReleaseOnlyPluginShards: true,
|
||
});
|
||
const capacities = new Map([
|
||
[BUNDLED_NODE_TEST_RUNNER, 4],
|
||
[DEFAULT_NODE_TEST_RUNNER, 8],
|
||
[EXTRA_LARGE_NODE_TEST_RUNNER, 32],
|
||
]);
|
||
for (const job of plan) {
|
||
expect(job.planConcurrency).toBe(1);
|
||
expect(job.runner).toBe(job.groups[0]?.runner);
|
||
for (const group of job.groups) {
|
||
expect(expectDefined(capacities.get(job.runner), "job capacity")).toBeGreaterThanOrEqual(
|
||
expectDefined(capacities.get(group.runner), "group capacity"),
|
||
);
|
||
}
|
||
}
|
||
for (const { config, create } of [
|
||
{ config: "test/vitest/vitest.logging.config.ts", create: createLoggingVitestConfig },
|
||
{ config: "test/vitest/vitest.process.config.ts", create: createProcessVitestConfig },
|
||
{ config: "test/vitest/vitest.plugins.config.ts", create: createPluginsVitestConfig },
|
||
{ config: "test/vitest/vitest.plugin-sdk.config.ts", create: createPluginSdkVitestConfig },
|
||
{
|
||
config: "test/vitest/vitest.plugin-sdk-light.config.ts",
|
||
create: createPluginSdkLightVitestConfig,
|
||
},
|
||
]) {
|
||
const expected = listMatchedTestFiles(create(env));
|
||
const actual = plan
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.configs.includes(config))
|
||
.flatMap((group) => {
|
||
if (!group.includePatterns) {
|
||
return listMatchedTestFiles(create(env));
|
||
}
|
||
writeFileSync(includeFile, JSON.stringify(group.includePatterns));
|
||
return listMatchedTestFiles(
|
||
create({ ...env, OPENCLAW_VITEST_INCLUDE_FILE: includeFile }),
|
||
);
|
||
});
|
||
expect(actual.toSorted(), config).toEqual(expected.toSorted());
|
||
expect(new Set(actual).size, config).toBe(actual.length);
|
||
}
|
||
} finally {
|
||
process.argv = originalArgv;
|
||
}
|
||
});
|
||
|
||
it.each([false, true])(
|
||
"retains measured storage module work despite a stale parent timing (companion: %s)",
|
||
async (companion) => {
|
||
const config = "test/vitest/vitest.infra.config.ts";
|
||
const longest = MEASURED_STORAGE_RECOVERY_TEST;
|
||
const ordinary = "src/infra/sqlite-readonly-location.copy.test.ts";
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [{ name: "core-runtime", config, projects: [config] }],
|
||
}));
|
||
vi.doMock("../vitest/vitest.database-worker-core-paths.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<
|
||
typeof import("../vitest/vitest.database-worker-core-paths.mjs")
|
||
>()),
|
||
databaseWorkerCoreTestFiles: [longest],
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (root: string) =>
|
||
companion && root === "src/infra" ? [ordinary] : [],
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => ({ "core-runtime-infra-storage-state": 1 }),
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
vi.doMock("../../scripts/lib/vitest-build-prerequisites.mts", async (importOriginal) => ({
|
||
...(await importOriginal<
|
||
typeof import("../../scripts/lib/vitest-build-prerequisites.mts")
|
||
>()),
|
||
resolveVitestPretestBuildMode: () => undefined,
|
||
}));
|
||
try {
|
||
const { createNodeTestShardBundles: createPlan } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const plan = createPlan({ compactMode: "push", runnerBackend: "blacksmith" });
|
||
expect(
|
||
plan.flatMap((job) => job.groups.flatMap((group) => group.includePatterns!)).toSorted(),
|
||
).toEqual((companion ? [longest, ordinary] : [longest]).toSorted());
|
||
const longestJob = expectDefined(
|
||
plan.find((job) => job.groups.some((group) => group.includePatterns?.includes(longest))),
|
||
"measured storage file row",
|
||
);
|
||
// The measured file span is indivisible; its wrapper is paid once even at more workers.
|
||
expect(longestJob.predictedTestSeconds).toBeGreaterThanOrEqual(279.963 + 20);
|
||
expect(longestJob.runner).toBe(EXTRA_LARGE_NODE_TEST_RUNNER);
|
||
expect(plan.every((job) => job.predictedTestSeconds! <= 300)).toBe(true);
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/vitest-build-prerequisites.mts");
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.database-worker-core-paths.mjs");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.resetModules();
|
||
}
|
||
},
|
||
);
|
||
|
||
it.each([
|
||
{
|
||
owner: "core-runtime-config",
|
||
config: "test/vitest/vitest.runtime-config.config.ts",
|
||
suite: "core-runtime",
|
||
root: "src/config",
|
||
pinned: true,
|
||
runnerBackend: "hybrid",
|
||
},
|
||
{
|
||
owner: "agentic-agents-support",
|
||
config: agentVitestProjectOwners.support.config,
|
||
suite: "agentic",
|
||
root: "src/agents",
|
||
pinned: false,
|
||
runnerBackend: "blacksmith",
|
||
},
|
||
])(
|
||
"preserves the measured worker policy when splitting $owner",
|
||
async ({ owner, config, suite, root, pinned, runnerBackend }) => {
|
||
// The support owner selects subdirectories; top-level files belong to agents-core.
|
||
const files = Array.from(
|
||
{ length: 70 },
|
||
(_, index) => `${root}/fixture/entry-${String(index).padStart(3, "0")}.test.ts`,
|
||
);
|
||
const timings: Record<string, number> = { [owner]: 240 };
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [{ name: suite, config, projects: [config] }],
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (candidate: string) => (candidate === root ? files : []),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-node-test-inventory.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/ci-node-test-inventory.mts")>()),
|
||
listWholeConfigSplitFiles: (candidate: string) => (candidate === owner ? files : undefined),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => timings,
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
vi.doMock("../../scripts/lib/vitest-build-prerequisites.mts", async (importOriginal) => ({
|
||
...(await importOriginal<
|
||
typeof import("../../scripts/lib/vitest-build-prerequisites.mts")
|
||
>()),
|
||
resolveVitestPretestBuildMode: () => undefined,
|
||
}));
|
||
try {
|
||
const { createNodeTestShardBundles: createPlan } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const options = { compactMode: "push" as const, runnerBackend };
|
||
const before = createPlan(options);
|
||
const [observed, sibling] = before.flatMap((job) => job.groups);
|
||
expect(before.flatMap((job) => job.groups)).toHaveLength(2);
|
||
expect(observed?.includePatterns).toHaveLength(35);
|
||
const timingKey = observed!.timing_key!;
|
||
timings[timingKey] = 362;
|
||
const after = createPlan(options);
|
||
const groups = after.flatMap((job) => job.groups);
|
||
expect(groups.flatMap((group) => group.includePatterns!).toSorted()).toEqual(files);
|
||
expect(groups.find((group) => group.timing_key === sibling!.timing_key)).toEqual(sibling);
|
||
if (!pinned) {
|
||
const measuredJob = expectDefined(
|
||
after.find((job) => job.groups.some((group) => group.timing_key === timingKey)),
|
||
"unpinned measured child",
|
||
);
|
||
expect(measuredJob.groups).toEqual([observed]);
|
||
expect(measuredJob.predictedTestSeconds).toBeGreaterThanOrEqual(362);
|
||
expect(measuredJob.planConcurrency).toBe(1);
|
||
expect(measuredJob.runner).toBe(EXTRA_LARGE_NODE_TEST_RUNNER);
|
||
expect(measuredJob.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
expect(observed?.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
return;
|
||
}
|
||
const descendants = groups.filter((group) =>
|
||
parseCompactSplitTimingKey(group.timing_key!)?.parentShardName.startsWith(timingKey),
|
||
);
|
||
expect(descendants.length).toBeGreaterThan(1);
|
||
expect(descendants.flatMap((group) => group.includePatterns!).toSorted()).toEqual(
|
||
observed!.includePatterns!.toSorted(),
|
||
);
|
||
expect(descendants.every((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === "2")).toBe(
|
||
true,
|
||
);
|
||
expect(after.every((job) => job.predictedTestSeconds! <= 300)).toBe(true);
|
||
const totalSeconds = after.reduce((total, job) => total + job.predictedSeconds!, 0);
|
||
expect(totalSeconds).toBeGreaterThanOrEqual(482);
|
||
expect(totalSeconds).toBeLessThan(487);
|
||
expect(createPlan(options)).toEqual(after);
|
||
delete timings[timingKey];
|
||
timings[timingKey.replace(/#generation-[^#]+/u, "#generation-000000000000")] = 362;
|
||
expect(createPlan(options)).toEqual(after);
|
||
|
||
const nestedKey = descendants[0]!.timing_key!;
|
||
timings[nestedKey] = 3_600;
|
||
const singleton = createPlan(options)
|
||
.flatMap((job) => job.groups)
|
||
.find(
|
||
(group) =>
|
||
group.timing_key?.startsWith(nestedKey) && group.includePatterns?.length === 1,
|
||
);
|
||
expect(singleton).toBeDefined();
|
||
timings[singleton!.timing_key!] = 500;
|
||
const indivisible = createPlan(options).find((job) =>
|
||
job.groups.some((group) => group.timing_key === singleton!.timing_key),
|
||
);
|
||
expect(indivisible?.groups).toHaveLength(1);
|
||
expect(indivisible?.predictedTestSeconds).toBeGreaterThanOrEqual(500);
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/vitest-build-prerequisites.mts");
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/ci-node-test-inventory.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.resetModules();
|
||
}
|
||
},
|
||
);
|
||
|
||
it("partitions whole-config runtime consumers from ordinary serial CLI work", () => {
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
const config = "test/vitest/vitest.cli-process.config.ts";
|
||
const selected = originalShards
|
||
.map((shard) => ({ ...shard, projects: shard.projects.filter((entry) => entry === config) }))
|
||
.filter((shard) => shard.projects.length > 0);
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...selected);
|
||
const timings = vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue({});
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "push" as const,
|
||
runnerBackend: "hybrid",
|
||
};
|
||
try {
|
||
const plan = createNodeTestShardBundles(options);
|
||
const runtimeJobs = plan.filter((job) => job.pretestBuildMode);
|
||
expect(runtimeJobs).toHaveLength(1);
|
||
const [runtimeJob] = runtimeJobs;
|
||
expect(runtimeJob).toMatchObject({ planConcurrency: 1, pretestBuildMode: "runtime" });
|
||
expect(runtimeJob!.predictedSeconds).toBeLessThanOrEqual(150);
|
||
expect(runtimeJob!.groups).toHaveLength(1);
|
||
const runtimeFiles = listVitestRuntimeConsumerFiles([config]).toSorted();
|
||
expect(runtimeJob!.groups[0]!.includePatterns?.toSorted()).toEqual(runtimeFiles);
|
||
const catalogFiles = listMatchedTestFiles(createCliProcessVitestConfig({})).toSorted();
|
||
const ordinaryJobs = plan.filter((job) => !job.pretestBuildMode);
|
||
expect(
|
||
ordinaryJobs
|
||
.flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
).toEqual(catalogFiles.filter((file) => !runtimeFiles.includes(file)));
|
||
for (const job of plan) {
|
||
expect(job).toMatchObject({ planConcurrency: 1, requiresDist: false });
|
||
for (const group of job.groups) {
|
||
expect(group.configs).toEqual([config]);
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe("2");
|
||
expect(group.pretestBuildMode).toBe(job.pretestBuildMode);
|
||
expect(group.requiresDist).toBe(false);
|
||
}
|
||
}
|
||
expect(
|
||
plan
|
||
.flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
).toEqual(catalogFiles);
|
||
|
||
// An oversized measured runtime child remains truthful and alone; ordinary
|
||
// files must not inherit its prerequisite through a sibling exemption.
|
||
timings.mockReturnValue(
|
||
Object.fromEntries(runtimeJob!.groups.map((group) => [group.timing_key!, 100])),
|
||
);
|
||
const expensive = createNodeTestShardBundles(options).filter((job) => job.pretestBuildMode);
|
||
expect(expensive).toHaveLength(1);
|
||
// Runtime preparation is charged once, outside the predicted test step.
|
||
expect(expensive[0]).toMatchObject({
|
||
predictedSeconds: 160,
|
||
predictedTestSeconds: 100,
|
||
planConcurrency: 1,
|
||
});
|
||
expect(expensive[0]!.groups).toHaveLength(1);
|
||
expect(expensive[0]!.groups[0]!.includePatterns?.toSorted()).toEqual(runtimeFiles);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
}
|
||
});
|
||
|
||
it("spends the hybrid CLI budget only on complete affordable non-build bins", () => {
|
||
const originalShards = fullSuiteVitestShards.slice();
|
||
const originalProcessFiles = cliProcessTestFiles.slice();
|
||
const configs = new Set([
|
||
"test/vitest/vitest.cli.config.ts",
|
||
"test/vitest/vitest.cli-process.config.ts",
|
||
]);
|
||
const selected = originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((entry) => configs.has(entry)),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0);
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...selected);
|
||
const timings = vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue({
|
||
"agentic-cli": 136,
|
||
});
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "pull-request" as const,
|
||
runnerBackend: "hybrid",
|
||
};
|
||
try {
|
||
const plan = createNodeTestShardBundles(options);
|
||
const combined = plan.filter(isCombinedUnbuiltCliJob);
|
||
expect(combined).toHaveLength(2);
|
||
const cliJobs = plan.filter((job) =>
|
||
job.groups.some((group) => group.shard_name === "agentic-cli"),
|
||
);
|
||
expect(cliJobs).toHaveLength(1);
|
||
expect(cliJobs[0]).toMatchObject({
|
||
planConcurrency: 1,
|
||
runner: EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
});
|
||
// The combined bin uses the larger CLI budget, beyond the 150s child limit.
|
||
expect(cliJobs[0]!.predictedSeconds).toBeGreaterThan(150);
|
||
expect(cliJobs[0]!.pretestBuildMode).toBeUndefined();
|
||
expect(cliJobs[0]!.groups).toHaveLength(2);
|
||
expect(cliJobs[0]!.groups[0]!.includePatterns).toBeUndefined();
|
||
const processGroups = plan.flatMap((job) =>
|
||
job.groups.filter((group) =>
|
||
group.configs.includes("test/vitest/vitest.cli-process.config.ts"),
|
||
),
|
||
);
|
||
expect(processGroups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
listMatchedTestFiles(createCliProcessVitestConfig({})).toSorted(),
|
||
);
|
||
expect(
|
||
combined.every((job) => job.predictedSeconds! <= 250 && job.planConcurrency === 1),
|
||
).toBe(true);
|
||
for (const job of plan.filter((candidate) => candidate.pretestBuildMode)) {
|
||
expect(job.predictedSeconds).toBeLessThanOrEqual(150);
|
||
expect(job.groups.every((group) => group.pretestBuildMode === "runtime")).toBe(true);
|
||
}
|
||
const combinedProcessGroups = combined
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.configs.includes("test/vitest/vitest.cli-process.config.ts"));
|
||
const processKeys = combinedProcessGroups.flatMap((group) =>
|
||
group.timing_key ? [group.timing_key] : [],
|
||
);
|
||
expect(processKeys).toHaveLength(combinedProcessGroups.length);
|
||
expect(new Set(processKeys).size).toBe(processKeys.length);
|
||
// Each child still fits its 150s limit, but two no longer fit a 250s bin.
|
||
timings.mockReturnValue(Object.fromEntries(processKeys.map((key) => [key, 160])));
|
||
const overBudget = createNodeTestShardBundles(options);
|
||
expect(
|
||
overBudget.every(
|
||
(job) =>
|
||
job.groups.filter((group) => processKeys.includes(group.timing_key ?? "")).length <= 1,
|
||
),
|
||
).toBe(true);
|
||
// A truthful oversized child must remain alone even beside a tiny CLI.
|
||
timings.mockReturnValue({
|
||
"agentic-cli": 3,
|
||
...Object.fromEntries(processKeys.map((key) => [key, 200])),
|
||
});
|
||
const oversized = createNodeTestShardBundles(options).filter((job) =>
|
||
job.groups.some((group) => processKeys.includes(group.timing_key ?? "")),
|
||
);
|
||
expect(oversized).toHaveLength(processKeys.length);
|
||
expect(oversized.every((job) => job.groups.length === 1)).toBe(true);
|
||
|
||
// Cheaper complete CLI children can share below 150s. A later unrelated
|
||
// non-dist owner must not invalidate that earlier sibling exemption.
|
||
const processFiles = ["src/cli/help-exit.process.test.ts", "src/cli/one-shot-exit.test.ts"];
|
||
cliProcessTestFiles.splice(0, cliProcessTestFiles.length, ...processFiles);
|
||
const smallerConfigs = new Set([
|
||
"test/vitest/vitest.cli-process.config.ts",
|
||
"test/vitest/vitest.tooling-isolated.config.ts",
|
||
]);
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...originalShards
|
||
.map((shard) => ({
|
||
...shard,
|
||
projects: shard.projects.filter((config) => smallerConfigs.has(config)),
|
||
}))
|
||
.filter((shard) => shard.projects.length > 0),
|
||
);
|
||
timings.mockImplementation((profile) => ({
|
||
"agentic-cli-process": profile === "github" ? 200 : 120,
|
||
"core-tooling-isolated": 20,
|
||
}));
|
||
const cheaper = createNodeTestShardBundles(options);
|
||
const cliJob = cheaper.find((job) =>
|
||
job.groups.some((group) =>
|
||
group.configs.includes("test/vitest/vitest.cli-process.config.ts"),
|
||
),
|
||
)!;
|
||
expect(cliJob.groups).toHaveLength(2);
|
||
expect(cliJob.groups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
processFiles.toSorted(),
|
||
);
|
||
const unrelated = cheaper.find((job) =>
|
||
job.groups.some((group) => group.shard_name === "core-tooling-isolated"),
|
||
)!;
|
||
expect(unrelated).toMatchObject({ runner: cliJob.runner, requiresDist: false });
|
||
expect(unrelated.groups).toHaveLength(1);
|
||
expect(unrelated.pretestBuildMode).toBeUndefined();
|
||
expect(cliJob.predictedSeconds! + unrelated.predictedSeconds!).toBeLessThanOrEqual(150);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...originalShards);
|
||
cliProcessTestFiles.splice(0, cliProcessTestFiles.length, ...originalProcessFiles);
|
||
}
|
||
});
|
||
|
||
function checkCommittedCompactPolicies(host: PlannerHost) {
|
||
pinPlannerHost(host);
|
||
const base = createNodeTestShards({ includeReleaseOnlyPluginShards: false });
|
||
const compact = getCommittedCompactPlan("push");
|
||
const pullRequestCompact = getCommittedCompactPlan("pull-request");
|
||
const githubCompact = getCommittedCompactPlan("push", "github");
|
||
const githubPullRequestCompact = getCommittedCompactPlan("pull-request", "github");
|
||
const hybridCompact = getCommittedCompactPlan("push", "hybrid");
|
||
const hybridPullRequestCompact = getCommittedCompactPlan("pull-request", "hybrid");
|
||
const placementTimings = vi
|
||
.spyOn(testTimings, "readRuntimePlacementTimings")
|
||
.mockReturnValue([]);
|
||
const hybridBeforePlacement = createNodeTestShardBundles({
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "pull-request",
|
||
runnerBackend: "hybrid",
|
||
});
|
||
placementTimings.mockRestore();
|
||
const expectedToolingOwnerNames = Array.from(
|
||
{ length: 16 },
|
||
(_, index) => `core-tooling-${index + 1}`,
|
||
);
|
||
const pushExcludedShardNames = new Set([
|
||
"core-runtime-tui-pty",
|
||
...expectedToolingOwnerNames,
|
||
"core-tooling-isolated",
|
||
]);
|
||
const gatewayFiles = [
|
||
"src/cli/gateway-backed-exit.process.test.ts",
|
||
"src/cli/gateway-backed-exit-health.process.test.ts",
|
||
];
|
||
const sharesNativeCapacity = (shard: CompactNodeTestShard) =>
|
||
[DEFAULT_NODE_TEST_RUNNER, EXTRA_LARGE_NODE_TEST_RUNNER].includes(shard.runner) &&
|
||
!shard.requiresDist &&
|
||
!shard.pretestBuildMode &&
|
||
(shard.planConcurrency === 1 ||
|
||
(shard.planConcurrency === 2 &&
|
||
shard.runner === EXTRA_LARGE_NODE_TEST_RUNNER &&
|
||
shard.groups.every((group) => !group.configs.some(isExclusiveCiTestConfig)))) &&
|
||
shard.groups.every(
|
||
(group) =>
|
||
[BUNDLED_NODE_TEST_RUNNER, DEFAULT_NODE_TEST_RUNNER].includes(group.runner) &&
|
||
!group.requiresDist &&
|
||
!group.pretestBuildMode &&
|
||
!isExclusiveCompactShardName(group.shard_name),
|
||
);
|
||
|
||
for (const profile of [
|
||
{
|
||
name: "Blacksmith",
|
||
pullRequest: pullRequestCompact,
|
||
push: compact,
|
||
largeOwners: [
|
||
"agentic-cli",
|
||
"agentic-control-plane-http-plugin-ws",
|
||
"agentic-commands-doctor-platform",
|
||
"agentic-commands-doctor-sessions-cron",
|
||
"agentic-commands-doctor-sessions-cron-memory",
|
||
"agentic-commands-doctor-sessions-cron-sqlite",
|
||
"agentic-commands-doctor-sessions-cron-sqlite-recovery",
|
||
"agentic-control-plane-auth-node",
|
||
"core-runtime-infra-storage-state",
|
||
"agentic-control-plane-runtime-ui-tools",
|
||
"core-runtime-infra-heartbeat-runner",
|
||
"core-runtime-infra-system-runtime",
|
||
"auto-reply-reply-agent-runner",
|
||
"agentic-commands-status-tools",
|
||
"agentic-commands-doctor",
|
||
"agentic-agents-core-isolated",
|
||
],
|
||
largeFiles: ["src/cli/update-dry-run-state.process.test.ts"],
|
||
},
|
||
{
|
||
name: "GitHub-hosted",
|
||
pullRequest: githubPullRequestCompact,
|
||
push: githubCompact,
|
||
largeOwners: [
|
||
"core-runtime-cron-parallel-service",
|
||
"agentic-agents-tools",
|
||
"core-runtime-infra-storage-state",
|
||
],
|
||
largeFiles: [],
|
||
},
|
||
{
|
||
name: "hybrid",
|
||
pullRequest: hybridPullRequestCompact,
|
||
push: hybridCompact,
|
||
largeOwners: [
|
||
"agentic-commands-doctor-config-state",
|
||
"core-runtime-cron-parallel-service",
|
||
"agentic-control-plane-runtime-shared-token",
|
||
"agentic-commands-doctor-platform",
|
||
],
|
||
largeFiles: gatewayFiles,
|
||
},
|
||
]) {
|
||
expect(profile.push.length, `${profile.name} excludes PR-only work`).toBeLessThan(
|
||
profile.pullRequest.length,
|
||
);
|
||
for (const [mode, plan] of [
|
||
["push", profile.push],
|
||
["pull-request", profile.pullRequest],
|
||
] as const) {
|
||
// Capacity belongs to the workload even when timing changes reorder rows.
|
||
const groups = plan.flatMap((shard) => shard.groups);
|
||
const twoWorkerCommands = new Set<(typeof groups)[number]>();
|
||
for (const group of groups.filter((entry) =>
|
||
entry.configs.includes("test/vitest/vitest.commands.config.ts"),
|
||
)) {
|
||
const explicitWorkers = group.env?.OPENCLAW_VITEST_MAX_WORKERS;
|
||
if (explicitWorkers !== undefined) {
|
||
expect(explicitWorkers, group.shard_name).toBe("2");
|
||
const selector =
|
||
parseCompactSplitTimingKey(group.timing_key!)?.selectorKey ??
|
||
createCompactSplitTimingGeneration({
|
||
configs: group.configs,
|
||
env: group.env,
|
||
parentShardName: group.timing_key!,
|
||
stripes: [group.includePatterns!],
|
||
}).selectorKey;
|
||
expect(
|
||
Math.max(
|
||
...(["blacksmith", "github"] as const).map(
|
||
(runner) =>
|
||
testTimings.readCompleteSplitGenerationSeconds(
|
||
testTimings.readCompactGroupTimings(runner),
|
||
selector,
|
||
) ?? 0,
|
||
),
|
||
),
|
||
`${group.shard_name}: measured two-worker generation`,
|
||
).toBeGreaterThan(0);
|
||
twoWorkerCommands.add(group);
|
||
}
|
||
const job = plan.find((entry) => entry.groups.includes(group))!;
|
||
const jobWorkers =
|
||
profile.name !== "GitHub-hosted" &&
|
||
job.runner === EXTRA_LARGE_NODE_TEST_RUNNER &&
|
||
job.planConcurrency === 1 &&
|
||
job.env?.OPENCLAW_VITEST_MAX_WORKERS === undefined
|
||
? 8
|
||
: 2;
|
||
const workers = Math.min(jobWorkers, Number(explicitWorkers ?? jobWorkers));
|
||
expect(group.timing_key, group.shard_name).toContain(`#file-parallel-${workers}`);
|
||
expect(group.fallbackMaxWorkers, group.shard_name).toBe(2);
|
||
}
|
||
for (const owner of [
|
||
"agentic-gateway-core-1",
|
||
"agentic-gateway-core-2",
|
||
"agentic-gateway-core-3",
|
||
"agentic-gateway-server-isolated",
|
||
]) {
|
||
const isolated = owner === "agentic-gateway-server-isolated";
|
||
const gatewayGroups = groups
|
||
.filter((group) => group.shard_name.replace(/-hosted-\d+$/u, "") === owner)
|
||
// Timing generations follow shard numbers, including double-digit children.
|
||
.toSorted((left, right) =>
|
||
left.shard_name.localeCompare(right.shard_name, undefined, { numeric: true }),
|
||
);
|
||
const measured =
|
||
(owner === "agentic-gateway-core-2" || isolated) && profile.name !== "GitHub-hosted";
|
||
expect(gatewayGroups.length, `${profile.name}: ${owner}`).toBeGreaterThan(0);
|
||
expect(gatewayGroups.flatMap((group) => group.includePatterns!).toSorted()).toEqual(
|
||
isolated
|
||
? [...gatewayServerIsolatedTestFiles, ...gatewayDatabaseWorkerTestFiles]
|
||
.filter((file) => mode === "push" || !isCiProofTestFile(file))
|
||
.toSorted()
|
||
: base.find((shard) => shard.shardName === owner)?.includePatterns?.toSorted(),
|
||
);
|
||
for (const group of gatewayGroups) {
|
||
expect(group.env).toEqual(
|
||
isolated
|
||
? measured
|
||
? { OPENCLAW_VITEST_MAX_WORKERS: "8" }
|
||
: undefined
|
||
: measured
|
||
? undefined
|
||
: { OPENCLAW_VITEST_MAX_WORKERS: "2" },
|
||
);
|
||
expect(group.fallbackMaxWorkers).toBe(measured ? 2 : undefined);
|
||
expect(group.minTotalMemoryBytes).toBe(
|
||
measured && isolated ? 28 * 1024 ** 3 : undefined,
|
||
);
|
||
const job = expectDefined(
|
||
plan.find((candidate) => candidate.groups.includes(group)),
|
||
owner,
|
||
);
|
||
expect(job.planConcurrency).toBe(1);
|
||
if (measured && isolated) {
|
||
expect(job.runner, group.shard_name).toBe(EXTRA_LARGE_NODE_TEST_RUNNER);
|
||
}
|
||
const effectiveWorkers = (entry: (typeof groups)[number]) =>
|
||
Math.min(
|
||
8,
|
||
Number(job.env?.OPENCLAW_VITEST_MAX_WORKERS ?? 8),
|
||
Number(entry.env?.OPENCLAW_VITEST_MAX_WORKERS ?? 8),
|
||
);
|
||
expect(effectiveWorkers(group)).toBe(measured || isolated ? 8 : 2);
|
||
if (measured) {
|
||
expect(job.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
for (const sibling of job.groups.filter(
|
||
(entry) => entry !== group && usesParallelPacking(job),
|
||
)) {
|
||
const expectedWorkers = twoWorkerCommands.has(sibling)
|
||
? 2
|
||
: /^agentic-gateway-methods(?:-hosted-\d+)?$/u.test(sibling.shard_name)
|
||
? 4
|
||
: sibling.fallbackMaxWorkers === 2
|
||
? 8
|
||
: 2;
|
||
expect(effectiveWorkers(sibling), sibling.shard_name).toBe(expectedWorkers);
|
||
}
|
||
}
|
||
}
|
||
if (gatewayGroups.length > 1) {
|
||
const generation = createCompactSplitTimingGeneration({
|
||
configs: gatewayGroups[0]!.configs,
|
||
env: gatewayGroups[0]!.env,
|
||
parentShardName: owner,
|
||
stripes: gatewayGroups.map((group) => group.includePatterns!),
|
||
});
|
||
expect(gatewayGroups.map((group) => group.timing_key)).toEqual(generation.timingKeys);
|
||
}
|
||
}
|
||
const parallelAgentGroups = groups.filter((group) =>
|
||
fileParallelAgentGroupNames.has(group.shard_name),
|
||
);
|
||
expect(new Set(parallelAgentGroups.map((group) => group.shard_name))).toEqual(
|
||
fileParallelAgentGroupNames,
|
||
);
|
||
for (const group of parallelAgentGroups) {
|
||
expect(group.timing_key).toBe(`${group.shard_name}#file-parallel`);
|
||
expect(group.fallbackMaxWorkers).toBe(profile.name === "GitHub-hosted" ? undefined : 2);
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
}
|
||
for (const owner of profile.largeOwners) {
|
||
const selected = groups.filter(
|
||
(group) => group.shard_name.replace(/-hosted-\d+$/u, "") === owner,
|
||
);
|
||
expect(selected.length, `${profile.name}: ${owner}`).toBeGreaterThan(0);
|
||
for (const group of selected) {
|
||
const measuredStorage =
|
||
owner === "core-runtime-infra-storage-state" &&
|
||
profile.name !== "GitHub-hosted" &&
|
||
group.includePatterns?.includes(MEASURED_STORAGE_RECOVERY_TEST);
|
||
expect(group.runner, `${profile.name}: ${group.shard_name}`).toBe(
|
||
measuredStorage ? EXTRA_LARGE_NODE_TEST_RUNNER : DEFAULT_NODE_TEST_RUNNER,
|
||
);
|
||
}
|
||
}
|
||
for (const largeFile of profile.largeFiles) {
|
||
const selected = groups.filter((group) => group.includePatterns?.includes(largeFile));
|
||
expect(selected, largeFile).toHaveLength(1);
|
||
expect(selected[0]?.runner, largeFile).toBe(DEFAULT_NODE_TEST_RUNNER);
|
||
}
|
||
const supportGroups = groups.filter((group) =>
|
||
group.configs.includes(agentVitestProjectOwners.support.config),
|
||
);
|
||
expect(supportGroups.length, profile.name).toBeGreaterThan(0);
|
||
if (profile.name === "Blacksmith") {
|
||
for (const owner of [
|
||
"agentic-control-plane-agent-chat",
|
||
"agentic-gateway-core-3",
|
||
"core-runtime-infra-storage-state",
|
||
]) {
|
||
expect(
|
||
groups.filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)).length,
|
||
owner,
|
||
).toBeGreaterThan(1);
|
||
}
|
||
expect(supportGroups.length).toBeGreaterThan(1);
|
||
for (const group of supportGroups) {
|
||
expect(group).toMatchObject({
|
||
configs: [agentVitestProjectOwners.support.config],
|
||
requiresDist: false,
|
||
runner: EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
});
|
||
expect(group.includePatterns?.length).toBeGreaterThan(0);
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
const job = expectDefined(
|
||
plan.find((shard) => shard.groups.includes(group)),
|
||
"agent support row",
|
||
);
|
||
expect(job).toMatchObject({
|
||
runner: EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
planConcurrency: 1,
|
||
});
|
||
expect(job.timeoutMinutes).toBeUndefined();
|
||
}
|
||
} else {
|
||
expect(supportGroups.every((group) => group.runner === DEFAULT_NODE_TEST_RUNNER)).toBe(
|
||
true,
|
||
);
|
||
if (profile.name === "GitHub-hosted") {
|
||
expect(plan.some((shard) => shard.runner === EXTRA_LARGE_NODE_TEST_RUNNER)).toBe(false);
|
||
expect(plan.every((shard) => shard.planConcurrency === 1)).toBe(true);
|
||
}
|
||
}
|
||
const cliProcessJobs = plan.filter((shard) =>
|
||
shard.groups.some((group) =>
|
||
group.configs.includes("test/vitest/vitest.cli-process.config.ts"),
|
||
),
|
||
);
|
||
const gatewayJobs = new Set(
|
||
gatewayFiles.map((file) => {
|
||
const gatewayJob = cliProcessJobs.find((shard) =>
|
||
shard.groups.some((group) => group.includePatterns?.includes(file)),
|
||
);
|
||
expect(gatewayJob?.groups, `${profile.name}: ${file}`).toEqual([
|
||
expect.objectContaining({ includePatterns: [file] }),
|
||
]);
|
||
return gatewayJob;
|
||
}),
|
||
);
|
||
const runtimeCliJobs = cliProcessJobs.filter((shard) => shard.pretestBuildMode);
|
||
for (const file of [
|
||
"src/cli/acp-cli-exit.process.test.ts",
|
||
"src/cli/update-dry-run-state.process.test.ts",
|
||
]) {
|
||
const job = expectDefined(
|
||
runtimeCliJobs.find((shard) =>
|
||
shard.groups.some((group) => group.includePatterns?.includes(file)),
|
||
),
|
||
`runtime CLI owner for ${file}`,
|
||
);
|
||
expect(job.pretestBuildMode).toBe(
|
||
job.groups.some((group) => group.includePatterns?.includes(PRIVATE_QA_TOOLING_TEST))
|
||
? "private-qa"
|
||
: "runtime",
|
||
);
|
||
expect(job.groups).toContainEqual(
|
||
expect.objectContaining({
|
||
pretestBuildMode: "runtime",
|
||
includePatterns: expect.arrayContaining([file]),
|
||
}),
|
||
);
|
||
}
|
||
for (const shard of cliProcessJobs) {
|
||
const observedFloor = profile.name === "hybrid" ? measuredCliWallFloor(shard) : undefined;
|
||
if (observedFloor !== undefined) {
|
||
expect(shard).toMatchObject({ runner: DEFAULT_NODE_TEST_RUNNER, planConcurrency: 1 });
|
||
expect(shard.pretestBuildMode).toBeUndefined();
|
||
expect(shard.predictedSeconds).toBeGreaterThanOrEqual(observedFloor);
|
||
continue;
|
||
}
|
||
// Hosted runtime groups share one preparation within 210s; no-build
|
||
// gateway files retain 200s and complete hybrid CLI bins retain 250s.
|
||
const budget = gatewayJobs.has(shard)
|
||
? 200
|
||
: profile.name === "GitHub-hosted" && shard.pretestBuildMode
|
||
? 210
|
||
: profile.name === "hybrid" && isCombinedUnbuiltCliJob(shard)
|
||
? 250
|
||
: 150;
|
||
if (profile.name === "GitHub-hosted" && shard.pretestBuildMode) {
|
||
expect(shard.groups.every((group) => group.pretestBuildMode)).toBe(true);
|
||
}
|
||
expect(shard.predictedSeconds, profile.name).toBeLessThanOrEqual(budget);
|
||
}
|
||
expect(
|
||
plan.every(
|
||
(shard) =>
|
||
shard.groups.length > 0 && (usesParallelPacking(shard) || shard.groups.length <= 10),
|
||
),
|
||
).toBe(true);
|
||
expect(plan.every((shard) => Number.isFinite(shard.predictedSeconds))).toBe(true);
|
||
const names = plan.flatMap((shard) => shard.groups.map((group) => group.shard_name));
|
||
expect(new Set(names).size).toBe(names.length);
|
||
expect(new Set(plan.map((shard) => shard.checkName)).size).toBe(plan.length);
|
||
expect(new Set(plan.map((shard) => shard.shardName)).size).toBe(plan.length);
|
||
expect(plan.length, `${profile.name} row budget`).toBeLessThanOrEqual(90);
|
||
}
|
||
}
|
||
expect(compact.every((shard) => Array.isArray(shard.groups))).toBe(true);
|
||
expect(compact.every((shard) => usesParallelPacking(shard) || shard.groups.length <= 10)).toBe(
|
||
true,
|
||
);
|
||
expect(compact.some((shard) => shard.requiresDist)).toBe(true);
|
||
expect(
|
||
compact.every((shard) =>
|
||
shard.groups.every(
|
||
(group) =>
|
||
group.requiresDist === shard.requiresDist &&
|
||
(group.runner === shard.groups[0]?.runner || sharesNativeCapacity(shard)),
|
||
),
|
||
),
|
||
).toBe(true);
|
||
const jobOf = (name: string) =>
|
||
compact.findIndex((shard) => shard.groups.some((group) => group.shard_name === name));
|
||
expect(jobOf("agentic-agents-core-runner-embedded")).toBeGreaterThanOrEqual(0);
|
||
for (const prefix of [
|
||
"agentic-agents-embedded-base",
|
||
"agentic-gateway-core",
|
||
"core-runtime-media-ui",
|
||
"core-unit-src-security",
|
||
]) {
|
||
const jobs = [1, 2, 3].flatMap((stripe) => {
|
||
const owner = `${prefix}-${stripe}`;
|
||
const placements = compact.flatMap((shard, index) =>
|
||
shard.groups
|
||
.filter((group) => group.shard_name.replace(/-hosted-\d+$/u, "") === owner)
|
||
.map(() => index),
|
||
);
|
||
expect(placements.length, owner).toBeGreaterThan(0);
|
||
return placements;
|
||
});
|
||
expect(new Set(jobs).size).toBe(jobs.length);
|
||
}
|
||
// Cheap stripes may legally co-locate in one bin; only existence matters.
|
||
for (const owner of ["core-unit-fast-1", "core-unit-fast-2"]) {
|
||
expect(
|
||
compact.some((job) =>
|
||
job.groups.some((group) => group.shard_name.replace(/-hosted-\d+$/u, "") === owner),
|
||
),
|
||
).toBe(true);
|
||
}
|
||
// Timing-sensitive and runtime-building jobs stay serial. Ordinary Blacksmith
|
||
// placements may overlap only with the larger request; logical groups stay intact.
|
||
for (const shard of [
|
||
...pullRequestCompact,
|
||
...githubPullRequestCompact,
|
||
...hybridPullRequestCompact,
|
||
]) {
|
||
const exclusiveCount = shard.groups.filter((group) =>
|
||
isExclusiveCompactShardName(group.shard_name),
|
||
).length;
|
||
if (exclusiveCount > 0) {
|
||
expect(exclusiveCount).toBe(shard.groups.length);
|
||
expect(shard.planConcurrency).toBe(1);
|
||
}
|
||
const originalHybridJob = hybridPullRequestCompact.includes(shard)
|
||
? expectDefined(
|
||
hybridBeforePlacement.find((job) => job.checkName === shard.checkName),
|
||
"original hybrid runner anchor",
|
||
)
|
||
: undefined;
|
||
if (
|
||
originalHybridJob?.planConcurrency === 2 &&
|
||
shard.planConcurrency === 1 &&
|
||
!usesParallelPacking(shard)
|
||
) {
|
||
expect(shard.pretestBuildMode).toBe("runtime");
|
||
}
|
||
const promoted =
|
||
originalHybridJob !== undefined &&
|
||
originalHybridJob.pretestBuildMode === undefined &&
|
||
shard.pretestBuildMode === "runtime";
|
||
if (promoted) {
|
||
expect(shard.pretestBuildMode).toBe("runtime");
|
||
expect(shard.planConcurrency).toBe(1);
|
||
expect(exclusiveCount).toBe(0);
|
||
expect(shard.requiresDist).toBe(false);
|
||
expect(shard.env).toStrictEqual(originalHybridJob.env);
|
||
for (const original of originalHybridJob.groups) {
|
||
const retained = expectDefined(
|
||
shard.groups.find((group) => group.shard_name === original.shard_name),
|
||
"retained ordinary group",
|
||
);
|
||
// Already-serial recipients keep their job cap; only parallel recipients need group pins.
|
||
if (originalHybridJob.planConcurrency === 2) {
|
||
expect(retained).toEqual({
|
||
...original,
|
||
env: { OPENCLAW_VITEST_MAX_WORKERS: "2", ...original.env },
|
||
});
|
||
} else {
|
||
expect(retained).toStrictEqual(original);
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
!githubPullRequestCompact.includes(shard) &&
|
||
!exclusiveCount &&
|
||
!shard.requiresDist &&
|
||
!promoted
|
||
) {
|
||
expect(
|
||
shard.groups.every(
|
||
(group) => Boolean(group.pretestBuildMode) === Boolean(shard.pretestBuildMode),
|
||
),
|
||
).toBe(true);
|
||
}
|
||
if (shard.planConcurrency === 2) {
|
||
expect(githubPullRequestCompact).not.toContain(shard);
|
||
expect(shard.runner).toBe(EXTRA_LARGE_NODE_TEST_RUNNER);
|
||
expect(shard.groups.length).toBeGreaterThan(1);
|
||
expect(shard.pretestBuildMode).toBeUndefined();
|
||
expect(shard.requiresDist).toBe(false);
|
||
} else {
|
||
expect(shard.planConcurrency).toBe(1);
|
||
const blacksmithTooling =
|
||
pullRequestCompact.includes(shard) &&
|
||
shard.groups.some((group) =>
|
||
group.configs.includes("test/vitest/vitest.tooling.config.ts"),
|
||
);
|
||
const nativeFullCli =
|
||
!githubPullRequestCompact.includes(shard) &&
|
||
shard.groups.some((group) => /^agentic-cli(?:-hosted-\d+)?$/u.test(group.shard_name));
|
||
expect(shard.runner).toBe(
|
||
originalHybridJob
|
||
? originalHybridJob.runner
|
||
: blacksmithTooling ||
|
||
usesParallelPacking(shard) ||
|
||
nativeFullCli ||
|
||
shard.groups.some((group) => group.minTotalMemoryBytes !== undefined) ||
|
||
shard.groups[0]?.runner === EXTRA_LARGE_NODE_TEST_RUNNER
|
||
? EXTRA_LARGE_NODE_TEST_RUNNER
|
||
: !githubPullRequestCompact.includes(shard) &&
|
||
shard.groups[0]?.runner === BUNDLED_NODE_TEST_RUNNER
|
||
? DEFAULT_NODE_TEST_RUNNER
|
||
: shard.groups[0]?.runner,
|
||
);
|
||
}
|
||
}
|
||
expect(
|
||
pullRequestCompact.filter((shard) =>
|
||
shard.groups.some((group) => isExclusiveCompactShardName(group.shard_name)),
|
||
).length,
|
||
).toBeGreaterThan(0);
|
||
const hybridJobFor = (name: string) =>
|
||
hybridPullRequestCompact.find((shard) =>
|
||
shard.groups.some(
|
||
(group) => group.shard_name === name || group.shard_name.startsWith(`${name}-hosted-`),
|
||
),
|
||
);
|
||
const hybridCliJob = hybridJobFor("agentic-cli");
|
||
const hybridToolingIsolatedJob = hybridJobFor("core-tooling-isolated");
|
||
expect(hybridCliJob).toBeDefined();
|
||
expect(hybridToolingIsolatedJob).toBeDefined();
|
||
expect(hybridToolingIsolatedJob?.checkName).not.toBe(hybridCliJob?.checkName);
|
||
const expectedEmbeddedAgentGroupNames = [
|
||
"agentic-agents-embedded-base-1",
|
||
"agentic-agents-embedded-base-2",
|
||
"agentic-agents-embedded-base-3",
|
||
"agentic-agents-embedded-incomplete-turn",
|
||
"agentic-agents-embedded-overflow-compaction",
|
||
"agentic-agents-embedded-run",
|
||
];
|
||
// Scoped configs drop unit-fast files and the shared live/e2e suffixes, so
|
||
// a striped owner covers only the files its config runs; the rest are inert.
|
||
const ownerScopedTestFiles = (owner: { dir: string; include: string[]; exclude: string[] }) => {
|
||
const unitFastFiles = new Set(
|
||
getUnitFastTestFilesForIncludePatterns(owner.include, { dir: owner.dir }),
|
||
);
|
||
return globSync(owner.include)
|
||
.map(toRepoPath)
|
||
.filter(
|
||
(file) =>
|
||
!unitFastFiles.has(file) &&
|
||
!file.endsWith(".live.test.ts") &&
|
||
!file.endsWith(".e2e.test.ts") &&
|
||
!owner.exclude.some((pattern) => matchesGlob(file, pattern)),
|
||
);
|
||
};
|
||
// The embedded composite expands into per-config groups and stripes its
|
||
// base config; whole-config runtime consumers may also be striped.
|
||
const embeddedBaseOwnerFiles = ownerScopedTestFiles(agentVitestProjectOwners.embedded);
|
||
const supportOwnerFiles = ownerScopedTestFiles(agentVitestProjectOwners.support);
|
||
const cliOwnerFiles = listMatchedTestFiles(createCliVitestConfig({}));
|
||
const cliProcessOwnerFiles = listMatchedTestFiles(createCliProcessVitestConfig({}));
|
||
const runtimeConfigOwnerFiles = listMatchedTestFiles(createRuntimeConfigVitestConfig({}));
|
||
const pluginSdkOwnerFiles = listMatchedTestFiles(createPluginSdkVitestConfig({}));
|
||
const pluginSdkLightOwnerFiles = listMatchedTestFiles(createPluginSdkLightVitestConfig({}));
|
||
const gatewayMethodsOwnerFiles = [
|
||
...listMatchedTestFiles(createGatewayMethodsVitestConfig({})),
|
||
...listMatchedTestFiles(createGatewayMethodsIsolatedVitestConfig({})),
|
||
];
|
||
const gatewayServerIsolatedOwnerFiles = [
|
||
...listMatchedTestFiles(createGatewayServerIsolatedVitestConfig({})),
|
||
...listMatchedTestFiles(createGatewayDatabaseWorkersVitestConfig({})),
|
||
];
|
||
const gatewayFilesByConfig = new Map([
|
||
[
|
||
"test/vitest/vitest.gateway-methods.config.ts",
|
||
listMatchedTestFiles(createGatewayMethodsVitestConfig({})),
|
||
],
|
||
[
|
||
"test/vitest/vitest.gateway-methods-isolated.config.ts",
|
||
listMatchedTestFiles(createGatewayMethodsIsolatedVitestConfig({})),
|
||
],
|
||
[
|
||
"test/vitest/vitest.gateway-server-isolated.config.ts",
|
||
listMatchedTestFiles(createGatewayServerIsolatedVitestConfig({})),
|
||
],
|
||
[
|
||
"test/vitest/vitest.gateway-database-workers.config.ts",
|
||
listMatchedTestFiles(createGatewayDatabaseWorkersVitestConfig({})),
|
||
],
|
||
]);
|
||
const materializedIncludes = (group: CompactNodeTestShard["groups"][number]) =>
|
||
group.includePatterns ??
|
||
group.configs.flatMap((config) => gatewayFilesByConfig.get(config) ?? []);
|
||
const compactGroups = compact.flatMap((shard) => shard.groups);
|
||
const pullRequestCompactGroups = pullRequestCompact.flatMap((shard) => shard.groups);
|
||
const expectedGroupNames = base.flatMap((shard) =>
|
||
shard.shardName === "agentic-agents-embedded"
|
||
? expectedEmbeddedAgentGroupNames
|
||
: [shard.shardName],
|
||
);
|
||
const compactOwnerNames = (plan: typeof githubCompact) =>
|
||
new Set(
|
||
plan.flatMap((shard) =>
|
||
shard.groups.map((group) => group.shard_name.replace(/-hosted-\d+$/u, "")),
|
||
),
|
||
);
|
||
expect(compactOwnerNames(compact)).toEqual(
|
||
new Set(expectedGroupNames.filter((name) => !pushExcludedShardNames.has(name))),
|
||
);
|
||
expect(compactOwnerNames(pullRequestCompact)).toEqual(new Set(expectedGroupNames));
|
||
expect(compactOwnerNames(githubCompact)).toEqual(compactOwnerNames(compact));
|
||
expect(compactOwnerNames(githubPullRequestCompact)).toEqual(
|
||
compactOwnerNames(pullRequestCompact),
|
||
);
|
||
expect(compactOwnerNames(hybridCompact)).toEqual(compactOwnerNames(githubCompact));
|
||
expect(compactOwnerNames(hybridPullRequestCompact)).toEqual(
|
||
compactOwnerNames(githubPullRequestCompact),
|
||
);
|
||
for (const plan of [
|
||
compact,
|
||
pullRequestCompact,
|
||
githubCompact,
|
||
githubPullRequestCompact,
|
||
hybridCompact,
|
||
hybridPullRequestCompact,
|
||
]) {
|
||
const retainsProofTests = [compact, githubCompact, hybridCompact].includes(plan);
|
||
const retainedFiles = (files: string[]) =>
|
||
files.filter((file) => retainsProofTests || !isCiProofTestFile(file));
|
||
const pluginOwners = plan
|
||
.flatMap((shard) => shard.groups)
|
||
.filter((group) =>
|
||
materializedIncludes(group).includes("test/plugins/codex-model-catalog.gateway.test.ts"),
|
||
);
|
||
expect(pluginOwners).toHaveLength(1);
|
||
expect(pluginOwners[0]?.configs).toContain(
|
||
"test/vitest/vitest.gateway-database-workers.config.ts",
|
||
);
|
||
expect(pluginOwners[0]?.pretestBuildMode).toBe("runtime");
|
||
for (const owner of base) {
|
||
const groups = plan
|
||
.flatMap((shard) => shard.groups)
|
||
.filter((group) => group.shard_name.startsWith(`${owner.shardName}-hosted-`));
|
||
if (groups.length === 0) {
|
||
continue;
|
||
}
|
||
const jobs = groups.map((group) => plan.findIndex((shard) => shard.groups.includes(group)));
|
||
for (const jobIndex of new Set(jobs)) {
|
||
const job = plan[jobIndex]!;
|
||
const siblings = groups.filter((group) => job.groups.includes(group));
|
||
if (siblings.length === 1) {
|
||
continue;
|
||
}
|
||
expect(job).toMatchObject({ planConcurrency: 1, requiresDist: false });
|
||
expect(job.predictedSeconds).toBeLessThanOrEqual(job.pretestBuildMode ? 150 : 250);
|
||
expect(job.groups.every((group) => isExclusiveCompactShardName(group.shard_name))).toBe(
|
||
true,
|
||
);
|
||
if (job.pretestBuildMode === undefined) {
|
||
expect([hybridCompact, hybridPullRequestCompact]).toContain(plan);
|
||
expect(isCombinedUnbuiltCliJob(job)).toBe(true);
|
||
} else {
|
||
expect(job.pretestBuildMode).toBe("runtime");
|
||
}
|
||
expect(
|
||
siblings.every((group) => /^agentic-cli-process-hosted-\d+$/u.test(group.shard_name)),
|
||
).toBe(true);
|
||
expect(siblings.every((group) => group.pretestBuildMode === job.pretestBuildMode)).toBe(
|
||
true,
|
||
);
|
||
}
|
||
const actual = groups.flatMap((group) => group.includePatterns ?? []);
|
||
expect(new Set(actual).size, owner.shardName).toBe(actual.length);
|
||
if (owner.includePatterns) {
|
||
const expectedFiles = [compact, githubCompact, hybridCompact].includes(plan)
|
||
? owner.includePatterns.filter(
|
||
(file) => !file.startsWith("test/scripts/") && !file.startsWith("src/scripts/"),
|
||
)
|
||
: owner.includePatterns;
|
||
expect(actual.toSorted(), owner.shardName).toEqual(
|
||
retainedFiles(expectedFiles).toSorted(),
|
||
);
|
||
} else if (owner.shardName === "agentic-agents-support") {
|
||
expect(actual.toSorted()).toEqual(supportOwnerFiles.toSorted());
|
||
} else if (owner.shardName === "agentic-cli-process") {
|
||
expect(actual.toSorted()).toEqual(cliProcessOwnerFiles.toSorted());
|
||
} else if (owner.shardName === "agentic-cli") {
|
||
expect(actual.toSorted()).toEqual(retainedFiles(cliOwnerFiles).toSorted());
|
||
} else if (owner.shardName === "core-runtime-config") {
|
||
expect(actual.toSorted()).toEqual(runtimeConfigOwnerFiles.toSorted());
|
||
} else if (owner.shardName === "agentic-gateway-methods") {
|
||
expect(actual.toSorted()).toEqual(gatewayMethodsOwnerFiles.toSorted());
|
||
} else if (owner.shardName === "agentic-gateway-server-isolated") {
|
||
expect(actual.toSorted()).toEqual(
|
||
retainedFiles(gatewayServerIsolatedOwnerFiles).toSorted(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// Pushes omit only the explicit low-signal families; PR fallback retains
|
||
// their include-pattern coverage when special setup prevents targeting.
|
||
expect(
|
||
compactGroups.flatMap(materializedIncludes).toSorted((a, b) => a.localeCompare(b)),
|
||
).toEqual(
|
||
base
|
||
.filter((shard) => !pushExcludedShardNames.has(shard.shardName))
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.concat(
|
||
embeddedBaseOwnerFiles,
|
||
supportOwnerFiles,
|
||
cliOwnerFiles,
|
||
gatewayMethodsOwnerFiles,
|
||
gatewayServerIsolatedOwnerFiles,
|
||
cliProcessOwnerFiles,
|
||
pluginSdkOwnerFiles,
|
||
pluginSdkLightOwnerFiles,
|
||
runtimeConfigOwnerFiles,
|
||
getUnitFastIsolatedTestFiles(),
|
||
getUnitFastTimerTestFiles(),
|
||
)
|
||
.filter((file) => !file.startsWith("test/scripts/") && !file.startsWith("src/scripts/"))
|
||
.toSorted((a, b) => a.localeCompare(b)),
|
||
);
|
||
expect(
|
||
pullRequestCompactGroups.flatMap(materializedIncludes).toSorted((a, b) => a.localeCompare(b)),
|
||
).toEqual(
|
||
base
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.concat(
|
||
embeddedBaseOwnerFiles,
|
||
supportOwnerFiles,
|
||
cliOwnerFiles,
|
||
gatewayMethodsOwnerFiles,
|
||
gatewayServerIsolatedOwnerFiles,
|
||
cliProcessOwnerFiles,
|
||
pluginSdkOwnerFiles,
|
||
pluginSdkLightOwnerFiles,
|
||
runtimeConfigOwnerFiles,
|
||
)
|
||
.filter((file) => !isCiProofTestFile(file))
|
||
.toSorted((a, b) => a.localeCompare(b)),
|
||
);
|
||
expect(compact.every((shard) => shard.groups.every((group) => group.configs.length > 0))).toBe(
|
||
true,
|
||
);
|
||
expect(
|
||
pullRequestCompact
|
||
.flatMap((shard) => shard.groups)
|
||
.find((group) => group.shard_name === "core-runtime-tui-pty")?.env,
|
||
).toEqual({
|
||
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1",
|
||
OPENCLAW_TUI_PTY_USE_BUILT_CLI: "1",
|
||
// Timing-sensitive groups pin the worker budget while the job-level
|
||
// default scales with the runner class.
|
||
OPENCLAW_VITEST_MAX_WORKERS: "2",
|
||
});
|
||
expect(
|
||
compact.flatMap((shard) => shard.groups).find((group) => group.shard_name === "agentic-cli")
|
||
?.env,
|
||
).toBeUndefined();
|
||
for (const suffix of ["1", "2", "3"]) {
|
||
const groups = compact
|
||
.flatMap((shard) => shard.groups)
|
||
.filter(
|
||
(group) =>
|
||
group.shard_name.replace(/-hosted-\d+$/u, "") === `core-runtime-media-ui-${suffix}`,
|
||
);
|
||
expect(groups.length).toBeGreaterThan(0);
|
||
for (const group of groups) {
|
||
expect(group.env).toEqual({ OPENCLAW_VITEST_MAX_WORKERS: "2" });
|
||
}
|
||
}
|
||
expect(
|
||
compact
|
||
.flatMap((shard) => shard.groups)
|
||
.find((group) => group.shard_name === "core-runtime-media-ui-support")?.env,
|
||
).toEqual({ OPENCLAW_VITEST_MAX_WORKERS: "2" });
|
||
const startupCoreJob = compact.find((shard) =>
|
||
shard.groups.some((group) => group.shard_name === "agentic-control-plane-startup-core"),
|
||
);
|
||
expect(startupCoreJob?.runner).toBe(
|
||
usesParallelPacking(startupCoreJob) ? EXTRA_LARGE_NODE_TEST_RUNNER : DEFAULT_NODE_TEST_RUNNER,
|
||
);
|
||
expect(
|
||
startupCoreJob?.groups.find(
|
||
(group) => group.shard_name === "agentic-control-plane-startup-core",
|
||
)?.runner,
|
||
).toBe(DEFAULT_NODE_TEST_RUNNER);
|
||
const startupHealthJob = expectDefined(
|
||
compact.find((job) =>
|
||
job.groups.some(
|
||
(group) => group.shard_name === "agentic-control-plane-startup-health-runtime",
|
||
),
|
||
),
|
||
"startup health runtime job",
|
||
);
|
||
const measuredSiblings = startupHealthJob.groups.filter(
|
||
(group) => group.fallbackMaxWorkers === 2,
|
||
);
|
||
expect(
|
||
startupHealthJob.groups.find(
|
||
(group) => group.shard_name === "agentic-control-plane-startup-health-runtime",
|
||
)?.env,
|
||
).toEqual({
|
||
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "60000",
|
||
OPENCLAW_VITEST_MAX_WORKERS: "2",
|
||
});
|
||
if (measuredSiblings.length > 0) {
|
||
expect(startupHealthJob.planConcurrency).toBe(1);
|
||
expect(startupHealthJob.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
for (const sibling of measuredSiblings) {
|
||
const isolated = /^agentic-gateway-server-isolated(?:-hosted-\d+)?$/u.test(
|
||
sibling.shard_name,
|
||
);
|
||
const methods = /^agentic-gateway-methods(?:-hosted-\d+)?$/u.test(sibling.shard_name);
|
||
expect(sibling.env?.OPENCLAW_VITEST_MAX_WORKERS, sibling.shard_name).toBe(
|
||
isolated ? "8" : methods ? "4" : undefined,
|
||
);
|
||
if (isolated || methods) {
|
||
expect(sibling.minTotalMemoryBytes).toBe(28 * 1024 ** 3);
|
||
expect(startupHealthJob.runner).toBe(EXTRA_LARGE_NODE_TEST_RUNNER);
|
||
}
|
||
}
|
||
}
|
||
const largeJobs = compact.filter(
|
||
(shard) => !shard.requiresDist && shard.checkName.startsWith("checks-node-compact-large-"),
|
||
);
|
||
const smallJobs = compact.filter(
|
||
(shard) => !shard.requiresDist && shard.checkName.startsWith("checks-node-compact-small-"),
|
||
);
|
||
const extraLargeJobs = compact.filter(
|
||
(shard) => !shard.requiresDist && shard.checkName.startsWith("checks-node-compact-large32-"),
|
||
);
|
||
const distJobs = compact.filter((shard) => shard.requiresDist);
|
||
expect(largeJobs.length).toBeGreaterThan(0);
|
||
expect(smallJobs.length).toBeGreaterThan(0);
|
||
expect(extraLargeJobs.length).toBeGreaterThan(0);
|
||
expect(distJobs).toHaveLength(1);
|
||
expect(largeJobs.length + smallJobs.length + extraLargeJobs.length + distJobs.length).toBe(
|
||
compact.length,
|
||
);
|
||
expect(
|
||
largeJobs.every(
|
||
(shard) =>
|
||
shard.groups.every((group) => group.runner === DEFAULT_NODE_TEST_RUNNER) ||
|
||
sharesNativeCapacity(shard),
|
||
),
|
||
).toBe(true);
|
||
expect(
|
||
smallJobs.every(
|
||
(shard) =>
|
||
shard.groups.every((group) => group.runner === BUNDLED_NODE_TEST_RUNNER) ||
|
||
sharesNativeCapacity(shard),
|
||
),
|
||
).toBe(true);
|
||
expect(extraLargeJobs.every((job) => job.runner === EXTRA_LARGE_NODE_TEST_RUNNER)).toBe(true);
|
||
for (const shard of [
|
||
...compact,
|
||
...pullRequestCompact,
|
||
...githubCompact,
|
||
...githubPullRequestCompact,
|
||
...hybridCompact,
|
||
...hybridPullRequestCompact,
|
||
]) {
|
||
for (const group of shard.groups) {
|
||
if (
|
||
!group.configs.some((config) => /vitest\.cli(?:-process)?\.config\.ts$/u.test(config))
|
||
) {
|
||
continue;
|
||
}
|
||
expect(isExclusiveCompactShardName(group.shard_name)).toBe(true);
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe(
|
||
/^agentic-cli(?:-hosted-\d+)?$/u.test(group.shard_name) &&
|
||
!githubCompact.includes(shard) &&
|
||
!githubPullRequestCompact.includes(shard)
|
||
? undefined
|
||
: "2",
|
||
);
|
||
if ((shard.predictedSeconds ?? 0) > 150) {
|
||
if (isCombinedUnbuiltCliJob(shard)) {
|
||
expect([...hybridCompact, ...hybridPullRequestCompact]).toContain(shard);
|
||
expect(shard.predictedSeconds).toBeLessThanOrEqual(250);
|
||
} else if (shard.pretestBuildMode && shard.groups.length > 1) {
|
||
expect([...githubCompact, ...githubPullRequestCompact]).toContain(shard);
|
||
expect(shard).toMatchObject({ planConcurrency: 1, requiresDist: false });
|
||
expect(shard.groups.every((entry) => entry.pretestBuildMode)).toBe(true);
|
||
expect(shard.predictedSeconds).toBeLessThanOrEqual(210);
|
||
} else {
|
||
expect(shard.groups).toHaveLength(1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
expect(compact).toEqual(
|
||
createNodeTestShardBundles({
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "push",
|
||
}),
|
||
);
|
||
const embeddedAgentGroups = compact
|
||
.flatMap((shard) => shard.groups)
|
||
.filter((group) => group.shard_name.startsWith("agentic-agents-embedded-"));
|
||
expect(embeddedAgentGroups.map((group) => group.shard_name).toSorted()).toEqual(
|
||
expectedEmbeddedAgentGroupNames,
|
||
);
|
||
expect(
|
||
compact.some((shard) =>
|
||
shard.groups.some((group) => group.shard_name === "agentic-agents-embedded"),
|
||
),
|
||
).toBe(false);
|
||
// The base config repeats once per stripe; its files stay partitioned below.
|
||
expect(new Set(embeddedAgentGroups.flatMap((group) => group.configs))).toEqual(
|
||
new Set(embeddedAgentVitestProjectOwners.map((owner) => owner.config)),
|
||
);
|
||
expect(
|
||
embeddedAgentGroups.every(
|
||
(group) => group.env?.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS === "660000",
|
||
),
|
||
).toBe(true);
|
||
// Base stripes must partition the owner's files: a repeat re-runs a suite,
|
||
// and a missing file silently loses coverage.
|
||
const embeddedBaseGroups = embeddedAgentGroups.filter((group) =>
|
||
/^agentic-agents-embedded-base-\d+$/u.test(group.shard_name),
|
||
);
|
||
const embeddedBaseFiles = embeddedBaseGroups.flatMap((group) => group.includePatterns ?? []);
|
||
expect(embeddedBaseGroups).toHaveLength(3);
|
||
expect(
|
||
embeddedBaseGroups.every(
|
||
(group) => group.configs[0] === agentVitestProjectOwners.embedded.config,
|
||
),
|
||
).toBe(true);
|
||
expect(new Set(embeddedBaseFiles).size).toBe(embeddedBaseFiles.length);
|
||
expect(embeddedBaseFiles.toSorted((a, b) => a.localeCompare(b))).toEqual(
|
||
embeddedBaseOwnerFiles.toSorted((a, b) => a.localeCompare(b)),
|
||
);
|
||
expect(
|
||
compact
|
||
.filter((shard) => shard.groups.some((group) => !group.includePatterns))
|
||
.every((shard) => shard.timeoutMinutes === 120),
|
||
).toBe(true);
|
||
// Whole-config groups now pack into the same runtime-balanced bins as
|
||
// include-pattern groups; the separate "-whole-" job class is gone.
|
||
expect(compact.some((shard) => shard.checkName.includes("-whole-"))).toBe(false);
|
||
expect(
|
||
compact.some((shard) => shard.groups.some((group) => group.shard_name === "core-tooling")),
|
||
).toBe(false);
|
||
expect(
|
||
pullRequestCompact
|
||
.flatMap((shard) => shard.groups)
|
||
.find((group) => group.shard_name === "core-tooling-isolated"),
|
||
).toEqual(
|
||
expect.objectContaining({
|
||
configs: [
|
||
"test/vitest/vitest.tooling-docker.config.ts",
|
||
"test/vitest/vitest.tooling-isolated.config.ts",
|
||
],
|
||
}),
|
||
);
|
||
// The docker helper config rides with the isolated shard on both plans;
|
||
// no standalone core-tooling-docker group remains.
|
||
expect(
|
||
pullRequestCompact
|
||
.flatMap((shard) => shard.groups)
|
||
.some((group) => group.shard_name === "core-tooling-docker"),
|
||
).toBe(false);
|
||
const toolingGroups = pullRequestCompactGroups.filter((group) =>
|
||
/^core-tooling-\d+(?:-hosted-\d+)?$/u.test(group.shard_name),
|
||
);
|
||
const toolingFiles = toolingGroups.flatMap((group) => group.includePatterns ?? []);
|
||
expect(
|
||
new Set(
|
||
[...compactOwnerNames(pullRequestCompact)].filter((name) =>
|
||
/^core-tooling-\d+$/u.test(name),
|
||
),
|
||
),
|
||
).toEqual(new Set(expectedToolingOwnerNames));
|
||
expect(
|
||
toolingGroups.every((group) => group.configs[0] === "test/vitest/vitest.tooling.config.ts"),
|
||
).toBe(true);
|
||
expect(new Set(toolingFiles).size).toBe(toolingFiles.length);
|
||
const allToolingFiles = listAllToolingTestFiles();
|
||
expect(toolingFiles.toSorted((a, b) => a.localeCompare(b))).toEqual(
|
||
allToolingFiles.filter((file) => !isCiProofTestFile(file)),
|
||
);
|
||
expect(
|
||
base
|
||
.filter((shard) => shard.configs[0] === "test/vitest/vitest.tooling.config.ts")
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b)),
|
||
).toEqual(allToolingFiles);
|
||
}
|
||
it.each(plannerHosts)(
|
||
"preserves coverage and execution policies with committed compact measurements ($label)",
|
||
checkCommittedCompactPolicies,
|
||
);
|
||
|
||
it("splits the slow core unit shards while keeping paired source/security coverage", () => {
|
||
const coreUnitShards = defaultShards
|
||
.filter((shard) => shard.shardName.startsWith("core-unit-"))
|
||
.map((shard) => ({
|
||
configs: shard.configs,
|
||
requiresDist: shard.requiresDist,
|
||
shardName: shard.shardName,
|
||
}));
|
||
|
||
expect(coreUnitShards).toEqual([
|
||
{
|
||
configs: ["test/vitest/vitest.unit-fast.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-fast-1",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-fast.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-fast-2",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-fast-isolated.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-fast-isolated",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-fast-fake-timers.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-fast-fake-timers",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-src.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-src-security-1",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-src.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-src-security-2",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-src.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-src-security-3",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-security.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-src-security-support",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.unit-support.config.ts"],
|
||
requiresDist: false,
|
||
shardName: "core-unit-support",
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("partitions each giant compact group across three deterministic stripes", () => {
|
||
const env = { ...process.env, OPENCLAW_VITEST_INCLUDE_FILE: undefined };
|
||
const cases = [
|
||
{
|
||
stripeConfigs: [createUiVitestConfig(env)],
|
||
supportConfigs: [
|
||
createMediaVitestConfig(env),
|
||
createMediaUnderstandingVitestConfig(env),
|
||
createTuiVitestConfig(env),
|
||
createUiIsolatedVitestConfig(env),
|
||
createUiTimingVitestConfig(env),
|
||
createWizardVitestConfig(env),
|
||
],
|
||
prefix: "core-runtime-media-ui",
|
||
},
|
||
{
|
||
stripeConfigs: [createGatewayCoreVitestConfig(env), createGatewayClientVitestConfig(env)],
|
||
supportConfigs: [],
|
||
prefix: "agentic-gateway-core",
|
||
},
|
||
{
|
||
stripeConfigs: [
|
||
createUnitVitestConfigWithOptions(env, {
|
||
name: "unit-src",
|
||
includePatterns: ["src/**/*.test.ts"],
|
||
extraExcludePatterns: ["src/acp/**", "src/security/**"],
|
||
}),
|
||
],
|
||
supportConfigs: [
|
||
createUnitVitestConfigWithOptions(env, {
|
||
name: "unit-security",
|
||
includePatterns: ["src/security/**/*.test.ts"],
|
||
passWithNoTests: true,
|
||
}),
|
||
],
|
||
prefix: "core-unit-src-security",
|
||
},
|
||
];
|
||
|
||
const shards = defaultShards;
|
||
for (const { prefix, stripeConfigs, supportConfigs } of cases) {
|
||
const stripes = shards.filter(
|
||
(shard) => /^.+-\d+$/u.test(shard.shardName) && shard.shardName.startsWith(`${prefix}-`),
|
||
);
|
||
// Files needing a pretest build are pulled into a sibling `-runtime`
|
||
// shard so only one job builds; coverage must still be complete across
|
||
// the stripes plus that shard.
|
||
const coverageShards = shards.filter((shard) => shard.shardName.startsWith(`${prefix}-`));
|
||
const actual = coverageShards
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
const expected = stripeConfigs
|
||
.flatMap((config) => listMatchedTestFiles(config))
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
|
||
expect(stripes.map((stripe) => stripe.shardName)).toEqual([
|
||
`${prefix}-1`,
|
||
`${prefix}-2`,
|
||
`${prefix}-3`,
|
||
]);
|
||
expect(stripes.every((stripe) => (stripe.includePatterns?.length ?? 0) > 0)).toBe(true);
|
||
expect(new Set(actual).size).toBe(actual.length);
|
||
expect(actual).toEqual(expected);
|
||
|
||
const support = shards.find((shard) => shard.shardName === `${prefix}-support`);
|
||
if (supportConfigs.length === 0) {
|
||
expect(support).toBeUndefined();
|
||
} else {
|
||
expect(support?.includePatterns).toBeUndefined();
|
||
expect(support?.configs).toHaveLength(supportConfigs.length);
|
||
}
|
||
}
|
||
});
|
||
|
||
it.each(["blacksmith", "github", "hybrid"])(
|
||
"runs timing budgets once through their support owner in %s hosted plans",
|
||
(runnerBackend) => {
|
||
const groups = getCommittedCompactPlan("pull-request", runnerBackend).flatMap(
|
||
(job) => job.groups,
|
||
);
|
||
const owners = groups.filter((group) =>
|
||
group.configs.includes("test/vitest/vitest.ui-timing.config.ts"),
|
||
);
|
||
expect(owners).toHaveLength(1);
|
||
expect(owners[0]?.shard_name).toBe("core-runtime-media-ui-support");
|
||
expect(owners[0]?.includePatterns).toBeUndefined();
|
||
const stripedFiles = groups.flatMap((group) => group.includePatterns ?? []);
|
||
for (const file of uiTimingTestFiles) {
|
||
// Shared UI excludes these files; leaving them in its stripes silently skips them.
|
||
expect(stripedFiles).not.toContain(file);
|
||
}
|
||
expect(
|
||
listMatchedTestFiles(
|
||
createUiTimingVitestConfig({ OPENCLAW_VITEST_INCLUDE_FILE: undefined }),
|
||
),
|
||
).toEqual(uiTimingTestFiles);
|
||
},
|
||
);
|
||
|
||
it("names the node shard checks as core test lanes", () => {
|
||
const shards = defaultShards;
|
||
|
||
expect(shards).not.toHaveLength(0);
|
||
expect(shards.map((shard) => shard.checkName)).toEqual(
|
||
shards.map((shard) =>
|
||
shard.shardName.startsWith("core-unit-")
|
||
? `checks-node-core-${shard.shardName.slice("core-unit-".length)}`
|
||
: `checks-node-${shard.shardName}`,
|
||
),
|
||
);
|
||
});
|
||
|
||
it("keeps extension, bundled, contracts, and channels configs out of the core node lane", () => {
|
||
const configs = defaultShards.flatMap((shard) => shard.configs);
|
||
|
||
expect(configs).not.toContain("test/vitest/vitest.channels.config.ts");
|
||
expect(configs).not.toContain("test/vitest/vitest.contracts.config.ts");
|
||
expect(configs).not.toContain("test/vitest/vitest.bundled.config.ts");
|
||
expect(configs).not.toContain("test/vitest/vitest.full-extensions.config.ts");
|
||
expect(configs).not.toContain("test/vitest/vitest.extension-telegram.config.ts");
|
||
});
|
||
|
||
it("keeps compact agentic config ownership aligned with the full agentic project set", () => {
|
||
const fullAgenticShard = fullSuiteVitestShards.find((shard) => shard.name === "agentic");
|
||
const intentionallyExcludedConfigs = new Set(["test/vitest/vitest.channels.config.ts"]);
|
||
const expectedConfigs = (fullAgenticShard?.projects ?? [])
|
||
.filter((config) => !intentionallyExcludedConfigs.has(config))
|
||
.toSorted((left, right) => left.localeCompare(right));
|
||
const actualConfigs = [
|
||
...new Set(
|
||
defaultShards
|
||
.filter((shard) => shard.shardName.startsWith("agentic-"))
|
||
.flatMap((shard) => shard.configs),
|
||
),
|
||
].toSorted((left, right) => left.localeCompare(right));
|
||
|
||
expect(fullAgenticShard).toBeDefined();
|
||
expect(actualConfigs).toEqual(expectedConfigs);
|
||
});
|
||
|
||
it("marks only dist-dependent shards for built artifact restore", () => {
|
||
const requiresDistShardNames = defaultShards
|
||
.filter((shard) => shard.requiresDist)
|
||
.map((shard) => shard.shardName);
|
||
|
||
expect(requiresDistShardNames).toEqual(["core-support-boundary", "core-runtime-tui-pty"]);
|
||
});
|
||
|
||
it("keeps lifecycle proofs on main while excluding PR and manual PR-fallback plans", () => {
|
||
const proofFiles = ["src/gateway/server.codex-failure-recovery.test.ts"];
|
||
const files = (mode: "push" | "pull-request") =>
|
||
getCommittedCompactPlan(mode).flatMap((shard) =>
|
||
shard.groups.flatMap((group) => group.includePatterns ?? []),
|
||
);
|
||
const mainFiles = files("push");
|
||
const prFiles = files("pull-request");
|
||
const manual = createNodeTestShards({ includeProofTests: false });
|
||
for (const file of proofFiles) {
|
||
expect(mainFiles.filter((target) => target === file)).toHaveLength(1);
|
||
expect(prFiles).not.toContain(file);
|
||
expect(manual.flatMap((shard) => shard.includePatterns ?? [])).not.toContain(file);
|
||
}
|
||
expect(
|
||
manual.find((shard) => shard.shardName === "agentic-gateway-server-isolated")
|
||
?.includePatterns,
|
||
).toContain("src/gateway/server.chat-recovered-output.test.ts");
|
||
});
|
||
|
||
it.each(["blacksmith", "github", "hybrid"])(
|
||
"defers exactly the runtime release inventory from automatic plans on %s",
|
||
(runnerBackend) => {
|
||
const reducedOwners = defaultShards
|
||
.filter(
|
||
(shard) =>
|
||
shard.shardName === "core-runtime-config" ||
|
||
shard.shardName === "agentic-cli" ||
|
||
shard.includePatterns?.some(isReleaseOnlyRuntimeTestFile),
|
||
)
|
||
.map((shard) => shard.shardName);
|
||
for (const compactMode of ["push", "pull-request"] as const) {
|
||
const before = getCommittedCompactPlan(compactMode, runnerBackend);
|
||
const after = createNodeTestShardBundles({
|
||
compactMode,
|
||
runnerBackend,
|
||
includeReleaseOnlyPluginShards: false,
|
||
includeReleaseOnlyRuntimeTests: false,
|
||
changedPaths: ["src/config/state-startup-corpus.test-support.ts"],
|
||
});
|
||
expectRuntimeReleaseInventory({ before, after, reducedOwners, compactMode });
|
||
}
|
||
},
|
||
);
|
||
|
||
it("keeps noncompact reduced runtime bundles separate from release timing history", () => {
|
||
const full = createNodeTestShardBundles();
|
||
const reduced = createNodeTestShardBundles({ includeReleaseOnlyRuntimeTests: false });
|
||
for (const config of [
|
||
"test/vitest/vitest.runtime-config.config.ts",
|
||
"test/vitest/vitest.infra.config.ts",
|
||
"test/vitest/vitest.unit-src.config.ts",
|
||
]) {
|
||
expect(
|
||
full.filter((shard) => shard.configs.includes(config)).some((shard) => shard.timing_key),
|
||
).toBe(false);
|
||
const owners = reduced.filter((shard) => shard.configs.includes(config) && shard.timing_key);
|
||
expect(owners.length, config).toBeGreaterThan(0);
|
||
for (const owner of owners) {
|
||
const key = expectDefined(owner.timing_key, "reduced bundle timing identity");
|
||
expect(parseCompactSplitTimingKey(key)?.parentShardName ?? key).toBe(
|
||
`changed-${owner.shardName}`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
it("preserves runtime preparation and core-only ownership in full and compact plans", () => {
|
||
const qaConfig = "test/vitest/vitest.extension-qa.config.ts";
|
||
const doctorRuntimeTargets = [
|
||
"src/commands/doctor-agent-database-order.process.test.ts",
|
||
"src/commands/doctor-config-flow.legacy-composition.test.ts",
|
||
"src/commands/doctor-config-preflight.process.test.ts",
|
||
"src/commands/doctor-config-preflight.refusal.process.test.ts",
|
||
"src/commands/doctor-config-preflight.test.ts",
|
||
"src/commands/doctor-config-preflight.v17-atomicity.process.test.ts",
|
||
];
|
||
const runtimeTargets = [
|
||
"test/e2e/qa-lab/runtime/gateway-support-export-runtime.test.ts",
|
||
"src/infra/update-managed-service-handoff-lifecycle.test.ts",
|
||
...doctorRuntimeTargets,
|
||
"src/commands/doctor-plugin-install-config.process.test.ts",
|
||
"src/gateway/gateway-active-memory.test.ts",
|
||
"src/gateway/gateway-auth-recovery.test.ts",
|
||
"src/gateway/gateway-concurrent-streams.test.ts",
|
||
"src/gateway/gateway-cron-process-identity.windows.test.ts",
|
||
"src/gateway/gateway-route-model-reuse.test.ts",
|
||
"src/gateway/gateway-ssh-upload-signal.test.ts",
|
||
"src/gateway/server.config-patch.test.ts",
|
||
];
|
||
const databaseWorkerFiles = new Set(
|
||
listMatchedTestFiles(createGatewayDatabaseWorkersVitestConfig({})),
|
||
);
|
||
const ownsRuntimeTarget = (
|
||
group: { configs: string[]; includePatterns?: string[] },
|
||
file: string,
|
||
) =>
|
||
group.includePatterns
|
||
? group.includePatterns.includes(file)
|
||
: group.configs.includes("test/vitest/vitest.gateway-database-workers.config.ts") &&
|
||
databaseWorkerFiles.has(file);
|
||
const full = defaultShards;
|
||
const compact = createNodeTestShardBundles({ compact: true, compactMode: "pull-request" });
|
||
for (const shards of [full, compact]) {
|
||
expect(
|
||
shards.flatMap((shard) =>
|
||
"configs" in shard ? shard.configs : shard.groups.flatMap((group) => group.configs),
|
||
),
|
||
).not.toContain(qaConfig);
|
||
for (const runtimeTarget of [...runtimeTargets, PRIVATE_QA_TOOLING_TEST]) {
|
||
if (shards === compact && isCiProofTestFile(runtimeTarget)) {
|
||
expect(
|
||
compact
|
||
.flatMap((shard) => shard.groups)
|
||
.some((group) => ownsRuntimeTarget(group, runtimeTarget)),
|
||
).toBe(false);
|
||
continue;
|
||
}
|
||
const owner = expectDefined(
|
||
shards.find((shard) =>
|
||
("configs" in shard ? [shard] : shard.groups).some((group) =>
|
||
ownsRuntimeTarget(group, runtimeTarget),
|
||
),
|
||
),
|
||
`runtime owner for ${runtimeTarget}`,
|
||
);
|
||
const groups = "configs" in owner ? [owner] : owner.groups;
|
||
// A shared build takes the strongest requirement of its complete selection.
|
||
const containsPrivateQa = groups.some((group) =>
|
||
group.includePatterns?.includes(PRIVATE_QA_TOOLING_TEST),
|
||
);
|
||
expect(owner.pretestBuildMode, runtimeTarget).toBe(
|
||
containsPrivateQa ? "private-qa" : "runtime",
|
||
);
|
||
const group = groups.find((entry) => ownsRuntimeTarget(entry, runtimeTarget));
|
||
expect(group?.pretestBuildMode, runtimeTarget).toBe(
|
||
group?.includePatterns?.includes(PRIVATE_QA_TOOLING_TEST) ? "private-qa" : "runtime",
|
||
);
|
||
}
|
||
}
|
||
|
||
const doctorName = "agentic-commands-doctor-config-state";
|
||
const doctor = full.find((shard) => shard.shardName === doctorName)!;
|
||
const placements = compact.flatMap((job, jobIndex) =>
|
||
job.groups
|
||
.filter((group) => group.shard_name.replace(/-hosted-\d+$/u, "") === doctorName)
|
||
.map((group) => ({ group, job, jobIndex })),
|
||
);
|
||
const commandRuntime = compact
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name === "agentic-commands-runtime");
|
||
expect(commandRuntime).toHaveLength(1);
|
||
expect(commandRuntime[0]?.pretestBuildMode).toBe("runtime");
|
||
expect(commandRuntime[0]?.includePatterns?.toSorted()).toEqual(
|
||
[...doctorRuntimeTargets, "src/commands/doctor-plugin-install-config.process.test.ts"]
|
||
.filter((file) => !isCiProofTestFile(file))
|
||
.toSorted(),
|
||
);
|
||
expect(placements.every(({ group }) => group.pretestBuildMode === undefined)).toBe(true);
|
||
expect(placements.flatMap(({ group }) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
doctor.includePatterns?.filter((file) => !isCiProofTestFile(file)).toSorted(),
|
||
);
|
||
expect(new Set(placements.map(({ jobIndex }) => jobIndex)).size).toBe(placements.length);
|
||
for (const { group } of placements) {
|
||
expect(group.configs).toEqual(doctor.configs);
|
||
expect(group.env).toEqual(doctor.env);
|
||
expect(group.fallbackMaxWorkers).toBe(2);
|
||
expect(group.requiresDist).toBe(doctor.requiresDist);
|
||
expect(group.runner).toBe(BUNDLED_NODE_TEST_RUNNER);
|
||
}
|
||
});
|
||
|
||
it("splits tooling checks independently from built artifacts", () => {
|
||
const compilerFixture = "test/scripts/write-unified-entry-dts.test.ts";
|
||
const toolingShards = defaultShards.filter((shard) =>
|
||
shard.shardName.startsWith("core-tooling"),
|
||
);
|
||
const compilerParent = toolingShards.find((shard) =>
|
||
shard.includePatterns?.includes(compilerFixture),
|
||
)!;
|
||
for (const runnerBackend of ["blacksmith", "hybrid", "github"]) {
|
||
const jobs = getCommittedCompactPlan("pull-request", runnerBackend);
|
||
const owner = jobs.find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(compilerFixture)),
|
||
);
|
||
// This fixture runs the real full-build guard, which needs more than the
|
||
// available heap observed inside a small runner's retained tooling graph.
|
||
expect(owner?.runner, runnerBackend).toBe(
|
||
runnerBackend === "github" ? DEFAULT_NODE_TEST_RUNNER : EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
);
|
||
const precise = createSelectedNodeTestShardBundles([compilerFixture], { runnerBackend });
|
||
const preciseOwner = precise?.find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(compilerFixture)),
|
||
);
|
||
expect(preciseOwner?.runner, runnerBackend).toBe(owner?.runner);
|
||
expect(preciseOwner?.planConcurrency).toBe(1);
|
||
expect(preciseOwner?.groups).toEqual([
|
||
expect.objectContaining({
|
||
includePatterns: [compilerFixture],
|
||
env: expect.objectContaining({ OPENCLAW_VITEST_MAX_WORKERS: "2" }),
|
||
}),
|
||
]);
|
||
expect(
|
||
jobs
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.includePatterns?.includes(compilerFixture)),
|
||
).toHaveLength(1);
|
||
if (runnerBackend !== "blacksmith") {
|
||
const siblings = jobs
|
||
.flatMap((job) => job.groups)
|
||
.filter(
|
||
(group) =>
|
||
group.shard_name.startsWith(`${compilerParent.shardName}-hosted-`) &&
|
||
!group.includePatterns?.includes(compilerFixture),
|
||
);
|
||
expect(siblings.length).toBeGreaterThan(0);
|
||
expect(siblings.every((group) => group.runner === BUNDLED_NODE_TEST_RUNNER)).toBe(true);
|
||
}
|
||
}
|
||
|
||
const sdkFixture = "test/scripts/write-plugin-sdk-entry-dts.test.ts";
|
||
for (const runnerBackend of ["blacksmith", "hybrid", "github"]) {
|
||
const sdkJobs = createSelectedNodeTestShardBundles([sdkFixture], { runnerBackend });
|
||
const fullOwner = getCommittedCompactPlan("pull-request", runnerBackend).find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(sdkFixture)),
|
||
);
|
||
const selectedOwner = sdkJobs?.find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(sdkFixture)),
|
||
);
|
||
for (const owner of [fullOwner, selectedOwner]) {
|
||
expect(owner?.runner, runnerBackend).toBe(
|
||
runnerBackend === "github" ? BUNDLED_NODE_TEST_RUNNER : EXTRA_LARGE_NODE_TEST_RUNNER,
|
||
);
|
||
expect(owner?.planConcurrency).toBe(1);
|
||
}
|
||
expect(selectedOwner?.groups).toEqual([
|
||
expect.objectContaining({
|
||
includePatterns: [sdkFixture],
|
||
env: expect.objectContaining({ OPENCLAW_VITEST_MAX_WORKERS: "2" }),
|
||
}),
|
||
]);
|
||
}
|
||
|
||
const stripes = toolingShards.filter((shard) => /^core-tooling-\d+$/u.test(shard.shardName));
|
||
expect(stripes).toHaveLength(16);
|
||
for (const stripe of stripes) {
|
||
expect(stripe.configs).toEqual(["test/vitest/vitest.tooling.config.ts"]);
|
||
expect(stripe.requiresDist).toBe(false);
|
||
expect(stripe.includePatterns?.length ?? 0).toBeGreaterThan(0);
|
||
}
|
||
// Stripes partition the tooling files: no overlap, nothing dropped.
|
||
const stripeFiles = stripes.flatMap((stripe) => stripe.includePatterns ?? []);
|
||
expect(new Set(stripeFiles).size).toBe(stripeFiles.length);
|
||
const processProofFiles = Object.keys(historicalProcessProofSeconds);
|
||
const processProofStripes = processProofFiles.map(
|
||
(file) => stripes.find((stripe) => stripe.includePatterns?.includes(file))?.shardName,
|
||
);
|
||
expect(processProofStripes).not.toContain(undefined);
|
||
expect(
|
||
processProofFiles.map((file) => stripeFiles.filter((entry) => entry === file).length),
|
||
).toEqual(processProofFiles.map(() => 1));
|
||
const runtimeStripe = stripes.find((stripe) =>
|
||
stripe.includePatterns?.includes(
|
||
"test/e2e/qa-lab/runtime/gateway-support-export-runtime.test.ts",
|
||
),
|
||
);
|
||
expect(runtimeStripe?.pretestBuildMode).toBe(
|
||
runtimeStripe?.includePatterns?.includes(PRIVATE_QA_TOOLING_TEST) ? "private-qa" : "runtime",
|
||
);
|
||
expect(
|
||
toolingShards.find((shard) => shard.shardName === "core-tooling-isolated"),
|
||
).toMatchObject({
|
||
configs: [
|
||
"test/vitest/vitest.tooling-docker.config.ts",
|
||
"test/vitest/vitest.tooling-isolated.config.ts",
|
||
],
|
||
requiresDist: false,
|
||
});
|
||
});
|
||
|
||
it("separates the seven process proofs under their historical heavy-file costs", async () => {
|
||
vi.spyOn(testTimings, "readToolingFileTimings").mockReturnValue(historicalProcessProofSeconds);
|
||
const files = Object.keys(historicalProcessProofSeconds);
|
||
const plan = await createToolingFixturePlan({
|
||
files: [
|
||
...files,
|
||
...Array.from({ length: 32 }, (_, index) => `test/scripts/cold-fixture-${index}.test.ts`),
|
||
],
|
||
shards: [
|
||
{
|
||
name: "core-tooling",
|
||
config: "test/vitest/vitest.full-core-tooling.config.ts",
|
||
projects: ["test/vitest/vitest.tooling.config.ts"],
|
||
},
|
||
],
|
||
timings: {},
|
||
fileSeconds: () => 3,
|
||
options: {
|
||
compactMode: "pull-request",
|
||
runnerBackend: "blacksmith",
|
||
includeReleaseOnlyPluginShards: false,
|
||
},
|
||
});
|
||
const groups = plan.flatMap((job) => job.groups);
|
||
const owners = files.map((file) =>
|
||
groups
|
||
.find((group) => group.includePatterns?.includes(file))
|
||
?.shard_name.replace(/-hosted-\d+$/u, ""),
|
||
);
|
||
expect(owners).not.toContain(undefined);
|
||
expect(new Set(owners).size).toBe(files.length);
|
||
});
|
||
|
||
it("keeps non-tooling placement checks independent of singleton tooling split names", () => {
|
||
const anchor: CompactNodeTestShard = {
|
||
checkName: "checks-node-compact-small-1",
|
||
shardName: "compact-small-1",
|
||
runner: BUNDLED_NODE_TEST_RUNNER,
|
||
requiresDist: false,
|
||
planConcurrency: 1,
|
||
groups: [
|
||
{
|
||
shard_name: "core-unit-fast",
|
||
configs: ["test/vitest/vitest.unit-fast.config.ts"],
|
||
requiresDist: false,
|
||
runner: BUNDLED_NODE_TEST_RUNNER,
|
||
},
|
||
],
|
||
};
|
||
// Projection fixture from the real six-file inventory proof: the 330-second
|
||
// file stays alone with the same execution policy when its stripe is named.
|
||
const unsplit: CompactNodeTestShard = {
|
||
...anchor,
|
||
checkName: "checks-node-compact-small-22",
|
||
shardName: "compact-small-22",
|
||
predictedSeconds: 330,
|
||
groups: [
|
||
{
|
||
shard_name: "core-tooling-1",
|
||
configs: ["test/vitest/vitest.tooling.config.ts"],
|
||
includePatterns: ["test/scripts/pr-merge-outcome.test.ts"],
|
||
requiresDist: false,
|
||
runner: BUNDLED_NODE_TEST_RUNNER,
|
||
env: { OPENCLAW_VITEST_MAX_WORKERS: "2" },
|
||
},
|
||
],
|
||
};
|
||
const split: CompactNodeTestShard = {
|
||
...unsplit,
|
||
checkName: "checks-node-compact-small-33",
|
||
shardName: "compact-small-33",
|
||
groups: [{ ...unsplit.groups[0]!, shard_name: "core-tooling-1-hosted-1" }],
|
||
};
|
||
const original = structuredClone({ anchor, unsplit, split });
|
||
const expected = [
|
||
{
|
||
groups: ["core-unit-fast"],
|
||
planConcurrency: 1,
|
||
pretestBuildMode: undefined,
|
||
requiresDist: false,
|
||
runner: BUNDLED_NODE_TEST_RUNNER,
|
||
},
|
||
];
|
||
expect(nonToolingPlacement([anchor, unsplit])).toEqual(expected);
|
||
expect(nonToolingPlacement([anchor, split])).toEqual(expected);
|
||
expect(nonToolingPlacement([split])).not.toEqual(expected);
|
||
for (const changed of [
|
||
{ ...anchor, runner: DEFAULT_NODE_TEST_RUNNER },
|
||
{ ...anchor, planConcurrency: 2 },
|
||
{ ...anchor, requiresDist: true },
|
||
{ ...anchor, pretestBuildMode: "runtime" as const },
|
||
]) {
|
||
expect(nonToolingPlacement([changed, split])).not.toEqual(expected);
|
||
}
|
||
expect({ anchor, unsplit, split }).toEqual(original);
|
||
});
|
||
|
||
it("keeps precise tooling selection through hosted overflow refusal", () => {
|
||
const tooling = defaultShards.filter((shard) => /^core-tooling-\d+$/u.test(shard.shardName));
|
||
const selected = tooling.flatMap((shard) => shard.includePatterns ?? []).slice(0, 96);
|
||
expect(selected).toHaveLength(96);
|
||
vi.spyOn(shardMetadata, "estimateVitestToolingFileSeconds").mockReturnValue(20_000);
|
||
// Every selected file is now indivisible above the admission cap. Overflow
|
||
// must retain these 96 files without adding unrelated dist owners or the full suite.
|
||
expect(() => createSelectedNodeTestShardBundles(selected, { runnerBackend: "github" })).toThrow(
|
||
"exceeds 90 jobs (96 planned)",
|
||
);
|
||
});
|
||
|
||
it("keeps the private runtime prerequisite on precise tooling readers", () => {
|
||
const shards = createSelectedNodeTestShardBundles([PRIVATE_QA_TOOLING_TEST]);
|
||
expect(shards).not.toBeNull();
|
||
const readers = shards?.filter((shard) => !shard.requiresDist) ?? [];
|
||
expect(readers).toHaveLength(1);
|
||
expect(readers[0]?.pretestBuildMode).toBe("private-qa");
|
||
expect(readers[0]?.planConcurrency).toBe(1);
|
||
expect(readers[0]?.groups.flatMap((group) => group.includePatterns ?? [])).toEqual([
|
||
PRIVATE_QA_TOOLING_TEST,
|
||
]);
|
||
});
|
||
|
||
it.each(["blacksmith", "github", "hybrid"])(
|
||
"runs large workspace inventory without sibling files in %s plans",
|
||
(runnerBackend) => {
|
||
const inventory = "src/gateway/worker-environments/workspace-large-inventory.test.ts";
|
||
const owners = defaultShards.filter((shard) => shard.includePatterns?.includes(inventory));
|
||
expect(owners).toHaveLength(1);
|
||
expect(owners[0]!.includePatterns).toEqual([inventory]);
|
||
|
||
for (const plan of [
|
||
getCommittedCompactPlan("pull-request", runnerBackend),
|
||
getCommittedCompactPlan("push", runnerBackend),
|
||
]) {
|
||
const jobs = plan.filter((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(inventory)),
|
||
);
|
||
expect(jobs).toHaveLength(1);
|
||
expect(jobs[0]!.planConcurrency).toBe(1);
|
||
const groups = jobs[0]!.groups.filter((group) =>
|
||
group.includePatterns?.includes(inventory),
|
||
);
|
||
expect(groups).toHaveLength(1);
|
||
expect(groups[0]!.includePatterns).toEqual([inventory]);
|
||
expect(groups[0]!.configs).toEqual(["test/vitest/vitest.gateway-core.config.ts"]);
|
||
}
|
||
},
|
||
);
|
||
|
||
it.each([
|
||
{ name: "Blacksmith", runnerBackend: undefined, measured: true },
|
||
{ name: "hybrid", runnerBackend: "hybrid", measured: true },
|
||
{ name: "GitHub-hosted", runnerBackend: "github", measured: false },
|
||
])(
|
||
"retains complete CLI coverage and worker policies on $name",
|
||
({ runnerBackend, measured }) => {
|
||
pinPlannerHost(plannerHosts[0]);
|
||
for (const compactMode of ["push", "pull-request"] as const) {
|
||
const plan = getCommittedCompactPlan(compactMode, runnerBackend);
|
||
const groups = plan.flatMap((job) => job.groups);
|
||
const cliGroups = groups.filter((group) =>
|
||
/^agentic-cli(?:-hosted-\d+)?$/u.test(group.shard_name),
|
||
);
|
||
expect(cliGroups.length).toBeGreaterThan(0);
|
||
if (measured) {
|
||
expect(cliGroups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
listMatchedTestFiles(createCliVitestConfig({}))
|
||
.filter((file) => compactMode === "push" || !isCiProofTestFile(file))
|
||
.toSorted(),
|
||
);
|
||
} else {
|
||
expect(cliGroups).toHaveLength(1);
|
||
expect(cliGroups[0]?.includePatterns).toBeUndefined();
|
||
expect(cliGroups[0]?.timing_key).toBeUndefined();
|
||
}
|
||
for (const cli of cliGroups) {
|
||
expect(cli.configs).toEqual(["test/vitest/vitest.cli.config.ts"]);
|
||
if (measured) {
|
||
expect(cli.includePatterns?.length).toBeGreaterThan(0);
|
||
expect(cli.timing_key).toBeDefined();
|
||
}
|
||
expect(cli.env).toEqual(measured ? undefined : { OPENCLAW_VITEST_MAX_WORKERS: "2" });
|
||
expect(cli.fallbackMaxWorkers).toBe(measured ? 2 : undefined);
|
||
expect(isExclusiveCompactShardName(cli.shard_name)).toBe(true);
|
||
const job = expectDefined(
|
||
plan.find((entry) => entry.groups.includes(cli)),
|
||
"CLI file partition job",
|
||
);
|
||
expect(job.planConcurrency).toBe(1);
|
||
expect(job.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
if (measured) {
|
||
expect(job.runner).toBe(EXTRA_LARGE_NODE_TEST_RUNNER);
|
||
}
|
||
}
|
||
const processGroups = groups.filter((group) =>
|
||
group.configs.includes("test/vitest/vitest.cli-process.config.ts"),
|
||
);
|
||
expect(processGroups.length).toBeGreaterThan(0);
|
||
for (const group of processGroups) {
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe("2");
|
||
expect(group.fallbackMaxWorkers).toBeUndefined();
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it.each(
|
||
["blacksmith", "github", "hybrid"].flatMap((runnerBackend) =>
|
||
["agentic-gateway-core-2", "agentic-cli"].map((owner) => ({ runnerBackend, owner })),
|
||
),
|
||
)(
|
||
"retains $owner worker fallback in precise $runnerBackend plans",
|
||
({ runnerBackend, owner }) => {
|
||
const shard = expectDefined(
|
||
defaultShards.find((entry) => entry.shardName === owner),
|
||
`${owner} owner`,
|
||
);
|
||
const target =
|
||
owner === "agentic-cli"
|
||
? "src/cli/nodes-cli.coverage.test.ts"
|
||
: expectDefined(
|
||
shard.includePatterns?.find((file) =>
|
||
file.startsWith("packages/gateway-client/src/"),
|
||
),
|
||
"precisely routed core-2 client target",
|
||
);
|
||
const plan = expectDefined(
|
||
createSelectedNodeTestShardBundles([target], { runnerBackend }),
|
||
`precise ${owner} plan`,
|
||
);
|
||
const groups = plan.flatMap((job) => job.groups);
|
||
expect(groups).toHaveLength(1);
|
||
expect(groups[0]!.configs).toEqual([
|
||
owner === "agentic-cli"
|
||
? "test/vitest/vitest.cli.config.ts"
|
||
: "test/vitest/vitest.gateway-client.config.ts",
|
||
]);
|
||
if (owner === "agentic-cli") {
|
||
expect(groups[0]!.shard_name).toMatch(/^agentic-cli(?:-hosted-\d+)?$/u);
|
||
}
|
||
expect(groups[0]!.includePatterns).toEqual([target]);
|
||
expect(groups[0]!.fallbackMaxWorkers).toBe(runnerBackend === "github" ? undefined : 2);
|
||
expect(groups[0]!.env).toEqual(
|
||
runnerBackend === "github" ? { OPENCLAW_VITEST_MAX_WORKERS: "2" } : undefined,
|
||
);
|
||
expect(plan.map((job) => job.planConcurrency)).toEqual([1]);
|
||
if (owner === "agentic-cli" && runnerBackend !== "github") {
|
||
expect(plan.map((job) => job.runner)).toEqual([EXTRA_LARGE_NODE_TEST_RUNNER]);
|
||
}
|
||
if (owner === "agentic-gateway-core-2") {
|
||
const changed = expectDefined(
|
||
createChangedNodeTestShards([target], { runnerBackend }),
|
||
"changed Gateway client plan",
|
||
);
|
||
expect(changed.flatMap((job) => job.targets ?? [])).not.toContain(target);
|
||
const changedOwner = expectDefined(
|
||
changed.find((job) =>
|
||
job.groups?.some((group) => group.includePatterns?.includes(target)),
|
||
),
|
||
"changed Gateway client process owner",
|
||
);
|
||
expect(changedOwner.groups).toEqual(groups);
|
||
expect(changedOwner.planConcurrency).toBe(1);
|
||
}
|
||
},
|
||
);
|
||
|
||
it.each(["blacksmith", "github", "hybrid"])(
|
||
"shares one prepared runtime across affordable %s tooling groups",
|
||
(runnerBackend) => {
|
||
const targets = [
|
||
PRIVATE_QA_TOOLING_TEST,
|
||
"test/e2e/qa-lab/runtime/gateway-support-export-runtime.test.ts",
|
||
];
|
||
vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue(
|
||
Object.fromEntries(defaultShards.map((shard) => [shard.shardName, 1])),
|
||
);
|
||
const plan = createSelectedNodeTestShardBundles(targets, { runnerBackend });
|
||
expect(plan).not.toBeNull();
|
||
const readers = plan!.filter((shard) => !shard.requiresDist);
|
||
expect(readers).toHaveLength(1);
|
||
expect(readers[0]).toMatchObject({ pretestBuildMode: "private-qa", planConcurrency: 1 });
|
||
expect(readers[0]!.predictedSeconds).toBeLessThanOrEqual(
|
||
runnerBackend === "github" ? 210 : 150,
|
||
);
|
||
expect(readers[0]!.groups.every((group) => group.pretestBuildMode)).toBe(true);
|
||
expect(readers[0]!.groups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
targets.toSorted(),
|
||
);
|
||
},
|
||
);
|
||
|
||
it("allocates sparse selections without reserving their full-suite rows", async () => {
|
||
const heavyCli = "src/cli/gateway-backed-exit-health.process.test.ts";
|
||
const targets = [
|
||
"src/config/allowed-values.test.ts",
|
||
"src/commands/doctor-heartbeat-cadence-migration.test.ts",
|
||
"src/infra/heartbeat-runner.ack-token-heartbeat-acks.test.ts",
|
||
"src/infra/runtime-guard.test.ts",
|
||
heavyCli,
|
||
"src/cli/directory-cli.test.ts",
|
||
"src/config/config.backup-rotation.test.ts",
|
||
"src/config/commands.test.ts",
|
||
];
|
||
const configs = {
|
||
infra: "test/vitest/vitest.infra.config.ts",
|
||
config: "test/vitest/vitest.runtime-config.config.ts",
|
||
commands: "test/vitest/vitest.commands.config.ts",
|
||
cli: "test/vitest/vitest.cli-process.config.ts",
|
||
};
|
||
const fillers = (prefix: string, count = 9) =>
|
||
Array.from({ length: count }, (_, index) => `${prefix}.fixture-${index}.test.ts`);
|
||
const configFiles = [
|
||
...targets.filter((file) => file.startsWith("src/config/")),
|
||
...fillers("src/config/fixture", 15),
|
||
];
|
||
const cliFiles = [heavyCli, "src/cli/directory-cli.test.ts", ...fillers("src/cli/fixture", 8)];
|
||
const infraFiles = [
|
||
...targets.filter((file) => file.startsWith("src/infra/")),
|
||
...fillers("src/infra/heartbeat-runner"),
|
||
...fillers("src/infra/runtime-guard"),
|
||
...fillers("src/infra/os-summary", 10),
|
||
...fillers("src/infra/provider-usage", 10),
|
||
...fillers("src/infra/channel-runtime-context", 10),
|
||
...fillers("src/infra/diagnostic-trace-context", 10),
|
||
];
|
||
const commandFiles = [targets[1]!, ...fillers("src/commands/doctor-heartbeat-cadence")];
|
||
const inventory = [...configFiles, ...cliFiles, ...infraFiles, ...commandFiles];
|
||
const timings: Record<string, number> = {
|
||
"core-runtime-config": 240,
|
||
"core-runtime-infra-heartbeat-runner": 170,
|
||
"core-runtime-infra-system-runtime": 160,
|
||
"core-runtime-infra-misc-os": 170,
|
||
"core-runtime-infra-provider-push": 160,
|
||
"core-runtime-infra-channel-plugin": 150,
|
||
"core-runtime-infra-diagnostics-state": 150,
|
||
"agentic-cli-process": 240,
|
||
"agentic-commands-doctor-sessions-cron": 120,
|
||
};
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [
|
||
{ name: "core-runtime", config: configs.infra, projects: [configs.infra, configs.config] },
|
||
{ name: "agentic", config: configs.commands, projects: [configs.commands, configs.cli] },
|
||
],
|
||
}));
|
||
vi.doMock("../vitest/vitest.database-worker-core-paths.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.database-worker-core-paths.mjs")>()),
|
||
databaseWorkerCoreTestFiles: [],
|
||
isDatabaseWorkerCoreTestFile: () => false,
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (root: string) =>
|
||
inventory.filter((file) => file.startsWith(`${root}/`)),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-node-test-inventory.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/ci-node-test-inventory.mts")>()),
|
||
listWholeConfigFiles: (owner: string) =>
|
||
owner === "core-runtime-config"
|
||
? configFiles
|
||
: owner === "agentic-cli-process"
|
||
? cliFiles
|
||
: undefined,
|
||
listWholeConfigSplitFiles: (owner: string) =>
|
||
owner === "core-runtime-config"
|
||
? configFiles
|
||
: owner === "agentic-cli-process"
|
||
? cliFiles
|
||
: undefined,
|
||
listNodeTestConfigFiles: (config: string) =>
|
||
({
|
||
[configs.infra]: infraFiles,
|
||
[configs.config]: configFiles,
|
||
[configs.commands]: commandFiles,
|
||
[configs.cli]: cliFiles,
|
||
})[config],
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => timings,
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
try {
|
||
const {
|
||
createNodeTestShardBundles: createBundles,
|
||
createSelectedNodeTestShardBundles: createSelected,
|
||
} = await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const options = {
|
||
runnerBackend: "hybrid",
|
||
includeReleaseOnlyRuntimeTests: false,
|
||
} satisfies NonNullable<Parameters<typeof createSelected>[1]>;
|
||
let full = createBundles({
|
||
...options,
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "pull-request",
|
||
});
|
||
const heavyGroup = expectDefined(
|
||
full
|
||
.flatMap((job) => job.groups)
|
||
.find((group) => group.includePatterns?.includes(heavyCli)),
|
||
"indivisible CLI timing fixture",
|
||
);
|
||
timings[heavyGroup.timing_key!] = 200;
|
||
full = createBundles({
|
||
...options,
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactMode: "pull-request",
|
||
});
|
||
const owners = full.filter((job) =>
|
||
job.groups.some((group) => group.includePatterns?.some((file) => targets.includes(file))),
|
||
);
|
||
expect(owners.length).toBeGreaterThan(1);
|
||
const selected = createSelected(targets, options)!;
|
||
expect(selected).not.toBeNull();
|
||
expect(selected.length).toBeLessThan(owners.length);
|
||
expect(
|
||
selected
|
||
.flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
).toEqual(targets.toSorted());
|
||
expect(
|
||
selected.every((job) => job.groups.every((group) => group.includePatterns!.length > 0)),
|
||
).toBe(true);
|
||
expect(selected.reduce((sum, job) => sum + job.predictedSeconds!, 0)).toBeLessThan(
|
||
owners.reduce((sum, job) => sum + job.predictedSeconds!, 0),
|
||
);
|
||
const heavyJob = selected.find((job) =>
|
||
job.groups.some((group) => group.includePatterns?.includes(heavyCli)),
|
||
)!;
|
||
expect(heavyJob.predictedSeconds).toBeGreaterThan(150);
|
||
expect(heavyJob.groups.flatMap((group) => group.includePatterns ?? [])).toEqual([heavyCli]);
|
||
for (const job of selected) {
|
||
expect(job.predictedTestSeconds).toBeGreaterThan(0);
|
||
expect(job.predictedTestSeconds).toBeLessThanOrEqual(job.predictedSeconds!);
|
||
if (job.planConcurrency === 2) {
|
||
expect(job.predictedTestSeconds).toBeLessThanOrEqual(300);
|
||
expect(
|
||
job.groups.filter((group) => group.shard_name.startsWith("core-runtime-config-hosted-"))
|
||
.length,
|
||
).toBeLessThanOrEqual(1);
|
||
}
|
||
for (const group of job.groups) {
|
||
const original = full.find((owner) =>
|
||
owner.groups.some((entry) => entry.shard_name === group.shard_name),
|
||
)!;
|
||
expect(job).toMatchObject({
|
||
runner: original.runner,
|
||
planConcurrency: original.planConcurrency,
|
||
requiresDist: original.requiresDist,
|
||
});
|
||
expect(job.pretestBuildMode).toBe(original.pretestBuildMode);
|
||
expect(job.timeoutMinutes).toBe(original.timeoutMinutes);
|
||
expect(job.env).toEqual(original.env);
|
||
const owner = original.groups.find((entry) => entry.shard_name === group.shard_name)!;
|
||
expect(group.env).toEqual(owner.env);
|
||
expect(group.fallbackMaxWorkers).toBe(owner.fallbackMaxWorkers);
|
||
expect(group.minTotalMemoryBytes).toBe(owner.minTotalMemoryBytes);
|
||
}
|
||
}
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/ci-node-test-inventory.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.database-worker-core-paths.mjs");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.resetModules();
|
||
}
|
||
});
|
||
|
||
it("retains the ordered wall budget when repacking selected files", async () => {
|
||
const config = "test/vitest/vitest.infra.config.ts";
|
||
const entries = [
|
||
["src/infra/env.test.ts", "core-runtime-infra-env-auth", 170],
|
||
["src/infra/os-summary.test.ts", "core-runtime-infra-misc-os", 170],
|
||
["src/infra/ports-probe.test.ts", "core-runtime-infra-system-runtime", 160],
|
||
["src/infra/provider-usage.test.ts", "core-runtime-infra-provider-push", 160],
|
||
["src/infra/channel-runtime-context.test.ts", "core-runtime-infra-channel-plugin", 150],
|
||
["src/infra/diagnostic-trace-context.test.ts", "core-runtime-infra-diagnostics-state", 150],
|
||
] as const;
|
||
const files = entries.map(([file]) => file);
|
||
const targets = files.slice(0, 3);
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: [{ name: "core-runtime", config, projects: [config] }],
|
||
}));
|
||
vi.doMock("../vitest/vitest.database-worker-core-paths.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.database-worker-core-paths.mjs")>()),
|
||
databaseWorkerCoreTestFiles: [],
|
||
isDatabaseWorkerCoreTestFile: () => false,
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (root: string) => (root === "src/infra" ? files : []),
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () =>
|
||
Object.fromEntries(entries.map(([, owner, seconds]) => [owner, seconds])),
|
||
readRuntimePlacementTimings: () => [],
|
||
}));
|
||
vi.doMock("../../scripts/lib/vitest-build-prerequisites.mts", async (importOriginal) => ({
|
||
...(await importOriginal<
|
||
typeof import("../../scripts/lib/vitest-build-prerequisites.mts")
|
||
>()),
|
||
resolveVitestPretestBuildMode: () => undefined,
|
||
}));
|
||
try {
|
||
const { createSelectedNodeTestShardBundles: createSelected } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
const selected = expectDefined(
|
||
createSelected(targets, { runnerBackend: "blacksmith" }),
|
||
"selected infra plan",
|
||
);
|
||
// The selected 170/170/160 envelopes fit the aggregate limit but need 330 seconds together.
|
||
expect(selected).toHaveLength(2);
|
||
expect(selected.every((job) => job.planConcurrency === 2)).toBe(true);
|
||
expect(selected.every((job) => job.predictedTestSeconds! <= 300)).toBe(true);
|
||
expect(selected.map((job) => job.predictedTestSeconds!).toSorted((a, b) => a - b)).toEqual([
|
||
160, 170,
|
||
]);
|
||
expect(selected.reduce((sum, job) => sum + job.predictedSeconds!, 0)).toBe(500);
|
||
expect(
|
||
selected.flatMap((job) => job.groups.flatMap((group) => group.includePatterns!)).toSorted(),
|
||
).toEqual(targets.toSorted());
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/vitest-build-prerequisites.mts");
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../vitest/vitest.database-worker-core-paths.mjs");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.resetModules();
|
||
}
|
||
});
|
||
|
||
it("packs serial groups within both the time and group-count budgets", () => {
|
||
const groups = [34, 33, 33, 32, 32, 32, 10, 21, 17, 17, 15, 10, 8, 7, 7, 7, 6, 5, 5];
|
||
const bins = packNodeTestGroups(
|
||
groups,
|
||
(bin, group) => bin.length < 10 && bin.reduce((sum, value) => sum + value, group) <= 210,
|
||
true,
|
||
);
|
||
expect(bins).toHaveLength(2);
|
||
expect(bins.flat().toSorted((a, b) => a - b)).toEqual(groups.toSorted((a, b) => a - b));
|
||
for (const bin of bins) {
|
||
expect(bin.length).toBeLessThanOrEqual(10);
|
||
expect(bin.reduce((sum, value) => sum + value, 0)).toBeLessThanOrEqual(210);
|
||
}
|
||
});
|
||
|
||
it.each(["runner", "dist", "preparation"] as const)(
|
||
"keeps group exchanges within the owner's %s boundary",
|
||
(boundary) => {
|
||
const groups = [144, 120, 72, 48, 48, 48].map((seconds, index) => ({
|
||
index,
|
||
seconds,
|
||
runner: boundary === "runner" && index === 2 ? "other" : "shared",
|
||
requiresDist: boundary === "dist" && index === 2,
|
||
preparation: boundary === "preparation" && index === 2,
|
||
}));
|
||
const bins = packNodeTestGroups(
|
||
groups,
|
||
(bin, group) =>
|
||
!group.preparation &&
|
||
bin.every(
|
||
(entry) =>
|
||
!entry.preparation &&
|
||
entry.runner === group.runner &&
|
||
entry.requiresDist === group.requiresDist,
|
||
) &&
|
||
bin.reduce((sum, entry) => sum + entry.seconds, group.seconds) <= 240,
|
||
true,
|
||
);
|
||
expect(bins).toHaveLength(3);
|
||
expect(bins.flat().toSorted((a, b) => a.index - b.index)).toEqual(groups);
|
||
expect(bins.find((bin) => bin.some((group) => group.index === 2))).toEqual([groups[2]]);
|
||
for (const bin of bins) {
|
||
expect(bin.reduce((sum, group) => sum + group.seconds, 0)).toBeLessThanOrEqual(240);
|
||
}
|
||
},
|
||
);
|
||
|
||
async function createToolingFixturePlan(params: {
|
||
files: string[];
|
||
shards: typeof fullSuiteVitestShards;
|
||
timings: Record<string, number>;
|
||
fileSeconds: (file: string) => number;
|
||
options: {
|
||
compactMode: "push" | "pull-request";
|
||
runnerBackend: string;
|
||
includeReleaseOnlyPluginShards: false;
|
||
compactNodeJobCap?: number;
|
||
};
|
||
}) {
|
||
const unitFastPaths = await vi.importActual<
|
||
typeof import("../vitest/vitest.unit-fast-paths.mjs")
|
||
>("../vitest/vitest.unit-fast-paths.mjs");
|
||
vi.resetModules();
|
||
vi.doMock("../vitest/vitest.unit-fast-paths.mjs", () => ({
|
||
...unitFastPaths,
|
||
getUnitFastTestFiles: () => [],
|
||
getUnitFastIsolatedTestFiles: () => [],
|
||
getUnitFastTimerTestFiles: () => [],
|
||
getUnitFastTestFilesForIncludePatterns: () => [],
|
||
}));
|
||
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
|
||
fullSuiteVitestShards: params.shards,
|
||
}));
|
||
vi.doMock("../../scripts/lib/ci-test-timings.mts", () => ({
|
||
...testTimings,
|
||
readCompactGroupTimings: () => params.timings,
|
||
}));
|
||
vi.doMock("../../scripts/lib/vitest-shard-metadata.mts", () => ({
|
||
...shardMetadata,
|
||
estimateVitestToolingFileSeconds: params.fileSeconds,
|
||
}));
|
||
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
|
||
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
|
||
listTrackedTestFiles: (rootDir: string) =>
|
||
rootDir === "test" ? params.files.toSorted() : [],
|
||
}));
|
||
try {
|
||
const { createNodeTestShardBundles: createPlan } =
|
||
await import("../../scripts/lib/ci-node-test-plan.mts");
|
||
return createPlan(params.options);
|
||
} finally {
|
||
vi.doUnmock("../../scripts/lib/list-test-files.mts");
|
||
vi.doUnmock("../../scripts/lib/ci-test-timings.mts");
|
||
vi.doUnmock("../../scripts/lib/vitest-shard-metadata.mts");
|
||
vi.doUnmock("../vitest/vitest.test-shards.mjs");
|
||
vi.doUnmock("../vitest/vitest.unit-fast-paths.mjs");
|
||
vi.resetModules();
|
||
}
|
||
}
|
||
|
||
it.each(["push", "pull-request"] as const)(
|
||
"exchanges hosted anchors before opening another %s job",
|
||
async (compactMode) => {
|
||
const anchors = [126, 105, 63, 42, 42, 42].map((seconds, index) => ({
|
||
name: `exchange-anchor-${index}`,
|
||
seconds,
|
||
}));
|
||
const shards = anchors.map(({ name }) => ({
|
||
config: `test/vitest/vitest.${name}.config.ts`,
|
||
name,
|
||
projects: [`test/vitest/vitest.${name}.config.ts`],
|
||
}));
|
||
const plan = await createToolingFixturePlan({
|
||
files: [],
|
||
shards,
|
||
timings: Object.fromEntries(anchors.map(({ name, seconds }) => [name, seconds])),
|
||
fileSeconds: () => 0,
|
||
options: {
|
||
compactMode,
|
||
runnerBackend: "github",
|
||
includeReleaseOnlyPluginShards: false,
|
||
compactNodeJobCap: 2,
|
||
},
|
||
});
|
||
expect(plan).toHaveLength(2);
|
||
expect(
|
||
plan
|
||
.flatMap((job) => job.groups)
|
||
.toSorted((a, b) => a.shard_name.localeCompare(b.shard_name)),
|
||
).toEqual(
|
||
shards.map(({ name, projects }) => ({
|
||
shard_name: name,
|
||
configs: projects,
|
||
requiresDist: false,
|
||
runner: BUNDLED_NODE_TEST_RUNNER,
|
||
})),
|
||
);
|
||
for (const job of plan) {
|
||
expect(job.predictedSeconds).toBe(210);
|
||
expect(job.planConcurrency).toBe(1);
|
||
expect(job.groups.length).toBeLessThanOrEqual(10);
|
||
}
|
||
},
|
||
);
|
||
|
||
it.each([
|
||
{ profile: "blacksmith", expectedSeconds: 840, whaleSeconds: 200 },
|
||
{ profile: "hybrid", expectedSeconds: 840, whaleSeconds: 200 },
|
||
{ profile: "github", expectedSeconds: 1_344, whaleSeconds: 320 },
|
||
])(
|
||
"prices parallel tooling files without dividing the slowest file in $profile",
|
||
async ({ profile, expectedSeconds, whaleSeconds }) => {
|
||
const whale = "test/scripts/fixture-whale.test.ts";
|
||
const files = [
|
||
...Array.from({ length: 64 }, (_, index) => `test/scripts/fixture-${index}.test.ts`),
|
||
whale,
|
||
];
|
||
const params = {
|
||
files,
|
||
shards: [
|
||
{
|
||
config: "test/vitest/vitest.full-core-tooling.config.ts",
|
||
name: "core-tooling",
|
||
projects: ["test/vitest/vitest.tooling.config.ts"],
|
||
},
|
||
],
|
||
timings: Object.fromEntries(
|
||
Array.from({ length: 16 }, (_, index) => [`core-tooling-${index + 1}`, 20_000]),
|
||
),
|
||
fileSeconds: (file: string) => (file === whale ? 200 : 20),
|
||
options: {
|
||
compactMode: "pull-request" as const,
|
||
runnerBackend: profile,
|
||
includeReleaseOnlyPluginShards: false as const,
|
||
},
|
||
};
|
||
const plan = await createToolingFixturePlan(params);
|
||
expect(
|
||
plan
|
||
.flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
).toEqual(files.toSorted());
|
||
expect(
|
||
plan.every(
|
||
(job) =>
|
||
job.planConcurrency === 1 &&
|
||
job.groups.every((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === "2"),
|
||
),
|
||
).toBe(true);
|
||
// Setup is separate from the sum of the test-step packing prices.
|
||
expect(plan.reduce((seconds, job) => seconds + job.predictedSeconds!, 0)).toBe(
|
||
expectedSeconds,
|
||
);
|
||
expect(
|
||
plan.find((job) => job.groups.some((group) => group.includePatterns?.includes(whale)))
|
||
?.predictedSeconds,
|
||
).toBeGreaterThanOrEqual(whaleSeconds);
|
||
const whaleOnly = await createToolingFixturePlan({ ...params, files: [whale] });
|
||
expect(
|
||
whaleOnly
|
||
.filter((job) => job.groups.some((group) => group.includePatterns?.includes(whale)))
|
||
.map((job) => job.predictedSeconds),
|
||
).toEqual([whaleSeconds]);
|
||
expect(whaleOnly.reduce((seconds, job) => seconds + job.predictedSeconds!, 0)).toBe(
|
||
whaleSeconds,
|
||
);
|
||
for (const group of plan.flatMap((job) => job.groups)) {
|
||
if (group.timing_key) {
|
||
params.timings[group.timing_key] = 20_000;
|
||
}
|
||
}
|
||
expect(await createToolingFixturePlan(params)).toEqual(plan);
|
||
},
|
||
);
|
||
|
||
it("shares a small hosted tooling tail without lowering its runner owner", async () => {
|
||
const compiler = "test/scripts/write-unified-entry-dts.test.ts";
|
||
const shortFiles = ["test/scripts/fixture-short.test.ts"];
|
||
const fileSeconds = new Map<string, number>([
|
||
...Array.from(
|
||
{ length: 32 },
|
||
(_, index) => [`test/scripts/fixture-long-${index}.test.ts`, 300] as const,
|
||
),
|
||
[compiler, 74],
|
||
[shortFiles[0]!, 10],
|
||
]);
|
||
const files = [...fileSeconds.keys()];
|
||
const plan = await createToolingFixturePlan({
|
||
files,
|
||
shards: [
|
||
{
|
||
config: "test/vitest/vitest.full-core-tooling.config.ts",
|
||
name: "core-tooling",
|
||
projects: ["test/vitest/vitest.tooling.config.ts"],
|
||
},
|
||
],
|
||
timings: {},
|
||
fileSeconds: (file) => fileSeconds.get(file)!,
|
||
options: {
|
||
compactMode: "pull-request",
|
||
runnerBackend: "github",
|
||
includeReleaseOnlyPluginShards: false,
|
||
},
|
||
});
|
||
// The compiler child costs 118.4s and the separate 10s file costs 16s on hosted.
|
||
// Their 135s job must retain the compiler's stronger runner and serial children.
|
||
const shared = plan.filter((job) => job.groups.some((group) => group.runner !== job.runner));
|
||
expect(shared).toHaveLength(1);
|
||
const job = shared[0]!;
|
||
expect(job).toMatchObject({
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
predictedSeconds: 135,
|
||
requiresDist: false,
|
||
planConcurrency: 1,
|
||
});
|
||
expect(job.pretestBuildMode).toBeUndefined();
|
||
expect(job.groups.map((group) => group.runner)).toEqual([
|
||
DEFAULT_NODE_TEST_RUNNER,
|
||
BUNDLED_NODE_TEST_RUNNER,
|
||
]);
|
||
expect(
|
||
job.groups.every(
|
||
(group) =>
|
||
group.pretestBuildMode === undefined &&
|
||
/^core-tooling-\d+-hosted-\d+$/u.test(group.shard_name),
|
||
),
|
||
).toBe(true);
|
||
expect(
|
||
new Set(job.groups.map((group) => group.shard_name.replace(/-hosted-\d+$/u, ""))).size,
|
||
).toBe(2);
|
||
expect(job.groups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
[compiler, ...shortFiles].toSorted(),
|
||
);
|
||
expect(
|
||
plan
|
||
.flatMap((entry) => entry.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
).toEqual(files.toSorted());
|
||
});
|
||
|
||
it("keeps hosted tooling within the GitHub job cap when its inventory grows", async () => {
|
||
// Seventy-eight full-budget anchors leave 12 of the 90 jobs for tooling.
|
||
const anchors = Array.from({ length: 78 }, (_, index) => ({
|
||
config: `test/vitest/vitest.capacity-anchor-${index}.config.ts`,
|
||
name: `capacity-anchor-${index}`,
|
||
projects: [`test/vitest/vitest.capacity-anchor-${index}.config.ts`],
|
||
}));
|
||
const fixtureShards = [
|
||
...anchors,
|
||
{
|
||
config: "test/vitest/vitest.full-core-tooling.config.ts",
|
||
name: "core-tooling",
|
||
projects: ["test/vitest/vitest.tooling.config.ts"],
|
||
},
|
||
];
|
||
// Ten files per tooling family exercise full stripes and packable tails; the compiler
|
||
// fixture puts a stronger runner in a tail that can absorb smaller families.
|
||
const fixtureFiles = [
|
||
...Array.from(
|
||
{ length: 160 },
|
||
(_, index) => `test/scripts/fixture-${String(index).padStart(3, "0")}.test.ts`,
|
||
),
|
||
"test/scripts/write-unified-entry-dts.test.ts",
|
||
];
|
||
const fixtureTimings = Object.fromEntries([
|
||
...anchors.map(({ name }): [string, number] => [name, 210]),
|
||
...Array.from({ length: 16 }, (_, index): [string, number] => [
|
||
`core-tooling-${index + 1}`,
|
||
200,
|
||
]),
|
||
]);
|
||
const options = {
|
||
compactMode: "pull-request",
|
||
runnerBackend: "github",
|
||
includeReleaseOnlyPluginShards: false,
|
||
} as const;
|
||
const inventoryGrowthFile = "test/scripts/resolve-fs-safe-native-contract.test.ts";
|
||
const extraInventories = [
|
||
["test/scripts/npm-package-locks-report.test.ts"],
|
||
Array.from({ length: 10 }, (_, index) => `test/scripts/zz-growth-probe-${index}.test.ts`),
|
||
["test/scripts/openclaw-performance-crabbox.test.ts"],
|
||
["test/scripts/install-smoke-ref-admission.test.ts"],
|
||
[
|
||
"test/scripts/npm-package-locks-report.test.ts",
|
||
"test/scripts/openclaw-performance-crabbox.test.ts",
|
||
"test/scripts/install-smoke-ref-admission.test.ts",
|
||
],
|
||
];
|
||
const isHostedToolingGroup = (group: { shard_name: string }) =>
|
||
/^core-tooling-\d+-hosted-\d+$/u.test(group.shard_name);
|
||
const runnerRanks = new Map([
|
||
[BUNDLED_NODE_TEST_RUNNER, 0],
|
||
[DEFAULT_NODE_TEST_RUNNER, 1],
|
||
[EXTRA_LARGE_NODE_TEST_RUNNER, 2],
|
||
]);
|
||
const measuredFixtureFiles = new Set(fixtureFiles);
|
||
const shorterFixtureFiles = new Set(fixtureFiles.slice(0, 64));
|
||
const createPlanWithInventory = async (
|
||
includeGrowthFile: boolean,
|
||
extraFiles: string[] = [],
|
||
compactNodeJobCap?: number,
|
||
shortFileSeconds = 23,
|
||
) => {
|
||
return createToolingFixturePlan({
|
||
files: [
|
||
...fixtureFiles,
|
||
...extraFiles,
|
||
...(includeGrowthFile ? [inventoryGrowthFile] : []),
|
||
],
|
||
shards: fixtureShards,
|
||
timings: fixtureTimings,
|
||
// Mixed costs keep the 300s tooling budget under the same 90-row pressure.
|
||
// One tail donation still makes the tighter cap feasible.
|
||
// New files retain the cold fallback.
|
||
fileSeconds: (file) =>
|
||
file === "test/scripts/write-unified-entry-dts.test.ts"
|
||
? 74
|
||
: shorterFixtureFiles.has(file)
|
||
? shortFileSeconds
|
||
: measuredFixtureFiles.has(file)
|
||
? 25
|
||
: shardMetadata.estimateVitestToolingFileSeconds(file),
|
||
options: { ...options, compactNodeJobCap },
|
||
});
|
||
};
|
||
const baseline = await createPlanWithInventory(false);
|
||
const baselineToolingFiles = baseline
|
||
.flatMap((job) => job.groups)
|
||
.filter(isNumberedToolingGroup)
|
||
.flatMap((group) => group.includePatterns ?? []);
|
||
expect(baseline).toHaveLength(90);
|
||
expect(baselineToolingFiles.toSorted()).toEqual(fixtureFiles.toSorted());
|
||
const grown = await createPlanWithInventory(true);
|
||
const toolingGroups = grown.flatMap((job) => job.groups).filter(isNumberedToolingGroup);
|
||
const toolingFiles = toolingGroups.flatMap((group) => group.includePatterns ?? []);
|
||
|
||
expect(grown.length).toBeLessThanOrEqual(90);
|
||
expect(new Set(toolingFiles).size).toBe(toolingFiles.length);
|
||
expect(toolingFiles.toSorted()).toEqual(
|
||
[...baselineToolingFiles, inventoryGrowthFile].toSorted(),
|
||
);
|
||
expect(nonToolingPlacement(grown)).toEqual(nonToolingPlacement(baseline));
|
||
|
||
const budgeted = await createPlanWithInventory(true, [], 89);
|
||
expect(budgeted.filter((job) => !job.requiresDist).length).toBeLessThanOrEqual(89);
|
||
expect(nonToolingPlacement(budgeted)).toEqual(nonToolingPlacement(grown));
|
||
expect(
|
||
budgeted
|
||
.flatMap((job) => job.groups)
|
||
.filter(isNumberedToolingGroup)
|
||
.flatMap((group) => group.includePatterns ?? [])
|
||
.toSorted(),
|
||
).toEqual(toolingFiles.toSorted());
|
||
|
||
// Eleven 300s tooling rows cannot hold 160 ordinary 25s files plus the
|
||
// compiler at the hosted worker price. Preserve that infeasible-case refusal.
|
||
await expect(createPlanWithInventory(true, [], 89, 25)).rejects.toThrow(
|
||
"compact github node test plan exceeds 89 jobs",
|
||
);
|
||
|
||
for (const job of grown) {
|
||
const hostedToolingGroups = job.groups.filter(isHostedToolingGroup);
|
||
if (hostedToolingGroups.length === 0) {
|
||
continue;
|
||
}
|
||
const families = hostedToolingGroups.map((group) =>
|
||
group.shard_name.replace(/-hosted-\d+$/u, ""),
|
||
);
|
||
expect(new Set(families).size).toBe(families.length);
|
||
expect(job.requiresDist).toBe(false);
|
||
expect(job.planConcurrency).toBe(1);
|
||
expect(job.groups.length).toBeLessThanOrEqual(10);
|
||
if (job.groups.length > 1) {
|
||
expect(job.predictedSeconds).toBeLessThanOrEqual(
|
||
job.pretestBuildMode ? 210 : job.groups.every(isHostedToolingGroup) ? 300 : 150,
|
||
);
|
||
if (job.pretestBuildMode) {
|
||
expect(job.groups.every((group) => group.pretestBuildMode)).toBe(true);
|
||
}
|
||
}
|
||
expect(job.runner).toBe(job.groups[0]?.runner);
|
||
expect(
|
||
hostedToolingGroups.every(
|
||
(group) => (runnerRanks.get(job.runner) ?? -1) >= (runnerRanks.get(group.runner) ?? 0),
|
||
),
|
||
).toBe(true);
|
||
if (hostedToolingGroups.some((group) => group.runner !== job.runner)) {
|
||
expect(job.groups.every(isHostedToolingGroup)).toBe(true);
|
||
expect(job.pretestBuildMode).toBeUndefined();
|
||
expect(job.groups.every((group) => group.pretestBuildMode === undefined)).toBe(true);
|
||
}
|
||
}
|
||
for (const extraFiles of extraInventories) {
|
||
const extraBaseline = await createPlanWithInventory(false, extraFiles);
|
||
const expanded = await createPlanWithInventory(true, extraFiles);
|
||
const expandedToolingFiles = expanded
|
||
.flatMap((job) => job.groups)
|
||
.filter(isNumberedToolingGroup)
|
||
.flatMap((group) => group.includePatterns ?? []);
|
||
expect(expanded.length).toBeLessThanOrEqual(90);
|
||
expect(new Set(expandedToolingFiles).size).toBe(expandedToolingFiles.length);
|
||
expect(expandedToolingFiles.toSorted()).toEqual(
|
||
[...new Set([...baselineToolingFiles, inventoryGrowthFile, ...extraFiles])].toSorted(),
|
||
);
|
||
expect(nonToolingPlacement(extraBaseline)).toEqual(nonToolingPlacement(baseline));
|
||
expect(nonToolingPlacement(expanded)).toEqual(nonToolingPlacement(extraBaseline));
|
||
for (const job of expanded.filter((candidate) =>
|
||
candidate.groups.some(isHostedToolingGroup),
|
||
)) {
|
||
const families = job.groups
|
||
.filter(isHostedToolingGroup)
|
||
.map((group) => group.shard_name.replace(/-hosted-\d+$/u, ""));
|
||
expect(new Set(families).size).toBe(families.length);
|
||
expect(job.requiresDist).toBe(false);
|
||
expect(job.planConcurrency).toBe(1);
|
||
expect(job.groups.length).toBeLessThanOrEqual(10);
|
||
if (job.groups.length > 1) {
|
||
expect(job.predictedSeconds).toBeLessThanOrEqual(
|
||
job.pretestBuildMode ? 210 : job.groups.every(isHostedToolingGroup) ? 300 : 150,
|
||
);
|
||
if (job.pretestBuildMode) {
|
||
expect(job.groups.every((group) => group.pretestBuildMode)).toBe(true);
|
||
}
|
||
}
|
||
expect(job.runner).toBe(job.groups[0]?.runner);
|
||
expect(
|
||
job.groups.every(
|
||
(group) => (runnerRanks.get(job.runner) ?? -1) >= (runnerRanks.get(group.runner) ?? 0),
|
||
),
|
||
).toBe(true);
|
||
}
|
||
}
|
||
});
|
||
|
||
it("assigns Blacksmith runners to every core node shard", () => {
|
||
const shards = defaultShards;
|
||
|
||
expect(shards).not.toHaveLength(0);
|
||
expect(shards.every((shard) => shard.runner?.startsWith("blacksmith-"))).toBe(true);
|
||
});
|
||
|
||
it("splits core runtime configs into smaller source-only shards", () => {
|
||
const runtimeShards = defaultShards
|
||
.filter((shard) => shard.shardName.startsWith("core-runtime-"))
|
||
.map((shard) => ({
|
||
configs: shard.configs,
|
||
requiresDist: shard.requiresDist,
|
||
runner: shard.runner,
|
||
shardName: shard.shardName,
|
||
}));
|
||
|
||
expect(runtimeShards).toEqual([
|
||
{
|
||
configs: ["test/vitest/vitest.hooks.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-hooks",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-approval-exec",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-channel-plugin",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-cli-ui",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-device",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-diagnostics-state",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-core-utils",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-env-auth",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-events-runtime",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-file-safety",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-files-commands",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-gateway-lock-argv",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-gateway-processes",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-gateway-watch",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-heartbeat-core",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-heartbeat-runner",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-misc",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-misc-dedupe-disk",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-misc-os",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-misc-values",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-net-install",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-network-node",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-network-platform",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-outbound-actions",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-outbound-core",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-provider-push",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-repo-tooling",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-storage-state",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.infra.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-system-runtime",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.secrets.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-secrets",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.logging.config.ts", "test/vitest/vitest.process.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-infra-process",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.runtime-config.config.ts"],
|
||
requiresDist: false,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-config",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.tui-pty.config.ts"],
|
||
requiresDist: true,
|
||
runner: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: "core-runtime-tui-pty",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.ui.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-media-ui-1",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.ui.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-media-ui-2",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.ui.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-media-ui-3",
|
||
},
|
||
{
|
||
configs: [
|
||
"test/vitest/vitest.media.config.ts",
|
||
"test/vitest/vitest.media-understanding.config.ts",
|
||
"test/vitest/vitest.tui.config.ts",
|
||
"test/vitest/vitest.ui-isolated.config.ts",
|
||
"test/vitest/vitest.ui-timing.config.ts",
|
||
"test/vitest/vitest.wizard.config.ts",
|
||
],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-media-ui-support",
|
||
},
|
||
{
|
||
configs: [
|
||
"test/vitest/vitest.acp.config.ts",
|
||
"test/vitest/vitest.shared-core.config.ts",
|
||
"test/vitest/vitest.utils.config.ts",
|
||
],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-shared",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.cron.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-cron-parallel-core",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.cron.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-cron-parallel-isolated-agent",
|
||
},
|
||
{
|
||
configs: ["test/vitest/vitest.cron.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "core-runtime-cron-parallel-service",
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("keeps the full TUI PTY suite in its dedicated built-CLI shard", () => {
|
||
const tuiPtyShard = defaultShards.find((shard) => shard.shardName === "core-runtime-tui-pty");
|
||
|
||
expect(tuiPtyShard).toMatchObject({
|
||
checkName: "checks-node-core-runtime-tui-pty",
|
||
configs: ["test/vitest/vitest.tui-pty.config.ts"],
|
||
env: {
|
||
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1",
|
||
OPENCLAW_TUI_PTY_USE_BUILT_CLI: "1",
|
||
},
|
||
requiresDist: true,
|
||
});
|
||
expect(tuiPtyShard?.includePatterns).toBeUndefined();
|
||
});
|
||
|
||
it("covers every infra test exactly once across core runtime infra shards", () => {
|
||
const infraShards = defaultShards.filter((shard) =>
|
||
shard.shardName.startsWith("core-runtime-infra-"),
|
||
);
|
||
const actual = infraShards
|
||
.filter((shard) => shard.configs.includes("test/vitest/vitest.infra.config.ts"))
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
|
||
expect(infraShards.map((shard) => shard.shardName)).toEqual([
|
||
"core-runtime-infra-approval-exec",
|
||
"core-runtime-infra-channel-plugin",
|
||
"core-runtime-infra-cli-ui",
|
||
"core-runtime-infra-device",
|
||
"core-runtime-infra-diagnostics-state",
|
||
"core-runtime-infra-core-utils",
|
||
"core-runtime-infra-env-auth",
|
||
"core-runtime-infra-events-runtime",
|
||
"core-runtime-infra-file-safety",
|
||
"core-runtime-infra-files-commands",
|
||
"core-runtime-infra-gateway-lock-argv",
|
||
"core-runtime-infra-gateway-processes",
|
||
"core-runtime-infra-gateway-watch",
|
||
"core-runtime-infra-heartbeat-core",
|
||
"core-runtime-infra-heartbeat-runner",
|
||
"core-runtime-infra-misc",
|
||
"core-runtime-infra-misc-dedupe-disk",
|
||
"core-runtime-infra-misc-os",
|
||
"core-runtime-infra-misc-values",
|
||
"core-runtime-infra-net-install",
|
||
"core-runtime-infra-network-node",
|
||
"core-runtime-infra-network-platform",
|
||
"core-runtime-infra-outbound-actions",
|
||
"core-runtime-infra-outbound-core",
|
||
"core-runtime-infra-provider-push",
|
||
"core-runtime-infra-repo-tooling",
|
||
"core-runtime-infra-storage-state",
|
||
"core-runtime-infra-system-runtime",
|
||
"core-runtime-infra-process",
|
||
]);
|
||
expect(actual).toEqual(
|
||
[...new Set([...listTestFiles("src/infra"), ...databaseWorkerCoreTestFiles])].toSorted(
|
||
(a, b) => a.localeCompare(b),
|
||
),
|
||
);
|
||
expect(new Set(actual).size).toBe(actual.length);
|
||
});
|
||
|
||
it.each(["github", "blacksmith", "hybrid"] as const)(
|
||
"preserves storage-state coverage and hosted file bounds for %s",
|
||
(runnerBackend) => {
|
||
const owner = "core-runtime-infra-storage-state";
|
||
const expected = defaultShards.find((shard) => shard.shardName === owner)!.includePatterns!;
|
||
const plan = getCommittedCompactPlan("pull-request", runnerBackend);
|
||
const actual: string[] = [];
|
||
for (const job of plan) {
|
||
const files = job.groups
|
||
.filter((group) => group.shard_name.replace(/-hosted-\d+$/u, "") === owner)
|
||
.flatMap((group) => group.includePatterns ?? []);
|
||
if (runnerBackend === "github") {
|
||
expect(files.length, job.shardName).toBeLessThanOrEqual(64);
|
||
}
|
||
actual.push(...files);
|
||
}
|
||
expect(actual.toSorted()).toEqual(expected.toSorted());
|
||
expect(new Set(actual).size).toBe(actual.length);
|
||
expect(plan.length).toBeLessThanOrEqual(90);
|
||
const config = createInfraVitestConfig({});
|
||
expect(config.test?.fileParallelism).toBe(sharedVitestConfig.test.fileParallelism);
|
||
expect(config.test?.maxWorkers).toBe(sharedVitestConfig.test.maxWorkers);
|
||
expect(config.test?.isolate).toBe(true);
|
||
expect(config.test?.pool).toBe(diagnosticForksPool);
|
||
},
|
||
);
|
||
|
||
it("runs only native Gateway lifecycle fixtures before parallel files without widening selectors", () => {
|
||
const config = createGatewayServerVitestConfig({ OPENCLAW_VITEST_MAX_WORKERS: "8" }, true);
|
||
const native = gatewayServerSerialTestFiles.map((file) => relative("src/gateway", file));
|
||
const sequence = config.test?.sequence?.groupOrder ?? 0;
|
||
expect(config.test?.projects).toMatchObject([
|
||
{
|
||
extends: false,
|
||
test: {
|
||
name: "gateway-server-native",
|
||
pool: "forks",
|
||
isolate: false,
|
||
fileParallelism: false,
|
||
maxWorkers: 1,
|
||
include: native,
|
||
runner: config.test?.runner,
|
||
setupFiles: config.test?.setupFiles,
|
||
sequence: { groupOrder: sequence },
|
||
},
|
||
},
|
||
{
|
||
extends: false,
|
||
test: {
|
||
name: "gateway-server-parallel",
|
||
pool: "forks",
|
||
isolate: false,
|
||
fileParallelism: true,
|
||
maxWorkers: undefined,
|
||
include: config.test?.include,
|
||
exclude: expect.arrayContaining(native),
|
||
runner: config.test?.runner,
|
||
setupFiles: config.test?.setupFiles,
|
||
sequence: { groupOrder: sequence + 1 },
|
||
},
|
||
},
|
||
]);
|
||
expect(listMatchedTestFiles(config)).toEqual(
|
||
defaultShards
|
||
.filter((shard) => shard.configs.includes("test/vitest/vitest.gateway-server.config.ts"))
|
||
.flatMap((shard) => shard.includePatterns!)
|
||
.toSorted(),
|
||
);
|
||
const selected = [
|
||
gatewayServerSerialTestFiles[0]!,
|
||
"src/gateway/server.models-voicewake-misc.test.ts",
|
||
];
|
||
const includeFile = join(tempDirs.make("gateway-native-selection-"), "include.json");
|
||
writeFileSync(includeFile, JSON.stringify(selected));
|
||
const narrowed = createGatewayServerVitestConfig(
|
||
{
|
||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||
OPENCLAW_VITEST_MAX_WORKERS: "8",
|
||
},
|
||
true,
|
||
);
|
||
expect(listMatchedTestFiles(narrowed)).toEqual(selected.toSorted());
|
||
expect(narrowed.test?.projects).toMatchObject([
|
||
{ test: { include: [native[0]] } },
|
||
{
|
||
test: {
|
||
include: selected.map((file) => relative("src/gateway", file)),
|
||
exclude: expect.arrayContaining(native),
|
||
},
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("preserves Gateway runner hooks while assigning database consumers to parallel forks", () => {
|
||
const worker = createGatewayDatabaseWorkersVitestConfig({});
|
||
const core = createGatewayCoreVitestConfig({});
|
||
const server = createGatewayServerVitestConfig({ OPENCLAW_VITEST_MAX_WORKERS: "8" });
|
||
const methods = createGatewayMethodsVitestConfig({});
|
||
expect(server.test?.fileParallelism).toBe(true);
|
||
expect(
|
||
createGatewayServerVitestConfig({ OPENCLAW_VITEST_MAX_WORKERS: "1" }).test?.fileParallelism,
|
||
).toBe(false);
|
||
expect(methods.test?.pool).toBe("forks");
|
||
expect(worker.test?.pool).toBe("forks");
|
||
expect(worker.test?.fileParallelism).toBe(true);
|
||
expect(core.test?.isolate).toBe(true);
|
||
for (const shared of [worker, server, methods]) {
|
||
expect(shared.test?.isolate).toBe(false);
|
||
}
|
||
for (const previous of [core, server, methods]) {
|
||
expect(worker.test?.runner).toBe(previous.test?.runner);
|
||
expect(worker.test?.setupFiles).toEqual(previous.test?.setupFiles);
|
||
}
|
||
expect(listMatchedTestFiles(worker)).toEqual(gatewayDatabaseWorkerTestFiles);
|
||
expect(listMatchedTestFiles(worker)).toEqual(
|
||
expect.arrayContaining([
|
||
"src/gateway/github-publication-transcript.test.ts",
|
||
"src/gateway/session-lifecycle-run-failure.test.ts",
|
||
"src/gateway/session-lifecycle-state.persistence.test.ts",
|
||
"src/gateway/worker-workspace-recovery-transcript.test.ts",
|
||
"src/gateway/session-utils.queued-collector-admission.test.ts",
|
||
"src/gateway/session-utils.queued-collector.test.ts",
|
||
]),
|
||
);
|
||
const former = new Set([core, server, methods].flatMap(listMatchedTestFiles));
|
||
for (const file of gatewayDatabaseWorkerTestFiles) {
|
||
expect(former.has(file), file).toBe(false);
|
||
expect(isGatewayServerTestFile(file), file).toBe(false);
|
||
}
|
||
expect(
|
||
defaultShards.filter((shard) =>
|
||
shard.configs.includes("test/vitest/vitest.gateway-database-workers.config.ts"),
|
||
),
|
||
).toHaveLength(1);
|
||
const includeFile = join(tempDirs.make("gateway-database-routing-"), "include.json");
|
||
writeFileSync(includeFile, JSON.stringify([gatewayDatabaseWorkerTestFiles[0]]));
|
||
expect(
|
||
listMatchedTestFiles(
|
||
createGatewayDatabaseWorkersVitestConfig({ OPENCLAW_VITEST_INCLUDE_FILE: includeFile }),
|
||
),
|
||
).toEqual([gatewayDatabaseWorkerTestFiles[0]]);
|
||
});
|
||
|
||
it("keeps host-owned database consumers in forks and out of their former projects", () => {
|
||
const infra = createInfraVitestConfig({});
|
||
const support = createAgentsSupportVitestConfig({});
|
||
expect(infra.test?.pool).toBe(diagnosticForksPool);
|
||
expect(infra.test?.isolate).toBe(true);
|
||
expect(infra.test?.setupFiles).toEqual(support.test?.setupFiles);
|
||
const admitted = new Set(listMatchedTestFiles(infra));
|
||
for (const file of [
|
||
"src/agents/subagents/registry/subagent-registry.session-failure.test.ts",
|
||
"src/plugin-sdk/session-transcript-runtime.test.ts",
|
||
"src/agents/sessions/sdk.auth-migration.test.ts",
|
||
"src/agents/subagents/spawn/subagent-spawn.in-process-gateway.test.ts",
|
||
"src/agents/subagents/spawn/subagent-spawn.authority.test.ts",
|
||
"src/agents/tools/swarm-tools.integration.test.ts",
|
||
"src/config/sessions/disk-budget.physical-usage.test.ts",
|
||
]) {
|
||
expect(admitted.has(file), file).toBe(true);
|
||
}
|
||
const former = new Set(
|
||
[
|
||
createUnitVitestConfigWithOptions({}),
|
||
createUnitFastVitestConfig(),
|
||
createAgentsCoreVitestConfig({}),
|
||
createAgentsCoreIsolatedVitestConfig({}),
|
||
support,
|
||
createAgentsToolsVitestConfig({}),
|
||
createAgentsVitestConfig({}),
|
||
createPluginSdkLightVitestConfig({}),
|
||
createPluginSdkVitestConfig({}),
|
||
createPluginsVitestConfig({}),
|
||
createToolingVitestConfig({}),
|
||
createWizardVitestConfig({}),
|
||
createCommandsVitestConfig({}),
|
||
createRuntimeConfigVitestConfig({}),
|
||
createGatewayVitestConfig({}),
|
||
createGatewayCoreVitestConfig({}),
|
||
createGatewayClientVitestConfig({}),
|
||
createGatewayMethodsVitestConfig({}),
|
||
createGatewayMethodsIsolatedVitestConfig({}),
|
||
createGatewayServerVitestConfig({}),
|
||
createGatewayServerIsolatedVitestConfig({}),
|
||
createGatewayDatabaseWorkersVitestConfig({}),
|
||
].flatMap(listMatchedTestFiles),
|
||
);
|
||
for (const file of databaseWorkerCoreTestFiles) {
|
||
expect(admitted.has(file), file).toBe(true);
|
||
expect(former.has(file), file).toBe(false);
|
||
}
|
||
const gatewayWorkerFiles = databaseWorkerCoreTestFiles.filter((file) =>
|
||
file.startsWith("src/gateway/"),
|
||
);
|
||
const gatewayPlanFiles = defaultShards
|
||
.filter((shard) => shard.shardName.startsWith("agentic-gateway-core"))
|
||
.flatMap((shard) => shard.includePatterns ?? []);
|
||
expect(gatewayPlanFiles.filter((file) => gatewayWorkerFiles.includes(file))).toEqual([]);
|
||
const infraPlanFiles = defaultShards
|
||
.filter((shard) => shard.configs.includes("test/vitest/vitest.infra.config.ts"))
|
||
.flatMap((shard) => shard.includePatterns ?? []);
|
||
expect(infraPlanFiles.filter((file) => gatewayWorkerFiles.includes(file)).toSorted()).toEqual(
|
||
gatewayWorkerFiles.toSorted(),
|
||
);
|
||
const recoveryTest = "src/wizard/setup.inference-recovery.integration.test.ts";
|
||
expect(admitted.has(recoveryTest), recoveryTest).toBe(true);
|
||
expect(former.has(recoveryTest), recoveryTest).toBe(false);
|
||
const selected = [
|
||
"src/plugin-state/plugin-state-store.test.ts",
|
||
"test/plugins/beam-http-identity.test.ts",
|
||
];
|
||
const includeFile = join(tempDirs.make("database-worker-routing-"), "include.json");
|
||
writeFileSync(includeFile, JSON.stringify(selected));
|
||
expect(
|
||
listMatchedTestFiles(
|
||
createInfraVitestConfig({ OPENCLAW_VITEST_INCLUDE_FILE: includeFile }),
|
||
).toSorted(),
|
||
).toEqual(selected.toSorted());
|
||
});
|
||
|
||
it("covers every cron test exactly once across core runtime cron shards", () => {
|
||
const cronShards = defaultShards.filter((shard) =>
|
||
shard.shardName.startsWith("core-runtime-cron-"),
|
||
);
|
||
const actual = cronShards
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
|
||
expect(cronShards.map((shard) => shard.shardName)).toEqual([
|
||
"core-runtime-cron-parallel-core",
|
||
"core-runtime-cron-parallel-isolated-agent",
|
||
"core-runtime-cron-parallel-service",
|
||
]);
|
||
expect(actual).toEqual(listTestFiles("src/cron"));
|
||
expect(new Set(actual).size).toBe(actual.length);
|
||
});
|
||
|
||
it.each(["blacksmith", "github", "hybrid"])(
|
||
"prices parallel cron from serial work until %s has direct measurements",
|
||
(runnerBackend) => {
|
||
const original = fullSuiteVitestShards.slice();
|
||
try {
|
||
const cron = "test/vitest/vitest.cron.config.ts";
|
||
fullSuiteVitestShards.splice(
|
||
0,
|
||
fullSuiteVitestShards.length,
|
||
...original
|
||
.filter((shard) => shard.projects.includes(cron))
|
||
.map((shard) => Object.assign({}, shard, { projects: [cron] })),
|
||
);
|
||
const timings = vi.spyOn(testTimings, "readCompactGroupTimings").mockReturnValue({
|
||
"core-runtime-cron-core": 40,
|
||
"core-runtime-cron-isolated-agent": 100,
|
||
"core-runtime-cron-service": 140,
|
||
});
|
||
const options = { compactMode: "pull-request" as const, runnerBackend };
|
||
const baseline = createNodeTestShardBundles(options);
|
||
const totalSeconds = (plan: typeof baseline) =>
|
||
plan.reduce((total, job) => total + job.predictedSeconds!, 0);
|
||
expect(totalSeconds(baseline)).toBe(140);
|
||
expect(baseline.flatMap((job) => job.groups)).toHaveLength(3);
|
||
timings.mockReturnValue({
|
||
"core-runtime-cron-core": 400,
|
||
"core-runtime-cron-isolated-agent": 1_000,
|
||
"core-runtime-cron-service": 1_400,
|
||
"core-runtime-cron-parallel-core": 20,
|
||
"core-runtime-cron-parallel-isolated-agent": 60,
|
||
"core-runtime-cron-parallel-service": 80,
|
||
});
|
||
const measured = createNodeTestShardBundles(options);
|
||
expect(totalSeconds(measured)).toBe(160);
|
||
const groups = measured.flatMap((job) => job.groups);
|
||
expect(groups).toHaveLength(3);
|
||
expect(groups.every((group) => group.env === undefined)).toBe(true);
|
||
expect(groups.every((group) => group.fallbackMaxWorkers === undefined)).toBe(true);
|
||
expect(groups.flatMap((group) => group.includePatterns ?? []).toSorted()).toEqual(
|
||
baseline
|
||
.flatMap((job) => job.groups.flatMap((group) => group.includePatterns ?? []))
|
||
.toSorted(),
|
||
);
|
||
} finally {
|
||
fullSuiteVitestShards.splice(0, fullSuiteVitestShards.length, ...original);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("splits the agentic lane into control-plane, command, agent, gateway, SDK, and plugin shards", () => {
|
||
const shards = defaultShards;
|
||
const controlPlaneShards = shards.filter((shard) =>
|
||
shard.shardName.startsWith("agentic-control-plane-"),
|
||
);
|
||
const cliShard = shards.find((shard) => shard.shardName === "agentic-cli");
|
||
const cliProcessShard = shards.find((shard) => shard.shardName === "agentic-cli-process");
|
||
const commandSupportShard = shards.find(
|
||
(shard) => shard.shardName === "agentic-command-support",
|
||
);
|
||
const commandShards = shards.filter((shard) => shard.shardName.startsWith("agentic-commands-"));
|
||
const agentShards = shards.filter((shard) => shard.shardName.startsWith("agentic-agents-"));
|
||
const gatewayCoreShards = shards.filter((shard) =>
|
||
shard.shardName.startsWith("agentic-gateway-core-"),
|
||
);
|
||
const gatewayMethodsShard = shards.find(
|
||
(shard) => shard.shardName === "agentic-gateway-methods",
|
||
);
|
||
const pluginSdkShard = shards.find((shard) => shard.shardName === "agentic-plugin-sdk");
|
||
const pluginsShard = shards.find((shard) => shard.shardName === "agentic-plugins");
|
||
|
||
expect(controlPlaneShards.map((shard) => shard.shardName)).toEqual([
|
||
"agentic-control-plane-agent-chat",
|
||
"agentic-control-plane-auth-node",
|
||
"agentic-control-plane-http-models",
|
||
"agentic-control-plane-http-plugin-ws",
|
||
"agentic-control-plane-runtime",
|
||
"agentic-control-plane-runtime-config",
|
||
"agentic-control-plane-runtime-cron",
|
||
"agentic-control-plane-runtime-server",
|
||
"agentic-control-plane-runtime-shared-token",
|
||
"agentic-control-plane-runtime-state",
|
||
"agentic-control-plane-runtime-ui-tools",
|
||
"agentic-control-plane-startup-config",
|
||
"agentic-control-plane-startup-core",
|
||
"agentic-control-plane-startup-health-runtime",
|
||
"agentic-control-plane-startup-restart-close",
|
||
]);
|
||
expect(controlPlaneShards).toEqual(
|
||
controlPlaneShards.map((shard) => ({
|
||
checkName: `checks-node-${shard.shardName}`,
|
||
configs: ["test/vitest/vitest.gateway-server.config.ts"],
|
||
...(shard.shardName === "agentic-control-plane-startup-health-runtime"
|
||
? { env: { OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "60000" } }
|
||
: {}),
|
||
...(shard.includePatterns?.includes("src/gateway/server.config-patch.test.ts") ||
|
||
shard.includePatterns?.includes("src/gateway/server-sidecar-retention.test.ts") ||
|
||
shard.includePatterns?.includes("src/gateway/server.acp-native-model.product.test.ts")
|
||
? { pretestBuildMode: "runtime" }
|
||
: {}),
|
||
includePatterns: shard.includePatterns,
|
||
requiresDist: false,
|
||
runner:
|
||
shard.shardName === "agentic-control-plane-startup-core"
|
||
? DEFAULT_NODE_TEST_RUNNER
|
||
: "blacksmith-4vcpu-ubuntu-2404",
|
||
shardName: shard.shardName,
|
||
})),
|
||
);
|
||
const controlPlaneShardFiles = controlPlaneShards
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
const expectedControlPlaneFiles = listMatchedTestFiles(
|
||
createGatewayServerVitestConfig({
|
||
...process.env,
|
||
OPENCLAW_VITEST_INCLUDE_FILE: undefined,
|
||
}),
|
||
);
|
||
expect(
|
||
listTestFiles("src/gateway")
|
||
.filter(isGatewayServerTestFile)
|
||
.toSorted((a, b) => a.localeCompare(b)),
|
||
).toEqual(expectedControlPlaneFiles);
|
||
expect(controlPlaneShardFiles).toEqual(expectedControlPlaneFiles);
|
||
expect(new Set(controlPlaneShardFiles).size).toBe(controlPlaneShardFiles.length);
|
||
expect(cliShard).toEqual({
|
||
checkName: "checks-node-agentic-cli",
|
||
shardName: "agentic-cli",
|
||
configs: ["test/vitest/vitest.cli.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
});
|
||
expect(cliProcessShard).toEqual({
|
||
checkName: "checks-node-agentic-cli-process",
|
||
shardName: "agentic-cli-process",
|
||
configs: ["test/vitest/vitest.cli-process.config.ts"],
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
});
|
||
expect(commandSupportShard).toEqual({
|
||
checkName: "checks-node-agentic-command-support",
|
||
shardName: "agentic-command-support",
|
||
configs: [
|
||
"test/vitest/vitest.commands-light.config.ts",
|
||
"test/vitest/vitest.daemon.config.ts",
|
||
],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
});
|
||
expect(commandShards.map((shard) => shard.shardName)).toEqual([
|
||
"agentic-commands-agent-channel",
|
||
"agentic-commands-doctor",
|
||
"agentic-commands-doctor-auth",
|
||
"agentic-commands-doctor-config-state",
|
||
"agentic-commands-doctor-gateway",
|
||
"agentic-commands-doctor-platform",
|
||
"agentic-commands-doctor-plugins-tools",
|
||
"agentic-commands-doctor-sessions-cron",
|
||
"agentic-commands-doctor-sessions-cron-memory",
|
||
"agentic-commands-doctor-sessions-cron-sqlite",
|
||
"agentic-commands-doctor-sessions-cron-sqlite-recovery",
|
||
"agentic-commands-doctor-shared",
|
||
"agentic-commands-doctor-whatsapp",
|
||
"agentic-commands-doctor-workspace",
|
||
"agentic-commands-models",
|
||
"agentic-commands-onboard-config",
|
||
"agentic-commands-status-tools",
|
||
"agentic-commands-runtime",
|
||
]);
|
||
expect(commandShards).toEqual(
|
||
commandShards.map((shard) => ({
|
||
checkName: `checks-node-${shard.shardName}`,
|
||
configs: ["test/vitest/vitest.commands.config.ts"],
|
||
includePatterns: shard.includePatterns,
|
||
...(shard.shardName === "agentic-commands-runtime" ? { pretestBuildMode: "runtime" } : {}),
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: shard.shardName,
|
||
})),
|
||
);
|
||
expect(
|
||
commandShards.find((shard) => shard.shardName === "agentic-commands-doctor-auth")
|
||
?.includePatterns,
|
||
).toContain("src/commands/oauth-tls-preflight.doctor.test.ts");
|
||
const commandShardFiles = commandShards
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
const expectedCommandFiles = listMatchedTestFiles(createCommandsVitestConfig({}));
|
||
expect(commandShardFiles).toEqual(expectedCommandFiles);
|
||
expect(new Set(commandShardFiles).size).toBe(commandShardFiles.length);
|
||
expect(agentShards).toEqual([
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-auth",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[0]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-auth",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-models",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[1]?.includePatterns,
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-models",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-tools",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[2]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-tools",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-subagents",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[3]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-subagents",
|
||
},
|
||
// Retain the import-heavy CLI stripes alongside bounded file workers.
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-runner-cli-1",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[4]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-runner-cli-1",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-runner-cli-2",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[5]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-runner-cli-2",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-runner-cli-3",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[6]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-runner-cli-3",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-runner-commands",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[7]?.includePatterns,
|
||
requiresDist: false,
|
||
pretestBuildMode: "runtime",
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-runner-commands",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-runner-embedded",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[8]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-runner-embedded",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-runner-sessions",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[9]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-runner-sessions",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-runtime",
|
||
configs: ["test/vitest/vitest.agents-core.config.ts"],
|
||
includePatterns: agentShards[10]?.includePatterns,
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-runtime",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-spawn-production-boundary",
|
||
configs: ["test/vitest/vitest.agents-spawn-production-boundary.config.ts"],
|
||
includePatterns: agentShards[11]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-spawn-production-boundary",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-core-isolated",
|
||
configs: ["test/vitest/vitest.agents-core-isolated.config.ts"],
|
||
includePatterns: agentShards[12]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-core-isolated",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-embedded",
|
||
configs: [
|
||
"test/vitest/vitest.agents-embedded-agent.config.ts",
|
||
"test/vitest/vitest.agents-embedded-agent-incomplete-turn.config.ts",
|
||
"test/vitest/vitest.agents-embedded-agent-overflow-compaction.config.ts",
|
||
"test/vitest/vitest.agents-embedded-agent-run.config.ts",
|
||
],
|
||
env: { OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "660000" },
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-embedded",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-support",
|
||
configs: ["test/vitest/vitest.agents-support.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-support",
|
||
},
|
||
{
|
||
checkName: "checks-node-agentic-agents-tools",
|
||
configs: ["test/vitest/vitest.agents-tools.config.ts"],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-agents-tools",
|
||
},
|
||
]);
|
||
expect(pluginSdkShard).toEqual({
|
||
checkName: "checks-node-agentic-plugin-sdk",
|
||
shardName: "agentic-plugin-sdk",
|
||
configs: [
|
||
"test/vitest/vitest.plugin-sdk-light.config.ts",
|
||
"test/vitest/vitest.plugin-sdk.config.ts",
|
||
],
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
});
|
||
const gatewayCoreConfigs = [
|
||
"test/vitest/vitest.gateway-core.config.ts",
|
||
"test/vitest/vitest.gateway-client.config.ts",
|
||
];
|
||
expect(gatewayCoreShards.slice(0, 3)).toEqual(
|
||
[1, 2, 3].map((stripe) => ({
|
||
checkName: `checks-node-agentic-gateway-core-${stripe}`,
|
||
shardName: `agentic-gateway-core-${stripe}`,
|
||
configs: gatewayCoreConfigs,
|
||
includePatterns: gatewayCoreShards[stripe - 1]?.includePatterns,
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
})),
|
||
);
|
||
// The pretest runtime build is charged per job, so the files that need it
|
||
// stay in one shard; the ordinary stripes must never carry a build mode.
|
||
expect(gatewayCoreShards.filter((shard) => shard.pretestBuildMode != null)).toEqual([
|
||
{
|
||
checkName: "checks-node-agentic-gateway-core-runtime",
|
||
shardName: "agentic-gateway-core-runtime",
|
||
configs: gatewayCoreConfigs,
|
||
includePatterns: [
|
||
"src/gateway/gateway-active-memory.test.ts",
|
||
"src/gateway/gateway-concurrent-streams.test.ts",
|
||
],
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
},
|
||
]);
|
||
expect(gatewayMethodsShard).toEqual({
|
||
checkName: "checks-node-agentic-gateway-methods",
|
||
shardName: "agentic-gateway-methods",
|
||
configs: [
|
||
"test/vitest/vitest.gateway-methods.config.ts",
|
||
"test/vitest/vitest.gateway-methods-isolated.config.ts",
|
||
],
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
});
|
||
expect(pluginsShard).toEqual({
|
||
checkName: "checks-node-agentic-plugins",
|
||
shardName: "agentic-plugins",
|
||
configs: ["test/vitest/vitest.plugins.config.ts"],
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
});
|
||
});
|
||
|
||
it("keeps plugin prerelease npm install behavior on the release-only agentic plugin shard", () => {
|
||
const pluginsShard = defaultShards.find((shard) => shard.shardName === "agentic-plugins");
|
||
|
||
expect(pluginsShard).toEqual({
|
||
checkName: "checks-node-agentic-plugins",
|
||
configs: ["test/vitest/vitest.plugins.config.ts"],
|
||
pretestBuildMode: "runtime",
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
shardName: "agentic-plugins",
|
||
});
|
||
expect(listMatchedTestFiles(createPluginsVitestConfig({}))).toContain(
|
||
PLUGIN_PRERELEASE_NPM_SPEC_TEST,
|
||
);
|
||
});
|
||
|
||
it("covers flat agents-core and explicitly nested isolated tests exactly once", () => {
|
||
const actual = defaultShards
|
||
.filter((shard) => shard.shardName.startsWith("agentic-agents-core-"))
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
const expected = [
|
||
...listTestFiles("src/agents").filter(
|
||
(file) => !relative("src/agents", file).replaceAll("\\", "/").includes("/"),
|
||
),
|
||
...agentVitestProjectOwners.coreIsolated.include.filter((file) =>
|
||
relative("src/agents", file).replaceAll("\\", "/").includes("/"),
|
||
),
|
||
...agentVitestProjectOwners.spawnProductionBoundary.include,
|
||
]
|
||
.filter((file) => !databaseWorkerCoreTestFiles.includes(file))
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
|
||
expect(actual).toEqual(expected);
|
||
expect(new Set(actual).size).toBe(actual.length);
|
||
});
|
||
|
||
it("keeps embedded-agent tests in four bounded config surfaces", () => {
|
||
const shard = defaultShards.find(
|
||
(candidate) => candidate.shardName === "agentic-agents-embedded",
|
||
);
|
||
const incompleteTurnFiles = new Set(agentVitestProjectOwners.embeddedIncompleteTurn.include);
|
||
const overflowCompactionFiles = new Set(
|
||
agentVitestProjectOwners.embeddedOverflowCompaction.include,
|
||
);
|
||
const actual = [
|
||
...globSync(agentVitestProjectOwners.embedded.include)
|
||
.map(toRepoPath)
|
||
.filter((file) => !incompleteTurnFiles.has(file) && !overflowCompactionFiles.has(file)),
|
||
...agentVitestProjectOwners.embeddedIncompleteTurn.include,
|
||
...agentVitestProjectOwners.embeddedOverflowCompaction.include,
|
||
...globSync(agentVitestProjectOwners.embeddedRun.include).map(toRepoPath),
|
||
].toSorted((left, right) => left.localeCompare(right));
|
||
const expected = listTestFiles("src/agents/embedded-agent-runner").toSorted((left, right) =>
|
||
left.localeCompare(right),
|
||
);
|
||
|
||
expect(shard?.configs).toEqual(embeddedAgentVitestProjectOwners.map((owner) => owner.config));
|
||
expect(actual).toEqual(expected);
|
||
expect(new Set(actual).size).toBe(actual.length);
|
||
});
|
||
|
||
it("keeps expensive plugin shards release-only when normal CI asks for the cheaper plan", () => {
|
||
const shards = createNodeTestShards({ includeReleaseOnlyPluginShards: false });
|
||
const shardNames = shards.map((shard) => shard.shardName);
|
||
|
||
expect(shardNames).not.toContain("agentic-plugins");
|
||
expect(shardNames).toContain("agentic-gateway-core-1");
|
||
expect(shardNames).toContain("agentic-gateway-core-2");
|
||
expect(shardNames).toContain("agentic-gateway-core-3");
|
||
expect(shardNames).toContain("agentic-gateway-methods");
|
||
expect(shardNames).toContain("agentic-plugin-sdk");
|
||
});
|
||
|
||
it.each(
|
||
(["blacksmith", "github", "hybrid"] as const).flatMap((runnerBackend) =>
|
||
[{ file: "test/plugins/codex-model-catalog.gateway.test.ts", buildMode: "runtime" }].map(
|
||
({ file, buildMode }) => ({ file, buildMode, runnerBackend }),
|
||
),
|
||
),
|
||
)(
|
||
"keeps changed Gateway build selection for $file on $runnerBackend",
|
||
({ file, buildMode, runnerBackend }) => {
|
||
const shards = createChangedNodeTestShards([file], { runnerBackend });
|
||
const owners = (shards ?? []).flatMap((shard) =>
|
||
(shard.groups ?? []).flatMap((group) =>
|
||
(group.includePatterns ?? [])
|
||
.filter((selected) => selected === file)
|
||
.map(() => ({ shard, group })),
|
||
),
|
||
);
|
||
expect(owners).toHaveLength(1);
|
||
const owner = expectDefined(owners[0], "selected Gateway execution");
|
||
expect(owner.group.includePatterns).toEqual([file]);
|
||
expect(owner.group.configs.toSorted()).toEqual([
|
||
"test/vitest/vitest.gateway-database-workers.config.ts",
|
||
]);
|
||
expect(owner.group.pretestBuildMode).toBe(buildMode);
|
||
expect(owner.shard.planConcurrency).toBe(1);
|
||
if (buildMode) {
|
||
expect(owner.shard.pretestBuildMode).toBe(buildMode);
|
||
}
|
||
},
|
||
);
|
||
|
||
it("keeps the complete tooling family in manual plans and omits it from product plans", () => {
|
||
const legacyFinalize = "src/cli/update-cli/update-command-legacy-finalize.test.ts";
|
||
expect(listMatchedTestFiles(createCliVitestConfig({}))).not.toContain(legacyFinalize);
|
||
expect(listMatchedTestFiles(createToolingIsolatedVitestConfig({}))).toContain(legacyFinalize);
|
||
const automatic = createNodeTestShards({
|
||
includeReleaseOnlyToolingShards: false,
|
||
changedPaths: ["src/plugin-sdk/core.ts", "src/infra/home-dir.test.ts"],
|
||
});
|
||
expect(defaultShards.some((shard) => shard.shardName.startsWith("core-tooling-"))).toBe(true);
|
||
expect(automatic.some((shard) => shard.shardName.startsWith("core-tooling-"))).toBe(false);
|
||
expect(automatic.flatMap((shard) => shard.includePatterns ?? [])).not.toEqual(
|
||
expect.arrayContaining([RELEASE_REPORT_OWNER_TEST, "test/scripts/arg-utils.test.ts"]),
|
||
);
|
||
for (const shard of automatic) {
|
||
expect(shard.includePatterns?.some((file) => file.startsWith("test/scripts/"))).not.toBe(
|
||
true,
|
||
);
|
||
}
|
||
expect(automatic.flatMap((shard) => shard.includePatterns ?? [])).toContain(
|
||
"src/infra/home-dir.test.ts",
|
||
);
|
||
});
|
||
|
||
it.each([
|
||
RELEASE_REPORT_OWNER_TEST,
|
||
"test/scripts/arg-utils.test.ts",
|
||
"scripts/lib/vitest-report-owner.mts",
|
||
"scripts/test-extension-batch.mts",
|
||
"src/scripts/example.ts",
|
||
"scripts/README.md",
|
||
"config/ci-budget.md",
|
||
"test/vitest/vitest.tooling.config.ts",
|
||
"test/helpers/temp-dir.ts",
|
||
"config/ci-test-timings.json",
|
||
"package.json",
|
||
"pnpm-lock.yaml",
|
||
"pnpm-workspace.yaml",
|
||
"patches/vitest@5.0.1.patch",
|
||
".github/workflows/ci.yml",
|
||
".github/ISSUE_TEMPLATE/bug_report.md",
|
||
".crabbox.yaml",
|
||
"Dockerfile",
|
||
"apps/ios/fastlane/Fastfile",
|
||
"extensions/matrix/package.json",
|
||
"extensions/matrix/scripts/build.mjs",
|
||
])("retains the complete tooling family when owner %s changes", (changedPath) => {
|
||
expect(
|
||
createNodeTestShards({
|
||
includeReleaseOnlyToolingShards: false,
|
||
changedPaths: ["src/plugin-sdk/core.ts", changedPath],
|
||
}),
|
||
).toEqual(defaultShards);
|
||
});
|
||
|
||
it.each(["blacksmith", "github", "hybrid"])(
|
||
"preserves product coverage and canonical tooling owners across the %s release tier",
|
||
(runnerBackend) => {
|
||
const options = {
|
||
compactMode: "pull-request" as const,
|
||
includeReleaseOnlyPluginShards: false,
|
||
runnerBackend,
|
||
};
|
||
const full = getCommittedCompactPlan("pull-request", runnerBackend);
|
||
const automatic = createNodeTestShardBundles({
|
||
...options,
|
||
includeReleaseOnlyToolingShards: false,
|
||
changedPaths: ["src/gateway/server.ts"],
|
||
});
|
||
const fullGroups = full.flatMap((job) => job.groups);
|
||
const automaticGroups = automatic.flatMap((job) => job.groups);
|
||
const productFiles = (groups: typeof fullGroups) =>
|
||
groups
|
||
.filter((group) => !group.configs.some((config) => config.includes("vitest.tooling")))
|
||
.flatMap(
|
||
(group) =>
|
||
group.includePatterns ??
|
||
group.configs.flatMap((config) => {
|
||
if (config === "test/vitest/vitest.unit-fast-isolated.config.ts") {
|
||
return getUnitFastIsolatedTestFiles();
|
||
}
|
||
if (config === "test/vitest/vitest.unit-fast-fake-timers.config.ts") {
|
||
return getUnitFastTimerTestFiles();
|
||
}
|
||
return [];
|
||
}),
|
||
)
|
||
.filter((file) => !file.startsWith("test/scripts/") && !file.startsWith("src/scripts/"))
|
||
.toSorted();
|
||
expect(
|
||
automaticGroups.some((group) =>
|
||
group.configs.some((config) => config.includes("vitest.tooling")),
|
||
),
|
||
).toBe(false);
|
||
expect(
|
||
automaticGroups
|
||
.flatMap((group) => group.includePatterns ?? [])
|
||
.some((file) => file.startsWith("test/scripts/")),
|
||
).toBe(false);
|
||
expect(productFiles(automaticGroups)).toEqual(productFiles(fullGroups));
|
||
expect(
|
||
createNodeTestShardBundles({
|
||
...options,
|
||
includeReleaseOnlyToolingShards: false,
|
||
changedPaths: ["package.json"],
|
||
}),
|
||
).toEqual(full);
|
||
const push = createNodeTestShardBundles({
|
||
...options,
|
||
compactMode: "push",
|
||
includeReleaseOnlyToolingShards: false,
|
||
changedPaths: ["package.json"],
|
||
});
|
||
expect(
|
||
push
|
||
.flatMap((job) => job.groups)
|
||
.some((group) => group.configs.some((config) => config.includes("vitest.tooling"))),
|
||
).toBe(false);
|
||
expect(
|
||
push
|
||
.flatMap((job) => job.groups)
|
||
.flatMap((group) => group.includePatterns ?? [])
|
||
.some((file) => file.startsWith("test/scripts/")),
|
||
).toBe(false);
|
||
},
|
||
);
|
||
|
||
it("keeps changed native browser tests in UI jobs and out of extension fallback", () => {
|
||
const target = "extensions/workboard/browser/catalog.test.ts";
|
||
const shards = createChangedNodeTestShards([target]);
|
||
expect(shards).not.toBeNull();
|
||
expect(shards?.flatMap((shard) => shard.targets ?? shard.includePatterns ?? [])).toContain(
|
||
target,
|
||
);
|
||
expect(createChangedExtensionFallbackShards([target])).toEqual([]);
|
||
});
|
||
|
||
it.each([
|
||
"extensions/telegram/src/sticker-cache.selection.test.ts",
|
||
"extensions/telegram/src/bot.create-telegram-bot.native-pipeline.test.ts",
|
||
])("prepares the provider runtime in extension fallback for %s", (target) => {
|
||
const owners = createChangedExtensionFallbackShards([target]).filter((shard) =>
|
||
(shard.groups ?? [shard]).some((group) => group.includePatterns?.includes(target)),
|
||
);
|
||
|
||
expect(owners).toHaveLength(1);
|
||
expect(owners[0]?.pretestBuildMode).toBe("runtime");
|
||
});
|
||
|
||
it("retains the changed host plugin test in a global fallback beside store aliases", () => {
|
||
const target = "src/plugins/tools.optional.test.ts";
|
||
const changedPaths = [...STORE_ALIAS_CHANGED_PATHS, "tsconfig.json"];
|
||
const onFallback = vi.fn();
|
||
expect(createChangedNodeTestShards(changedPaths, { onFallback })).toBeNull();
|
||
expect(onFallback).toHaveBeenCalledWith("global execution or resolution input: tsconfig.json");
|
||
const options = {
|
||
changedPaths,
|
||
includeReleaseOnlyPluginShards: false,
|
||
};
|
||
const shards = [
|
||
...createNodeTestShards(options),
|
||
...createChangedExtensionFallbackShards(changedPaths),
|
||
];
|
||
expect(shards.filter((shard) => shard.shardName === "agentic-plugins")).toEqual([
|
||
{
|
||
checkName: "checks-node-agentic-plugins",
|
||
shardName: "agentic-plugins",
|
||
configs: ["test/vitest/vitest.plugins.config.ts"],
|
||
includePatterns: [target],
|
||
requiresDist: false,
|
||
runner: DEFAULT_NODE_TEST_RUNNER,
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("retains only exact changed plugin-owner tests in deterministic order", () => {
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
changedPaths: [
|
||
...STORE_ALIAS_CHANGED_PATHS.toReversed(),
|
||
" src/plugins/tools.optional.test.ts",
|
||
String.raw`src\plugins\tools.optional.test.ts`,
|
||
"src/plugins/tools.optional.test.ts",
|
||
PLUGIN_PRERELEASE_NPM_SPEC_TEST,
|
||
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
||
"src/plugins/loader.test.ts",
|
||
"src/plugins/install.npm-spec.e2e.test.ts",
|
||
],
|
||
};
|
||
const shards = createNodeTestShards(options);
|
||
expect(shards.find((shard) => shard.shardName === "agentic-plugins")?.includePatterns).toEqual([
|
||
PLUGIN_PRERELEASE_NPM_SPEC_TEST,
|
||
"src/plugins/tools.optional.test.ts",
|
||
]);
|
||
expect(shards.filter((shard) => shard.shardName !== "agentic-plugins")).toEqual(
|
||
createNodeTestShards({ includeReleaseOnlyPluginShards: false }),
|
||
);
|
||
expect(createNodeTestShards({ ...options, includeReleaseOnlyPluginShards: true })).toEqual(
|
||
defaultShards,
|
||
);
|
||
});
|
||
|
||
it("does not widen plugin coverage for deleted tests, sources, docs, or directories", () => {
|
||
const deletedTest = "src/plugins/deleted-ci-routing.test.ts";
|
||
expect(existsSync(deletedTest)).toBe(false);
|
||
const options = {
|
||
includeReleaseOnlyPluginShards: false,
|
||
changedPaths: [deletedTest, "src/plugins/tools.ts", "src/plugins", "docs/ci.md"],
|
||
};
|
||
expect(createNodeTestShards(options)).toEqual(
|
||
createNodeTestShards({ includeReleaseOnlyPluginShards: false }),
|
||
);
|
||
});
|
||
|
||
it.each(
|
||
plannerHosts.flatMap((host) =>
|
||
["blacksmith", "github", "hybrid"].map((runnerBackend) =>
|
||
Object.assign({}, host, { runnerBackend }),
|
||
),
|
||
),
|
||
)(
|
||
"retains changed plugin tests once in $runnerBackend compact fallback without changing group policies ($label)",
|
||
({ runnerBackend, ...host }) => {
|
||
pinPlannerHost(host);
|
||
const options = {
|
||
compactMode: "pull-request" as const,
|
||
includeReleaseOnlyPluginShards: false,
|
||
runnerBackend,
|
||
};
|
||
const changedOptions = { ...options, changedPaths: STORE_ALIAS_CHANGED_PATHS };
|
||
// Each input has its own admission before runtime placement materializes caps.
|
||
const observations = vi.spyOn(testTimings, "readRuntimePlacementTimings").mockReturnValue([]);
|
||
const beforeAdmission = createNodeTestShardBundles(options);
|
||
const afterAdmission = createNodeTestShardBundles(changedOptions);
|
||
observations.mockRestore();
|
||
const before = getCommittedCompactPlan(options.compactMode, runnerBackend);
|
||
const after = createNodeTestShardBundles(changedOptions);
|
||
const groups = after.flatMap((shard) => shard.groups);
|
||
type Group = (typeof groups)[number];
|
||
const isRepartitionableTooling = (group: Group) =>
|
||
runnerBackend === "github" &&
|
||
/^core-tooling-\d+-hosted-\d+$/u.test(group.shard_name) &&
|
||
group.configs.includes("test/vitest/vitest.tooling.config.ts") &&
|
||
group.includePatterns !== undefined &&
|
||
group.includePatterns.length > 0 &&
|
||
!group.requiresDist &&
|
||
group.pretestBuildMode === undefined &&
|
||
[BUNDLED_NODE_TEST_RUNNER, DEFAULT_NODE_TEST_RUNNER, EXTRA_LARGE_NODE_TEST_RUNNER].includes(
|
||
group.runner,
|
||
);
|
||
const toolingParent = (group: Group) => group.shard_name.replace(/-hosted-\d+$/u, "");
|
||
const parallelTimingSuffix = (files: readonly string[] | undefined) =>
|
||
files?.some((file) => gatewayServerSerialTestFiles.includes(file))
|
||
? "-parallel-native-serial"
|
||
: "-parallel";
|
||
const timingFamilies = (plan: typeof before) => {
|
||
const families = new Map<string, Array<{ group: Group; part: number }>>();
|
||
for (const group of plan.flatMap((shard) => shard.groups)) {
|
||
const parallelAutoReply =
|
||
group.configs.length === 1 &&
|
||
group.configs[0] === "test/vitest/vitest.auto-reply-reply.config.ts";
|
||
if (parallelAutoReply || /^core-tooling-\d+-hosted-\d+$/u.test(group.shard_name)) {
|
||
expect(group.timing_key).toBeDefined();
|
||
}
|
||
if (
|
||
group.timing_key === undefined ||
|
||
(fileParallelAgentGroupNames.has(group.shard_name) &&
|
||
group.timing_key === `${group.shard_name}#file-parallel`)
|
||
) {
|
||
continue;
|
||
}
|
||
if (parallelAutoReply) {
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe("2");
|
||
}
|
||
const parallelCommands = group.configs.includes("test/vitest/vitest.commands.config.ts");
|
||
if (parallelCommands && /#file-parallel-(?:2|8)$/u.test(group.timing_key)) {
|
||
expect(group.timing_key).toMatch(
|
||
new RegExp(`^${group.shard_name}#file-parallel-(2|8)$`),
|
||
);
|
||
continue;
|
||
}
|
||
const isParallelServer =
|
||
group.configs.length === 1 &&
|
||
group.configs[0] === "test/vitest/vitest.gateway-server.config.ts";
|
||
const parallelAgentsCore =
|
||
group.configs.length === 1 && group.configs[0] === agentVitestProjectOwners.core.config;
|
||
const measuredMethods =
|
||
runnerBackend !== "github" &&
|
||
/^agentic-gateway-methods(?:-hosted-\d+)?$/u.test(group.shard_name);
|
||
if (parallelAgentsCore && !group.shard_name.includes("-hosted-")) {
|
||
expect(group.timing_key).toBe(`${group.shard_name}-parallel`);
|
||
expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe("2");
|
||
continue;
|
||
}
|
||
const key = parseCompactSplitTimingKey(group.timing_key);
|
||
if (!key) {
|
||
expect(parallelAutoReply || isParallelServer).toBe(true);
|
||
expect(group.timing_key).toBe(
|
||
`${group.shard_name}${parallelAutoReply ? "-parallel-2" : parallelTimingSuffix(group.includePatterns)}`,
|
||
);
|
||
continue;
|
||
}
|
||
const parent = key.parentShardName;
|
||
const part = key.part;
|
||
const hostedName = /^(.+)-hosted-([1-9]\d*)$/u.exec(group.shard_name);
|
||
const parentName = hostedName?.[1] ?? group.shard_name;
|
||
const parentFiles = defaultShards.find(
|
||
(shard) => shard.shardName === parentName,
|
||
)?.includePatterns;
|
||
expect(parent, "timing key parent must match hosted group name").toBe(
|
||
`${parentName}${parallelAutoReply ? "-parallel-2" : isParallelServer ? `${parallelTimingSuffix(parentFiles)}-stripes` : parallelAgentsCore ? "-parallel" : ""}${parallelCommands ? group.timing_key.match(/#file-parallel-(?:2|8)/u)?.[0] : ""}${measuredMethods ? "#workers-4" : ""}`,
|
||
);
|
||
expect(part, "timing key part must match hosted group ordinal").toBe(
|
||
hostedName ? Number(hostedName[2]) : 1,
|
||
);
|
||
const familyParent = parent.replace("#file-parallel-8", "#file-parallel-2");
|
||
const family = families.get(familyParent) ?? [];
|
||
family.push({ group, part });
|
||
families.set(familyParent, family);
|
||
}
|
||
for (const family of families.values()) {
|
||
family.sort((a, b) => a.part - b.part);
|
||
}
|
||
return families;
|
||
};
|
||
// Hosted declarations keep their original pins and never transfer a measured
|
||
// Gateway bin's job cap into sibling env. They remain an independent oracle.
|
||
const hostedDeclarations = new Map<string, Group["env"]>();
|
||
for (const group of getCommittedCompactPlan(options.compactMode, "github").flatMap(
|
||
(job) => job.groups,
|
||
)) {
|
||
const owner = group.shard_name.replace(/-hosted-\d+$/u, "");
|
||
if (hostedDeclarations.has(owner)) {
|
||
expect(group.env).toEqual(hostedDeclarations.get(owner));
|
||
}
|
||
hostedDeclarations.set(owner, group.env);
|
||
}
|
||
// The changed-only plugin fixture is absent from the normal compact plan.
|
||
for (const shard of defaultShards) {
|
||
if (!hostedDeclarations.has(shard.shardName)) {
|
||
hostedDeclarations.set(shard.shardName, shard.env);
|
||
}
|
||
}
|
||
const inheritedGroupsFor = (admission: typeof before) => {
|
||
const inherited = new Map(
|
||
admission.flatMap((job) =>
|
||
job.planConcurrency === 2
|
||
? job.groups
|
||
.filter(
|
||
(group) =>
|
||
group.env?.OPENCLAW_VITEST_MAX_WORKERS === undefined &&
|
||
group.fallbackMaxWorkers === undefined,
|
||
)
|
||
.map((group): [string, Group] => [group.shard_name, group])
|
||
: [],
|
||
),
|
||
);
|
||
for (const job of admission) {
|
||
// Memory-gated rows also preserve sibling caps when requesting more RAM,
|
||
// including runtime rows that were already serial before promotion.
|
||
if (
|
||
runnerBackend === "github" ||
|
||
!usesParallelPacking(job) ||
|
||
job.runner !== EXTRA_LARGE_NODE_TEST_RUNNER ||
|
||
(job.pretestBuildMode !== undefined &&
|
||
!job.groups.some((group) => group.minTotalMemoryBytes !== undefined)) ||
|
||
job.requiresDist ||
|
||
!job.groups.some((group) => group.fallbackMaxWorkers === 2) ||
|
||
!job.groups.some((group) => group.configs.some(isExclusiveCiTestConfig))
|
||
) {
|
||
continue;
|
||
}
|
||
expect(job.planConcurrency).toBe(1);
|
||
expect(job.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined();
|
||
for (const group of job.groups.filter(
|
||
(entry) => entry.fallbackMaxWorkers === undefined,
|
||
)) {
|
||
const owner = group.shard_name.replace(/-hosted-\d+$/u, "");
|
||
expect(hostedDeclarations.has(owner), owner).toBe(true);
|
||
const declaredEnv = hostedDeclarations.get(owner);
|
||
expect(group.env).toEqual({
|
||
...declaredEnv,
|
||
OPENCLAW_VITEST_MAX_WORKERS: String(
|
||
Math.min(2, Number(declaredEnv?.OPENCLAW_VITEST_MAX_WORKERS ?? 2)),
|
||
),
|
||
});
|
||
if (declaredEnv?.OPENCLAW_VITEST_MAX_WORKERS === undefined) {
|
||
inherited.set(group.shard_name, { ...group, env: declaredEnv });
|
||
}
|
||
}
|
||
}
|
||
return inherited;
|
||
};
|
||
const beforeInherited = inheritedGroupsFor(beforeAdmission);
|
||
const afterInherited = inheritedGroupsFor(afterAdmission);
|
||
const declarationEnv = (
|
||
group: Pick<Group, "shard_name" | "env">,
|
||
plan: typeof before,
|
||
originals: Map<string, Group>,
|
||
) => {
|
||
const original = originals.get(group.shard_name);
|
||
const job = plan.find((entry) =>
|
||
entry.groups.some((candidate) => candidate.shard_name === group.shard_name),
|
||
);
|
||
if (original && job?.planConcurrency === 1) {
|
||
expect(
|
||
group.env?.OPENCLAW_VITEST_MAX_WORKERS ?? job.env?.OPENCLAW_VITEST_MAX_WORKERS,
|
||
).toBe("2");
|
||
const { OPENCLAW_VITEST_MAX_WORKERS: _workers, ...otherEnv } = group.env ?? {};
|
||
expect(otherEnv).toEqual(original.env ?? {});
|
||
return original.env;
|
||
}
|
||
return group.env;
|
||
};
|
||
const expectPluginPolicy = (plan: typeof after, originals: Map<string, Group>) => {
|
||
expect(
|
||
plan
|
||
.flatMap((job) => job.groups)
|
||
.filter((group) => group.shard_name === "agentic-plugins")
|
||
.map((group) =>
|
||
Object.assign({}, group, { env: declarationEnv(group, plan, originals) }),
|
||
),
|
||
).toEqual([
|
||
{
|
||
shard_name: "agentic-plugins",
|
||
configs: ["test/vitest/vitest.plugins.config.ts"],
|
||
includePatterns: ["src/plugins/tools.optional.test.ts"],
|
||
requiresDist: false,
|
||
runner: expect.stringMatching(/^blacksmith-(?:4|8)vcpu-ubuntu-2404$/u),
|
||
},
|
||
]);
|
||
};
|
||
expectPluginPolicy(after, afterInherited);
|
||
// Keep the transition controls independent of current inventory placement.
|
||
const pluginDeclaration = {
|
||
...expectDefined(
|
||
afterAdmission
|
||
.flatMap((job) => job.groups)
|
||
.find((group) => group.shard_name === "agentic-plugins"),
|
||
"declared plugin group",
|
||
),
|
||
};
|
||
const pluginEnv = declarationEnv(pluginDeclaration, afterAdmission, afterInherited);
|
||
if (pluginEnv === undefined) {
|
||
delete pluginDeclaration.env;
|
||
} else {
|
||
pluginDeclaration.env = pluginEnv;
|
||
}
|
||
const pluginAdmission: CompactNodeTestShard = {
|
||
checkName: "plugin-policy-control",
|
||
shardName: "plugin-policy-control",
|
||
groups: [pluginDeclaration],
|
||
requiresDist: false,
|
||
runner: pluginDeclaration.runner,
|
||
planConcurrency: 2,
|
||
};
|
||
const pinnedPlugin: Group = {
|
||
...pluginDeclaration,
|
||
env: { OPENCLAW_VITEST_MAX_WORKERS: "2" },
|
||
};
|
||
const promotedPlugin = [{ ...pluginAdmission, groups: [pinnedPlugin], planConcurrency: 1 }];
|
||
const pluginInherited = inheritedGroupsFor([pluginAdmission]);
|
||
expectPluginPolicy(promotedPlugin, pluginInherited);
|
||
const invalidPluginEnvs: Array<Group["env"]> = [
|
||
undefined,
|
||
{ OPENCLAW_VITEST_MAX_WORKERS: "1" },
|
||
{ OPENCLAW_VITEST_MAX_WORKERS: "3" },
|
||
{ OPENCLAW_VITEST_MAX_WORKERS: "2", UNDECLARED_POLICY: "1" },
|
||
];
|
||
for (const env of invalidPluginEnvs) {
|
||
pinnedPlugin.env = env;
|
||
expect(() => expectPluginPolicy(promotedPlugin, pluginInherited)).toThrow();
|
||
}
|
||
pinnedPlugin.env = { OPENCLAW_VITEST_MAX_WORKERS: "2" };
|
||
expect(() =>
|
||
expectPluginPolicy(
|
||
promotedPlugin,
|
||
inheritedGroupsFor([{ ...pluginAdmission, planConcurrency: 1 }]),
|
||
),
|
||
).toThrow();
|
||
const expectedTimingKeys = (
|
||
parent: string,
|
||
family: Array<{ group: Group; part: number }>,
|
||
plan: typeof before,
|
||
originals: Map<string, Group>,
|
||
) => {
|
||
const first = expectDefined(family[0], "first timing family entry").group;
|
||
const env = declarationEnv(first, plan, originals);
|
||
return createCompactSplitTimingGeneration({
|
||
parentShardName: parent,
|
||
configs: first.configs,
|
||
env: first.configs.includes("test/vitest/vitest.commands.config.ts")
|
||
? { ...env, OPENCLAW_VITEST_MAX_WORKERS: "2" }
|
||
: env,
|
||
stripes: family.map(({ group }) => {
|
||
expect(group.configs).toEqual(first.configs);
|
||
expect(declarationEnv(group, plan, originals)).toEqual(env);
|
||
const files = expectDefined(group.includePatterns, "timing family group membership");
|
||
expect(files.length).toBeGreaterThan(0);
|
||
return files;
|
||
}),
|
||
}).timingKeys;
|
||
};
|
||
const expectTimingFamilies = (plan: typeof before, originals: Map<string, Group>) => {
|
||
for (const [parent, family] of timingFamilies(plan)) {
|
||
expect(family.map(({ part }) => part)).toEqual(
|
||
Array.from({ length: family.length }, (_, index) => index + 1),
|
||
);
|
||
expect(
|
||
family.map(({ group }) =>
|
||
group.timing_key?.replace("#file-parallel-8", "#file-parallel-2"),
|
||
),
|
||
).toEqual(expectedTimingKeys(parent, family, plan, originals));
|
||
}
|
||
};
|
||
const policies = (plan: typeof before, originals: Map<string, Group>) => {
|
||
const nonPlugin = plan
|
||
.flatMap((shard) => shard.groups)
|
||
.filter((group) => group.shard_name !== "agentic-plugins");
|
||
return {
|
||
descriptors: nonPlugin
|
||
.filter((group) => !isRepartitionableTooling(group))
|
||
.map(({ runner: _runner, ...group }) => {
|
||
if (group.configs.includes("test/vitest/vitest.commands.config.ts")) {
|
||
group.timing_key = group.timing_key?.replace(
|
||
"#file-parallel-8",
|
||
"#file-parallel-2",
|
||
);
|
||
}
|
||
const env = declarationEnv(group, plan, originals);
|
||
if (env === undefined) {
|
||
delete group.env;
|
||
} else {
|
||
group.env = env;
|
||
}
|
||
return group;
|
||
})
|
||
.toSorted((a, b) => a.shard_name.localeCompare(b.shard_name)),
|
||
// Allocation may change, but every file must retain its complete execution policy.
|
||
tooling: nonPlugin
|
||
.filter(isRepartitionableTooling)
|
||
.flatMap((group) => {
|
||
const files = expectDefined(
|
||
group.includePatterns,
|
||
"repartitionable tooling membership",
|
||
);
|
||
// A split can move tests out of the compiler's larger runner group.
|
||
expect(group.runner).toBe(
|
||
files.includes("test/scripts/write-unified-entry-dts.test.ts")
|
||
? DEFAULT_NODE_TEST_RUNNER
|
||
: BUNDLED_NODE_TEST_RUNNER,
|
||
);
|
||
return files.map((file) => ({
|
||
parent: toolingParent(group),
|
||
file,
|
||
configs: group.configs,
|
||
env: group.env,
|
||
pretestBuildMode: group.pretestBuildMode,
|
||
requiresDist: group.requiresDist,
|
||
exclusive: isExclusiveCompactShardName(group.shard_name),
|
||
}));
|
||
})
|
||
.toSorted((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
||
};
|
||
};
|
||
expectTimingFamilies(before, beforeInherited);
|
||
expectTimingFamilies(after, afterInherited);
|
||
expect(policies(after, afterInherited)).toEqual(policies(before, beforeInherited));
|
||
if (runnerBackend === "hybrid") {
|
||
const serial = structuredClone(before);
|
||
const serialGroup = expectDefined(
|
||
serial
|
||
.filter(
|
||
(job) => job.planConcurrency === 1 && job.env?.OPENCLAW_VITEST_MAX_WORKERS === "2",
|
||
)
|
||
.flatMap((job) => job.groups)
|
||
.find((group) => group.env?.OPENCLAW_VITEST_MAX_WORKERS === undefined),
|
||
"already-serial group using its job worker cap",
|
||
);
|
||
serialGroup.env = { ...serialGroup.env, OPENCLAW_VITEST_MAX_WORKERS: "2" };
|
||
expect(() =>
|
||
expect(policies(serial, beforeInherited)).toEqual(policies(before, beforeInherited)),
|
||
).toThrow();
|
||
const promoted = structuredClone(before);
|
||
const recipient = expectDefined(
|
||
promoted.find(
|
||
(job) =>
|
||
job.planConcurrency === 2 &&
|
||
job.groups.some(
|
||
(group) =>
|
||
group.timing_key &&
|
||
parseCompactSplitTimingKey(group.timing_key) &&
|
||
group.env?.OPENCLAW_VITEST_MAX_WORKERS === undefined,
|
||
) &&
|
||
job.groups.every(
|
||
(group) =>
|
||
group.env?.OPENCLAW_VITEST_MAX_WORKERS !== undefined ||
|
||
isRuntimePlacementIncludePatterns(group.includePatterns),
|
||
),
|
||
),
|
||
"existing parallel recipient with hosted timing parts",
|
||
);
|
||
recipient.planConcurrency = 1;
|
||
recipient.pretestBuildMode = "runtime";
|
||
const keys = recipient.groups.map((group) => group.timing_key);
|
||
for (const group of recipient.groups.filter(
|
||
(entry) => entry.fallbackMaxWorkers === undefined,
|
||
)) {
|
||
group.env = { OPENCLAW_VITEST_MAX_WORKERS: "2", ...group.env };
|
||
}
|
||
expectTimingFamilies(promoted, beforeInherited);
|
||
expect(policies(promoted, beforeInherited)).toEqual(policies(before, beforeInherited));
|
||
expect(recipient.groups.map((group) => group.timing_key)).toEqual(keys);
|
||
recipient.env = { ...recipient.env, OPENCLAW_VITEST_MAX_WORKERS: "2" };
|
||
for (const group of recipient.groups) {
|
||
const original = beforeInherited.get(group.shard_name);
|
||
if (original) {
|
||
group.env = original.env;
|
||
}
|
||
}
|
||
expectTimingFamilies(promoted, beforeInherited);
|
||
expect(policies(promoted, beforeInherited)).toEqual(policies(before, beforeInherited));
|
||
delete recipient.env.OPENCLAW_VITEST_MAX_WORKERS;
|
||
for (const group of recipient.groups.filter(
|
||
(entry) => entry.fallbackMaxWorkers === undefined,
|
||
)) {
|
||
group.env = { OPENCLAW_VITEST_MAX_WORKERS: "2", ...group.env };
|
||
}
|
||
const hosted = expectDefined(
|
||
recipient.groups.find(
|
||
(group) =>
|
||
group.timing_key &&
|
||
parseCompactSplitTimingKey(group.timing_key) &&
|
||
beforeInherited.has(group.shard_name),
|
||
),
|
||
"hosted recipient group",
|
||
);
|
||
const original = expectDefined(
|
||
beforeInherited.get(hosted.shard_name),
|
||
"original parallel group",
|
||
);
|
||
for (const env of [original.env, { ...original.env, OPENCLAW_VITEST_MAX_WORKERS: "3" }]) {
|
||
if (env === undefined) {
|
||
delete hosted.env;
|
||
} else {
|
||
hosted.env = env;
|
||
}
|
||
expect(() => expectTimingFamilies(promoted, beforeInherited)).toThrow();
|
||
expect(() =>
|
||
expect(policies(promoted, beforeInherited)).toEqual(policies(before, beforeInherited)),
|
||
).toThrow();
|
||
}
|
||
}
|
||
if (runnerBackend === "github") {
|
||
const regenerateTimingKeys = (plan: typeof before) => {
|
||
for (const [parent, family] of timingFamilies(plan)) {
|
||
const keys = expectedTimingKeys(parent, family, plan, afterInherited);
|
||
family.forEach(({ group }, index) => {
|
||
group.timing_key = expectDefined(keys[index], "regenerated family timing key");
|
||
});
|
||
}
|
||
};
|
||
for (const mutation of ["missing", "duplicated", "env", "runner"] as const) {
|
||
const mutated = structuredClone(after);
|
||
const tooling = mutated.flatMap((shard) => shard.groups).filter(isRepartitionableTooling);
|
||
const target = expectDefined(
|
||
tooling.find(
|
||
(group) =>
|
||
expectDefined(group.includePatterns, "tooling mutation membership").length > 1,
|
||
),
|
||
"multi-file tooling mutation target",
|
||
);
|
||
const files = expectDefined(target.includePatterns, "tooling mutation target membership");
|
||
if (mutation === "missing") {
|
||
files.pop();
|
||
} else if (mutation === "duplicated") {
|
||
const otherFamily = expectDefined(
|
||
tooling.find((group) => toolingParent(group) !== toolingParent(target)),
|
||
"tooling group from another family",
|
||
);
|
||
files.push(
|
||
expectDefined(
|
||
expectDefined(otherFamily.includePatterns, "other tooling family membership")[0],
|
||
"file from another tooling family",
|
||
),
|
||
);
|
||
} else if (mutation === "env") {
|
||
for (const { group } of expectDefined(
|
||
timingFamilies(mutated).get(toolingParent(target)),
|
||
"timing family for environment mutation",
|
||
)) {
|
||
group.env = { ...group.env, OPENCLAW_VITEST_MAX_WORKERS: "3" };
|
||
}
|
||
} else {
|
||
target.runner =
|
||
target.runner === BUNDLED_NODE_TEST_RUNNER
|
||
? DEFAULT_NODE_TEST_RUNNER
|
||
: BUNDLED_NODE_TEST_RUNNER;
|
||
}
|
||
regenerateTimingKeys(mutated);
|
||
expectTimingFamilies(mutated, afterInherited);
|
||
expect(
|
||
() =>
|
||
expect(policies(mutated, afterInherited)).toEqual(policies(before, beforeInherited)),
|
||
`${mutation} must fail policy validation even with valid timing keys`,
|
||
).toThrow();
|
||
}
|
||
for (const identity of ["parent", "part"] as const) {
|
||
const forged = structuredClone(after);
|
||
const families = timingFamilies(forged);
|
||
const [parent, family] = expectDefined(
|
||
[...families].find(([, entries]) => entries.length >= 2),
|
||
"multi-part timing family for identity control",
|
||
);
|
||
const wrongParent = `${parent}-forged`;
|
||
expect(families.has(wrongParent)).toBe(false);
|
||
const ordered = identity === "part" ? family.toReversed() : family;
|
||
const keys = expectedTimingKeys(
|
||
identity === "parent" ? wrongParent : parent,
|
||
ordered,
|
||
forged,
|
||
afterInherited,
|
||
);
|
||
ordered.forEach(({ group }, index) => {
|
||
group.timing_key = expectDefined(keys[index], "forged family timing key");
|
||
});
|
||
expect(() => expectTimingFamilies(forged, afterInherited)).toThrow(
|
||
identity === "parent"
|
||
? "timing key parent must match hosted group name"
|
||
: "timing key part must match hosted group ordinal",
|
||
);
|
||
}
|
||
const stale = structuredClone(after);
|
||
const staleGroup = expectDefined(
|
||
stale
|
||
.flatMap((shard) => shard.groups)
|
||
.find(
|
||
(group) =>
|
||
isRepartitionableTooling(group) &&
|
||
expectDefined(group.includePatterns, "stale-key control membership").length > 1,
|
||
),
|
||
"multi-file tooling group for stale-key control",
|
||
);
|
||
const savedKey = expectDefined(staleGroup.timing_key, "original stale-control timing key");
|
||
expectDefined(staleGroup.includePatterns, "stale-control group membership").pop();
|
||
regenerateTimingKeys(stale);
|
||
expectTimingFamilies(stale, afterInherited);
|
||
staleGroup.timing_key = savedKey;
|
||
expect(
|
||
() => expectTimingFamilies(stale, afterInherited),
|
||
"stale generation key must fail identity",
|
||
).toThrow();
|
||
}
|
||
expect(
|
||
after.every(
|
||
(shard) =>
|
||
(usesParallelPacking(shard) || shard.groups.length <= 10) &&
|
||
(shard.planConcurrency === 1 ||
|
||
(runnerBackend !== "github" &&
|
||
shard.planConcurrency === 2 &&
|
||
shard.runner === EXTRA_LARGE_NODE_TEST_RUNNER)),
|
||
),
|
||
).toBe(true);
|
||
expect(after.length).toBeLessThanOrEqual(90);
|
||
},
|
||
);
|
||
|
||
it("preserves auto-reply project ownership when splitting shards", () => {
|
||
const expected = fullSuiteVitestShards
|
||
.filter((shard) => shard.name === "auto-reply")
|
||
.flatMap((shard) => shard.projects);
|
||
const actual = defaultShards
|
||
.filter((shard) => shard.shardName.startsWith("auto-reply-"))
|
||
.flatMap((shard) => shard.configs);
|
||
expect(new Set(actual)).toEqual(new Set(expected));
|
||
});
|
||
|
||
it("covers every auto-reply reply test exactly once across split shards", () => {
|
||
const actual = defaultShards
|
||
.filter((shard) => shard.shardName.startsWith("auto-reply-reply-"))
|
||
.flatMap((shard) => shard.includePatterns ?? [])
|
||
.toSorted((a, b) => a.localeCompare(b));
|
||
|
||
expect(actual).toEqual(listTestFiles("src/auto-reply/reply"));
|
||
expect(new Set(actual).size).toBe(actual.length);
|
||
});
|
||
|
||
it("keeps each dispatch entrypoint in its own dedicated shard", () => {
|
||
const dispatchEntrypoints = new Map([
|
||
["auto-reply-reply-dispatch-core", "src/auto-reply/reply/dispatch-from-config.test.ts"],
|
||
[
|
||
"auto-reply-reply-dispatch-delivery",
|
||
"src/auto-reply/reply/dispatch-from-config.delivery.test.ts",
|
||
],
|
||
[
|
||
"auto-reply-reply-dispatch-lifecycle",
|
||
"src/auto-reply/reply/dispatch-from-config.lifecycle.test.ts",
|
||
],
|
||
]);
|
||
const shards = defaultShards;
|
||
|
||
for (const [shardName, entrypoint] of dispatchEntrypoints) {
|
||
expect(shards.find((shard) => shard.shardName === shardName)?.includePatterns).toEqual([
|
||
entrypoint,
|
||
]);
|
||
}
|
||
});
|
||
});
|