From f7bd61fe622dd6ca022f3657720901a25d2ce745 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 22:02:16 +0000 Subject: [PATCH 1/4] ci: split extension coverage into cacheable lanes --- .github/workflows/code-qa.yml | 16 +++++++---- src/package.json | 2 ++ src/scripts/verify-coverage-contract.mjs | 36 ++++++++++++++++++------ src/turbo.json | 23 +++++++++++++++ src/vitest.general.config.ts | 19 +++++++++++++ src/vitest.tree-sitter.config.ts | 15 ++++++++++ 6 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 src/vitest.general.config.ts create mode 100644 src/vitest.tree-sitter.config.ts diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 8c2d45d605..aec4115dc9 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -152,8 +152,8 @@ jobs: ${{ runner.os }}-turbo- - name: Run non-extension package coverage run: pnpm turbo run test:coverage --filter="!@roo-code/core" --filter="!zoo-code" --log-order grouped --output-logs new-only - - name: Run extension unit coverage - run: pnpm turbo run test:coverage:unit --filter="zoo-code" --log-order grouped --output-logs new-only + - name: Run extension coverage lanes + run: pnpm turbo run test:coverage:general test:coverage:tree-sitter --filter="zoo-code" --log-order grouped --output-logs new-only - name: Verify extension coverage contract run: pnpm --dir src run verify:coverage-contract - name: Run extension dist smoke test @@ -162,8 +162,10 @@ jobs: run: pnpm turbo run test:coverage:unit --filter="@roo-code/core" --log-order grouped --output-logs new-only - name: Run core integration coverage run: pnpm turbo run test:coverage:integration --filter="@roo-code/core" --log-order grouped --output-logs new-only - - name: Verify extension unit coverage report - run: node src/scripts/verify-lcov.mjs src/coverage/unit/lcov.info + - name: Verify extension coverage reports + run: | + node src/scripts/verify-lcov.mjs src/coverage/general/lcov.info + node src/scripts/verify-lcov.mjs src/coverage/tree-sitter/lcov.info - name: Save Turbo cache if: steps.turbo-cache.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -185,7 +187,8 @@ jobs: uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: >- - src/coverage/unit/lcov.info, + src/coverage/general/lcov.info, + src/coverage/tree-sitter/lcov.info, packages/cloud/coverage/lcov.info, packages/telemetry/coverage/lcov.info, apps/cli/coverage/lcov.info @@ -222,7 +225,8 @@ jobs: with: name: coverage-reports-${{ matrix.name }} path: | - src/coverage/unit/lcov.info + src/coverage/general/lcov.info + src/coverage/tree-sitter/lcov.info webview-ui/coverage/lcov.info packages/cloud/coverage/lcov.info packages/telemetry/coverage/lcov.info diff --git a/src/package.json b/src/package.json index 3e873ebbf7..6d487d6e73 100644 --- a/src/package.json +++ b/src/package.json @@ -447,6 +447,8 @@ "test:dist": "vitest run --config vitest.dist.config.ts", "test:coverage": "vitest run --coverage", "test:coverage:unit": "vitest run --config vitest.unit.config.ts --coverage", + "test:coverage:general": "vitest run --config vitest.general.config.ts --coverage", + "test:coverage:tree-sitter": "vitest run --config vitest.tree-sitter.config.ts --coverage", "format": "prettier --write .", "bundle": "node esbuild.mjs", "vscode:prepublish": "pnpm bundle --production", diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 1f6003dff1..5555f509e4 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -7,7 +7,16 @@ const command = process.platform === "win32" ? process.execPath : pnpm const args = process.platform === "win32" ? [pnpm] : [] const result = spawnSync( command, - [...args, "turbo", "run", "test:coverage:unit", "test:dist", "--filter=zoo-code", "--dry=json"], + [ + ...args, + "turbo", + "run", + "test:coverage:general", + "test:coverage:tree-sitter", + "test:dist", + "--filter=zoo-code", + "--dry=json", + ], { encoding: "utf8" }, ) if (result.status !== 0) { @@ -16,14 +25,25 @@ if (result.status !== 0) { } const graph = JSON.parse(result.stdout) -const coverageTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:unit") +const generalTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:general") +const treeSitterTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:tree-sitter") const distTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:dist") -if (!coverageTask) throw new Error("Unit coverage task missing") +if (!generalTask || !treeSitterTask) throw new Error("Extension coverage lane missing") if (graph.tasks.some(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms")) throw new Error("Removed WASM preparation task remains in the graph") -if (coverageTask.dependencies.includes("zoo-code#bundle")) throw new Error("Unit coverage must not depend on bundle") -if (!coverageTask.dependencies.includes("@roo-code/types#build")) - throw new Error("Unit coverage must depend on the types build") -if (!Object.hasOwn(coverageTask.inputs, "package.json")) throw new Error("Unit coverage must hash package.json") -if (!coverageTask.hashOfExternalDependencies) throw new Error("Unit coverage must hash external dependencies") +for (const task of [generalTask, treeSitterTask]) { + if (task.dependencies.includes("zoo-code#bundle")) throw new Error("Coverage lanes must not depend on bundle") + if (!task.dependencies.includes("@roo-code/types#build")) + throw new Error("Coverage lanes must depend on the types build") + if (!Object.hasOwn(task.inputs, "package.json")) throw new Error("Coverage lanes must hash package.json") + if (!task.hashOfExternalDependencies) throw new Error("Coverage lanes must hash external dependencies") +} +if (!Object.hasOwn(generalTask.inputs, "services/tree-sitter/index.ts")) + throw new Error("General coverage must hash tree-sitter sources used by external consumers") +if (Object.hasOwn(generalTask.inputs, "services/tree-sitter/__tests__/wasm.spec.ts")) + throw new Error("General coverage must not hash tree-sitter-owned tests") +if (!Object.hasOwn(treeSitterTask.inputs, "services/tree-sitter/index.ts")) + throw new Error("Tree-sitter coverage must hash tree-sitter sources") +if (Object.hasOwn(treeSitterTask.inputs, "core/task/Task.ts")) + throw new Error("Tree-sitter coverage must not hash unrelated core sources") if (!distTask?.dependencies.includes("zoo-code#bundle")) throw new Error("Dist smoke test must depend on bundle") diff --git a/src/turbo.json b/src/turbo.json index 024971987f..20f965af25 100644 --- a/src/turbo.json +++ b/src/turbo.json @@ -20,6 +20,29 @@ "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts"], "outputs": ["coverage/unit/**"] }, + "test:coverage:general": { + "dependsOn": ["@roo-code/types#build"], + "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts", "!services/tree-sitter/__tests__/**"], + "outputs": ["coverage/general/**"] + }, + "test:coverage:tree-sitter": { + "dependsOn": ["@roo-code/types#build"], + "inputs": [ + "services/tree-sitter/**", + "services/shared/fallback-extensions.ts", + "core/ignore/RooIgnoreController.ts", + "utils/fs.ts", + "utils/path.ts", + "utils/vitest-verbosity.ts", + "__mocks__/**", + "vitest.setup.ts", + "vitest.config.ts", + "vitest.tree-sitter.config.ts", + "package.json", + "tsconfig.json" + ], + "outputs": ["coverage/tree-sitter/**"] + }, "bundle": { "dependsOn": ["^build", "@roo-code/vscode-webview#build"], "outputs": ["dist/**"] diff --git a/src/vitest.general.config.ts b/src/vitest.general.config.ts new file mode 100644 index 0000000000..7c4d7d1a9d --- /dev/null +++ b/src/vitest.general.config.ts @@ -0,0 +1,19 @@ +import { configDefaults, defineConfig, mergeConfig } from "vitest/config" + +import baseConfig from "./vitest.config" + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + exclude: [ + ...configDefaults.exclude, + "__tests__/dist_assets.spec.ts", + "services/tree-sitter/**/*.{test,spec}.{ts,tsx}", + ], + coverage: { + reportsDirectory: "coverage/general", + }, + }, + }), +) diff --git a/src/vitest.tree-sitter.config.ts b/src/vitest.tree-sitter.config.ts new file mode 100644 index 0000000000..343a2f644a --- /dev/null +++ b/src/vitest.tree-sitter.config.ts @@ -0,0 +1,15 @@ +import { defineConfig, mergeConfig } from "vitest/config" + +import baseConfig from "./vitest.config" + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ["services/tree-sitter/**/*.{test,spec}.{ts,tsx}"], + coverage: { + reportsDirectory: "coverage/tree-sitter", + }, + }, + }), +) From 6340b16170d469ae33927e261cdfd530405cd3f2 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 23:30:42 +0000 Subject: [PATCH 2/4] ci: serialize duplicate platform cache producers --- .github/workflows/code-qa.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index aec4115dc9..1636149edb 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -125,6 +125,10 @@ jobs: unit-test: name: platform-unit-test (${{ matrix.name }}) runs-on: ${{ matrix.os }} + concurrency: + # Duplicate runs for one commit must not race the OS-specific Turbo cache save. + group: unit-test-${{ github.repository }}-${{ github.sha }}-${{ matrix.name }} + cancel-in-progress: false strategy: matrix: include: From aea5ab986627ecbab8157cef41dad1d3481f7964 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 14 Sep 2026 00:20:10 +0000 Subject: [PATCH 3/4] ci: split extension coverage by ownership --- .github/workflows/code-qa.yml | 17 ++- src/package.json | 5 +- src/scripts/verify-coverage-contract.mjs | 126 +++++++++++++++++++++-- src/turbo.json | 69 ++++++++++++- src/vitest.api.config.ts | 15 +++ src/vitest.core.config.ts | 15 +++ src/vitest.general.config.ts | 19 ---- src/vitest.misc.config.ts | 26 +++++ src/vitest.services.config.ts | 16 +++ 9 files changed, 270 insertions(+), 38 deletions(-) create mode 100644 src/vitest.api.config.ts create mode 100644 src/vitest.core.config.ts delete mode 100644 src/vitest.general.config.ts create mode 100644 src/vitest.misc.config.ts create mode 100644 src/vitest.services.config.ts diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 1636149edb..abb344dbd5 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -157,7 +157,7 @@ jobs: - name: Run non-extension package coverage run: pnpm turbo run test:coverage --filter="!@roo-code/core" --filter="!zoo-code" --log-order grouped --output-logs new-only - name: Run extension coverage lanes - run: pnpm turbo run test:coverage:general test:coverage:tree-sitter --filter="zoo-code" --log-order grouped --output-logs new-only + run: pnpm turbo run test:coverage:api test:coverage:core test:coverage:services test:coverage:misc test:coverage:tree-sitter --filter="zoo-code" --concurrency=2 --log-order grouped --output-logs new-only - name: Verify extension coverage contract run: pnpm --dir src run verify:coverage-contract - name: Run extension dist smoke test @@ -168,7 +168,10 @@ jobs: run: pnpm turbo run test:coverage:integration --filter="@roo-code/core" --log-order grouped --output-logs new-only - name: Verify extension coverage reports run: | - node src/scripts/verify-lcov.mjs src/coverage/general/lcov.info + node src/scripts/verify-lcov.mjs src/coverage/api/lcov.info + node src/scripts/verify-lcov.mjs src/coverage/core/lcov.info + node src/scripts/verify-lcov.mjs src/coverage/services/lcov.info + node src/scripts/verify-lcov.mjs src/coverage/misc/lcov.info node src/scripts/verify-lcov.mjs src/coverage/tree-sitter/lcov.info - name: Save Turbo cache if: steps.turbo-cache.outputs.cache-hit != 'true' @@ -191,7 +194,10 @@ jobs: uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: >- - src/coverage/general/lcov.info, + src/coverage/api/lcov.info, + src/coverage/core/lcov.info, + src/coverage/services/lcov.info, + src/coverage/misc/lcov.info, src/coverage/tree-sitter/lcov.info, packages/cloud/coverage/lcov.info, packages/telemetry/coverage/lcov.info, @@ -229,7 +235,10 @@ jobs: with: name: coverage-reports-${{ matrix.name }} path: | - src/coverage/general/lcov.info + src/coverage/api/lcov.info + src/coverage/core/lcov.info + src/coverage/services/lcov.info + src/coverage/misc/lcov.info src/coverage/tree-sitter/lcov.info webview-ui/coverage/lcov.info packages/cloud/coverage/lcov.info diff --git a/src/package.json b/src/package.json index 6d487d6e73..0f26ad0a6c 100644 --- a/src/package.json +++ b/src/package.json @@ -447,7 +447,10 @@ "test:dist": "vitest run --config vitest.dist.config.ts", "test:coverage": "vitest run --coverage", "test:coverage:unit": "vitest run --config vitest.unit.config.ts --coverage", - "test:coverage:general": "vitest run --config vitest.general.config.ts --coverage", + "test:coverage:api": "vitest run --config vitest.api.config.ts --coverage", + "test:coverage:core": "vitest run --config vitest.core.config.ts --coverage", + "test:coverage:services": "vitest run --config vitest.services.config.ts --coverage", + "test:coverage:misc": "vitest run --config vitest.misc.config.ts --coverage", "test:coverage:tree-sitter": "vitest run --config vitest.tree-sitter.config.ts --coverage", "format": "prettier --write .", "bundle": "node esbuild.mjs", diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index 5555f509e4..f0eda7a5e3 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -1,4 +1,7 @@ import { spawnSync } from "node:child_process" +import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs" +import { tmpdir } from "node:os" +import { relative, resolve } from "node:path" import process from "node:process" const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" @@ -11,7 +14,10 @@ const result = spawnSync( ...args, "turbo", "run", - "test:coverage:general", + "test:coverage:api", + "test:coverage:core", + "test:coverage:services", + "test:coverage:misc", "test:coverage:tree-sitter", "test:dist", "--filter=zoo-code", @@ -25,25 +31,125 @@ if (result.status !== 0) { } const graph = JSON.parse(result.stdout) -const generalTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:general") +const ownershipLanes = ["api", "core", "services", "misc"] +const ownershipTasks = ownershipLanes.map((lane) => + graph.tasks.find(({ taskId }) => taskId === `zoo-code#test:coverage:${lane}`), +) const treeSitterTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:tree-sitter") const distTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:dist") -if (!generalTask || !treeSitterTask) throw new Error("Extension coverage lane missing") +if (ownershipTasks.some((task) => !task) || !treeSitterTask) throw new Error("Extension coverage lane missing") if (graph.tasks.some(({ taskId }) => taskId === "zoo-code#prepare:tree-sitter-wasms")) throw new Error("Removed WASM preparation task remains in the graph") -for (const task of [generalTask, treeSitterTask]) { +for (const task of [...ownershipTasks, treeSitterTask]) { if (task.dependencies.includes("zoo-code#bundle")) throw new Error("Coverage lanes must not depend on bundle") - if (!task.dependencies.includes("@roo-code/types#build")) - throw new Error("Coverage lanes must depend on the types build") if (!Object.hasOwn(task.inputs, "package.json")) throw new Error("Coverage lanes must hash package.json") if (!task.hashOfExternalDependencies) throw new Error("Coverage lanes must hash external dependencies") } -if (!Object.hasOwn(generalTask.inputs, "services/tree-sitter/index.ts")) - throw new Error("General coverage must hash tree-sitter sources used by external consumers") -if (Object.hasOwn(generalTask.inputs, "services/tree-sitter/__tests__/wasm.spec.ts")) - throw new Error("General coverage must not hash tree-sitter-owned tests") +for (const task of ownershipTasks) { + for (const dependency of [ + "@roo-code/cloud", + "@roo-code/core", + "@roo-code/ipc", + "@roo-code/telemetry", + "@roo-code/types", + ]) + if (!task.dependencies.includes(`${dependency}#build`)) + throw new Error(`Ownership coverage lanes must depend on ${dependency}#build`) +} +if (!treeSitterTask.dependencies.includes("@roo-code/types#build")) + throw new Error("Tree-sitter coverage must depend on @roo-code/types#build") +for (const task of ownershipTasks) { + if (!Object.hasOwn(task.inputs, "services/tree-sitter/index.ts")) + throw new Error("Ownership coverage lanes must hash shared tree-sitter production sources") +} +const representativeTests = { + api: "api/providers/__tests__/anthropic.spec.ts", + core: "core/task/__tests__/Task.spec.ts", + services: "services/mcp/__tests__/McpHub.spec.ts", + misc: "utils/__tests__/path.spec.ts", +} +for (const [index, task] of ownershipTasks.entries()) { + for (const [lane, testPath] of Object.entries(representativeTests)) { + const ownsInput = Object.hasOwn(task.inputs, testPath) + if (ownsInput !== (lane === ownershipLanes[index])) + throw new Error(`${ownershipLanes[index]} coverage has incorrect ownership for ${testPath}`) + } + if (Object.hasOwn(task.inputs, "services/tree-sitter/__tests__/wasm.spec.ts")) + throw new Error("Ownership coverage lanes must not hash tree-sitter-owned tests") +} if (!Object.hasOwn(treeSitterTask.inputs, "services/tree-sitter/index.ts")) throw new Error("Tree-sitter coverage must hash tree-sitter sources") if (Object.hasOwn(treeSitterTask.inputs, "core/task/Task.ts")) throw new Error("Tree-sitter coverage must not hash unrelated core sources") if (!distTask?.dependencies.includes("zoo-code#bundle")) throw new Error("Dist smoke test must depend on bundle") + +const root = resolve(import.meta.dirname, "..") +const testPattern = /\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/ +const ignoredDirectories = new Set(["coverage", "dist", "node_modules"]) +const testFiles = [] +const collectTests = (directory) => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (!ignoredDirectories.has(entry.name)) collectTests(resolve(directory, entry.name)) + } else if (testPattern.test(entry.name)) { + testFiles.push(relative(root, resolve(directory, entry.name)).replaceAll("\\", "/")) + } + } +} +collectTests(root) + +const laneForTest = (file) => { + if (file === "__tests__/dist_assets.spec.ts") return "dist" + if (file.startsWith("services/tree-sitter/")) return "tree-sitter" + for (const lane of ["api", "core", "services"]) if (file.startsWith(`${lane}/`)) return lane + if ( + ["__tests__", "activate", "extension", "i18n", "integrations", "scripts", "shared", "test-utils", "utils"].some( + (directory) => file.startsWith(`${directory}/`), + ) + ) + return "misc" + throw new Error(`Unit test is not assigned to a coverage lane: ${file}`) +} +const laneCounts = Object.groupBy(testFiles, laneForTest) +if (Object.values(laneCounts).flat().length !== testFiles.length) + throw new Error("Coverage lanes do not form an exact test partition") + +const collectionDirectory = mkdtempSync(resolve(tmpdir(), "zoo-code-coverage-contract-")) +try { + const collect = (config) => { + const output = resolve(collectionDirectory, `${config}.json`) + const collection = spawnSync( + command, + [...args, "exec", "vitest", "list", "--config", `vitest.${config}.config.ts`, `--json=${output}`], + { encoding: "utf8" }, + ) + if (collection.status !== 0) throw new Error(collection.stderr || `Vitest collection failed for ${config}`) + return new Set(JSON.parse(readFileSync(output, "utf8")).map(({ file, name }) => `${file}\0${name}`)) + } + const unitTests = collect("unit") + const laneTests = new Set() + for (const lane of [...ownershipLanes, "tree-sitter"]) { + for (const test of collect(lane)) { + if (laneTests.has(test)) throw new Error(`Test belongs to multiple coverage lanes: ${test}`) + laneTests.add(test) + } + } + if (unitTests.size !== laneTests.size || [...unitTests].some((test) => !laneTests.has(test))) + throw new Error("Coverage lane test collection differs from monolithic unit coverage") +} finally { + rmSync(collectionDirectory, { recursive: true, force: true }) +} + +const coverageSources = new Map() +for (const lane of [...ownershipLanes, "tree-sitter"]) { + let source + for (const line of readFileSync(resolve(root, "coverage", lane, "lcov.info"), "utf8").split(/\r?\n/)) { + if (line.startsWith("SF:")) source = line.slice(3) + if (line.startsWith("LF:")) coverageSources.set(source, Number(line.slice(3))) + } +} +const instrumentedLines = [...coverageSources.values()].reduce((sum, lines) => sum + lines, 0) +if (coverageSources.size !== 469 || instrumentedLines !== 30_229) + throw new Error( + `Coverage source population changed: ${coverageSources.size} records and ${instrumentedLines} lines; verify equivalence and update the baseline deliberately`, + ) diff --git a/src/turbo.json b/src/turbo.json index 20f965af25..0d023b5598 100644 --- a/src/turbo.json +++ b/src/turbo.json @@ -20,10 +20,71 @@ "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts"], "outputs": ["coverage/unit/**"] }, - "test:coverage:general": { - "dependsOn": ["@roo-code/types#build"], - "inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts", "!services/tree-sitter/__tests__/**"], - "outputs": ["coverage/general/**"] + "test:coverage:api": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!core/**/*.{test,spec}.{ts,tsx}", + "!services/**/*.{test,spec}.{ts,tsx}", + "!__tests__/**/*.{test,spec}.{ts,tsx}", + "!activate/**/*.{test,spec}.{ts,tsx}", + "!extension/**/*.{test,spec}.{ts,tsx}", + "!i18n/**/*.{test,spec}.{ts,tsx}", + "!integrations/**/*.{test,spec}.{ts,tsx}", + "!scripts/**/*.{test,spec}.{mjs,ts}", + "!shared/**/*.{test,spec}.{ts,tsx}", + "!test-utils/**/*.{test,spec}.{ts,tsx}", + "!utils/**/*.{test,spec}.{ts,tsx}" + ], + "outputs": ["coverage/api/**"] + }, + "test:coverage:core": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!api/**/*.{test,spec}.{ts,tsx}", + "!services/**/*.{test,spec}.{ts,tsx}", + "!__tests__/**/*.{test,spec}.{ts,tsx}", + "!activate/**/*.{test,spec}.{ts,tsx}", + "!extension/**/*.{test,spec}.{ts,tsx}", + "!i18n/**/*.{test,spec}.{ts,tsx}", + "!integrations/**/*.{test,spec}.{ts,tsx}", + "!scripts/**/*.{test,spec}.{mjs,ts}", + "!shared/**/*.{test,spec}.{ts,tsx}", + "!test-utils/**/*.{test,spec}.{ts,tsx}", + "!utils/**/*.{test,spec}.{ts,tsx}" + ], + "outputs": ["coverage/core/**"] + }, + "test:coverage:services": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!api/**/*.{test,spec}.{ts,tsx}", + "!core/**/*.{test,spec}.{ts,tsx}", + "!services/tree-sitter/**/*.{test,spec}.{ts,tsx}", + "!__tests__/**/*.{test,spec}.{ts,tsx}", + "!activate/**/*.{test,spec}.{ts,tsx}", + "!extension/**/*.{test,spec}.{ts,tsx}", + "!i18n/**/*.{test,spec}.{ts,tsx}", + "!integrations/**/*.{test,spec}.{ts,tsx}", + "!scripts/**/*.{test,spec}.{mjs,ts}", + "!shared/**/*.{test,spec}.{ts,tsx}", + "!test-utils/**/*.{test,spec}.{ts,tsx}", + "!utils/**/*.{test,spec}.{ts,tsx}" + ], + "outputs": ["coverage/services/**"] + }, + "test:coverage:misc": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!api/**/*.{test,spec}.{ts,tsx}", + "!core/**/*.{test,spec}.{ts,tsx}", + "!services/**/*.{test,spec}.{ts,tsx}", + "!__tests__/dist_assets.spec.ts" + ], + "outputs": ["coverage/misc/**"] }, "test:coverage:tree-sitter": { "dependsOn": ["@roo-code/types#build"], diff --git a/src/vitest.api.config.ts b/src/vitest.api.config.ts new file mode 100644 index 0000000000..3d72620906 --- /dev/null +++ b/src/vitest.api.config.ts @@ -0,0 +1,15 @@ +import { defineConfig, mergeConfig } from "vitest/config" + +import baseConfig from "./vitest.config" + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ["api/**/*.{test,spec}.{ts,tsx}"], + coverage: { + reportsDirectory: "coverage/api", + }, + }, + }), +) diff --git a/src/vitest.core.config.ts b/src/vitest.core.config.ts new file mode 100644 index 0000000000..38d778985a --- /dev/null +++ b/src/vitest.core.config.ts @@ -0,0 +1,15 @@ +import { defineConfig, mergeConfig } from "vitest/config" + +import baseConfig from "./vitest.config" + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ["core/**/*.{test,spec}.{ts,tsx}"], + coverage: { + reportsDirectory: "coverage/core", + }, + }, + }), +) diff --git a/src/vitest.general.config.ts b/src/vitest.general.config.ts deleted file mode 100644 index 7c4d7d1a9d..0000000000 --- a/src/vitest.general.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { configDefaults, defineConfig, mergeConfig } from "vitest/config" - -import baseConfig from "./vitest.config" - -export default mergeConfig( - baseConfig, - defineConfig({ - test: { - exclude: [ - ...configDefaults.exclude, - "__tests__/dist_assets.spec.ts", - "services/tree-sitter/**/*.{test,spec}.{ts,tsx}", - ], - coverage: { - reportsDirectory: "coverage/general", - }, - }, - }), -) diff --git a/src/vitest.misc.config.ts b/src/vitest.misc.config.ts new file mode 100644 index 0000000000..49d3b7155b --- /dev/null +++ b/src/vitest.misc.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, mergeConfig } from "vitest/config" + +import baseConfig from "./vitest.config" + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: [ + "__tests__/**/*.{test,spec}.{ts,tsx}", + "activate/**/*.{test,spec}.{ts,tsx}", + "extension/**/*.{test,spec}.{ts,tsx}", + "i18n/**/*.{test,spec}.{ts,tsx}", + "integrations/**/*.{test,spec}.{ts,tsx}", + "scripts/**/*.{test,spec}.{mjs,ts}", + "shared/**/*.{test,spec}.{ts,tsx}", + "test-utils/**/*.{test,spec}.{ts,tsx}", + "utils/**/*.{test,spec}.{ts,tsx}", + ], + exclude: ["__tests__/dist_assets.spec.ts"], + coverage: { + reportsDirectory: "coverage/misc", + }, + }, + }), +) diff --git a/src/vitest.services.config.ts b/src/vitest.services.config.ts new file mode 100644 index 0000000000..506b0dcf62 --- /dev/null +++ b/src/vitest.services.config.ts @@ -0,0 +1,16 @@ +import { defineConfig, mergeConfig } from "vitest/config" + +import baseConfig from "./vitest.config" + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ["services/**/*.{test,spec}.{ts,tsx}"], + exclude: ["services/tree-sitter/**/*.{test,spec}.{ts,tsx}"], + coverage: { + reportsDirectory: "coverage/services", + }, + }, + }), +) From 110a1079ff1f4cab1d5f17c1e7fbe7a375865d55 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 14 Sep 2026 00:36:17 +0000 Subject: [PATCH 4/4] ci: parallelize coverage contract collection --- src/scripts/verify-coverage-contract.mjs | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/scripts/verify-coverage-contract.mjs b/src/scripts/verify-coverage-contract.mjs index f0eda7a5e3..14b19c47ea 100644 --- a/src/scripts/verify-coverage-contract.mjs +++ b/src/scripts/verify-coverage-contract.mjs @@ -1,8 +1,9 @@ -import { spawnSync } from "node:child_process" +import { execFile, spawnSync } from "node:child_process" import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs" import { tmpdir } from "node:os" import { relative, resolve } from "node:path" import process from "node:process" +import { promisify } from "node:util" const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm" if (!pnpm) throw new Error("pnpm executable path is unavailable") @@ -116,20 +117,30 @@ if (Object.values(laneCounts).flat().length !== testFiles.length) const collectionDirectory = mkdtempSync(resolve(tmpdir(), "zoo-code-coverage-contract-")) try { - const collect = (config) => { + const execFileAsync = promisify(execFile) + const collect = async (config) => { const output = resolve(collectionDirectory, `${config}.json`) - const collection = spawnSync( + await execFileAsync( command, - [...args, "exec", "vitest", "list", "--config", `vitest.${config}.config.ts`, `--json=${output}`], - { encoding: "utf8" }, + [ + ...args, + "exec", + "vitest", + "list", + "--staticParse", + "--config", + `vitest.${config}.config.ts`, + `--json=${output}`, + ], + { maxBuffer: 10 * 1024 * 1024 }, ) - if (collection.status !== 0) throw new Error(collection.stderr || `Vitest collection failed for ${config}`) return new Set(JSON.parse(readFileSync(output, "utf8")).map(({ file, name }) => `${file}\0${name}`)) } - const unitTests = collect("unit") + const collections = ["unit", ...ownershipLanes, "tree-sitter"] + const [unitTests, ...resolvedLanes] = await Promise.all(collections.map(collect)) const laneTests = new Set() - for (const lane of [...ownershipLanes, "tree-sitter"]) { - for (const test of collect(lane)) { + for (const tests of resolvedLanes) { + for (const test of tests) { if (laneTests.has(test)) throw new Error(`Test belongs to multiple coverage lanes: ${test}`) laneTests.add(test) }