Skip to content

Commit 4beadbf

Browse files
committed
ci(release): apply exact extension batch excludes
1 parent 6c5b392 commit 4beadbf

3 files changed

Lines changed: 117 additions & 4 deletions

File tree

scripts/lib/extension-test-plan.mjs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ function isPathInsideRepo(relativePath) {
7070
}
7171

7272
function isSkippedTrackedTestFile(relativePath) {
73-
return relativePath.split("/").some((segment) => segment === "dist" || segment === "node_modules");
73+
return relativePath
74+
.split("/")
75+
.some((segment) => segment === "dist" || segment === "node_modules");
7476
}
7577

7678
function listTrackedTestFiles(rootPath) {
@@ -99,6 +101,18 @@ function listTrackedTestFiles(rootPath) {
99101
);
100102
}
101103

104+
export function listTrackedTestFilesForRoots(roots) {
105+
const files = [];
106+
for (const root of roots) {
107+
const trackedFiles = listTrackedTestFiles(path.join(repoRoot, root));
108+
if (!trackedFiles) {
109+
return null;
110+
}
111+
files.push(...trackedFiles);
112+
}
113+
return [...new Set(files)].toSorted((left, right) => left.localeCompare(right));
114+
}
115+
102116
function countTestFiles(rootPath) {
103117
const trackedFiles = listTrackedTestFiles(rootPath);
104118
if (trackedFiles) {

scripts/test-extension-batch.mjs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#!/usr/bin/env node
22

33
import path from "node:path";
4-
import { resolveExtensionBatchPlan } from "./lib/extension-test-plan.mjs";
4+
import {
5+
listTrackedTestFilesForRoots,
6+
resolveExtensionBatchPlan,
7+
} from "./lib/extension-test-plan.mjs";
58
import { isDirectScriptRun, runVitestBatch } from "./lib/vitest-batch-runner.mjs";
69

710
const FS_MODULE_CACHE_PATH_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_PATH";
@@ -87,9 +90,62 @@ function orderPlanGroups(planGroups, parallelism) {
8790
});
8891
}
8992

93+
function normalizeRelativePath(inputPath) {
94+
return path
95+
.relative(process.cwd(), path.resolve(process.cwd(), inputPath))
96+
.split(path.sep)
97+
.join("/");
98+
}
99+
100+
function isExactExcludePath(inputPath) {
101+
return !/[*!?[\]{}]/u.test(inputPath);
102+
}
103+
104+
export function parseExactVitestExcludePaths(vitestArgs) {
105+
const excludePaths = new Set();
106+
for (let index = 0; index < vitestArgs.length; index += 1) {
107+
const arg = vitestArgs[index];
108+
if (arg === "--exclude") {
109+
const value = vitestArgs[index + 1];
110+
if (value && isExactExcludePath(value)) {
111+
excludePaths.add(normalizeRelativePath(value));
112+
}
113+
index += 1;
114+
continue;
115+
}
116+
const prefix = "--exclude=";
117+
if (arg.startsWith(prefix)) {
118+
const value = arg.slice(prefix.length);
119+
if (value && isExactExcludePath(value)) {
120+
excludePaths.add(normalizeRelativePath(value));
121+
}
122+
}
123+
}
124+
return excludePaths;
125+
}
126+
127+
function resolveGroupTargets(group, exactExcludePaths) {
128+
if (exactExcludePaths.size === 0) {
129+
return group.roots;
130+
}
131+
132+
const testFiles = listTrackedTestFilesForRoots(group.roots);
133+
if (!testFiles) {
134+
return group.roots;
135+
}
136+
137+
return testFiles.filter((file) => !exactExcludePaths.has(file));
138+
}
139+
90140
async function runPlanGroup(group, params) {
141+
const targets = resolveGroupTargets(group, params.exactExcludePaths);
142+
if (targets.length === 0) {
143+
console.log(`[test-extension-batch] ${group.config}: no test files remain after excludes`);
144+
return 0;
145+
}
146+
91147
console.log(
92-
`[test-extension-batch] ${group.config}: ${group.extensionIds.join(", ")} (${group.testFileCount} files)`,
148+
`[test-extension-batch] ${group.config}: ${group.extensionIds.join(", ")} (${targets.length} targets)`,
93149
);
94150
return await params.runGroup({
95151
args: params.vitestArgs,
@@ -100,13 +156,14 @@ async function runPlanGroup(group, params) {
100156
groupIndex: params.groupIndex,
101157
useDedicatedCache: params.useDedicatedCache,
102158
}),
103-
targets: group.roots,
159+
targets,
104160
});
105161
}
106162

107163
export async function runExtensionBatchPlan(batchPlan, params = {}) {
108164
const env = params.env ?? process.env;
109165
const vitestArgs = params.vitestArgs ?? [];
166+
const exactExcludePaths = parseExactVitestExcludePaths(vitestArgs);
110167
const runGroup = params.runGroup ?? runVitestBatch;
111168
const parallelism = resolveExtensionBatchParallelism(batchPlan.planGroups.length, env);
112169
const orderedGroups = orderPlanGroups(batchPlan.planGroups, parallelism);
@@ -130,6 +187,7 @@ export async function runExtensionBatchPlan(batchPlan, params = {}) {
130187
env,
131188
groupIndex,
132189
runGroup,
190+
exactExcludePaths,
133191
useDedicatedCache,
134192
vitestArgs,
135193
});

test/scripts/test-extension.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { buildVitestBatchPnpmArgs } from "../../scripts/lib/vitest-batch-runner.mjs";
1717
import {
1818
parseExtensionIds,
19+
parseExactVitestExcludePaths,
1920
resolveExtensionBatchParallelism,
2021
runExtensionBatchPlan,
2122
} from "../../scripts/test-extension-batch.mjs";
@@ -626,6 +627,46 @@ describe("scripts/test-extension.mjs", () => {
626627
]);
627628
});
628629

630+
it("expands extension batch roots before applying exact Vitest excludes", async () => {
631+
const runGroup = vi.fn<() => Promise<number>>().mockResolvedValue(0);
632+
await runExtensionBatchPlan(
633+
{
634+
extensionCount: 1,
635+
extensionIds: ["codex"],
636+
estimatedCost: 1,
637+
hasTests: true,
638+
planGroups: [
639+
{
640+
config: "test/vitest/vitest.extensions.config.ts",
641+
estimatedCost: 1,
642+
extensionIds: ["codex"],
643+
roots: [bundledPluginRoot("codex")],
644+
testFileCount: 1,
645+
},
646+
],
647+
testFileCount: 1,
648+
},
649+
{
650+
runGroup,
651+
vitestArgs: ["--exclude", "extensions/codex/src/app-server/run-attempt.test.ts"],
652+
},
653+
);
654+
655+
const runParams = requireFirstMockArg<RunGroupParams>(runGroup);
656+
expect(runParams.targets).not.toContain("extensions/codex/src/app-server/run-attempt.test.ts");
657+
expect(runParams.targets).toContain("extensions/codex/src/app-server/client.test.ts");
658+
});
659+
660+
it("detects exact Vitest excludes in extension batch args", () => {
661+
expect([
662+
...parseExactVitestExcludePaths([
663+
"--exclude",
664+
"extensions/codex/src/app-server/run-attempt.test.ts",
665+
]),
666+
]).toEqual(["extensions/codex/src/app-server/run-attempt.test.ts"]);
667+
expect([...parseExactVitestExcludePaths(["--exclude=extensions/**/*.test.ts"])]).toEqual([]);
668+
});
669+
629670
it("treats extensions without tests as a no-op by default", () => {
630671
const extensionId = findExtensionWithoutTests();
631672
const stdout = runScript([extensionId]);

0 commit comments

Comments
 (0)