Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions .github/workflows/code-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -152,8 +156,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: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
Expand All @@ -162,8 +166,13 @@ 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/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'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand All @@ -185,7 +194,11 @@ jobs:
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: >-
src/coverage/unit/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,
apps/cli/coverage/lcov.info
Expand Down Expand Up @@ -222,7 +235,11 @@ jobs:
with:
name: coverage-reports-${{ matrix.name }}
path: |
src/coverage/unit/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
packages/telemetry/coverage/lcov.info
Expand Down
5 changes: 5 additions & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@
"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: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",
"vscode:prepublish": "pnpm bundle --production",
Expand Down
155 changes: 146 additions & 9 deletions src/scripts/verify-coverage-contract.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
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")
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:api",
"test:coverage:core",
"test:coverage:services",
"test:coverage:misc",
"test:coverage:tree-sitter",
"test:dist",
"--filter=zoo-code",
"--dry=json",
],
{ encoding: "utf8" },
)
if (result.status !== 0) {
Expand All @@ -16,14 +32,135 @@ if (result.status !== 0) {
}

const graph = JSON.parse(result.stdout)
const coverageTask = graph.tasks.find(({ taskId }) => taskId === "zoo-code#test:coverage:unit")
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 (!coverageTask) throw new Error("Unit coverage task 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")
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 [...ownershipTasks, treeSitterTask]) {
if (task.dependencies.includes("zoo-code#bundle")) throw new Error("Coverage lanes must not depend on bundle")
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")
}
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 execFileAsync = promisify(execFile)
const collect = async (config) => {
const output = resolve(collectionDirectory, `${config}.json`)
await execFileAsync(
command,
[
...args,
"exec",
"vitest",
"list",
"--staticParse",
"--config",
`vitest.${config}.config.ts`,
`--json=${output}`,
],
{ maxBuffer: 10 * 1024 * 1024 },
)
return new Set(JSON.parse(readFileSync(output, "utf8")).map(({ file, name }) => `${file}\0${name}`))
}
const collections = ["unit", ...ownershipLanes, "tree-sitter"]
const [unitTests, ...resolvedLanes] = await Promise.all(collections.map(collect))
const laneTests = new Set()
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)
}
}
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`,
)
84 changes: 84 additions & 0 deletions src/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,90 @@
"inputs": ["$TURBO_DEFAULT$", "!__tests__/dist_assets.spec.ts"],
"outputs": ["coverage/unit/**"]
},
"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"],
"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/**"]
Expand Down
15 changes: 15 additions & 0 deletions src/vitest.api.config.ts
Original file line number Diff line number Diff line change
@@ -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",
},
},
}),
)
15 changes: 15 additions & 0 deletions src/vitest.core.config.ts
Original file line number Diff line number Diff line change
@@ -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",
},
},
}),
)
Loading
Loading