test(FN-000): address PR review feedback

This commit is contained in:
Aron Prins
2026-05-07 17:23:28 +02:00
parent 28a87b4ee1
commit 1ed390052c
8 changed files with 329 additions and 128 deletions

View File

@@ -8,10 +8,13 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
buildReverseDependencyMap,
shouldForceFullSuite,
resolveAffectedPackages,
decideExecutionPlan,
computePackageHash,
expandWithReverseDependents,
listWorkspacePackageInfos,
readCache,
writeCache,
applyCacheToPlan,

View File

@@ -1,8 +1,10 @@
import test from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import fg from "fast-glob";
import { parse as parseYaml } from "yaml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -19,29 +21,27 @@ function readJson(filePath) {
function readWorkspacePackageDirs() {
const workspaceFile = path.join(repoRoot, "pnpm-workspace.yaml");
const workspaceYaml = readFileSync(workspaceFile, "utf8");
const patterns = [...workspaceYaml.matchAll(/^\s*-\s+"([^"]+)"\s*$/gm)].map((match) => match[1]);
const dirs = new Set();
const parsed = parseYaml(workspaceYaml);
const patterns = Array.isArray(parsed?.packages)
? parsed.packages.filter((pattern) => typeof pattern === "string")
: [];
return fg.sync(patterns.map(workspacePatternToPackageJsonGlob), {
absolute: true,
cwd: repoRoot,
dot: false,
onlyFiles: true,
unique: true,
}).map((packageJsonPath) => path.dirname(packageJsonPath)).sort();
}
for (const pattern of patterns) {
if (pattern.endsWith("/*")) {
const parentDir = path.join(repoRoot, pattern.slice(0, -2));
for (const entry of readdirSync(parentDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const packageDir = path.join(parentDir, entry.name);
if (existsSync(path.join(packageDir, "package.json"))) {
dirs.add(packageDir);
}
}
continue;
}
const packageDir = path.join(repoRoot, pattern);
if (existsSync(path.join(packageDir, "package.json"))) {
dirs.add(packageDir);
}
}
return [...dirs].sort();
function workspacePatternToPackageJsonGlob(pattern) {
const trimmed = pattern.trim();
const isNegated = trimmed.startsWith("!");
const body = (isNegated ? trimmed.slice(1) : trimmed)
.replace(/^\/+/, "")
.replace(/\/+$/, "");
const packageJsonGlob = body.endsWith("package.json") ? body : `${body}/package.json`;
return isNegated ? `!${packageJsonGlob}` : packageJsonGlob;
}
function hasSharedIsolation(config) {

View File

@@ -5,18 +5,7 @@ import { cpus } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ensureTestArtifacts } from "./ensure-test-artifacts.mjs";
const DEFAULT_TEST_PACKAGES = [
"@fusion/core",
"@fusion/engine",
"@fusion/dashboard",
"@runfusion/fusion",
"@fusion/plugin-sdk",
"@fusion/desktop",
"@fusion/mobile",
"@fusion/droid-cli",
"@fusion/pi-claude-cli",
];
import { listWorkspacePackageInfos } from "./test-changed.mjs";
function run(command, commandArgs, options = {}) {
const result = spawnSync(command, commandArgs, {
@@ -70,9 +59,15 @@ export function selectShardPackages(packages, shard, total) {
return packages.filter((_, index) => index % total === shard - 1);
}
export function listWorkspaceTestPackages({ projectRoot = process.cwd() } = {}) {
return listWorkspacePackageInfos({ projectRoot })
.filter((workspacePackage) => workspacePackage.hasTestScript)
.map((workspacePackage) => workspacePackage.name);
}
export function main(argv = process.argv.slice(2), env = process.env) {
const { shard, total } = parseShardArgs(argv, env);
const shardPackages = selectShardPackages(DEFAULT_TEST_PACKAGES, shard, total);
const shardPackages = selectShardPackages(listWorkspaceTestPackages(), shard, total);
if (shardPackages.length === 0) {
console.log(`[ci-test-shard] shard ${shard}/${total} has no assigned packages; skipping.`);

View File

@@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import { cpus, tmpdir } from "node:os";
import { ensureTestArtifacts } from "./ensure-test-artifacts.mjs";
import fg from "fast-glob";
import { parse as parseYaml } from "yaml";
const currentFilePath = fileURLToPath(import.meta.url);
const scriptDir = path.dirname(currentFilePath);
@@ -110,62 +112,144 @@ function getBaseBranch() {
return changesetConfig.baseBranch || "main";
}
function workspacePatterns() {
function readWorkspacePatterns(projectRoot = rootDir) {
try {
const workspacePath = path.join(rootDir, "pnpm-workspace.yaml");
const content = readFileSync(workspacePath, "utf8");
return content
.split("\n")
.map((line) => line.trim())
.filter((line) => line.startsWith("-"))
.map((line) => line.replace(/^-\s*/, "").replace(/^['"]|['"]$/g, ""))
.filter(Boolean);
const workspacePath = path.join(projectRoot, "pnpm-workspace.yaml");
const parsed = parseYaml(readFileSync(workspacePath, "utf8"));
return Array.isArray(parsed?.packages)
? parsed.packages.filter((pattern) => typeof pattern === "string")
: [];
} catch {
return ["packages/*"];
}
}
function expandWorkspacePattern(pattern) {
if (!pattern.includes("*")) {
return [pattern.replace(/\/$/, "")];
}
const normalized = pattern.replace(/\/$/, "");
if (!normalized.endsWith("/*")) {
function expandWorkspacePattern(projectRoot, pattern) {
if (pattern.trim().startsWith("!")) {
return [];
}
const base = normalized.slice(0, -2);
const basePath = path.join(rootDir, base);
try {
return readdirSync(basePath, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => `${base}/${entry.name}`);
} catch {
return [];
}
return fg.sync(workspacePatternToPackageJsonGlob(pattern), {
absolute: true,
cwd: projectRoot,
dot: false,
onlyFiles: true,
unique: true,
});
}
function listWorkspacePackages() {
const packageNameByDir = new Map();
const dirs = new Set(workspacePatterns().flatMap(expandWorkspacePattern));
function expandWorkspacePatterns(projectRoot, patterns) {
if (!patterns.some((pattern) => pattern.trim().startsWith("!"))) {
return patterns.flatMap((pattern) => expandWorkspacePattern(projectRoot, pattern));
}
for (const dir of dirs) {
try {
const packageJsonPath = path.join(rootDir, dir, "package.json");
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
if (typeof pkg.name === "string") {
packageNameByDir.set(dir, pkg.name);
return fg.sync(patterns.map(workspacePatternToPackageJsonGlob), {
absolute: true,
cwd: projectRoot,
dot: false,
onlyFiles: true,
unique: true,
});
}
function workspacePatternToPackageJsonGlob(pattern) {
const trimmed = pattern.trim();
const isNegated = trimmed.startsWith("!");
const body = (isNegated ? trimmed.slice(1) : trimmed)
.replace(/^\/+/, "")
.replace(/\/+$/, "");
const packageJsonGlob = body.endsWith("package.json") ? body : `${body}/package.json`;
return isNegated ? `!${packageJsonGlob}` : packageJsonGlob;
}
function collectWorkspaceDependencyNames(pkg) {
return [
pkg.dependencies,
pkg.devDependencies,
pkg.peerDependencies,
pkg.optionalDependencies,
].flatMap((deps) => deps && typeof deps === "object" ? Object.keys(deps) : []);
}
export function listWorkspacePackageInfos({ projectRoot = rootDir } = {}) {
const packageJsonPaths = [
...new Set(expandWorkspacePatterns(projectRoot, readWorkspacePatterns(projectRoot))),
];
return packageJsonPaths
.map((packageJsonPath) => {
try {
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
if (typeof pkg.name !== "string") {
return null;
}
const dir = path.relative(projectRoot, path.dirname(packageJsonPath)).split(path.sep).join("/");
return {
name: pkg.name,
dir,
hasTestScript: typeof pkg.scripts?.test === "string",
dependencyNames: collectWorkspaceDependencyNames(pkg),
};
} catch {
return null;
}
} catch {
// ignore directories without package.json
})
.filter(Boolean)
.sort((a, b) => a.dir.localeCompare(b.dir));
}
function listWorkspacePackages(workspacePackages = listWorkspacePackageInfos()) {
const packageNameByDir = new Map();
for (const workspacePackage of workspacePackages) {
packageNameByDir.set(workspacePackage.dir, workspacePackage.name);
if (workspacePackage.dir.startsWith("packages/")) {
packageNameByDir.set(workspacePackage.dir.split("/")[1], workspacePackage.name);
}
}
return packageNameByDir;
}
export function buildPackageDirByName(workspacePackages) {
const packageDirByName = new Map();
for (const workspacePackage of workspacePackages) {
packageDirByName.set(workspacePackage.name, workspacePackage.dir);
}
return packageDirByName;
}
export function buildReverseDependencyMap(workspacePackages) {
const workspaceNames = new Set(workspacePackages.map((workspacePackage) => workspacePackage.name));
const reverseDependencyMap = new Map(workspacePackages.map((workspacePackage) => [workspacePackage.name, []]));
for (const workspacePackage of workspacePackages) {
for (const dependencyName of workspacePackage.dependencyNames ?? []) {
if (workspaceNames.has(dependencyName)) {
reverseDependencyMap.get(dependencyName)?.push(workspacePackage.name);
}
}
}
return reverseDependencyMap;
}
export function expandWithReverseDependents(packageNames, reverseDependencyMap) {
const expanded = new Set(packageNames);
const queue = [...packageNames];
while (queue.length > 0) {
const current = queue.shift();
for (const dependent of reverseDependencyMap.get(current) ?? []) {
if (expanded.has(dependent)) continue;
expanded.add(dependent);
queue.push(dependent);
}
}
return [...expanded];
}
export function shouldForceFullSuite(changedFiles) {
const fullSuitePaths = [
"package.json",
@@ -188,7 +272,7 @@ export function shouldForceFullSuite(changedFiles) {
return true;
}
if (file.startsWith("packages/") && /vitest|test/.test(path.basename(file))) {
if ((file.startsWith("packages/") || file.startsWith("plugins/")) && /vitest|test/.test(path.basename(file))) {
return false;
}
@@ -230,23 +314,32 @@ function changedFilesSince(baseSha) {
export function resolveAffectedPackages(changedFiles, packageNameByDir) {
const affected = new Set();
const packageDirs = [...packageNameByDir.keys()]
.filter((dir) => dir.includes("/"))
.sort((a, b) => b.length - a.length);
for (const file of changedFiles) {
if (!file.startsWith("packages/") && !file.startsWith("plugins/")) {
let packageName = null;
for (const packageDir of packageDirs) {
if (file === packageDir || file.startsWith(`${packageDir}/`)) {
packageName = packageNameByDir.get(packageDir);
break;
}
}
if (!packageName && file.startsWith("packages/")) {
const [, dir] = file.split("/");
packageName = packageNameByDir.get(dir) ?? packageNameByDir.get(`packages/${dir}`) ?? null;
}
if (!packageName) {
if (file.startsWith("packages/") || file.startsWith("plugins/")) {
return null;
}
continue;
}
const workspaceDir = [...packageNameByDir.keys()]
.find((dir) => file === dir || file.startsWith(`${dir}/`));
if (!workspaceDir) {
return null;
}
const packageName = packageNameByDir.get(workspaceDir);
if (!packageName) {
return null;
}
affected.add(packageName);
}