Tests: align unit sharding with unit config

This commit is contained in:
Vincent Koc 2026-03-18 12:16:07 -07:00
parent e6911f0448
commit e9903c9133
4 changed files with 85 additions and 25 deletions

View File

@ -3,6 +3,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { channelTestPrefixes } from "../vitest.channel-paths.mjs";
import { isUnitConfigTestFile } from "../vitest.unit-paths.mjs";
import {
loadTestRunnerBehavior,
loadUnitTimingManifest,
@ -16,10 +17,11 @@ const pnpm = "pnpm";
const behaviorManifest = loadTestRunnerBehavior();
const existingFiles = (entries) =>
entries.map((entry) => entry.file).filter((file) => fs.existsSync(file));
const unitBehaviorIsolatedFiles = existingFiles(behaviorManifest.unit.isolated);
const unitSingletonIsolatedFiles = existingFiles(behaviorManifest.unit.singletonIsolated);
const unitThreadSingletonFiles = existingFiles(behaviorManifest.unit.threadSingleton);
const unitVmForkSingletonFiles = existingFiles(behaviorManifest.unit.vmForkSingleton);
const existingUnitConfigFiles = (entries) => existingFiles(entries).filter(isUnitConfigTestFile);
const unitBehaviorIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.isolated);
const unitSingletonIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.singletonIsolated);
const unitThreadSingletonFiles = existingUnitConfigFiles(behaviorManifest.unit.threadSingleton);
const unitVmForkSingletonFiles = existingUnitConfigFiles(behaviorManifest.unit.vmForkSingleton);
const unitBehaviorOverrideSet = new Set([
...unitBehaviorIsolatedFiles,
...unitSingletonIsolatedFiles,
@ -237,10 +239,7 @@ const parseEnvNumber = (name, fallback) => {
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
};
const allKnownUnitFiles = allKnownTestFiles.filter((file) => {
if (file.endsWith(".live.test.ts") || file.endsWith(".e2e.test.ts")) {
return false;
}
return inferTarget(file).owner !== "gateway";
return isUnitConfigTestFile(file);
});
const defaultHeavyUnitFileLimit =
testProfile === "serial" ? 0 : testProfile === "low" ? 20 : highMemLocalHost ? 80 : 60;
@ -730,10 +729,12 @@ const runOnce = (entry, extraArgs = []) =>
const run = async (entry, extraArgs = []) => {
const explicitFilterCount = countExplicitEntryFilters(entry.args);
// Wrapper-generated singleton/small-file lanes should not ask Vitest to shard
// into more buckets than there are explicit test filters.
// Vitest requires the shard count to stay strictly below the number of
// resolved test files, so explicit-filter lanes need a `< fileCount` cap.
const effectiveShardCount =
explicitFilterCount === null ? shardCount : Math.min(shardCount, explicitFilterCount);
explicitFilterCount === null
? shardCount
: Math.min(shardCount, Math.max(1, explicitFilterCount - 1));
if (effectiveShardCount <= 1) {
if (shardIndexOverride !== null && shardIndexOverride > effectiveShardCount) {

View File

@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { isUnitConfigTestFile } from "../vitest.unit-paths.mjs";
describe("isUnitConfigTestFile", () => {
it("accepts unit-config src, test, and whitelisted ui tests", () => {
expect(isUnitConfigTestFile("src/infra/git-commit.test.ts")).toBe(true);
expect(isUnitConfigTestFile("test/format-error.test.ts")).toBe(true);
expect(isUnitConfigTestFile("ui/src/ui/views/chat.test.ts")).toBe(true);
});
it("rejects files excluded from the unit config", () => {
expect(
isUnitConfigTestFile("extensions/imessage/src/monitor.shutdown.unhandled-rejection.test.ts"),
).toBe(false);
expect(isUnitConfigTestFile("src/agents/pi-embedded-runner.test.ts")).toBe(false);
expect(isUnitConfigTestFile("src/commands/onboard.test.ts")).toBe(false);
expect(isUnitConfigTestFile("ui/src/ui/views/other.test.ts")).toBe(false);
expect(isUnitConfigTestFile("src/infra/git-commit.live.test.ts")).toBe(false);
expect(isUnitConfigTestFile("src/infra/git-commit.e2e.test.ts")).toBe(false);
});
});

46
vitest.unit-paths.mjs Normal file
View File

@ -0,0 +1,46 @@
import path from "node:path";
export const unitTestIncludePatterns = [
"src/**/*.test.ts",
"test/**/*.test.ts",
"ui/src/ui/app-chat.test.ts",
"ui/src/ui/views/agents-utils.test.ts",
"ui/src/ui/views/chat.test.ts",
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
];
export const unitTestAdditionalExcludePatterns = [
"src/gateway/**",
"extensions/**",
"src/browser/**",
"src/line/**",
"src/agents/**",
"src/auto-reply/**",
"src/commands/**",
];
const sharedBaseExcludePatterns = [
"dist/**",
"apps/macos/**",
"apps/macos/.build/**",
"**/node_modules/**",
"**/vendor/**",
"dist/OpenClaw.app/**",
"**/*.live.test.ts",
"**/*.e2e.test.ts",
];
const normalizeRepoPath = (value) => value.split(path.sep).join("/");
const matchesAny = (file, patterns) => patterns.some((pattern) => path.matchesGlob(file, pattern));
export function isUnitConfigTestFile(file) {
const normalizedFile = normalizeRepoPath(file);
return (
matchesAny(normalizedFile, unitTestIncludePatterns) &&
!matchesAny(normalizedFile, sharedBaseExcludePatterns) &&
!matchesAny(normalizedFile, unitTestAdditionalExcludePatterns)
);
}

View File

@ -1,27 +1,19 @@
import { defineConfig } from "vitest/config";
import baseConfig from "./vitest.config.ts";
import {
unitTestAdditionalExcludePatterns,
unitTestIncludePatterns,
} from "./vitest.unit-paths.mjs";
const base = baseConfig as unknown as Record<string, unknown>;
const baseTest = (baseConfig as { test?: { include?: string[]; exclude?: string[] } }).test ?? {};
const include = (
baseTest.include ?? ["src/**/*.test.ts", "extensions/**/*.test.ts", "test/format-error.test.ts"]
).filter((pattern) => !pattern.includes("extensions/"));
const exclude = baseTest.exclude ?? [];
export default defineConfig({
...base,
test: {
...baseTest,
include,
exclude: [
...exclude,
"src/gateway/**",
"extensions/**",
"src/browser/**",
"src/line/**",
"src/agents/**",
"src/auto-reply/**",
"src/commands/**",
],
include: unitTestIncludePatterns,
exclude: [...exclude, ...unitTestAdditionalExcludePatterns],
},
});