mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(ci): reuse JavaScript caches across test layouts and runtimes (#155518)
* fix(ci): reuse JavaScript caches across test layouts and runtimes * ci: report memory and pressure around test shards Record Node memory estimates and Linux host pressure before shard execution and after worker cleanup so cache and replay timings retain resource context. Keep scheduling, cache policy, test inventory and deadlines unchanged. The original CLI custody timeout remains unexplained after exact-source native replay and bounded phase traces. These diagnostics do not claim a fix. Existing shard/warmer suites pass 121 cases in 6.85 seconds; scripts types, typed lint, docs sanity and independent P2 review pass.
This commit is contained in:
parent
cf32d5a527
commit
f9c524daa0
20 changed files with 528 additions and 194 deletions
7
.github/actions/setup-node-env/action.yml
vendored
7
.github/actions/setup-node-env/action.yml
vendored
|
|
@ -259,9 +259,10 @@ runs:
|
|||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: /var/tmp/openclaw-vitest-fs-cache
|
||||
key: ${{ github.repository }}-vitest-fs-v3-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-${{ steps.vitest-cache-generation.outputs.value }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
# v4 seeds config-owned leaves shared by single and mixed-config jobs.
|
||||
key: ${{ github.repository }}-vitest-fs-v4-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-${{ steps.vitest-cache-generation.outputs.value }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
${{ github.repository }}-vitest-fs-v3-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-${{ steps.vitest-cache-generation.outputs.value }}-
|
||||
${{ github.repository }}-vitest-fs-v4-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-${{ steps.vitest-cache-generation.outputs.value }}-
|
||||
|
||||
- name: Configure Vitest transform cache
|
||||
if: inputs.cache-mode != 'off' && (inputs.vitest-fs-cache == 'true' || inputs.restore-test-caches == 'true') && runner.os != 'Windows'
|
||||
|
|
@ -288,7 +289,7 @@ runs:
|
|||
printf '%s\n' "$CACHE_GENERATION" > "$generation_file"
|
||||
# The shard runner treats this as a persistent root and assigns one
|
||||
# isolated subdirectory per concurrent Vitest worker.
|
||||
echo "OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=$cache_root" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT=$cache_root" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_VITEST_FS_MODULE_CACHE_WRITER=$CACHE_WRITER" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Restore Node compile cache
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -7,6 +7,7 @@ import {
|
|||
cpSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
|
|
@ -28,6 +29,7 @@ import type { VitestWorkerRun } from "./lib/vitest-worker-run.mts";
|
|||
// CI admits at most two plans only when the actual host has room. Each plan
|
||||
// keeps inner test-projects parallelism 1; runner labels cannot establish capacity.
|
||||
const PLAN_CONCURRENCY = 2;
|
||||
const FS_MODULE_CACHE_ROOT_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT";
|
||||
const FS_MODULE_CACHE_PATH_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_PATH";
|
||||
const FS_MODULE_CACHE_WRITER_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_WRITER";
|
||||
const NODE_COMPILE_CACHE_PATH_ENV_KEY = "NODE_COMPILE_CACHE";
|
||||
|
|
@ -39,6 +41,31 @@ const FS_MODULE_CACHE_PRUNE_TARGET_RATIO = 0.75;
|
|||
const FS_MODULE_CACHE_METADATA_FILE = "_metadata.json";
|
||||
const FS_MODULE_CACHE_GENERATION_FILE = ".openclaw-transform-generation";
|
||||
|
||||
function reportCiResourceSnapshot(phase: "start" | "end") {
|
||||
const pressure = (resource: "cpu" | "memory" | "io") => {
|
||||
if (process.platform !== "linux") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return readFileSync(`/proc/pressure/${resource}`, "utf8").trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
console.log(
|
||||
`[shard:resource-snapshot] ${JSON.stringify({
|
||||
phase,
|
||||
uptimeSeconds: os.uptime(),
|
||||
cpuModel: os.cpus()[0]?.model ?? null,
|
||||
loadAverage: os.loadavg(),
|
||||
freeMemoryBytes: os.freemem(),
|
||||
availableMemoryBytes: process.availableMemory?.() ?? null,
|
||||
constrainedMemoryBytes: process.constrainedMemory?.() ?? null,
|
||||
pressure: { cpu: pressure("cpu"), memory: pressure("memory"), io: pressure("io") },
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
export type ShardTargetPlan = { kind: "target"; name: string; target: string };
|
||||
type ShardGroupConfig = {
|
||||
configs: string[];
|
||||
|
|
@ -166,7 +193,8 @@ export function buildChildEnv(
|
|||
index: number,
|
||||
options: { serial?: boolean; cacheSlot?: number; runtime?: "node" | "bun" } = {},
|
||||
) {
|
||||
const persistentCacheRoot = baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
const configuredCacheRoot = baseEnv[FS_MODULE_CACHE_ROOT_ENV_KEY]?.trim();
|
||||
const persistentCacheRoot = configuredCacheRoot || baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
const cachePrefix = options.runtime === "bun" ? "vitest-cache-bun" : "vitest-cache";
|
||||
const cacheDirectory = persistentCacheRoot
|
||||
? `${cachePrefix}-${options.cacheSlot ?? index}`
|
||||
|
|
@ -175,10 +203,16 @@ export function buildChildEnv(
|
|||
: `${cachePrefix}-${index}`;
|
||||
// Persistent worker slots let serial plans reuse transforms without concurrent
|
||||
// writers. Scratch caches stay per-plan; group overrides still apply last.
|
||||
const childEnv = prepareChildEnv(entry, {
|
||||
const cacheEnv: NodeJS.ProcessEnv = {
|
||||
...baseEnv,
|
||||
[FS_MODULE_CACHE_PATH_ENV_KEY]: join(persistentCacheRoot || scratchDir, cacheDirectory),
|
||||
});
|
||||
[FS_MODULE_CACHE_ROOT_ENV_KEY]: join(persistentCacheRoot || scratchDir, cacheDirectory),
|
||||
};
|
||||
// Legacy shard callers supplied the archive root through PATH. With ROOT,
|
||||
// PATH instead belongs to a caller that explicitly selected a final leaf.
|
||||
if (!configuredCacheRoot) {
|
||||
delete cacheEnv[FS_MODULE_CACHE_PATH_ENV_KEY];
|
||||
}
|
||||
const childEnv = prepareChildEnv(entry, cacheEnv);
|
||||
if (options.runtime) {
|
||||
childEnv.OPENCLAW_VITEST_RUNTIME = options.runtime;
|
||||
}
|
||||
|
|
@ -247,22 +281,23 @@ export function clonePersistentCacheSlots(root: string | undefined, concurrency:
|
|||
if (!root || concurrency <= 1) {
|
||||
return 0;
|
||||
}
|
||||
const seed = join(root, "vitest-cache-0");
|
||||
if (!existsSync(seed)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let clonedSlots = 0;
|
||||
for (let cacheSlot = 1; cacheSlot < concurrency; cacheSlot += 1) {
|
||||
const destination = join(root, `vitest-cache-${cacheSlot}`);
|
||||
rmSync(destination, { force: true, recursive: true });
|
||||
// Clone before workers start. Reflinks make the common Linux path cheap;
|
||||
// unsupported filesystems transparently fall back to a regular copy.
|
||||
cpSync(seed, destination, {
|
||||
mode: constants.COPYFILE_FICLONE,
|
||||
recursive: true,
|
||||
});
|
||||
clonedSlots += 1;
|
||||
for (const prefix of ["vitest-cache", "vitest-cache-bun"]) {
|
||||
const seed = join(root, `${prefix}-0`);
|
||||
if (!existsSync(seed)) {
|
||||
continue;
|
||||
}
|
||||
for (let cacheSlot = 1; cacheSlot < concurrency; cacheSlot += 1) {
|
||||
const destination = join(root, `${prefix}-${cacheSlot}`);
|
||||
rmSync(destination, { force: true, recursive: true });
|
||||
// Clone before workers start. Reflinks make the common Linux path cheap;
|
||||
// unsupported filesystems transparently fall back to a regular copy.
|
||||
cpSync(seed, destination, {
|
||||
mode: constants.COPYFILE_FICLONE,
|
||||
recursive: true,
|
||||
});
|
||||
clonedSlots += 1;
|
||||
}
|
||||
}
|
||||
return clonedSlots;
|
||||
}
|
||||
|
|
@ -487,6 +522,7 @@ export async function runShardPlans(plans: ShardPlan[], options: RunShardOptions
|
|||
console.log(
|
||||
`[shard:resources] logicalCpuCount=${hostResources.logicalCpuCount} totalMemoryBytes=${hostResources.totalMemoryBytes} requested plans=${requestedConcurrency} admitted plans=${concurrency}`,
|
||||
);
|
||||
reportCiResourceSnapshot("start");
|
||||
}
|
||||
const measuredHost =
|
||||
hostResources !== null &&
|
||||
|
|
@ -519,7 +555,8 @@ export async function runShardPlans(plans: ShardPlan[], options: RunShardOptions
|
|||
};
|
||||
});
|
||||
const scratchDir = options.scratchDir ?? mkdtempSync(join(tmpdir(), "openclaw-node-shard-"));
|
||||
const persistentCacheRoot = baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
const persistentCacheRoot =
|
||||
baseEnv[FS_MODULE_CACHE_ROOT_ENV_KEY]?.trim() || baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
const nodeCompileCacheRoot = baseEnv[NODE_COMPILE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
const clonedCacheSlots = clonePersistentCacheSlots(persistentCacheRoot, concurrency);
|
||||
if (clonedCacheSlots > 0) {
|
||||
|
|
@ -670,6 +707,9 @@ export async function runShardPlans(plans: ShardPlan[], options: RunShardOptions
|
|||
try {
|
||||
await context?.workerRun.dispose();
|
||||
} finally {
|
||||
if (hostResources) {
|
||||
reportCiResourceSnapshot("end");
|
||||
}
|
||||
process.off("SIGINT", onSignal);
|
||||
process.off("SIGTERM", onSignal);
|
||||
if (interrupted && context) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|||
import { join } from "node:path";
|
||||
import { buildChildEnv } from "./ci-run-node-test-shard.mts";
|
||||
import { createVitestCacheWarmGroups } from "./lib/ci-node-test-plan.mts";
|
||||
import { BUN_UI_TEST_ENV } from "./lib/ci-test-runtime.mts";
|
||||
import { BUN_UI_TEST_ENV, resolveCiTestRuntimeSelections } from "./lib/ci-test-runtime.mts";
|
||||
import { runManagedCommand } from "./lib/managed-child-process.mts";
|
||||
|
||||
// Consumer entrypoints choose their own reusable cache leaves; the planner owns
|
||||
|
|
@ -15,10 +15,11 @@ if (!ui?.includePatterns) {
|
|||
throw new Error("Missing UI cache seed");
|
||||
}
|
||||
const scratch = mkdtempSync(join(tmpdir(), "openclaw-cache-warm-"));
|
||||
const collectionArgs = ["--testNamePattern=(?!)"];
|
||||
const baseEnv: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
OPENCLAW_CI_TEST_RUNTIME_POLICY: "node",
|
||||
OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--testNamePattern=(?!)"]',
|
||||
OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: JSON.stringify(collectionArgs),
|
||||
OPENCLAW_NODE_TEST_PLAN_CONCURRENCY: "1",
|
||||
OPENCLAW_NODE_TEST_PLAN_CONTINUE_ON_FAILURE: "1",
|
||||
// The final Node UI collection prunes both roots after all producers join.
|
||||
|
|
@ -52,7 +53,7 @@ try {
|
|||
if (!tooling?.includePatterns) {
|
||||
throw new Error("Missing hosted CI-routing cache seed");
|
||||
}
|
||||
await collect("pnpm", ["test", ...tooling.includePatterns, "--testNamePattern=(?!)"], {
|
||||
await collect("pnpm", ["test", ...tooling.includePatterns, ...collectionArgs], {
|
||||
...baseEnv,
|
||||
OPENCLAW_TEST_PROJECTS_PARALLEL: "3",
|
||||
});
|
||||
|
|
@ -69,15 +70,41 @@ try {
|
|||
.flatMap((group) => group.includePatterns ?? []),
|
||||
),
|
||||
);
|
||||
await collect("pnpm", [script, "--testNamePattern=(?!)"], {
|
||||
await collect("pnpm", [script, ...collectionArgs], {
|
||||
...baseEnv,
|
||||
OPENCLAW_TEST_PROJECTS_PARALLEL: concurrency,
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||||
});
|
||||
}
|
||||
// Direct entrypoints retain the complete Node seed. Bun collects only
|
||||
// compatible files already admitted to the bounded hosted inventory.
|
||||
const bunGroups = tooling.configs.flatMap((config) =>
|
||||
resolveCiTestRuntimeSelections(
|
||||
{ configs: [config], includePatterns: tooling.includePatterns, vitestArgs: collectionArgs },
|
||||
"bun-compatible",
|
||||
).flatMap((selection) =>
|
||||
selection.runtime === "bun"
|
||||
? [
|
||||
{
|
||||
configs: [config],
|
||||
includePatterns: selection.includePatterns ?? tooling.includePatterns,
|
||||
shard_name: `cache-warm:hosted-bun:${config}`,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
if (bunGroups.length > 0) {
|
||||
await collect(process.execPath, ["--import", "tsx", "scripts/ci-run-node-test-shard.mts"], {
|
||||
...baseEnv,
|
||||
OPENCLAW_CI_TEST_RUNTIME_POLICY: "bun-compatible",
|
||||
OPENCLAW_NODE_TEST_GROUPS_JSON: JSON.stringify(bunGroups),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await collect(process.execPath, ["--import", "tsx", "scripts/ci-run-node-test-shard.mts"], {
|
||||
...baseEnv,
|
||||
OPENCLAW_CI_TEST_RUNTIME_POLICY: "dual",
|
||||
OPENCLAW_NODE_TEST_GROUPS_JSON: JSON.stringify(groups.filter((group) => group !== ui)),
|
||||
});
|
||||
}
|
||||
|
|
@ -95,7 +122,7 @@ try {
|
|||
delete bunEnv.OPENCLAW_VITEST_INCLUDE_FILE;
|
||||
await collect(
|
||||
process.execPath,
|
||||
["scripts/run-vitest.mjs", "run", "--config", ...ui.configs, "--testNamePattern=(?!)"],
|
||||
["scripts/run-vitest.mjs", "run", "--config", ...ui.configs, ...collectionArgs],
|
||||
bunEnv,
|
||||
);
|
||||
await collect(process.execPath, ["--import", "tsx", "scripts/ci-run-node-test-shard.mts"], {
|
||||
|
|
|
|||
|
|
@ -108,8 +108,12 @@ function selectionVitestArgs(selection: TestSelection): string[] | undefined {
|
|||
|
||||
function supportsRuntimePartition(args: string[]): boolean {
|
||||
// Native sharding, alternate roots/projects, filters and config overrides can
|
||||
// change membership. Admit only resource/deadline flags with known semantics.
|
||||
return args.every((arg) => /^--(?:maxWorkers|testTimeout|hookTimeout)=\d+$/u.test(arg));
|
||||
// change membership. Collection skips every body but preserves file imports.
|
||||
return args.every(
|
||||
(arg) =>
|
||||
arg === "--testNamePattern=(?!)" ||
|
||||
/^--(?:maxWorkers|testTimeout|hookTimeout)=\d+$/u.test(arg),
|
||||
);
|
||||
}
|
||||
|
||||
function supportsUiRuntime(args: string[]): boolean {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { resolveVitestFsModuleCacheRoot } from "../../test/vitest/vitest.performance-config.ts";
|
||||
import type { VitestCacheAssignment } from "../test-projects.test-support.mts";
|
||||
import { findRepoRoot } from "./repo-root.mjs";
|
||||
|
||||
type CacheSpec = {
|
||||
config: string;
|
||||
|
|
@ -9,10 +11,35 @@ type CacheSpec = {
|
|||
cacheAssignment?: VitestCacheAssignment;
|
||||
};
|
||||
|
||||
function configCacheKey(config: string, cwd = process.cwd()) {
|
||||
const relativeConfig = path
|
||||
.relative(findRepoRoot(cwd) ?? cwd, path.resolve(cwd, config))
|
||||
.split(path.sep)
|
||||
.join("/");
|
||||
return createHash("sha256").update(relativeConfig).digest("hex");
|
||||
}
|
||||
|
||||
export function resolveVitestCacheRoot(env: NodeJS.ProcessEnv, cwd = process.cwd()) {
|
||||
return (
|
||||
env.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT?.trim() ||
|
||||
resolveVitestFsModuleCacheRoot(findRepoRoot(cwd) ?? cwd)
|
||||
);
|
||||
}
|
||||
|
||||
/** Config identity precedes the writer slot so admission order cannot strand a seed. */
|
||||
export function resolveVitestCacheSlotPath(
|
||||
root: string,
|
||||
config: string,
|
||||
slot = 0,
|
||||
cwd = process.cwd(),
|
||||
) {
|
||||
return path.join(root, "slots", configCacheKey(config, cwd), String(slot));
|
||||
}
|
||||
|
||||
/** A slot remains borrowed through retries and the process owner's final join. */
|
||||
export function createVitestCacheSlots(platform = process.platform) {
|
||||
const idleSlots = new Map<string, number[]>();
|
||||
let nextSlot = 0;
|
||||
const nextSlots = new Map<string, number>();
|
||||
return async <T extends CacheSpec, R extends { groupJoined: boolean }>(
|
||||
spec: T,
|
||||
run: (assigned: T) => Promise<R>,
|
||||
|
|
@ -20,12 +47,17 @@ export function createVitestCacheSlots(platform = process.platform) {
|
|||
if (platform === "win32" || spec.watchMode || spec.cacheAssignment?.kind !== "scheduler") {
|
||||
return run(spec);
|
||||
}
|
||||
const configKey = createHash("sha256").update(path.resolve(spec.config)).digest("hex");
|
||||
const configKey = configCacheKey(spec.config);
|
||||
const cacheKey = path.join(path.resolve(spec.cacheAssignment.root), configKey);
|
||||
const available = idleSlots.get(cacheKey) ?? [];
|
||||
idleSlots.set(cacheKey, available);
|
||||
// Fresh indices stay global: distinct root spellings may alias one directory.
|
||||
const slot = available.pop() ?? nextSlot++;
|
||||
// Counters span roots: different root spellings may alias one directory.
|
||||
// Distinct configs have disjoint directories and each starts at its warm slot.
|
||||
let slot = available.pop();
|
||||
if (slot === undefined) {
|
||||
slot = nextSlots.get(configKey) ?? 0;
|
||||
nextSlots.set(configKey, slot + 1);
|
||||
}
|
||||
const result = await run({
|
||||
...spec,
|
||||
cacheAssignment: { ...spec.cacheAssignment, leased: true },
|
||||
|
|
@ -34,8 +66,8 @@ export function createVitestCacheSlots(platform = process.platform) {
|
|||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: path.join(
|
||||
spec.cacheAssignment.root,
|
||||
"slots",
|
||||
String(slot),
|
||||
configKey,
|
||||
String(slot),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ scripts/lib/test-selector-source-facts.mts
|
|||
scripts/lib/tsdown-declaration-boundary.mts
|
||||
scripts/lib/tsx-cli-shim.mjs
|
||||
scripts/lib/vitest-build-prerequisites.mts
|
||||
scripts/lib/vitest-cache-slots.mts
|
||||
scripts/lib/vitest-cli-mode.mts
|
||||
scripts/lib/vitest-cli.mts
|
||||
scripts/lib/vitest-home-selection.mts
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
resolveVitestCliEntry,
|
||||
prepareVitestRuntime,
|
||||
} from "./lib/vitest-build-prerequisites.mts";
|
||||
import { createVitestCacheSlots, resolveVitestCacheRoot } from "./lib/vitest-cache-slots.mts";
|
||||
import {
|
||||
hasNonRunVitestSubcommand,
|
||||
collectVitestFileFilters,
|
||||
|
|
@ -1012,6 +1013,7 @@ export async function runVitest(
|
|||
const workers = sourceMode
|
||||
? undefined
|
||||
: createVitestWorkerRun(resolveVitestProcessEnv(invocationEnv));
|
||||
const withCacheSlot = createVitestCacheSlots();
|
||||
let interrupted: NodeJS.Signals | undefined;
|
||||
const onSignal = (signal: NodeJS.Signals) => {
|
||||
interrupted ??= signal;
|
||||
|
|
@ -1028,20 +1030,41 @@ export async function runVitest(
|
|||
if (invocations.length > 1) {
|
||||
console.error("[vitest] bounded process " + (index + 1) + "/" + invocations.length);
|
||||
}
|
||||
const handle = spawnWatchedVitestProcess({
|
||||
workerRun: workers,
|
||||
pnpmArgs: [
|
||||
"exec",
|
||||
"node",
|
||||
...resolveVitestNodeArgs(invocationEnv),
|
||||
vitestCliEntry,
|
||||
...guardedVitestArgs,
|
||||
],
|
||||
spawnParams: resolveVitestSpawnParams(spawnEnv),
|
||||
env: spawnEnv,
|
||||
});
|
||||
const { code, signal } = await handle.completion;
|
||||
interrupted ??= handle.getForwardedSignal() ?? signal ?? undefined;
|
||||
const { code, signal } = await withCacheSlot(
|
||||
{
|
||||
config: config ?? "",
|
||||
env: spawnEnv,
|
||||
watchMode: sourceMode,
|
||||
...(!sourceMode &&
|
||||
config &&
|
||||
!spawnEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.trim() &&
|
||||
!hasVitestOption(guardedVitestArgs, "--fsModuleCachePath")
|
||||
? {
|
||||
cacheAssignment: {
|
||||
kind: "scheduler" as const,
|
||||
root: resolveVitestCacheRoot(spawnEnv),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
async ({ env: cacheEnv }) => {
|
||||
const handle = spawnWatchedVitestProcess({
|
||||
workerRun: workers,
|
||||
pnpmArgs: [
|
||||
"exec",
|
||||
"node",
|
||||
...resolveVitestNodeArgs(invocationEnv),
|
||||
vitestCliEntry,
|
||||
...guardedVitestArgs,
|
||||
],
|
||||
spawnParams: resolveVitestSpawnParams(cacheEnv),
|
||||
env: cacheEnv,
|
||||
});
|
||||
const result = await handle.completion;
|
||||
return { ...result, signal: handle.getForwardedSignal() ?? result.signal };
|
||||
},
|
||||
);
|
||||
interrupted ??= signal ?? undefined;
|
||||
const exitCode = code ?? 1;
|
||||
// Ordinary test failures must not hide later files; interruptions stop
|
||||
// admission and are re-raised only after the invocation has been disposed.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from "../test/vitest/vitest.database-worker-core-paths.mjs";
|
||||
import { databaseWorkerExtensionTestFiles } from "../test/vitest/vitest.extension-database-workers-paths.mjs";
|
||||
import { collectVitestExcludePatterns } from "../test/vitest/vitest.pattern-file.ts";
|
||||
import { resolveVitestFsModuleCacheRoot } from "../test/vitest/vitest.performance-config.ts";
|
||||
import {
|
||||
createExtensionTestProcessTargetChunks,
|
||||
listExtensionTestFilesForRoots,
|
||||
|
|
@ -29,6 +28,8 @@ import { parsePositiveInt } from "./lib/numeric-options.mjs";
|
|||
import { isDirectScriptRun, runVitestBatch } from "./lib/vitest-batch-runner.mts";
|
||||
import type { VitestBatchRunParams } from "./lib/vitest-batch-runner.mts";
|
||||
import { prepareVitestRuntime } from "./lib/vitest-build-prerequisites.mts";
|
||||
import { resolveVitestCacheRoot, resolveVitestCacheSlotPath } from "./lib/vitest-cache-slots.mts";
|
||||
import { resolveExplicitVitestMode } from "./lib/vitest-cli-mode.mts";
|
||||
import { resolveVitestHomeSelection } from "./lib/vitest-home-selection.mts";
|
||||
import { createVitestReportOwner, type VitestReportOutcome } from "./lib/vitest-report-owner.mts";
|
||||
import { resolveVitestRuntimeCliSelections } from "./lib/vitest-runtime-selection.mts";
|
||||
|
|
@ -87,36 +88,24 @@ export function resolveExtensionBatchParallelism(groupCount: number, env = proce
|
|||
return Math.min(Math.max(1, override), Math.max(1, groupCount));
|
||||
}
|
||||
|
||||
function sanitizeCacheSegment(value: string) {
|
||||
return (
|
||||
value
|
||||
.replace(/[^a-zA-Z0-9._-]+/gu, "-")
|
||||
.replace(/^-+|-+$/gu, "")
|
||||
.slice(0, 180) || "default"
|
||||
);
|
||||
}
|
||||
|
||||
function createGroupEnv({
|
||||
baseEnv,
|
||||
group,
|
||||
groupIndex,
|
||||
useDedicatedCache,
|
||||
watchMode,
|
||||
}: {
|
||||
baseEnv: NodeJS.ProcessEnv;
|
||||
group: ExtensionTestPlanGroup;
|
||||
groupIndex: number;
|
||||
useDedicatedCache: boolean;
|
||||
watchMode: boolean;
|
||||
}) {
|
||||
if (!useDedicatedCache || baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
|
||||
if (watchMode || baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
|
||||
return baseEnv;
|
||||
}
|
||||
|
||||
return {
|
||||
...baseEnv,
|
||||
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(
|
||||
resolveVitestFsModuleCacheRoot(),
|
||||
"extension-batch",
|
||||
sanitizeCacheSegment(`${groupIndex}-${group.config}`),
|
||||
[FS_MODULE_CACHE_PATH_ENV_KEY]: resolveVitestCacheSlotPath(
|
||||
resolveVitestCacheRoot(baseEnv),
|
||||
group.config,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
@ -180,11 +169,9 @@ function resolveGroupTargets(group: ExtensionTestPlanGroup, exactExcludePaths: S
|
|||
|
||||
function preparePlanGroup(
|
||||
group: ExtensionTestPlanGroup,
|
||||
groupIndex: number,
|
||||
env: NodeJS.ProcessEnv,
|
||||
vitestArgs: string[],
|
||||
exactExcludePaths: Set<string>,
|
||||
useDedicatedCache: boolean,
|
||||
) {
|
||||
const targets = resolveGroupTargets(group, exactExcludePaths);
|
||||
const targetChunks =
|
||||
|
|
@ -200,7 +187,11 @@ function preparePlanGroup(
|
|||
invocations: targetChunks.map<VitestBatchRunParams & { env: NodeJS.ProcessEnv }>((chunk) => ({
|
||||
args: relativizeExtensionVitestArgs(vitestArgs),
|
||||
config: group.config,
|
||||
env: createGroupEnv({ baseEnv: env, group, groupIndex, useDedicatedCache }),
|
||||
env: createGroupEnv({
|
||||
baseEnv: env,
|
||||
group,
|
||||
watchMode: resolveExplicitVitestMode(["run", ...vitestArgs]) === "watch",
|
||||
}),
|
||||
targets: chunk.map((target) => relativizeExtensionVitestPath(target)),
|
||||
})),
|
||||
};
|
||||
|
|
@ -244,7 +235,11 @@ function combineSinglePluginGroups(
|
|||
(target) => !targets.some((root) => target.startsWith(`${root}/`)),
|
||||
),
|
||||
env: {
|
||||
...env,
|
||||
...createGroupEnv({
|
||||
baseEnv: env,
|
||||
group: { ...owner.group, config },
|
||||
watchMode: resolveExplicitVitestMode(["run", ...vitestArgs]) === "watch",
|
||||
}),
|
||||
[DATABASE_WORKER_WATCH_OWNER_ENV_KEY]: owner.group.config,
|
||||
[DATABASE_WORKER_WATCH_TESTS_ENV_KEY]: JSON.stringify(worker.group.roots),
|
||||
},
|
||||
|
|
@ -304,15 +299,14 @@ export async function runExtensionBatchPlan(
|
|||
const runGroup = params.runGroup ?? runVitestBatch;
|
||||
const parallelism = resolveExtensionBatchParallelism(batchPlan.planGroups.length, env);
|
||||
const orderedGroups = orderPlanGroups(batchPlan.planGroups, parallelism);
|
||||
const useDedicatedCache = parallelism > 1;
|
||||
const allowEmptyAfterExclude = params.allowEmptyAfterExclude ?? false;
|
||||
|
||||
if (parallelism > 1) {
|
||||
console.log(`[test-extension-batch] Running up to ${parallelism} config groups in parallel`);
|
||||
}
|
||||
|
||||
const leafGroups = orderedGroups.map((group, index) =>
|
||||
preparePlanGroup(group, index, env, vitestArgs, exactExcludePaths, useDedicatedCache),
|
||||
const leafGroups = orderedGroups.map((group) =>
|
||||
preparePlanGroup(group, env, vitestArgs, exactExcludePaths),
|
||||
);
|
||||
const leafInvocations = leafGroups.flatMap((group) => group.invocations);
|
||||
const cwd = path.resolve(import.meta.dirname, "..");
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { getVitestWorkerDescriptor } from "./lib/vitest-worker-bootstrap.mts";
|
|||
import { createVitestWorkerRun, type VitestWorkerRun } from "./lib/vitest-worker-run.mts";
|
||||
import { resolveVitestSpawnParams, spawnWatchedVitestProcess } from "./run-vitest.mts";
|
||||
import {
|
||||
applyDefaultMultiSpecVitestCachePaths,
|
||||
applyDefaultVitestCachePaths,
|
||||
applyDefaultVitestNoOutputTimeout,
|
||||
applyFullExtensionsHeapBudget,
|
||||
applyParallelVitestCachePaths,
|
||||
|
|
@ -359,7 +359,7 @@ export async function runTestProjects(
|
|||
spec.timingIncludePatterns = inheritedIncludePatterns;
|
||||
}
|
||||
}
|
||||
const runSpecs: VitestRunSpec[] = applyDefaultMultiSpecVitestCachePaths(
|
||||
const runSpecs: VitestRunSpec[] = applyDefaultVitestCachePaths(
|
||||
applyDefaultVitestNoOutputTimeout(
|
||||
applyFullExtensionsHeapBudget(rawRunSpecs, { env: baseEnv }),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import {
|
|||
} from "../test/vitest/vitest.gateway-server-paths.mjs";
|
||||
import { intersectIncludePatterns } from "../test/vitest/vitest.include-patterns.ts";
|
||||
import { packageContractTestFiles } from "../test/vitest/vitest.package-contract-paths.mjs";
|
||||
import { resolveVitestFsModuleCacheRoot } from "../test/vitest/vitest.performance-config.ts";
|
||||
import {
|
||||
isPluginSdkLightTarget,
|
||||
pluginSdkLightTestFiles,
|
||||
|
|
@ -107,6 +106,7 @@ import {
|
|||
import { readTestSelectorSourceFacts } from "./lib/test-selector-source-facts.mts";
|
||||
// CI imports planning before dependency installation; execution owners stay outside this closure.
|
||||
import { resolveVitestCliEntry } from "./lib/vitest-build-prerequisites.mts";
|
||||
import { resolveVitestCacheRoot, resolveVitestCacheSlotPath } from "./lib/vitest-cache-slots.mts";
|
||||
import {
|
||||
collectVitestFileFilters,
|
||||
resolveBooleanModeFlag,
|
||||
|
|
@ -4705,53 +4705,63 @@ export function resolveParallelFullSuiteConcurrency(
|
|||
return Math.min(resolveLocalFullSuiteProfile(env, hostInfo).shardParallelism, specCount);
|
||||
}
|
||||
|
||||
function sanitizeVitestCachePathSegment(value: string) {
|
||||
return (
|
||||
value
|
||||
.replace(/[^a-zA-Z0-9._-]+/gu, "-")
|
||||
.replace(/^-+|-+$/gu, "")
|
||||
.slice(0, 180) || "default"
|
||||
);
|
||||
}
|
||||
|
||||
export function applyParallelVitestCachePaths<T extends VitestSpecShape>(
|
||||
specs: T[],
|
||||
params: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
||||
): Array<CacheAssignedSpec<T>> {
|
||||
const baseEnv = params.env ?? process.env;
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const configuredCacheRoot = baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim() || undefined;
|
||||
// CI publishes a persistent cache root, not a writer-safe leaf. Every
|
||||
// concurrent Vitest process still needs its own live directory below it.
|
||||
const cacheRoot = configuredCacheRoot ?? resolveVitestFsModuleCacheRoot(cwd);
|
||||
return specs.map((spec, index) => {
|
||||
const sharedRoot = baseEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT?.trim();
|
||||
// Project callers historically supplied a root through PATH. ROOT makes CI's
|
||||
// ownership explicit while a simultaneous PATH remains a caller-owned leaf.
|
||||
const legacyRoot = sharedRoot ? undefined : baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
const cacheRoot = legacyRoot || resolveVitestCacheRoot(baseEnv, cwd);
|
||||
const configSlots = new Map<string, number>();
|
||||
return specs.map((spec) => {
|
||||
const specCachePath = spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
if (specCachePath && specCachePath !== configuredCacheRoot) {
|
||||
if (
|
||||
spec.cacheAssignment?.kind === "caller" ||
|
||||
(spec.cacheAssignment?.kind !== "scheduler" && specCachePath && specCachePath !== legacyRoot)
|
||||
) {
|
||||
return { ...spec, cacheAssignment: spec.cacheAssignment ?? { kind: "caller" } };
|
||||
}
|
||||
const cacheSegment = sanitizeVitestCachePathSegment(`${index}-${spec.config}`);
|
||||
const firstSlot = resolveVitestCacheSlotPath(cacheRoot, spec.config, 0, cwd);
|
||||
const slot = configSlots.get(firstSlot) ?? 0;
|
||||
configSlots.set(firstSlot, slot + 1);
|
||||
return {
|
||||
...spec,
|
||||
cacheAssignment: { kind: "scheduler", root: cacheRoot },
|
||||
env: {
|
||||
...spec.env,
|
||||
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(cacheRoot, cacheSegment),
|
||||
[FS_MODULE_CACHE_PATH_ENV_KEY]: resolveVitestCacheSlotPath(
|
||||
cacheRoot,
|
||||
spec.config,
|
||||
slot,
|
||||
cwd,
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function applyDefaultMultiSpecVitestCachePaths<T extends WatchableVitestSpecShape>(
|
||||
export function applyDefaultVitestCachePaths<T extends WatchableVitestSpecShape>(
|
||||
specs: T[],
|
||||
params: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
||||
): Array<CacheAssignedSpec<T>> {
|
||||
if (specs.length <= 1 || specs.some((spec) => spec.watchMode)) {
|
||||
if (specs.some((spec) => spec.watchMode)) {
|
||||
return specs;
|
||||
}
|
||||
// Same-config process lifetimes run one after another and must keep the
|
||||
// restored CI seed. Isolating them would make every Telegram file pay a
|
||||
// silent cold import.
|
||||
if (specs.every((spec) => spec.config === specs[0]?.config)) {
|
||||
const baseEnv = params.env ?? process.env;
|
||||
const oneConfig = specs.length <= 1 || specs.every((spec) => spec.config === specs[0]?.config);
|
||||
// Before ROOT existed these serial callers owned PATH as an exact leaf.
|
||||
if (
|
||||
!baseEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT?.trim() &&
|
||||
baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim() &&
|
||||
oneConfig
|
||||
) {
|
||||
return specs;
|
||||
}
|
||||
if (process.platform === "win32" && oneConfig) {
|
||||
return specs;
|
||||
}
|
||||
return applyParallelVitestCachePaths(specs, params);
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
concurrency: 1,
|
||||
env: {
|
||||
OPENCLAW_CI_TEST_RUNTIME_POLICY: policy,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: persistentRoot,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT: persistentRoot,
|
||||
OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--maxWorkers=1"]',
|
||||
},
|
||||
scratchDir: makeScratchDir(),
|
||||
|
|
@ -223,7 +223,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
label,
|
||||
runtime: env.OPENCLAW_VITEST_RUNTIME,
|
||||
args,
|
||||
cache: env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH,
|
||||
cache: env.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT,
|
||||
});
|
||||
if (label.endsWith("eligible")) {
|
||||
expect(JSON.parse(readFileSync(env.OPENCLAW_VITEST_INCLUDE_FILE!, "utf8"))).toEqual([
|
||||
|
|
@ -283,7 +283,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
"bun",
|
||||
]);
|
||||
expect(
|
||||
new Set(runChild.mock.calls.map(([, env]) => env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).size,
|
||||
new Set(runChild.mock.calls.map(([, env]) => env.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT)).size,
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
|
|
@ -299,9 +299,13 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
expect(runChild).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["bun-compatible", "dual"] as const)(
|
||||
"runs every unit-fast file while keeping failures and Bun skips on Node under %s",
|
||||
async (policy) => {
|
||||
it.each([
|
||||
{ policy: "bun-compatible", vitestArgs: [] },
|
||||
{ policy: "dual", vitestArgs: [] },
|
||||
{ policy: "dual", vitestArgs: ["--testNamePattern=(?!)", "--maxWorkers=1"] },
|
||||
] as const)(
|
||||
"preserves runtime inventories under $policy with $vitestArgs",
|
||||
async ({ policy, vitestArgs }) => {
|
||||
const skippedOnBun = "src/process/spawn-broker/cleanup.test.ts";
|
||||
const v8HeapTest = "src/infra/worker-task-pool.memory.test.ts";
|
||||
const nodeHistoryBenchmark = "test/scripts/bench-session-history.test.ts";
|
||||
|
|
@ -312,7 +316,12 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
v8HeapTest,
|
||||
nodeHistoryBenchmark,
|
||||
];
|
||||
const shard = { configs: [bunConfig], includePatterns, shard_name: "partition" };
|
||||
const shard = {
|
||||
configs: [bunConfig],
|
||||
includePatterns,
|
||||
shard_name: "partition",
|
||||
env: { OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: JSON.stringify(vitestArgs) },
|
||||
};
|
||||
const seen: Array<{
|
||||
runtime: string | undefined;
|
||||
includes: string[];
|
||||
|
|
@ -330,7 +339,10 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
env: { OPENCLAW_CI_TEST_RUNTIME_POLICY: policy },
|
||||
scratchDir: makeScratchDir(),
|
||||
runChild: async (args, env, label, timing) => {
|
||||
expect(args).toEqual([bunConfig]);
|
||||
expect(args).toEqual([
|
||||
bunConfig,
|
||||
...(vitestArgs.length ? ["--", ...vitestArgs] : []),
|
||||
]);
|
||||
seen.push({
|
||||
runtime: env.OPENCLAW_VITEST_RUNTIME,
|
||||
includes: JSON.parse(readFileSync(env.OPENCLAW_VITEST_INCLUDE_FILE!, "utf8")),
|
||||
|
|
@ -416,6 +428,16 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
{ env: { OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--project=another-project"]' } },
|
||||
{ env: { OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--config=another.config.ts"]' } },
|
||||
{ env: { OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--testNamePattern=one case"]' } },
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--testNamePattern=(?!)","--project=other"]',
|
||||
},
|
||||
},
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: '["--testNamePattern=(?!)","--testNamePattern=one"]',
|
||||
},
|
||||
},
|
||||
{ env: { OPENCLAW_NODE_TEST_VITEST_ARGS_JSON: "invalid" } },
|
||||
{ env: { OPENCLAW_VITEST_INCLUDE_FILE: "external.json" } },
|
||||
{ configs: [bunConfig, "test/vitest/vitest.unit-fast-fake-timers.config.ts"] },
|
||||
|
|
@ -538,9 +560,10 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
expect(childEnv.IGNORED).toBeUndefined();
|
||||
expect(childEnv.OPENCLAW_VITEST_SHARD_NAME).toBe("g");
|
||||
expect(childEnv.OPENCLAW_TEST_PROJECTS_PARALLEL).toBe("1");
|
||||
expect(childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe(
|
||||
expect(childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT).toBe(
|
||||
path.join(scratchDir, "vitest-cache-3"),
|
||||
);
|
||||
expect(childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBeUndefined();
|
||||
expect(childEnv.OPENCLAW_VITEST_INCLUDE_FILE).toBe(
|
||||
path.join(scratchDir, "node-test-include-3.json"),
|
||||
);
|
||||
|
|
@ -555,6 +578,21 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
0,
|
||||
);
|
||||
expect(bare.OPENCLAW_VITEST_INCLUDE_FILE).toBeUndefined();
|
||||
|
||||
const explicit = buildChildEnv(
|
||||
{ kind: "group", name: "explicit", plan: { configs: ["cfg.ts"] } },
|
||||
{
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT: scratchDir,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "caller-leaf",
|
||||
},
|
||||
scratchDir,
|
||||
0,
|
||||
{ runtime: "bun" },
|
||||
);
|
||||
expect(explicit.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT).toBe(
|
||||
path.join(scratchDir, "vitest-cache-bun-0"),
|
||||
);
|
||||
expect(explicit.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe("caller-leaf");
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -719,7 +757,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
await Promise.resolve();
|
||||
seen.push({
|
||||
args,
|
||||
cache: childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH,
|
||||
cache: childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT,
|
||||
label,
|
||||
workers: childEnv.OPENCLAW_VITEST_MAX_WORKERS,
|
||||
});
|
||||
|
|
@ -896,7 +934,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
env: { ...env, OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: persistentRoot },
|
||||
scratchDir: makeScratchDir(),
|
||||
runChild: async (_args, childEnv) => {
|
||||
seen.push(childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH ?? "");
|
||||
seen.push(childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT ?? "");
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
|
|
@ -959,7 +997,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
) => {
|
||||
seen.push({
|
||||
args,
|
||||
cache: childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH,
|
||||
cache: childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT,
|
||||
label,
|
||||
includeFile: childEnv.OPENCLAW_VITEST_INCLUDE_FILE,
|
||||
});
|
||||
|
|
@ -1069,9 +1107,9 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
}),
|
||||
{
|
||||
concurrency: 2,
|
||||
env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: persistentRoot },
|
||||
env: { OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT: persistentRoot },
|
||||
runChild: async (_args: string[], childEnv: Record<string, string | undefined>) => {
|
||||
const cache = childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH ?? "";
|
||||
const cache = childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT ?? "";
|
||||
if (activeCaches.has(cache)) {
|
||||
sharedWriter = true;
|
||||
}
|
||||
|
|
@ -1093,22 +1131,34 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("clones a restored persistent seed into every concurrent cache slot", () => {
|
||||
it.each([
|
||||
{ prefixes: ["vitest-cache"] },
|
||||
{ prefixes: ["vitest-cache-bun"] },
|
||||
{ prefixes: ["vitest-cache", "vitest-cache-bun"] },
|
||||
])("clones restored runtime seeds into isolated concurrent slots: $prefixes", ({ prefixes }) => {
|
||||
const persistentRoot = makeScratchDir();
|
||||
const seed = path.join(persistentRoot, "vitest-cache-0");
|
||||
mkdirSync(seed, { recursive: true });
|
||||
writeFileSync(path.join(seed, "transform"), "cached", "utf8");
|
||||
const staleSlot = path.join(persistentRoot, "vitest-cache-1");
|
||||
mkdirSync(staleSlot, { recursive: true });
|
||||
writeFileSync(path.join(staleSlot, "stale"), "old", "utf8");
|
||||
|
||||
expect(clonePersistentCacheSlots(persistentRoot, 3)).toBe(2);
|
||||
for (const cacheSlot of [1, 2]) {
|
||||
expect(
|
||||
readFileSync(path.join(persistentRoot, `vitest-cache-${cacheSlot}`, "transform"), "utf8"),
|
||||
).toBe("cached");
|
||||
for (const prefix of prefixes) {
|
||||
const seed = path.join(persistentRoot, `${prefix}-0`);
|
||||
mkdirSync(seed, { recursive: true });
|
||||
writeFileSync(path.join(seed, "transform"), prefix, "utf8");
|
||||
const staleSlot = path.join(persistentRoot, `${prefix}-1`);
|
||||
mkdirSync(staleSlot, { recursive: true });
|
||||
writeFileSync(path.join(staleSlot, "stale"), "old", "utf8");
|
||||
}
|
||||
|
||||
expect(clonePersistentCacheSlots(persistentRoot, 3)).toBe(prefixes.length * 2);
|
||||
for (const prefix of prefixes) {
|
||||
for (const cacheSlot of [1, 2]) {
|
||||
expect(
|
||||
readFileSync(path.join(persistentRoot, `${prefix}-${cacheSlot}`, "transform"), "utf8"),
|
||||
).toBe(prefix);
|
||||
}
|
||||
expect(existsSync(path.join(persistentRoot, `${prefix}-1`, "stale"))).toBe(false);
|
||||
writeFileSync(path.join(persistentRoot, `${prefix}-1`, "transform"), "changed", "utf8");
|
||||
expect(readFileSync(path.join(persistentRoot, `${prefix}-0`, "transform"), "utf8")).toBe(
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
expect(existsSync(path.join(staleSlot, "stale"))).toBe(false);
|
||||
});
|
||||
|
||||
it("prunes oldest transform entries while preserving Vitest metadata", () => {
|
||||
|
|
@ -1149,7 +1199,7 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
|
|||
runShardPlans(plans, {
|
||||
concurrency: 1,
|
||||
env: {
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: persistentRoot,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT: persistentRoot,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_WRITER: writer,
|
||||
},
|
||||
fsModuleCacheMaxBytes: 0,
|
||||
|
|
|
|||
|
|
@ -18,13 +18,27 @@ vi.mock(import("../../scripts/lib/ci-node-test-plan.mts"), async (importOriginal
|
|||
...actual,
|
||||
createVitestCacheWarmGroups: vi.fn((profile: "full" | "hybrid-hosted" = "full") => {
|
||||
const hosted = actual.createVitestCacheWarmGroups("hybrid-hosted");
|
||||
// The planner suite owns full inventory. Two distinct Node envelopes are
|
||||
// enough to prove this collector continues after a child failure.
|
||||
// Keep one compatible file beside a Node-only file and a tooling file.
|
||||
// The planner suite owns the production inventory; this proves partitioning.
|
||||
const tooling = {
|
||||
...hosted[0]!,
|
||||
includePatterns: [
|
||||
"packages/media-core/src/mime.test.ts",
|
||||
"packages/markdown-core/src/render-aware-chunking.test.ts",
|
||||
"test/scripts/ci-workflow-guards.test.ts",
|
||||
],
|
||||
};
|
||||
return profile === "hybrid-hosted"
|
||||
? hosted
|
||||
: hosted.filter(
|
||||
(group, index) => index < 2 || group.shard_name === "cache-warm:ui-package",
|
||||
);
|
||||
? [tooling, ...hosted.slice(1)]
|
||||
: [
|
||||
{
|
||||
...tooling,
|
||||
configs: ["test/vitest/vitest.unit-fast.config.ts"],
|
||||
includePatterns: tooling.includePatterns.slice(0, 2),
|
||||
},
|
||||
hosted[1]!,
|
||||
hosted.find((group) => group.shard_name === "cache-warm:ui-package")!,
|
||||
];
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
|
@ -39,7 +53,7 @@ afterEach(() => {
|
|||
|
||||
describe("protected Vitest cache collection", () => {
|
||||
it.each(["linux", "linux-hosted"])(
|
||||
"collects both UI runtimes before final pruning and preserves an earlier failure (%s)",
|
||||
"preserves Node and compatible Bun seeds before final UI pruning after a failure (%s)",
|
||||
async (platform) => {
|
||||
const root = tempDirs.make("openclaw-cache-warm-test-");
|
||||
const cacheRoot = join(root, "transforms");
|
||||
|
|
@ -52,7 +66,7 @@ describe("protected Vitest cache collection", () => {
|
|||
CACHE_WARM_PLATFORM: platform,
|
||||
NODE_OPTIONS: "--max-old-space-size=8192",
|
||||
NODE_COMPILE_CACHE: compileRoot,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: cacheRoot,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT: cacheRoot,
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_WRITER: "1",
|
||||
OPENCLAW_NODE_COMPILE_CACHE_WRITER: "1",
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: "inherited-selection.json",
|
||||
|
|
@ -66,11 +80,16 @@ describe("protected Vitest cache collection", () => {
|
|||
"OPENCLAW_NODE_TEST_GROUPS_GZIP_BASE64",
|
||||
"OPENCLAW_NODE_TEST_ENV_JSON",
|
||||
"OPENCLAW_VITEST_MAX_WORKERS",
|
||||
"OPENCLAW_VITEST_FS_MODULE_CACHE_PATH",
|
||||
]) {
|
||||
vi.stubEnv(name, undefined);
|
||||
}
|
||||
const invocations: Array<{ args: string[]; env: NodeJS.ProcessEnv }> = [];
|
||||
const nodeGroups: string[][] = [];
|
||||
const nonUiCollections: Array<{
|
||||
configs: string[];
|
||||
runtime: string | undefined;
|
||||
files: string[];
|
||||
}> = [];
|
||||
const uiCollections: Array<{
|
||||
args: string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
|
|
@ -101,9 +120,15 @@ describe("protected Vitest cache collection", () => {
|
|||
if (childArgs.includes("ui/vitest.config.ts")) {
|
||||
recordUi(childArgs, childEnv);
|
||||
} else {
|
||||
nodeGroups.push(childArgs.slice(0, childArgs.indexOf("--")));
|
||||
nonUiCollections.push({
|
||||
configs: childArgs.slice(0, childArgs.indexOf("--")),
|
||||
runtime: childEnv.OPENCLAW_VITEST_RUNTIME,
|
||||
files: JSON.parse(readFileSync(childEnv.OPENCLAW_VITEST_INCLUDE_FILE!, "utf8")),
|
||||
});
|
||||
expect(childEnv.NODE_OPTIONS).toBe("--max-old-space-size=8192");
|
||||
return nodeGroups.length === 1 ? 19 : 0;
|
||||
return childEnv.OPENCLAW_VITEST_RUNTIME === "node" && nonUiCollections.length === 1
|
||||
? 19
|
||||
: 0;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
|
@ -134,12 +159,14 @@ describe("protected Vitest cache collection", () => {
|
|||
const [bun, node] = uiCollections;
|
||||
expect(bun!.env).toMatchObject(BUN_UI_TEST_ENV);
|
||||
expect(bun!.env.OPENCLAW_VITEST_INCLUDE_FILE).toBeUndefined();
|
||||
expect(bun!.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe(
|
||||
expect(bun!.env.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT).toBe(
|
||||
join(cacheRoot, "vitest-cache-bun-0"),
|
||||
);
|
||||
expect(node!.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe(
|
||||
expect(node!.env.OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT).toBe(
|
||||
join(cacheRoot, "vitest-cache-0"),
|
||||
);
|
||||
expect(bun!.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBeUndefined();
|
||||
expect(node!.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBeUndefined();
|
||||
expect(existsSync(bun!.env.OPENCLAW_VITEST_POST_SHARD_INCLUDE_FILE!)).toBe(false);
|
||||
expect(invocations.at(-1)!.args).toContain("scripts/ci-run-node-test-shard.mts");
|
||||
expect(
|
||||
|
|
@ -148,12 +175,25 @@ describe("protected Vitest cache collection", () => {
|
|||
env.OPENCLAW_NODE_COMPILE_CACHE_WRITER,
|
||||
]),
|
||||
).toEqual([...invocations.slice(0, -1).map(() => ["0", "0"]), ["1", "1"]]);
|
||||
expect(nonUiCollections.filter(({ runtime }) => runtime === "bun")).toEqual([
|
||||
{
|
||||
configs: ["test/vitest/vitest.unit-fast.config.ts"],
|
||||
runtime: "bun",
|
||||
files: ["packages/media-core/src/mime.test.ts"],
|
||||
},
|
||||
]);
|
||||
if (platform === "linux") {
|
||||
const nonUiGroups = groups.filter((group) => group !== ui);
|
||||
expect(JSON.parse(invocations[0]!.env.OPENCLAW_NODE_TEST_GROUPS_JSON!)).toEqual(
|
||||
nonUiGroups,
|
||||
);
|
||||
expect(nodeGroups).toEqual(nonUiGroups.map((group) => group.configs));
|
||||
expect(nonUiCollections.filter(({ runtime }) => runtime === "node")).toEqual(
|
||||
nonUiGroups.map((group) => ({
|
||||
configs: group.configs,
|
||||
runtime: "node",
|
||||
files: group.includePatterns,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
expect(invocations.slice(0, 3).map(({ args }) => args[0])).toEqual([
|
||||
"test",
|
||||
|
|
@ -163,6 +203,11 @@ describe("protected Vitest cache collection", () => {
|
|||
expect(
|
||||
invocations.slice(0, 3).map(({ env }) => env.OPENCLAW_TEST_PROJECTS_PARALLEL),
|
||||
).toEqual(["3", "1", "4"]);
|
||||
expect(invocations[0]!.args).toEqual([
|
||||
"test",
|
||||
...groups[0]!.includePatterns!,
|
||||
"--testNamePattern=(?!)",
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5234,7 +5234,7 @@ server.listen(0, "127.0.0.1", () => {
|
|||
"!**/node_modules/**",
|
||||
"!.ci-harness/**",
|
||||
]);
|
||||
const prefix = `openclaw/openclaw-vitest-fs-v3-protected-${os}-X64-node-24.x-${generation}-`;
|
||||
const prefix = `openclaw/openclaw-vitest-fs-v4-protected-${os}-X64-node-24.x-${generation}-`;
|
||||
expect(cacheInputs).toEqual({
|
||||
path: "/var/tmp/openclaw-vitest-fs-cache",
|
||||
key: `${prefix}10-2`,
|
||||
|
|
@ -5369,11 +5369,11 @@ server.listen(0, "127.0.0.1", () => {
|
|||
expect(readerStep.if).toContain("inputs.restore-test-caches == 'true'");
|
||||
expect(readerStep.if).toContain("runner.os != 'Windows'");
|
||||
expect(readerStep.if).not.toMatch(/runner\.(?:environment|labels|name)/u);
|
||||
expect(readerStep.with.key).toContain("vitest-fs-v3-protected-");
|
||||
expect(readerStep.with.key).toContain("vitest-fs-v4-protected-");
|
||||
expect(readerStep.with.key).toContain("github.run_id");
|
||||
expect(readerStep.with.key).toContain("github.run_attempt");
|
||||
expect(configureStep.if).toContain("inputs.restore-test-caches == 'true'");
|
||||
expect(configureStep.run).toContain("OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=$cache_root");
|
||||
expect(configureStep.run).toContain("OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT=$cache_root");
|
||||
expect(configureStep.run).toContain(".openclaw-transform-generation");
|
||||
expect(configureStep.run).not.toContain("protected Vitest transform seed");
|
||||
expect(configureStep.env.CACHE_WRITER).toBe("0");
|
||||
|
|
|
|||
|
|
@ -808,21 +808,23 @@ describe("scripts/test-extension.mts", () => {
|
|||
await expect(runPromise).resolves.toBe(0);
|
||||
expect(runGroup).toHaveBeenCalledTimes(3);
|
||||
const firstRunGroupParams = requireFirstMockArg<RunGroupParams>(runGroup);
|
||||
expect(firstRunGroupParams).toEqual({
|
||||
expect(firstRunGroupParams).toMatchObject({
|
||||
args: ["--reporter=dot"],
|
||||
config: "heavy",
|
||||
env: {
|
||||
OPENCLAW_EXTENSION_BATCH_PARALLEL: "2",
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: path.join(
|
||||
process.cwd(),
|
||||
".cache",
|
||||
"vitest",
|
||||
"extension-batch",
|
||||
"0-heavy",
|
||||
),
|
||||
},
|
||||
targets: ["two"],
|
||||
});
|
||||
const cachePaths = runGroup.mock.calls.map(([params]) =>
|
||||
params.env?.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.replaceAll("\\", "/"),
|
||||
);
|
||||
expect(new Set(cachePaths).size).toBe(3);
|
||||
expect(
|
||||
cachePaths.every((cachePath) =>
|
||||
/\/\.cache\/vitest\/slots\/[a-f\d]+\/0$/u.test(cachePath ?? ""),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("stops admitting extension batch groups after a parallel failure", async () => {
|
||||
|
|
|
|||
|
|
@ -1433,21 +1433,36 @@ describe("scripts/test-group-report run plans", () => {
|
|||
});
|
||||
|
||||
it("isolates Vitest filesystem module caches for parallel report configs", () => {
|
||||
const args = parseTestGroupReportArgs(["--config", "a.ts", "--config", "b.ts"]);
|
||||
const args = parseTestGroupReportArgs([
|
||||
"--config",
|
||||
"a.ts",
|
||||
"--config",
|
||||
"b.ts",
|
||||
"--config",
|
||||
"a.ts",
|
||||
]);
|
||||
const specs = resolveReportRunSpecs(
|
||||
args,
|
||||
[
|
||||
{ config: "a.ts", forwardedArgs: [], label: "a" },
|
||||
{ config: "b.ts", forwardedArgs: [], label: "b" },
|
||||
{ config: "a.ts", forwardedArgs: [], label: "a-again" },
|
||||
],
|
||||
{ cwd: "/repo", env: {} },
|
||||
);
|
||||
|
||||
expect(specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).toEqual([
|
||||
path.join("/repo", ".cache", "vitest", "0-a.ts"),
|
||||
path.join("/repo", ".cache", "vitest", "1-b.ts"),
|
||||
]);
|
||||
expect(specs.map((spec) => spec.vitestArgs)).toEqual([[], []]);
|
||||
const cachePaths = specs.map((spec) =>
|
||||
expectDefined(spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH, "report cache path"),
|
||||
);
|
||||
expect(new Set(cachePaths).size).toBe(3);
|
||||
for (const cachePath of cachePaths) {
|
||||
const relative = path.relative(path.join("/repo", ".cache", "vitest"), cachePath);
|
||||
expect(relative).not.toBe("");
|
||||
expect(path.isAbsolute(relative)).toBe(false);
|
||||
expect(relative.split(path.sep)).not.toContain("..");
|
||||
expect(cachePaths.filter((other) => other.startsWith(`${cachePath}${path.sep}`))).toEqual([]);
|
||||
}
|
||||
expect(specs.map((spec) => spec.vitestArgs)).toEqual([[], [], []]);
|
||||
});
|
||||
|
||||
it("uses leaf configs for full-suite profiling without requiring parallel env", () => {
|
||||
|
|
|
|||
|
|
@ -554,6 +554,12 @@ describe("full-suite timing metadata", () => {
|
|||
});
|
||||
|
||||
describe("cache lease completion", () => {
|
||||
beforeEach(() => {
|
||||
// The enclosing CI test worker owns its PATH; these fixtures exercise a new scheduler.
|
||||
vi.stubEnv("OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT", "");
|
||||
vi.stubEnv("OPENCLAW_VITEST_FS_MODULE_CACHE_PATH", "");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ platform: "linux", phase: "preflight" },
|
||||
{ platform: "linux", phase: "retry" },
|
||||
|
|
@ -619,7 +625,7 @@ describe("cache lease completion", () => {
|
|||
async ({ platform, concurrency }) => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue(platform);
|
||||
const cacheRoot = tempDirs.make("cache-policy-");
|
||||
vi.stubEnv("OPENCLAW_VITEST_FS_MODULE_CACHE_PATH", cacheRoot);
|
||||
vi.stubEnv("OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT", cacheRoot);
|
||||
vi.stubEnv("OPENCLAW_TEST_PROJECTS_PARALLEL", String(concurrency));
|
||||
vi.stubEnv("OPENCLAW_VITEST_NO_OUTPUT_RETRY", "1");
|
||||
const planner = await import("../../scripts/test-projects.test-support.mts");
|
||||
|
|
@ -687,12 +693,11 @@ describe("cache lease completion", () => {
|
|||
try {
|
||||
await withTestTimeout(started.promise, 5_000, "preflight and peer admission");
|
||||
expect(new Set(paths).size).toBe(concurrency);
|
||||
if (concurrency === 2) {
|
||||
for (const cache of paths) {
|
||||
expect(path.relative(cacheRoot, cache).startsWith(`slots${path.sep}`)).toBe(
|
||||
platform !== "win32",
|
||||
);
|
||||
}
|
||||
for (const cache of paths) {
|
||||
const relative = path.relative(cacheRoot, cache);
|
||||
expect(relative).not.toBe("");
|
||||
expect(path.isAbsolute(relative)).toBe(false);
|
||||
expect(relative.split(path.sep)).not.toContain("..");
|
||||
}
|
||||
firstPreflight.resolve(joined);
|
||||
await withTestTimeout(retryStarted.promise, 5_000, "retry preflight admission");
|
||||
|
|
|
|||
|
|
@ -489,12 +489,13 @@ describe("test-projects args", () => {
|
|||
|
||||
const firstEnv = specs[0]?.env;
|
||||
expect(firstEnv?.KEEP_ME).toBe("1");
|
||||
expect(firstEnv?.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.replaceAll("\\", "/")).toBe(
|
||||
"/repo/.cache/vitest/0-test-vitest-vitest.gateway.config.ts",
|
||||
);
|
||||
expect(specs[1]?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.replaceAll("\\", "/")).toBe(
|
||||
"/repo/.cache/vitest/1-test-vitest-vitest.gateway-server.config.ts",
|
||||
);
|
||||
const paths = specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH);
|
||||
expect(new Set(paths).size).toBe(2);
|
||||
for (const cachePath of paths) {
|
||||
expect(cachePath?.replaceAll("\\", "/")).toMatch(
|
||||
/^\/repo\/\.cache\/vitest\/slots\/[a-f\d]+\/0$/u,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("routes plugin targets to the plugins config", () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { resolveDefaultVitestNoOutputTimeoutMs } from "../../scripts/lib/vitest-
|
|||
import { resolveVitestRuntimeCliSelections } from "../../scripts/lib/vitest-runtime-selection.mts";
|
||||
import { resolveShardTimingKey } from "../../scripts/lib/vitest-shard-metadata.mts";
|
||||
import {
|
||||
applyDefaultMultiSpecVitestCachePaths,
|
||||
applyDefaultVitestCachePaths,
|
||||
applyDefaultVitestNoOutputTimeout,
|
||||
applyFullExtensionsHeapBudget,
|
||||
applyParallelVitestCachePaths,
|
||||
|
|
@ -5419,10 +5419,11 @@ describe("scripts/test-projects parallel cache paths", () => {
|
|||
{ cwd: "/repo", env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" } },
|
||||
);
|
||||
|
||||
expect(specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).toEqual([
|
||||
path.join("/tmp/cache", "0-test-vitest-vitest.gateway.config.ts"),
|
||||
path.join("/tmp/cache", "1-test-vitest-vitest.extension-telegram.config.ts"),
|
||||
]);
|
||||
const paths = specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH);
|
||||
expect(new Set(paths).size).toBe(2);
|
||||
for (const cachePath of paths) {
|
||||
expect(path.relative("/tmp/cache", cachePath!)).toMatch(/^slots[/\\][a-f\d]+[/\\]0$/u);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an already isolated cache path", () => {
|
||||
|
|
@ -5563,7 +5564,7 @@ describe("scripts/test-projects Vitest stall watchdog", () => {
|
|||
});
|
||||
|
||||
describe("scripts/test-projects Vitest cache isolation", () => {
|
||||
it("keeps same-config process lifetimes on one restored cache", () => {
|
||||
it("keeps same-config process lifetimes on their explicit cache leaf", () => {
|
||||
const specs = [
|
||||
{
|
||||
config: "test/vitest/vitest.extension-telegram.config.ts",
|
||||
|
|
@ -5583,11 +5584,12 @@ describe("scripts/test-projects Vitest cache isolation", () => {
|
|||
},
|
||||
];
|
||||
|
||||
const configured = applyDefaultMultiSpecVitestCachePaths(specs, {
|
||||
const configured = applyDefaultVitestCachePaths(specs, {
|
||||
cwd: "/repo",
|
||||
env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" },
|
||||
});
|
||||
|
||||
expect(configured).toBe(specs);
|
||||
expect(configured.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).toEqual([
|
||||
"/tmp/cache",
|
||||
"/tmp/cache",
|
||||
|
|
@ -5595,7 +5597,7 @@ describe("scripts/test-projects Vitest cache isolation", () => {
|
|||
});
|
||||
|
||||
it("assigns isolated fs-module caches to multi-spec non-watch runs", () => {
|
||||
const specs = applyDefaultMultiSpecVitestCachePaths(
|
||||
const specs = applyDefaultVitestCachePaths(
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.unit-fast.config.ts",
|
||||
|
|
@ -5617,13 +5619,16 @@ describe("scripts/test-projects Vitest cache isolation", () => {
|
|||
{ cwd: "/repo", env: {} },
|
||||
);
|
||||
|
||||
expect(specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).toEqual([
|
||||
path.join("/repo", ".cache", "vitest", "0-test-vitest-vitest.unit-fast.config.ts"),
|
||||
path.join("/repo", ".cache", "vitest", "1-test-vitest-vitest.extension-memory.config.ts"),
|
||||
]);
|
||||
const paths = specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH);
|
||||
expect(new Set(paths).size).toBe(2);
|
||||
for (const cachePath of paths) {
|
||||
expect(path.relative(path.join("/repo", ".cache", "vitest"), cachePath!)).toMatch(
|
||||
/^slots[/\\][a-f\d]+[/\\]0$/u,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps single-spec and watch runs on the default cache", () => {
|
||||
it("assigns single-spec runs while preserving the watch cache owner", () => {
|
||||
const single = [
|
||||
{
|
||||
config: "test/vitest/vitest.unit-fast.config.ts",
|
||||
|
|
@ -5634,7 +5639,15 @@ describe("scripts/test-projects Vitest cache isolation", () => {
|
|||
watchMode: false,
|
||||
},
|
||||
];
|
||||
expect(applyDefaultMultiSpecVitestCachePaths(single, { cwd: "/repo", env: {} })).toBe(single);
|
||||
const assigned = applyDefaultVitestCachePaths(single, { cwd: "/repo", env: {} });
|
||||
if (process.platform === "win32") {
|
||||
expect(assigned).toBe(single);
|
||||
} else {
|
||||
expect(assigned[0]?.cacheAssignment).toEqual({
|
||||
kind: "scheduler",
|
||||
root: path.join("/repo", ".cache", "vitest"),
|
||||
});
|
||||
}
|
||||
|
||||
const watch = [
|
||||
{
|
||||
|
|
@ -5654,7 +5667,7 @@ describe("scripts/test-projects Vitest cache isolation", () => {
|
|||
watchMode: false,
|
||||
},
|
||||
];
|
||||
expect(applyDefaultMultiSpecVitestCachePaths(watch, { cwd: "/repo", env: {} })).toBe(watch);
|
||||
expect(applyDefaultVitestCachePaths(watch, { cwd: "/repo", env: {} })).toBe(watch);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createVitestCacheSlots } from "../../scripts/lib/vitest-cache-slots.mts";
|
||||
import type { VitestCacheAssignment } from "../../scripts/test-projects.test-support.mts";
|
||||
import {
|
||||
applyDefaultVitestCachePaths,
|
||||
type VitestCacheAssignment,
|
||||
} from "../../scripts/test-projects.test-support.mts";
|
||||
import { createDeferred } from "../helpers/promise.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
import { loadVitestPerformanceConfig } from "../vitest/vitest.performance-config.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
const spec = {
|
||||
config: "test/vitest/vitest.tooling.config.ts",
|
||||
|
|
@ -13,6 +21,68 @@ const spec = {
|
|||
const cachePath = (assigned: typeof spec) => assigned.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH;
|
||||
|
||||
describe("Vitest cache slot ownership", () => {
|
||||
it.skipIf(process.platform === "win32").each(["root", "default"] as const)(
|
||||
"reuses separately warmed configs through %s in serial, reordered, and parallel project runs",
|
||||
async (mode) => {
|
||||
const root = tempDirs.make("vitest-cache-layout-");
|
||||
const env: NodeJS.ProcessEnv =
|
||||
mode === "root" ? { OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT: root } : {};
|
||||
const run = async (configs: string[], parallel: boolean, seed = false) => {
|
||||
const lease = createVitestCacheSlots("linux");
|
||||
const specs = applyDefaultVitestCachePaths(
|
||||
configs.map((config) => ({ config, env, watchMode: false })),
|
||||
{ env, cwd: root },
|
||||
);
|
||||
const visit = (entry: (typeof specs)[number]) =>
|
||||
lease(entry, async (assigned) => {
|
||||
const directory = loadVitestPerformanceConfig(
|
||||
assigned.env,
|
||||
"linux",
|
||||
root,
|
||||
).fsModuleCachePath!;
|
||||
const file = path.join(directory, `${entry.config}.js`);
|
||||
if (seed) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
fs.writeFileSync(file, entry.config);
|
||||
} else {
|
||||
expect(fs.existsSync(file), `${entry.config} did not consume its warmed cache`).toBe(
|
||||
true,
|
||||
);
|
||||
expect(fs.readFileSync(file, "utf8")).toBe(entry.config);
|
||||
}
|
||||
return { groupJoined: true };
|
||||
});
|
||||
if (parallel) {
|
||||
await Promise.all(specs.map(visit));
|
||||
} else {
|
||||
for (const entry of specs) {
|
||||
await visit(entry);
|
||||
}
|
||||
}
|
||||
};
|
||||
await run(["first.config.ts"], false, true);
|
||||
await run(["second.config.ts"], false, true);
|
||||
await run(["second.config.ts", "first.config.ts"], false);
|
||||
await run(["first.config.ts", "first.config.ts"], false);
|
||||
await run(["first.config.ts", "second.config.ts"], true);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves an explicit caller leaf alongside a shared root", async () => {
|
||||
const env = {
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT: "/shared-cache",
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/caller-owned-leaf",
|
||||
};
|
||||
const assigned = applyDefaultVitestCachePaths([{ ...spec, env, cacheAssignment: undefined }], {
|
||||
env,
|
||||
});
|
||||
const lease = createVitestCacheSlots("linux");
|
||||
await lease(assigned[0]!, async (entry) => {
|
||||
expect(entry.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe("/caller-owned-leaf");
|
||||
return { groupJoined: true };
|
||||
});
|
||||
});
|
||||
|
||||
it("reuses an idle config cache while another config occupies its former scheduler slot", async () => {
|
||||
const run = createVitestCacheSlots("linux");
|
||||
const first = createDeferred<{ groupJoined: boolean }>();
|
||||
|
|
@ -104,7 +174,7 @@ describe("Vitest cache slot ownership", () => {
|
|||
paths.push(cachePath(assigned));
|
||||
return { groupJoined: true };
|
||||
});
|
||||
expect(new Set(paths.map((value) => path.basename(path.dirname(value)))).size).toBe(3);
|
||||
expect(new Set(paths.map((value) => path.basename(value))).size).toBe(3);
|
||||
} finally {
|
||||
first.resolve({ groupJoined: false });
|
||||
await pending;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue