test(FN-3607): harden test workflow verification and isolation checks
- Expand test-changed coverage and shard selection assertions for CI workflows - Improve vitest worker temp-directory utilities and related core/CLI tests - Refine test isolation guardrails and runtime ignore handling for live .fusion noise - Update contributing guidance and root test script usage for the verified workflow Fusion-Task-Id: FN-3607
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
recordCachePass,
|
||||
cacheFilePath,
|
||||
shouldRunIsolationGuard,
|
||||
defaultTestWorkerBudget,
|
||||
} from "../test-changed.mjs";
|
||||
|
||||
import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
||||
@@ -105,7 +106,7 @@ test("shouldForceFullSuite: returns true when a GitHub workflow changed", () =>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveAffectedPackages: maps changed files to package names", () => {
|
||||
const map = pkgMap([["engine", "@fusion/engine"], ["core", "@fusion/core"]]);
|
||||
const map = pkgMap([["packages/engine", "@fusion/engine"], ["packages/core", "@fusion/core"]]);
|
||||
const result = resolveAffectedPackages(
|
||||
["packages/engine/src/index.ts", "packages/core/src/utils.ts"],
|
||||
map,
|
||||
@@ -113,23 +114,37 @@ test("resolveAffectedPackages: maps changed files to package names", () => {
|
||||
assert.deepEqual(result?.sort(), ["@fusion/core", "@fusion/engine"]);
|
||||
});
|
||||
|
||||
test("resolveAffectedPackages: ignores non-package files", () => {
|
||||
const map = pkgMap([["engine", "@fusion/engine"]]);
|
||||
test("resolveAffectedPackages: ignores non-workspace files", () => {
|
||||
const map = pkgMap([["packages/engine", "@fusion/engine"]]);
|
||||
const result = resolveAffectedPackages(["docs/readme.md"], map);
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
test("resolveAffectedPackages: returns null for unknown package dir", () => {
|
||||
const map = pkgMap([["engine", "@fusion/engine"]]);
|
||||
const map = pkgMap([["packages/engine", "@fusion/engine"]]);
|
||||
const result = resolveAffectedPackages(["packages/unknown-pkg/src/foo.ts"], map);
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
|
||||
test("resolveAffectedPackages: maps plugin workspace changes", () => {
|
||||
const map = pkgMap([
|
||||
["packages/engine", "@fusion/engine"],
|
||||
["plugins/fusion-plugin-hermes-runtime", "@fusion-plugin-examples/hermes-runtime"],
|
||||
]);
|
||||
|
||||
const result = resolveAffectedPackages([
|
||||
"plugins/fusion-plugin-hermes-runtime/src/runtime-adapter.ts",
|
||||
], map);
|
||||
|
||||
assert.deepEqual(result, ["@fusion-plugin-examples/hermes-runtime"]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// decideExecutionPlan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const basePackageMap = pkgMap([["engine", "@fusion/engine"], ["core", "@fusion/core"]]);
|
||||
const basePackageMap = pkgMap([["packages/engine", "@fusion/engine"], ["packages/core", "@fusion/core"]]);
|
||||
|
||||
test("decideExecutionPlan: forced full suite", () => {
|
||||
const plan = decideExecutionPlan({
|
||||
@@ -208,6 +223,33 @@ test("decideExecutionPlan: no affected package resolved → full", () => {
|
||||
assert.equal(plan.reason, "no-affected-package");
|
||||
});
|
||||
|
||||
test("decideExecutionPlan: plugin-only workspace changes stay in changed mode", () => {
|
||||
const plan = decideExecutionPlan({
|
||||
forceFullSuite: false,
|
||||
comparisonBase: "abc123",
|
||||
changedFiles: ["plugins/fusion-plugin-openclaw-runtime/src/runtime-adapter.ts"],
|
||||
packageNameByDir: pkgMap([
|
||||
["packages/engine", "@fusion/engine"],
|
||||
["plugins/fusion-plugin-openclaw-runtime", "@fusion-plugin-examples/openclaw-runtime"],
|
||||
]),
|
||||
});
|
||||
|
||||
assert.equal(plan.mode, "changed");
|
||||
assert.deepEqual(plan.packages, ["@fusion-plugin-examples/openclaw-runtime"]);
|
||||
});
|
||||
|
||||
test("decideExecutionPlan: plugin changes without mapping fail safe to full", () => {
|
||||
const plan = decideExecutionPlan({
|
||||
forceFullSuite: false,
|
||||
comparisonBase: "abc123",
|
||||
changedFiles: ["plugins/fusion-plugin-openclaw-runtime/src/runtime-adapter.ts"],
|
||||
packageNameByDir: basePackageMap,
|
||||
});
|
||||
|
||||
assert.equal(plan.mode, "full");
|
||||
assert.equal(plan.reason, "no-affected-package");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// computePackageHash
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -551,9 +593,9 @@ test("recordCachePass: empty package list skips write", () => {
|
||||
// cacheFilePath
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("cacheFilePath: ends with .fusion/test-cache.json", () => {
|
||||
test("cacheFilePath: ends with node_modules/.cache/fusion/test-cache.json", () => {
|
||||
const p = cacheFilePath();
|
||||
assert.ok(p.endsWith(path.join(".fusion", "test-cache.json")), `got: ${p}`);
|
||||
assert.ok(p.endsWith(path.join("node_modules", ".cache", "fusion", "test-cache.json")), `got: ${p}`);
|
||||
});
|
||||
|
||||
test("shouldRunIsolationGuard: enabled by default", () => {
|
||||
@@ -563,3 +605,23 @@ test("shouldRunIsolationGuard: enabled by default", () => {
|
||||
test("shouldRunIsolationGuard: disabled when env flag is set", () => {
|
||||
assert.equal(shouldRunIsolationGuard({ FUSION_TEST_DISABLE_ISOLATION_GUARD: "1" }), false);
|
||||
});
|
||||
|
||||
test("defaultTestWorkerBudget: uses env overrides when provided", () => {
|
||||
const budget = defaultTestWorkerBudget({
|
||||
FUSION_TEST_TOTAL_WORKERS: "9",
|
||||
FUSION_TEST_CONCURRENCY: "3",
|
||||
});
|
||||
|
||||
assert.deepEqual(budget, { totalWorkers: 9, concurrency: 3 });
|
||||
});
|
||||
|
||||
test("defaultTestWorkerBudget: uses CPU-aware defaults and clamps concurrency", () => {
|
||||
const budget = defaultTestWorkerBudget({
|
||||
FUSION_TEST_TOTAL_WORKERS: "",
|
||||
FUSION_TEST_CONCURRENCY: "999",
|
||||
});
|
||||
|
||||
assert.ok(budget.totalWorkers >= 4);
|
||||
assert.ok(budget.totalWorkers <= 12);
|
||||
assert.equal(budget.concurrency, budget.totalWorkers);
|
||||
});
|
||||
|
||||
@@ -58,22 +58,22 @@ function listProtectedFusionDirs() {
|
||||
// is expected to mutate. Tests still must not write to these — the filter only
|
||||
// suppresses noise from a live app sharing the same HOME during local dev.
|
||||
const RUNTIME_IGNORE_PATTERNS = [
|
||||
/^agent(?:[\/\\]|$)/,
|
||||
/^agents(?:[\/\\]|$)/,
|
||||
/^agent-memory(?:[\/\\]|$)/,
|
||||
/^automations(?:[\/\\]|$)/,
|
||||
/^backups(?:[\/\\]|$)/,
|
||||
/^plugins(?:[\/\\]|$)/,
|
||||
/^cache(?:[\/\\]|$)/,
|
||||
/^agent(?:[/\\]|$)/,
|
||||
/^agents(?:[/\\]|$)/,
|
||||
/^agent-memory(?:[/\\]|$)/,
|
||||
/^automations(?:[/\\]|$)/,
|
||||
/^backups(?:[/\\]|$)/,
|
||||
/^plugins(?:[/\\]|$)/,
|
||||
/^cache(?:[/\\]|$)/,
|
||||
/^config\.json$/,
|
||||
/^fusion-central\.db(?:-wal|-shm|-journal)?$/,
|
||||
/^fusion\.db(?:-wal|-shm|-journal)?(?:\.backup-[\w-]+)?$/,
|
||||
/^archive\.db(?:-wal|-shm|-journal)?(?:\.backup-[\w-]+)?$/,
|
||||
/^activity-log\.jsonl$/,
|
||||
/^settings\.json$/,
|
||||
/^logs(?:[\/\\]|$)/,
|
||||
/^tasks(?:[\/\\]|$)/,
|
||||
/^memory(?:[\/\\]|$)/,
|
||||
/^logs(?:[/\\]|$)/,
|
||||
/^tasks(?:[/\\]|$)/,
|
||||
/^memory(?:[/\\]|$)/,
|
||||
/^MEMORY\.md$/,
|
||||
/^DREAMS\.md$/,
|
||||
/^\d{4}-\d{2}-\d{2}\.md$/,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { cpus } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { ensureTestArtifacts } from "./ensure-test-artifacts.mjs";
|
||||
@@ -37,6 +38,18 @@ function parsePositiveInteger(value) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function defaultTestWorkerBudget(env = process.env) {
|
||||
const cpuCap = Math.max(1, cpus().length - 1);
|
||||
const defaultTotal = Math.min(12, Math.max(4, cpuCap));
|
||||
const totalWorkers = parsePositiveInteger(env.FUSION_TEST_TOTAL_WORKERS) ?? defaultTotal;
|
||||
const concurrency = Math.max(
|
||||
1,
|
||||
Math.min(parsePositiveInteger(env.FUSION_TEST_CONCURRENCY) ?? 2, totalWorkers),
|
||||
);
|
||||
|
||||
return { totalWorkers, concurrency };
|
||||
}
|
||||
|
||||
export function parseShardArgs(argv = process.argv.slice(2), env = process.env) {
|
||||
const byFlag = (name) => {
|
||||
const idx = argv.indexOf(name);
|
||||
@@ -68,10 +81,11 @@ export function main(argv = process.argv.slice(2), env = process.env) {
|
||||
|
||||
console.log(`[ci-test-shard] shard ${shard}/${total}: ${shardPackages.join(", ")}`);
|
||||
|
||||
const { totalWorkers, concurrency } = defaultTestWorkerBudget(env);
|
||||
const shardEnv = {
|
||||
...env,
|
||||
FUSION_TEST_TOTAL_WORKERS: env.FUSION_TEST_TOTAL_WORKERS || "4",
|
||||
FUSION_TEST_CONCURRENCY: env.FUSION_TEST_CONCURRENCY || "1",
|
||||
FUSION_TEST_TOTAL_WORKERS: env.FUSION_TEST_TOTAL_WORKERS || String(totalWorkers),
|
||||
FUSION_TEST_CONCURRENCY: env.FUSION_TEST_CONCURRENCY || String(concurrency),
|
||||
};
|
||||
|
||||
run("pnpm", ["sync:fusion-skill:check"], { env: shardEnv });
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createHash } from "node:crypto";
|
||||
import { cpus } from "node:os";
|
||||
import { ensureTestArtifacts } from "./ensure-test-artifacts.mjs";
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
@@ -76,16 +77,50 @@ function getBaseBranch() {
|
||||
return changesetConfig.baseBranch || "main";
|
||||
}
|
||||
|
||||
function listWorkspacePackages() {
|
||||
const packagesDir = path.join(rootDir, "packages");
|
||||
const packageDirs = readdirSync(packagesDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
function workspacePatterns() {
|
||||
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);
|
||||
} catch {
|
||||
return ["packages/*"];
|
||||
}
|
||||
}
|
||||
|
||||
function expandWorkspacePattern(pattern) {
|
||||
if (!pattern.includes("*")) {
|
||||
return [pattern.replace(/\/$/, "")];
|
||||
}
|
||||
|
||||
const normalized = pattern.replace(/\/$/, "");
|
||||
if (!normalized.endsWith("/*")) {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
function listWorkspacePackages() {
|
||||
const packageNameByDir = new Map();
|
||||
for (const dir of packageDirs) {
|
||||
const dirs = new Set(workspacePatterns().flatMap(expandWorkspacePattern));
|
||||
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
const packageJsonPath = path.join(packagesDir, dir, "package.json");
|
||||
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);
|
||||
@@ -124,7 +159,7 @@ export function shouldForceFullSuite(changedFiles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.startsWith("packages/") && !file.startsWith("docs/")) {
|
||||
if (!file.startsWith("packages/") && !file.startsWith("plugins/") && !file.startsWith("docs/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -164,12 +199,18 @@ export function resolveAffectedPackages(changedFiles, packageNameByDir) {
|
||||
const affected = new Set();
|
||||
|
||||
for (const file of changedFiles) {
|
||||
if (!file.startsWith("packages/")) {
|
||||
if (!file.startsWith("packages/") && !file.startsWith("plugins/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, dir] = file.split("/");
|
||||
const packageName = packageNameByDir.get(dir);
|
||||
const workspaceDir = [...packageNameByDir.keys()]
|
||||
.find((dir) => file === dir || file.startsWith(`${dir}/`));
|
||||
|
||||
if (!workspaceDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageName = packageNameByDir.get(workspaceDir);
|
||||
if (!packageName) {
|
||||
return null;
|
||||
}
|
||||
@@ -428,13 +469,34 @@ export function recordCachePass(packages, packageDirByName, options = {}) {
|
||||
// Execution plan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const workspaceConcurrency =
|
||||
process.env.FUSION_TEST_WORKSPACE_CONCURRENCY || "2";
|
||||
const workspaceConcurrency = process.env.FUSION_TEST_WORKSPACE_CONCURRENCY || "2";
|
||||
|
||||
function parsePositiveInteger(value) {
|
||||
const parsed = Number.parseInt(value ?? "", 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
export function defaultTestWorkerBudget(env = process.env) {
|
||||
const cpuCap = Math.max(1, cpus().length - 1);
|
||||
const defaultTotal = Math.min(12, Math.max(4, cpuCap));
|
||||
const totalWorkers = parsePositiveInteger(env.FUSION_TEST_TOTAL_WORKERS) ?? defaultTotal;
|
||||
const concurrency = Math.max(
|
||||
1,
|
||||
Math.min(parsePositiveInteger(env.FUSION_TEST_CONCURRENCY) ?? 2, totalWorkers),
|
||||
);
|
||||
|
||||
return {
|
||||
totalWorkers,
|
||||
concurrency,
|
||||
};
|
||||
}
|
||||
|
||||
const { totalWorkers, concurrency } = defaultTestWorkerBudget(process.env);
|
||||
|
||||
const fullSuiteEnv = {
|
||||
...process.env,
|
||||
FUSION_TEST_TOTAL_WORKERS: process.env.FUSION_TEST_TOTAL_WORKERS || "4",
|
||||
FUSION_TEST_CONCURRENCY: process.env.FUSION_TEST_CONCURRENCY || "2",
|
||||
FUSION_TEST_TOTAL_WORKERS: process.env.FUSION_TEST_TOTAL_WORKERS || String(totalWorkers),
|
||||
FUSION_TEST_CONCURRENCY: process.env.FUSION_TEST_CONCURRENCY || String(concurrency),
|
||||
};
|
||||
|
||||
function runFullSuite(forwardedArgs) {
|
||||
@@ -482,7 +544,7 @@ export function main(argv = process.argv.slice(2)) {
|
||||
// Build reverse map: pkg-name → relative dir (e.g. "packages/engine")
|
||||
const packageDirByName = new Map();
|
||||
for (const [dir, name] of packageNameByDir) {
|
||||
packageDirByName.set(name, `packages/${dir}`);
|
||||
packageDirByName.set(name, dir);
|
||||
}
|
||||
|
||||
const plan = decideExecutionPlan({
|
||||
|
||||
Reference in New Issue
Block a user