mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 11:27:01 +01:00
* ci: collapse preflight manifest routing * ci: fix preflight workflow outputs * ci: restore compat workflow tasks * ci: match macos shards to windows * ci: collapse macos swift jobs * ci: skip empty submodule setup * ci: drop submodule setup from node env
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
createExecutionArtifacts,
|
|
resolvePnpmCommandInvocation,
|
|
} from "../../scripts/test-planner/executor.mjs";
|
|
import {
|
|
buildCIExecutionManifest,
|
|
buildExecutionPlan,
|
|
explainExecutionTarget,
|
|
} from "../../scripts/test-planner/planner.mjs";
|
|
|
|
describe("test planner", () => {
|
|
it("builds a capability-aware plan for mid-memory local runs", () => {
|
|
const artifacts = createExecutionArtifacts({
|
|
RUNNER_OS: "macOS",
|
|
OPENCLAW_TEST_HOST_CPU_COUNT: "10",
|
|
OPENCLAW_TEST_HOST_MEMORY_GIB: "64",
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
});
|
|
const plan = buildExecutionPlan(
|
|
{
|
|
profile: null,
|
|
mode: "local",
|
|
surfaces: ["unit", "extensions"],
|
|
passthroughArgs: [],
|
|
},
|
|
{
|
|
env: {
|
|
RUNNER_OS: "macOS",
|
|
OPENCLAW_TEST_HOST_CPU_COUNT: "10",
|
|
OPENCLAW_TEST_HOST_MEMORY_GIB: "64",
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
},
|
|
platform: "darwin",
|
|
writeTempJsonArtifact: artifacts.writeTempJsonArtifact,
|
|
},
|
|
);
|
|
|
|
expect(plan.runtimeCapabilities.runtimeProfileName).toBe("local-darwin");
|
|
expect(plan.runtimeCapabilities.memoryBand).toBe("mid");
|
|
expect(plan.executionBudget.unitSharedWorkers).toBe(4);
|
|
expect(plan.executionBudget.topLevelParallelLimitNoIsolate).toBe(8);
|
|
expect(plan.executionBudget.topLevelParallelLimitIsolated).toBe(3);
|
|
expect(plan.selectedUnits.some((unit) => unit.id.startsWith("unit-fast"))).toBe(true);
|
|
expect(plan.selectedUnits.some((unit) => unit.id.startsWith("extensions"))).toBe(true);
|
|
expect(plan.topLevelParallelLimit).toBe(8);
|
|
artifacts.cleanupTempArtifacts();
|
|
});
|
|
|
|
it("scales down mid-tier local concurrency under saturated load", () => {
|
|
const artifacts = createExecutionArtifacts({
|
|
RUNNER_OS: "Linux",
|
|
OPENCLAW_TEST_HOST_CPU_COUNT: "10",
|
|
OPENCLAW_TEST_HOST_MEMORY_GIB: "64",
|
|
});
|
|
const plan = buildExecutionPlan(
|
|
{
|
|
profile: null,
|
|
mode: "local",
|
|
surfaces: ["unit", "extensions"],
|
|
passthroughArgs: [],
|
|
},
|
|
{
|
|
env: {
|
|
RUNNER_OS: "Linux",
|
|
OPENCLAW_TEST_HOST_CPU_COUNT: "10",
|
|
OPENCLAW_TEST_HOST_MEMORY_GIB: "64",
|
|
},
|
|
platform: "linux",
|
|
loadAverage: [11.5, 11.5, 11.5],
|
|
writeTempJsonArtifact: artifacts.writeTempJsonArtifact,
|
|
},
|
|
);
|
|
|
|
expect(plan.runtimeCapabilities.memoryBand).toBe("mid");
|
|
expect(plan.runtimeCapabilities.loadBand).toBe("saturated");
|
|
expect(plan.executionBudget.unitSharedWorkers).toBe(2);
|
|
expect(plan.executionBudget.topLevelParallelLimitNoIsolate).toBe(4);
|
|
expect(plan.executionBudget.topLevelParallelLimitIsolated).toBe(1);
|
|
expect(plan.topLevelParallelLimit).toBe(4);
|
|
expect(plan.deferredRunConcurrency).toBe(1);
|
|
artifacts.cleanupTempArtifacts();
|
|
});
|
|
|
|
it("honors the max-profile top-level no-isolate cap without adding extra lanes", () => {
|
|
const artifacts = createExecutionArtifacts({
|
|
RUNNER_OS: "Linux",
|
|
OPENCLAW_TEST_HOST_CPU_COUNT: "16",
|
|
OPENCLAW_TEST_HOST_MEMORY_GIB: "128",
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
OPENCLAW_TEST_PROFILE: "max",
|
|
});
|
|
const plan = buildExecutionPlan(
|
|
{
|
|
profile: "max",
|
|
mode: "local",
|
|
surfaces: ["unit", "extensions"],
|
|
passthroughArgs: [],
|
|
},
|
|
{
|
|
env: {
|
|
RUNNER_OS: "Linux",
|
|
OPENCLAW_TEST_HOST_CPU_COUNT: "16",
|
|
OPENCLAW_TEST_HOST_MEMORY_GIB: "128",
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
OPENCLAW_TEST_PROFILE: "max",
|
|
},
|
|
platform: "linux",
|
|
writeTempJsonArtifact: artifacts.writeTempJsonArtifact,
|
|
},
|
|
);
|
|
|
|
expect(plan.runtimeCapabilities.intentProfile).toBe("max");
|
|
expect(plan.executionBudget.topLevelParallelLimitNoIsolate).toBe(8);
|
|
expect(plan.topLevelParallelLimit).toBe(8);
|
|
artifacts.cleanupTempArtifacts();
|
|
});
|
|
|
|
it("splits mixed targeted file selections across surfaces", () => {
|
|
const artifacts = createExecutionArtifacts({});
|
|
const plan = buildExecutionPlan(
|
|
{
|
|
mode: "local",
|
|
surfaces: [],
|
|
passthroughArgs: [
|
|
"src/auto-reply/reply/followup-runner.test.ts",
|
|
"extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts",
|
|
],
|
|
},
|
|
{
|
|
env: {},
|
|
writeTempJsonArtifact: artifacts.writeTempJsonArtifact,
|
|
},
|
|
);
|
|
|
|
expect(plan.targetedUnits).toHaveLength(2);
|
|
expect(
|
|
plan.targetedUnits
|
|
.map((unit) => unit.surface)
|
|
.toSorted((left, right) => left.localeCompare(right)),
|
|
).toEqual(["base", "channels"]);
|
|
artifacts.cleanupTempArtifacts();
|
|
});
|
|
|
|
it("explains runtime truth using the same catalog and worker policy", () => {
|
|
const explanation = explainExecutionTarget(
|
|
{
|
|
mode: "local",
|
|
fileFilters: ["src/auto-reply/reply/followup-runner.test.ts"],
|
|
},
|
|
{
|
|
env: {},
|
|
},
|
|
);
|
|
|
|
expect(explanation.surface).toBe("base");
|
|
expect(explanation.pool).toBe("forks");
|
|
expect(explanation.reasons).toContain("base-pinned-manifest");
|
|
expect(explanation.intentProfile).toBe("normal");
|
|
});
|
|
|
|
it("uses hotspot-backed memory isolation when explaining unit tests", () => {
|
|
const explanation = explainExecutionTarget(
|
|
{
|
|
mode: "local",
|
|
fileFilters: ["src/infra/outbound/targets.channel-resolution.test.ts"],
|
|
},
|
|
{
|
|
env: {
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(explanation.isolate).toBe(true);
|
|
expect(explanation.reasons).toContain("unit-memory-isolated");
|
|
});
|
|
|
|
it("normalizes absolute explain targets before classification", () => {
|
|
const relativeExplanation = explainExecutionTarget(
|
|
{
|
|
mode: "local",
|
|
fileFilters: ["src/infra/outbound/targets.channel-resolution.test.ts"],
|
|
},
|
|
{
|
|
env: {
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
},
|
|
},
|
|
);
|
|
const absoluteExplanation = explainExecutionTarget(
|
|
{
|
|
mode: "local",
|
|
fileFilters: [
|
|
path.join(process.cwd(), "src/infra/outbound/targets.channel-resolution.test.ts"),
|
|
],
|
|
},
|
|
{
|
|
env: {
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(absoluteExplanation.file).toBe(relativeExplanation.file);
|
|
expect(absoluteExplanation.surface).toBe(relativeExplanation.surface);
|
|
expect(absoluteExplanation.pool).toBe(relativeExplanation.pool);
|
|
expect(absoluteExplanation.isolate).toBe(relativeExplanation.isolate);
|
|
expect(absoluteExplanation.reasons).toEqual(relativeExplanation.reasons);
|
|
});
|
|
|
|
it("does not leak default-plan shard assignments into targeted units with the same id", () => {
|
|
const artifacts = createExecutionArtifacts({});
|
|
const plan = buildExecutionPlan(
|
|
{
|
|
mode: "local",
|
|
fileFilters: ["src/cli/qr-dashboard.integration.test.ts"],
|
|
passthroughArgs: [],
|
|
},
|
|
{
|
|
env: {
|
|
OPENCLAW_TEST_SHARDS: "4",
|
|
OPENCLAW_TEST_SHARD_INDEX: "2",
|
|
OPENCLAW_TEST_LOAD_AWARE: "0",
|
|
},
|
|
writeTempJsonArtifact: artifacts.writeTempJsonArtifact,
|
|
},
|
|
);
|
|
|
|
const targetedUnit = plan.targetedUnits.at(0);
|
|
const defaultUnitWithSameId = plan.allUnits.find((unit) => unit.id === targetedUnit?.id);
|
|
|
|
expect(targetedUnit).toBeTruthy();
|
|
expect(defaultUnitWithSameId).toBeTruthy();
|
|
expect(defaultUnitWithSameId).not.toBe(targetedUnit);
|
|
expect(plan.topLevelSingleShardAssignments.get(targetedUnit)).toBeUndefined();
|
|
expect(plan.topLevelSingleShardAssignments.get(defaultUnitWithSameId)).toBeDefined();
|
|
|
|
artifacts.cleanupTempArtifacts();
|
|
});
|
|
|
|
it("removes planner temp artifacts when cleanup runs after planning", () => {
|
|
const artifacts = createExecutionArtifacts({});
|
|
buildExecutionPlan(
|
|
{
|
|
mode: "local",
|
|
surfaces: ["unit"],
|
|
passthroughArgs: [],
|
|
},
|
|
{
|
|
env: {},
|
|
writeTempJsonArtifact: artifacts.writeTempJsonArtifact,
|
|
},
|
|
);
|
|
|
|
const artifactDir = artifacts.ensureTempArtifactDir();
|
|
expect(fs.existsSync(artifactDir)).toBe(true);
|
|
artifacts.cleanupTempArtifacts();
|
|
expect(fs.existsSync(artifactDir)).toBe(false);
|
|
});
|
|
|
|
it("builds a CI manifest with planner-owned shard counts and matrices", () => {
|
|
const manifest = buildCIExecutionManifest(
|
|
{
|
|
eventName: "pull_request",
|
|
docsOnly: false,
|
|
docsChanged: false,
|
|
runNode: true,
|
|
runMacos: true,
|
|
runAndroid: true,
|
|
runWindows: true,
|
|
runSkillsPython: false,
|
|
hasChangedExtensions: true,
|
|
changedExtensionsMatrix: { include: [{ extension: "discord" }] },
|
|
},
|
|
{
|
|
env: {},
|
|
},
|
|
);
|
|
|
|
expect(manifest.jobs.buildArtifacts.enabled).toBe(true);
|
|
expect(manifest.shardCounts.unit).toBe(4);
|
|
expect(manifest.shardCounts.channels).toBe(3);
|
|
expect(manifest.shardCounts.windows).toBe(9);
|
|
expect(manifest.shardCounts.macosNode).toBe(9);
|
|
expect(manifest.jobs.checks.matrix.include).toHaveLength(7);
|
|
expect(manifest.jobs.checksWindows.matrix.include).toHaveLength(9);
|
|
expect(manifest.jobs.macosNode.matrix.include).toHaveLength(9);
|
|
expect(manifest.jobs.macosSwift.enabled).toBe(true);
|
|
expect(manifest.requiredCheckNames).toContain("macos-swift");
|
|
expect(manifest.requiredCheckNames).not.toContain("macos-swift-lint");
|
|
expect(manifest.requiredCheckNames).not.toContain("macos-swift-build");
|
|
expect(manifest.requiredCheckNames).not.toContain("macos-swift-test");
|
|
expect(manifest.jobs.extensionFast.matrix.include).toEqual([
|
|
{ check_name: "extension-fast-discord", extension: "discord" },
|
|
]);
|
|
});
|
|
|
|
it("suppresses heavy CI jobs in docs-only manifests", () => {
|
|
const manifest = buildCIExecutionManifest(
|
|
{
|
|
eventName: "pull_request",
|
|
docsOnly: true,
|
|
docsChanged: true,
|
|
runNode: false,
|
|
runMacos: false,
|
|
runAndroid: false,
|
|
runWindows: false,
|
|
runSkillsPython: false,
|
|
hasChangedExtensions: false,
|
|
},
|
|
{
|
|
env: {},
|
|
},
|
|
);
|
|
|
|
expect(manifest.jobs.buildArtifacts.enabled).toBe(false);
|
|
expect(manifest.jobs.checks.enabled).toBe(false);
|
|
expect(manifest.jobs.checksWindows.enabled).toBe(false);
|
|
expect(manifest.jobs.macosNode.enabled).toBe(false);
|
|
expect(manifest.jobs.checkDocs.enabled).toBe(true);
|
|
});
|
|
|
|
it("adds push-only compat and release lanes to push manifests", () => {
|
|
const manifest = buildCIExecutionManifest(
|
|
{
|
|
eventName: "push",
|
|
docsOnly: false,
|
|
docsChanged: false,
|
|
runNode: true,
|
|
runMacos: false,
|
|
runAndroid: false,
|
|
runWindows: false,
|
|
runSkillsPython: false,
|
|
hasChangedExtensions: false,
|
|
},
|
|
{
|
|
env: {},
|
|
},
|
|
);
|
|
|
|
expect(manifest.jobs.releaseCheck.enabled).toBe(true);
|
|
expect(
|
|
manifest.jobs.checks.matrix.include.some((entry) => entry.task === "compat-node22"),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("resolvePnpmCommandInvocation", () => {
|
|
it("prefers the parent pnpm CLI path when npm_execpath points to pnpm", () => {
|
|
expect(
|
|
resolvePnpmCommandInvocation({
|
|
npmExecPath: "/opt/homebrew/lib/node_modules/corepack/dist/pnpm.cjs",
|
|
nodeExecPath: "/usr/local/bin/node",
|
|
platform: "linux",
|
|
}),
|
|
).toEqual({
|
|
command: "/usr/local/bin/node",
|
|
args: ["/opt/homebrew/lib/node_modules/corepack/dist/pnpm.cjs"],
|
|
});
|
|
});
|
|
|
|
it("falls back to cmd.exe mediation on Windows when npm_execpath is unavailable", () => {
|
|
expect(
|
|
resolvePnpmCommandInvocation({
|
|
npmExecPath: "",
|
|
platform: "win32",
|
|
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
|
}),
|
|
).toEqual({
|
|
command: "C:\\Windows\\System32\\cmd.exe",
|
|
args: ["/d", "/s", "/c", "pnpm.cmd"],
|
|
});
|
|
});
|
|
});
|