Files
openclaw/test/scripts/plugin-prerelease-telegram-shards.test.ts

235 lines
9.5 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path, { join } from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import {
DEFAULT_EXTENSION_TEST_SHARD_COUNT,
createExtensionTestShards,
listExtensionTestFilesForRoots,
splitExtensionTestJobTargets,
} from "../../scripts/lib/extension-test-plan.mts";
import { createVitestRunSpecs } from "../../scripts/test-projects.test-support.mts";
import { createExtensionTelegramVitestConfig } from "../vitest/vitest.extension-telegram.config.ts";
type WorkflowStep = {
env?: Record<string, string>;
name?: string;
run?: string;
};
type PluginPrereleaseMatrixRow = {
extensions_csv: string;
includePatterns: string[];
task: string;
};
function readPluginPrereleaseWorkflow() {
return parse(readFileSync(".github/workflows/plugin-prerelease.yml", "utf8"));
}
function listTelegramRunnableTestFiles() {
const testConfig = createExtensionTelegramVitestConfig({}).test ?? {};
const dir = testConfig.dir ?? process.cwd();
const exclude = (testConfig.exclude ?? []).map((pattern) =>
path.isAbsolute(pattern) ? path.relative(dir, pattern).replaceAll("\\", "/") : pattern,
);
return globSync(testConfig.include ?? [], { cwd: dir, exclude })
.map((file) => path.relative(process.cwd(), path.resolve(dir, file)).replaceAll("\\", "/"))
.toSorted((left, right) => left.localeCompare(right));
}
function runPluginPrereleaseManifest() {
const workflow = readPluginPrereleaseWorkflow();
const manifestStep = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build plugin prerelease manifest",
);
if (!manifestStep?.run) {
throw new Error("Missing plugin prerelease manifest step");
}
const source = manifestStep.run.match(
/node --import tsx --input-type=module <<'EOF'\n([\s\S]*?)\nEOF/u,
)?.[1];
if (!source) {
throw new Error("Missing plugin prerelease manifest source");
}
const root = mkdtempSync(join(tmpdir(), "openclaw-plugin-prerelease-telegram-shards-"));
const outputPath = join(root, "github-output");
try {
const env: NodeJS.ProcessEnv = {
...process.env,
EXPECTED_SHA: "",
FULL_RELEASE_VALIDATION: "false",
GITHUB_OUTPUT: outputPath,
};
delete env.OPENCLAW_VITEST_INCLUDE_FILE;
const result = spawnSync(process.execPath, ["--import", "tsx", "--input-type=module"], {
cwd: process.cwd(),
encoding: "utf8",
env,
input: source,
});
expect(result.status, result.stderr).toBe(0);
const output = new Map(
readFileSync(outputPath, "utf8")
.trim()
.split("\n")
.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
return JSON.parse(output.get("plugin_prerelease_extension_matrix") ?? "{}") as {
include: PluginPrereleaseMatrixRow[];
};
} finally {
rmSync(root, { force: true, recursive: true });
}
}
describe("plugin prerelease Telegram extension shards", () => {
it("keeps Telegram out of balanced batches and covers every extension exactly once", () => {
const allShards = createExtensionTestShards({
cwd: process.cwd(),
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
});
const allExtensionIds = allShards.flatMap((shard) => shard.extensionIds);
const genericExtensionIds = allExtensionIds.filter((extensionId) => extensionId !== "telegram");
const genericShards = createExtensionTestShards({
cwd: process.cwd(),
extensionIds: genericExtensionIds,
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
});
expect(genericShards).toHaveLength(DEFAULT_EXTENSION_TEST_SHARD_COUNT);
expect(genericShards.flatMap((shard) => shard.extensionIds)).not.toContain("telegram");
expect(
genericShards
.flatMap((shard) => shard.extensionIds)
.toSorted((left, right) => left.localeCompare(right)),
).toEqual(genericExtensionIds.toSorted((left, right) => left.localeCompare(right)));
expect(allExtensionIds.filter((extensionId) => extensionId === "telegram")).toEqual([
"telegram",
]);
expect(
allShards
.flatMap((shard) => shard.planGroups)
.find((group) => group.extensionIds.includes("telegram")),
).toMatchObject({
config: "test/vitest/vitest.extension-telegram.config.ts",
extensionIds: ["telegram"],
roots: ["extensions/telegram"],
});
expect(new Set(genericShards.flatMap((shard) => shard.extensionIds)).size).toBe(
genericExtensionIds.length,
);
});
it("keeps dedicated Telegram shards inside the existing aggregate job contract", () => {
const workflow = readPluginPrereleaseWorkflow();
const extensionJob = workflow.jobs["plugin-prerelease-extension-shard"];
const runStep = extensionJob.steps.find(
(step: WorkflowStep) => step.name === "Run extension shard",
);
const suite = workflow.jobs["plugin-prerelease-suite"];
const matrix = runPluginPrereleaseManifest();
const genericRows = matrix.include.filter((row) => row.task === "extensions-batch");
const telegramRows = matrix.include.filter((row) => row.task === "extension-file-shard");
const allTelegramTestFiles = listExtensionTestFilesForRoots(["extensions/telegram"]);
const runnableTelegramTestFiles = listTelegramRunnableTestFiles();
expect(genericRows).toHaveLength(DEFAULT_EXTENSION_TEST_SHARD_COUNT);
expect(genericRows.some((row) => row.extensions_csv.split(",").includes("telegram"))).toBe(
false,
);
const telegramConfig = "test/vitest/vitest.extension-telegram.config.ts";
const expectedTelegramPartitions = splitExtensionTestJobTargets(
telegramConfig,
runnableTelegramTestFiles,
);
expect(telegramRows).toHaveLength(expectedTelegramPartitions.length);
expect(telegramRows).toEqual(
expectedTelegramPartitions.map((includePatterns, index) =>
expect.objectContaining({
check_name: `checks-node-extensions-telegram-shard-${index + 1}`,
extensions_csv: "telegram",
includePatterns,
runner: "blacksmith-8vcpu-ubuntu-2404",
vitest_config: telegramConfig,
}),
),
);
const telegramPartitions = telegramRows.map((row) => {
expect(row.includePatterns).toBeInstanceOf(Array);
return row.includePatterns;
});
expect(telegramPartitions.every((partition) => partition.length > 0)).toBe(true);
expect(telegramPartitions.flat().toSorted((left, right) => left.localeCompare(right))).toEqual(
runnableTelegramTestFiles,
);
expect(new Set(telegramPartitions.flat()).size).toBe(runnableTelegramTestFiles.length);
expect(telegramPartitions.every((partition) => partition.length <= 10)).toBe(true);
expect(Math.max(...telegramPartitions.map((partition) => partition.length))).toBe(10);
expect(
allTelegramTestFiles.filter((file) => !runnableTelegramTestFiles.includes(file)).length,
).toBeGreaterThan(0);
const tempDir = mkdtempSync(join(tmpdir(), "openclaw-plugin-prerelease-telegram-specs-"));
try {
for (const [index, partition] of telegramPartitions.entries()) {
const includeFile = join(tempDir, `telegram-shard-${index + 1}.json`);
writeFileSync(includeFile, JSON.stringify(partition));
const specs = createVitestRunSpecs(["test/vitest/vitest.extension-telegram.config.ts"], {
baseEnv: {
OPENCLAW_TEST_PROJECTS_PARALLEL: "2",
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
},
});
expect(specs).toHaveLength(partition.length);
expect(specs.map((spec) => spec.includePatterns)).toEqual(partition.map((file) => [file]));
expect(new Set(specs.map((spec) => spec.env.OPENCLAW_VITEST_INCLUDE_FILE)).size).toBe(
partition.length,
);
expect(specs.every((spec) => spec.env.OPENCLAW_VITEST_INCLUDE_FILE !== includeFile)).toBe(
true,
);
}
} finally {
rmSync(tempDir, { force: true, recursive: true });
}
expect(extensionJob.strategy["fail-fast"]).toBe(false);
expect(extensionJob.strategy["max-parallel"]).toBe(12);
expect(extensionJob["timeout-minutes"]).toBe(60);
expect(extensionJob.strategy.matrix).toBe(
"${{ fromJson(needs.preflight.outputs.plugin_prerelease_extension_matrix) }}",
);
expect(runStep?.env).toMatchObject({
OPENCLAW_EXTENSION_INCLUDE_PATTERNS_JSON: "${{ toJson(matrix.includePatterns) }}",
OPENCLAW_EXTENSION_TASK: "${{ matrix.task }}",
OPENCLAW_EXTENSION_VITEST_CONFIG: "${{ matrix.vitest_config }}",
});
expect(runStep?.run).toContain("extension-file-shard)");
expect(runStep?.run).toContain("OPENCLAW_TEST_PROJECTS_PARALLEL=2");
expect(runStep?.run).toContain('OPENCLAW_VITEST_INCLUDE_FILE="$include_file"');
expect(runStep?.run).toContain('pnpm test -- "$OPENCLAW_EXTENSION_VITEST_CONFIG"');
const shellCheck = spawnSync("bash", ["-n"], {
encoding: "utf8",
input: runStep?.run,
});
expect(shellCheck.status, shellCheck.stderr).toBe(0);
expect(runStep?.run?.match(/extension-file-shard\)([\s\S]*?)\n\s*;;/u)?.[1]).not.toContain(
"--retry",
);
expect(suite.needs).toContain("plugin-prerelease-extension-shard");
expect(
suite.steps.find((step: WorkflowStep) => step.name === "Verify plugin prerelease suite").run,
).toContain(
'check_required "plugin-prerelease-extensions" "$RUN_EXTENSIONS" "$EXTENSIONS_RESULT"',
);
});
});