diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index f1ba2a10cb..79b7b45a28 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -90,6 +90,8 @@ jobs: run: pnpm lint - name: Check types run: pnpm check-types + - name: Validate Code QA workflow + run: pnpm test:code-qa-ci - name: Model-check task lifecycle protocols run: pnpm lifecycle:model-check - name: Validate MCP OAuth integration @@ -135,11 +137,11 @@ jobs: - os: ubuntu-latest name: ubuntu-latest codecov-flag: ubuntu - upload-coverage: true + collect-coverage: true - os: windows-latest name: windows-latest codecov-flag: windows - upload-coverage: false + collect-coverage: false steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -154,19 +156,40 @@ jobs: restore-keys: | ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}- ${{ runner.os }}-turbo- + # Windows never uploads coverage, so Windows runs the same test + # suites through the uninstrumented Turbo tasks. Ubuntu stays the + # authoritative coverage lane. - name: Run non-extension package coverage + if: matrix.collect-coverage run: pnpm turbo run test:coverage --filter="!@roo-code/core" --filter="!zoo-code" --log-order grouped --output-logs new-only + - name: Run non-extension package tests + if: ${{ !matrix.collect-coverage }} + run: pnpm turbo run test --filter="!@roo-code/core" --filter="!zoo-code" --log-order grouped --output-logs new-only - name: Run extension coverage lanes + if: matrix.collect-coverage 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: Run extension test lanes + if: ${{ !matrix.collect-coverage }} + run: pnpm turbo run test:api test:core test:services test:misc test:tree-sitter --filter="zoo-code" --concurrency=2 --log-order grouped --output-logs new-only - name: Verify extension coverage contract + if: matrix.collect-coverage run: pnpm --dir src run verify:coverage-contract - name: Run extension dist smoke test run: pnpm turbo run test:dist --filter="zoo-code" --log-order grouped --output-logs new-only - name: Run core unit coverage + if: matrix.collect-coverage run: pnpm turbo run test:coverage:unit --filter="@roo-code/core" --log-order grouped --output-logs new-only + - name: Run core unit tests + if: ${{ !matrix.collect-coverage }} + run: pnpm turbo run test:unit --filter="@roo-code/core" --log-order grouped --output-logs new-only - name: Run core integration coverage + if: matrix.collect-coverage run: pnpm turbo run test:coverage:integration --filter="@roo-code/core" --log-order grouped --output-logs new-only + - name: Run core integration tests + if: ${{ !matrix.collect-coverage }} + run: pnpm turbo run test:integration --filter="@roo-code/core" --log-order grouped --output-logs new-only - name: Verify extension coverage reports + if: matrix.collect-coverage run: | node src/scripts/verify-lcov.mjs src/coverage/api/lcov.info node src/scripts/verify-lcov.mjs src/coverage/core/lcov.info @@ -174,12 +197,14 @@ jobs: node src/scripts/verify-lcov.mjs src/coverage/misc/lcov.info node src/scripts/verify-lcov.mjs src/coverage/tree-sitter/lcov.info - name: Merge extension coverage reports + if: matrix.collect-coverage run: | mkdir -p src/coverage/merged pnpm --dir src run merge:coverage node src/scripts/verify-lcov.mjs src/coverage/merged/lcov.info # Validate cache boundaries before publishing any new Turbo entries. - name: Verify coverage cache inputs + if: matrix.collect-coverage run: pnpm --dir src run verify:coverage-cache-inputs - name: Save Turbo cache if: steps.turbo-cache.outputs.cache-hit != 'true' @@ -197,7 +222,7 @@ jobs: # it. Core and webview reports retain their independent flags. # See https://docs.codecov.com/docs/flags - name: Upload non-core coverage to Codecov - if: matrix.upload-coverage + if: matrix.collect-coverage uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: >- @@ -209,7 +234,7 @@ jobs: flags: ${{ matrix.codecov-flag }} token: ${{ secrets.CODECOV_TOKEN }} - name: Upload webview JSDOM coverage to Codecov - if: matrix.upload-coverage + if: matrix.collect-coverage uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: webview-ui/coverage/lcov.info @@ -217,7 +242,7 @@ jobs: flags: webview-ui token: ${{ secrets.CODECOV_TOKEN }} - name: Upload core unit coverage to Codecov - if: matrix.upload-coverage + if: matrix.collect-coverage uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: packages/core/coverage/unit/lcov.info @@ -225,7 +250,7 @@ jobs: flags: ${{ matrix.codecov-flag }},core-unit token: ${{ secrets.CODECOV_TOKEN }} - name: Upload core integration coverage to Codecov - if: matrix.upload-coverage + if: matrix.collect-coverage uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: packages/core/coverage/integration/lcov.info @@ -233,7 +258,7 @@ jobs: flags: ${{ matrix.codecov-flag }},core-integration token: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage reports to GitHub - if: matrix.upload-coverage + if: matrix.collect-coverage uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-reports-${{ matrix.name }} diff --git a/package.json b/package.json index df3410bbc1..dde5222517 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "turbo lint --log-order grouped --output-logs new-only", "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", + "test:code-qa-ci": "node --test scripts/code-qa-workflow.test.mjs", "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts && tsx scripts/check-delegated-mode-readers.ts", "cleanup-protocol:model-check": "tsx scripts/check-task-cleanup-protocol.ts", diff --git a/scripts/code-qa-workflow.test.mjs b/scripts/code-qa-workflow.test.mjs new file mode 100644 index 0000000000..40ccc66348 --- /dev/null +++ b/scripts/code-qa-workflow.test.mjs @@ -0,0 +1,118 @@ +import assert from "node:assert/strict" +import fs from "node:fs" +import path from "node:path" +import { describe, it } from "node:test" +import { fileURLToPath } from "node:url" + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const workflow = fs.readFileSync(path.join(repositoryRoot, ".github/workflows/code-qa.yml"), "utf8") +const extensionTurbo = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "src/turbo.json"), "utf8")) +const coreTurbo = JSON.parse(fs.readFileSync(path.join(repositoryRoot, "packages/core/turbo.json"), "utf8")) + +const workflowStep = (name) => { + const match = workflow.match(new RegExp(`- name: ${name}\\n(?(?:\\s{14,}.*\\n?)*)`)) + assert.ok(match?.groups?.body, `missing workflow step: ${name}`) + return match.groups.body +} + +const parseWorkflowStep = (name) => { + const body = workflowStep(name) + const field = (key) => { + const line = body.split("\n").find((line) => line.trimStart().startsWith(`${key}:`)) + assert.ok(line, `missing ${key} field in workflow step: ${name}`) + return line.slice(line.indexOf(":") + 1).trim() + } + return { if: field("if"), run: field("run") } +} + +describe("platform unit-test workflow", () => { + it("keeps coverage authoritative on Ubuntu and runs equivalent uninstrumented Windows tests", () => { + assert.match(workflow, /name: ubuntu-latest[\s\S]*?collect-coverage: true/) + assert.match(workflow, /name: windows-latest[\s\S]*?collect-coverage: false/) + assert.ok(!workflow.includes("matrix.upload-coverage")) + + assert.deepEqual(parseWorkflowStep("Run extension coverage lanes"), { + if: "matrix.collect-coverage", + 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', + }) + + assert.deepEqual(parseWorkflowStep("Run extension test lanes"), { + if: "${{ !matrix.collect-coverage }}", + run: 'pnpm turbo run test:api test:core test:services test:misc test:tree-sitter --filter="zoo-code" --concurrency=2 --log-order grouped --output-logs new-only', + }) + + for (const [coverageStepName, plainStepName, coverageCommand, plainCommand] of [ + [ + "Run non-extension package coverage", + "Run non-extension package tests", + 'test:coverage --filter="!@roo-code/core" --filter="!zoo-code"', + 'test --filter="!@roo-code/core" --filter="!zoo-code"', + ], + [ + "Run core unit coverage", + "Run core unit tests", + 'test:coverage:unit --filter="@roo-code/core"', + 'test:unit --filter="@roo-code/core"', + ], + [ + "Run core integration coverage", + "Run core integration tests", + 'test:coverage:integration --filter="@roo-code/core"', + 'test:integration --filter="@roo-code/core"', + ], + ]) { + const coverageStep = parseWorkflowStep(coverageStepName) + assert.equal(coverageStep.if, "matrix.collect-coverage") + assert.ok(coverageStep.run.includes(coverageCommand), `missing command in step: ${coverageStepName}`) + + const plainStep = parseWorkflowStep(plainStepName) + assert.equal(plainStep.if, "${{ !matrix.collect-coverage }}") + assert.ok(plainStep.run.includes(plainCommand), `missing command in step: ${plainStepName}`) + assert.ok(!plainStep.run.includes("--coverage"), `${plainStepName} must not collect coverage on Windows`) + } + + assert.ok(!workflowStep("Run extension dist smoke test").includes("if:")) + }) + + it("does not run coverage verification or uploads on Windows", () => { + for (const stepName of [ + "Verify extension coverage contract", + "Verify extension coverage reports", + "Merge extension coverage reports", + "Verify coverage cache inputs", + "Upload non-core coverage to Codecov", + "Upload webview JSDOM coverage to Codecov", + "Upload core unit coverage to Codecov", + "Upload core integration coverage to Codecov", + "Upload coverage reports to GitHub", + ]) { + assert.match(workflowStep(stepName), /if: matrix\.collect-coverage/) + } + }) + + it("keeps plain extension lanes aligned with coverage cache boundaries", () => { + for (const lane of ["api", "core", "services", "misc", "tree-sitter"]) { + const plainTask = extensionTurbo.tasks[`test:${lane}`] + const coverageTask = extensionTurbo.tasks[`test:coverage:${lane}`] + + assert.deepEqual(plainTask.dependsOn, coverageTask.dependsOn) + assert.deepEqual(plainTask.inputs, coverageTask.inputs) + } + }) + + it("keeps plain core tasks aligned with coverage cache boundaries", () => { + for (const lane of ["unit", "integration"]) { + const plainTask = coreTurbo.tasks[`test:${lane}`] + const coverageTask = coreTurbo.tasks[`test:coverage:${lane}`] + + assert.deepEqual(plainTask.dependsOn, coverageTask.dependsOn) + assert.deepEqual(plainTask.inputs, coverageTask.inputs) + } + }) + + it("caps Windows CI extension lanes at two workers per lane to fill the runner vCPUs", () => { + const config = fs.readFileSync(path.join(repositoryRoot, "src/vitest.config.ts"), "utf8") + + assert.match(config, /maxWorkers: isWindowsCI \? 2 : undefined/) + }) +}) diff --git a/src/package.json b/src/package.json index e047f661d8..a6fe40b8e6 100644 --- a/src/package.json +++ b/src/package.json @@ -447,6 +447,11 @@ "merge:coverage": "node scripts/merge-lcov.mjs coverage/merged/lcov.info coverage/api/lcov.info coverage/core/lcov.info coverage/services/lcov.info coverage/misc/lcov.info coverage/tree-sitter/lcov.info", "test:unit": "vitest run --config vitest.unit.config.ts", "test:dist": "vitest run --config vitest.dist.config.ts", + "test:api": "vitest run --config vitest.api.config.ts", + "test:core": "vitest run --config vitest.core.config.ts", + "test:services": "vitest run --config vitest.services.config.ts", + "test:misc": "vitest run --config vitest.misc.config.ts", + "test:tree-sitter": "vitest run --config vitest.tree-sitter.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", diff --git a/src/scripts/verify-coverage-cache-inputs.mjs b/src/scripts/verify-coverage-cache-inputs.mjs index 1337bddef9..929e2295b9 100644 --- a/src/scripts/verify-coverage-cache-inputs.mjs +++ b/src/scripts/verify-coverage-cache-inputs.mjs @@ -1,3 +1,4 @@ +import assert from "node:assert/strict" import { spawnSync } from "node:child_process" import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" @@ -11,45 +12,88 @@ 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 lanes = ["api", "core", "services", "misc", "tree-sitter"] +const taskNames = lanes.flatMap((lane) => [`test:${lane}`, `test:coverage:${lane}`]) +// Data files owned by one lane's test area. A change here must invalidate only +// the owner, because no test or source file imports across lane test areas. +const laneOwnedDataFiles = { + api: "src/api/providers/fetchers/__tests__/fixtures/ollama-model-details.json", + core: "src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap", + misc: "src/__tests__/helpers/provider-stub.ts", + "tree-sitter": "src/services/tree-sitter/__tests__/fixtures/sample-json.ts", +} +// Directories where a lane's own test run may create transient files. Litter +// here must not change any other lane's hash. +const laneProbeDirs = { + api: "src/api/__tests__", + core: "src/core/task/__tests__", + services: "src/services/mcp/__tests__", + misc: "src/integrations/misc/__tests__", + "tree-sitter": "src/services/tree-sitter/__tests__", +} +const laneOwnedSpecFiles = { + api: "src/api/providers/__tests__/anthropic.spec.ts", + core: "src/core/prompts/__tests__/system-prompt.spec.ts", + services: "src/services/mcp/__tests__/McpHub.spec.ts", + misc: "src/utils/__tests__/safeWriteJson.test.ts", + "tree-sitter": "src/services/tree-sitter/__tests__/wasm.spec.ts", +} +// Specs outside __tests__ directories. The lane vitest include globs fix the owner. +const laneOwnedTopLevelSpecFiles = { + core: "src/core/message-manager/index.spec.ts", + misc: "src/scripts/verify-lcov.spec.mjs", +} let probeRoot +const gitError = (result) => { + const details = [result.error?.message, result.signal, result.stderr, result.stdout].filter(Boolean).join("\n") + return new Error(details || `git exited with status ${result.status ?? "unknown"}`) +} + const git = (gitArgs) => { const result = spawnSync("git", gitArgs, { cwd: root, encoding: "utf8" }) - if (result.status !== 0) { - const details = [result.error?.message, result.signal, result.stderr, result.stdout].filter(Boolean).join("\n") - throw new Error(details || `git exited with status ${result.status ?? "unknown"}`) - } + if (result.status !== 0) throw gitError(result) } -const coverageTasks = () => { +// Probe the working tree so local runs validate uncommitted task edits. CI +// runs on a clean tree, where stash create succeeds without a SHA and this +// falls back to HEAD. +const stashCommit = (result) => { + if (result.status !== 0) throw gitError(result) + return result.stdout.trim() || "HEAD" +} + +const worktreeCommit = () => stashCommit(spawnSync("git", ["stash", "create"], { cwd: root, encoding: "utf8" })) + +const turboTasks = () => { const result = spawnSync( command, - [ - ...args, - "turbo", - "--cwd", - probeRoot, - "run", - ...lanes.map((lane) => `test:coverage:${lane}`), - "--filter=zoo-code", - "--dry=json", - "--no-daemon", - ], + [...args, "turbo", "--cwd", probeRoot, "run", ...taskNames, "--filter=zoo-code", "--dry=json", "--no-daemon"], { cwd: root, encoding: "utf8" }, ) if (result.status !== 0) { const details = [result.error?.message, result.signal, result.stderr, result.stdout].filter(Boolean).join("\n") throw new Error(details || `pnpm exited with status ${result.status ?? "unknown"}`) } - const graph = JSON.parse(result.stdout) - return lanes.map((lane) => { - const task = graph.tasks.find(({ taskId }) => taskId === `zoo-code#test:coverage:${lane}`) - if (!task) throw new Error(`Coverage lane missing from Turbo graph: ${lane}`) + // pnpm may print warnings before the Turbo graph. The graph is the last + // top-level JSON object in the output. + const lines = result.stdout.split("\n") + let jsonStart = -1 + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith("{")) jsonStart = i + } + if (jsonStart < 0) { + const details = [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n") + throw new Error(details || "turbo dry run produced no JSON graph") + } + const graph = JSON.parse(lines.slice(jsonStart).join("\n")) + return taskNames.map((taskName) => { + const task = graph.tasks.find(({ taskId }) => taskId === `zoo-code#${taskName}`) + if (!task) throw new Error(`Extension lane missing from Turbo graph: ${taskName}`) return task }) } -const hashes = () => Object.fromEntries(coverageTasks().map((task) => [task.task.split(":").at(-1), task.hash])) +const hashes = () => Object.fromEntries(turboTasks().map((task) => [task.task, task.hash])) const withChangedFiles = (paths, run) => { const originals = paths.map((path) => [path, readFileSync(resolve(probeRoot, path), "utf8")]) @@ -62,7 +106,43 @@ const withChangedFiles = (paths, run) => { } } -const changedLanes = (before, after) => lanes.filter((lane) => before[lane] !== after[lane]) +const withCreatedFile = (path, run) => { + const target = resolve(probeRoot, path) + writeFileSync(target, "// cache-input-test\n") + try { + return run() + } finally { + rmSync(target, { force: true }) + } +} + +const changedTasks = (before, after) => taskNames.filter((task) => before[task] !== after[task]) + +const expectInvalidatesExactly = (task, changed) => { + const expected = taskNames.filter((name) => name === task || name === `test:coverage:${task.slice(5)}`) + const actual = changed.sort() + if (actual.join(",") !== expected.sort().join(",")) + throw new Error( + `${task} probe invalidated [${actual.join(", ") || "nothing"}] instead of [${expected.join(", ")}]`, + ) +} + +test("stash commit probe rejects git failures and keeps HEAD fallback", () => { + const failures = [ + { status: 128, stdout: "", stderr: "fatal: not a git repository" }, + { status: null, error: new Error("spawn git ENOENT") }, + { status: 1, signal: "SIGTERM", stdout: "", stderr: "" }, + ] + for (const result of failures) { + assert.throws( + () => stashCommit(result), + (error) => error.message.length > 0, + ) + } + assert.throws(() => stashCommit({ status: 1, stdout: "", stderr: "" }), /git exited with status 1/) + assert.equal(stashCommit({ status: 0, stdout: "0f53a1c\n" }), "0f53a1c") + assert.equal(stashCommit({ status: 0, stdout: "\n" }), "HEAD") +}) test("coverage cache input contract", async (context) => { probeRoot = mkdtempSync(resolve(tmpdir(), "zoo-code-coverage-cache-inputs-")) @@ -87,13 +167,65 @@ test("coverage cache input contract", async (context) => { process.once("SIGTERM", onSigterm) try { - git(["worktree", "add", "--detach", probeRoot, "HEAD"]) + git(["worktree", "add", "--detach", probeRoot, worktreeCommit()]) worktreeAdded = true - await context.test("coverage lane hashes ignore post-coverage verifier implementation", () => { + await context.test("plain and coverage lanes share identical input sets", () => { + const tasks = turboTasks() + const inputsByTask = Object.fromEntries(tasks.map((task) => [task.task, task.inputs])) + for (const lane of lanes) { + const plain = Object.keys(inputsByTask[`test:${lane}`]).sort() + const coverage = Object.keys(inputsByTask[`test:coverage:${lane}`]).sort() + const extraInPlain = plain.filter((path) => !coverage.includes(path)) + const extraInCoverage = coverage.filter((path) => !plain.includes(path)) + if (extraInPlain.length > 0 || extraInCoverage.length > 0) + throw new Error( + `test:${lane} and test:coverage:${lane} input sets differ. ` + + `Only in plain: [${extraInPlain.join(", ")}]. Only in coverage: [${extraInCoverage.join(", ")}]`, + ) + } + }) + + await context.test("lanes hash required inputs and exclude foreign test areas", () => { + const tasks = turboTasks() + for (const lane of lanes) { + for (const taskName of [`test:${lane}`, `test:coverage:${lane}`]) { + const task = tasks.find(({ task }) => task === taskName) + const inputs = task.inputs + const required = [ + "vitest.config.ts", + `vitest.${lane}.config.ts`, + "vitest.setup.ts", + "utils/vitest-verbosity.ts", + "__mocks__/vscode.js", + "package.json", + "tsconfig.json", + ] + for (const path of required) { + if (!Object.hasOwn(inputs, path)) + throw new Error(`${taskName} does not hash required input ${path}`) + } + const ownedSpecEntries = [ + ...Object.entries(laneOwnedSpecFiles), + ...Object.entries(laneOwnedTopLevelSpecFiles), + ] + for (const [owner, file] of ownedSpecEntries) { + const packagePath = file.slice("src/".length) + if (owner === lane) { + if (!Object.hasOwn(inputs, packagePath)) + throw new Error(`${taskName} does not hash its own spec ${packagePath}`) + } else if (Object.hasOwn(inputs, packagePath)) { + throw new Error(`${taskName} hashes foreign spec ${packagePath}`) + } + } + } + } + }) + + await context.test("lane hashes ignore post-coverage verifier implementation", () => { const before = hashes() const self = "scripts/verify-coverage-cache-inputs.mjs" - for (const task of coverageTasks()) { + for (const task of turboTasks()) { if (Object.hasOwn(task.inputs, self)) throw new Error(`${self} is an input of ${task.taskId}`) } for (const path of [ @@ -102,27 +234,37 @@ test("coverage cache input contract", async (context) => { "src/scripts/verify-lcov.mjs", ]) { const after = withChangedFiles([path], hashes) - const changed = changedLanes(before, after) - if (changed.length !== 0) throw new Error(`${path} invalidated coverage lanes: ${changed.join(", ")}`) + const changed = changedTasks(before, after) + if (changed.length !== 0) throw new Error(`${path} invalidated extension lanes: ${changed.join(", ")}`) } }) - await context.test("shared production changes invalidate every coverage lane that can import them", () => { + await context.test("shared production changes invalidate every extension lane", () => { const before = hashes() const after = withChangedFiles(["src/utils/path.ts"], hashes) - const changed = changedLanes(before, after) + const changed = changedTasks(before, after) - if (changed.join(",") !== lanes.join(",")) + if (changed.join(",") !== taskNames.join(",")) throw new Error(`Shared production change invalidated ${changed.join(", ") || "no lanes"}`) }) - await context.test("lane-owned tests invalidate only their general coverage lane", () => { - const before = hashes() - const after = withChangedFiles(["src/api/providers/__tests__/anthropic.spec.ts"], hashes) - const changed = changedLanes(before, after) + await context.test("lane-owned data files invalidate only their owner", () => { + for (const [lane, file] of Object.entries(laneOwnedDataFiles)) { + const before = hashes() + const after = withChangedFiles([file], hashes) + const changed = changedTasks(before, after) + expectInvalidatesExactly(`test:${lane}`, changed) + } + }) - if (changed.join(",") !== "api") - throw new Error(`API test change invalidated ${changed.join(", ") || "no lanes"}`) + await context.test("transient files in a lane test area invalidate only that lane", () => { + for (const [lane, dir] of Object.entries(laneProbeDirs)) { + const probe = `${dir}/cache-boundary-probe.tmp` + const before = hashes() + const after = withCreatedFile(probe, hashes) + const changed = changedTasks(before, after) + expectInvalidatesExactly(`test:${lane}`, changed) + } }) } finally { process.off("SIGINT", onSigint) diff --git a/src/turbo.json b/src/turbo.json index eac6f30f3c..3df079dc66 100644 --- a/src/turbo.json +++ b/src/turbo.json @@ -28,17 +28,19 @@ "!scripts/coverage-contract.mjs", "!scripts/verify-coverage-contract.mjs", "!scripts/verify-lcov.mjs", - "!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}" + "!core/**/__tests__/**", + "!services/**/__tests__/**", + "!__tests__/**", + "!activate/**/__tests__/**", + "!extension/**/__tests__/**", + "!i18n/**/__tests__/**", + "!integrations/**/__tests__/**", + "!scripts/**/__tests__/**", + "!shared/**/__tests__/**", + "!test-utils/**/__tests__/**", + "!utils/**/__tests__/**", + "!core/message-manager/index.spec.ts", + "!scripts/verify-lcov.spec.mjs" ], "outputs": ["coverage/api/**"] }, @@ -50,17 +52,18 @@ "!scripts/coverage-contract.mjs", "!scripts/verify-coverage-contract.mjs", "!scripts/verify-lcov.mjs", - "!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}" + "!api/**/__tests__/**", + "!services/**/__tests__/**", + "!__tests__/**", + "!activate/**/__tests__/**", + "!extension/**/__tests__/**", + "!i18n/**/__tests__/**", + "!integrations/**/__tests__/**", + "!scripts/**/__tests__/**", + "!shared/**/__tests__/**", + "!test-utils/**/__tests__/**", + "!utils/**/__tests__/**", + "!scripts/verify-lcov.spec.mjs" ], "outputs": ["coverage/core/**"] }, @@ -72,18 +75,20 @@ "!scripts/coverage-contract.mjs", "!scripts/verify-coverage-contract.mjs", "!scripts/verify-lcov.mjs", - "!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}" + "!api/**/__tests__/**", + "!core/**/__tests__/**", + "!services/tree-sitter/**/__tests__/**", + "!__tests__/**", + "!activate/**/__tests__/**", + "!extension/**/__tests__/**", + "!i18n/**/__tests__/**", + "!integrations/**/__tests__/**", + "!scripts/**/__tests__/**", + "!shared/**/__tests__/**", + "!test-utils/**/__tests__/**", + "!utils/**/__tests__/**", + "!core/message-manager/index.spec.ts", + "!scripts/verify-lcov.spec.mjs" ], "outputs": ["coverage/services/**"] }, @@ -95,10 +100,11 @@ "!scripts/coverage-contract.mjs", "!scripts/verify-coverage-contract.mjs", "!scripts/verify-lcov.mjs", - "!api/**/*.{test,spec}.{ts,tsx}", - "!core/**/*.{test,spec}.{ts,tsx}", - "!services/**/*.{test,spec}.{ts,tsx}", - "!__tests__/dist_assets.spec.ts" + "!api/**/__tests__/**", + "!core/**/__tests__/**", + "!services/**/__tests__/**", + "!__tests__/dist_assets.spec.ts", + "!core/message-manager/index.spec.ts" ], "outputs": ["coverage/misc/**"] }, @@ -120,6 +126,107 @@ ], "outputs": ["coverage/tree-sitter/**"] }, + "test:api": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!scripts/verify-coverage-cache-inputs.mjs", + "!scripts/coverage-contract.mjs", + "!scripts/verify-coverage-contract.mjs", + "!scripts/verify-lcov.mjs", + "!core/**/__tests__/**", + "!services/**/__tests__/**", + "!__tests__/**", + "!activate/**/__tests__/**", + "!extension/**/__tests__/**", + "!i18n/**/__tests__/**", + "!integrations/**/__tests__/**", + "!scripts/**/__tests__/**", + "!shared/**/__tests__/**", + "!test-utils/**/__tests__/**", + "!utils/**/__tests__/**", + "!core/message-manager/index.spec.ts", + "!scripts/verify-lcov.spec.mjs" + ] + }, + "test:core": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!scripts/verify-coverage-cache-inputs.mjs", + "!scripts/coverage-contract.mjs", + "!scripts/verify-coverage-contract.mjs", + "!scripts/verify-lcov.mjs", + "!api/**/__tests__/**", + "!services/**/__tests__/**", + "!__tests__/**", + "!activate/**/__tests__/**", + "!extension/**/__tests__/**", + "!i18n/**/__tests__/**", + "!integrations/**/__tests__/**", + "!scripts/**/__tests__/**", + "!shared/**/__tests__/**", + "!test-utils/**/__tests__/**", + "!utils/**/__tests__/**", + "!scripts/verify-lcov.spec.mjs" + ] + }, + "test:services": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!scripts/verify-coverage-cache-inputs.mjs", + "!scripts/coverage-contract.mjs", + "!scripts/verify-coverage-contract.mjs", + "!scripts/verify-lcov.mjs", + "!api/**/__tests__/**", + "!core/**/__tests__/**", + "!services/tree-sitter/**/__tests__/**", + "!__tests__/**", + "!activate/**/__tests__/**", + "!extension/**/__tests__/**", + "!i18n/**/__tests__/**", + "!integrations/**/__tests__/**", + "!scripts/**/__tests__/**", + "!shared/**/__tests__/**", + "!test-utils/**/__tests__/**", + "!utils/**/__tests__/**", + "!core/message-manager/index.spec.ts", + "!scripts/verify-lcov.spec.mjs" + ] + }, + "test:misc": { + "dependsOn": ["^build"], + "inputs": [ + "$TURBO_DEFAULT$", + "!scripts/verify-coverage-cache-inputs.mjs", + "!scripts/coverage-contract.mjs", + "!scripts/verify-coverage-contract.mjs", + "!scripts/verify-lcov.mjs", + "!api/**/__tests__/**", + "!core/**/__tests__/**", + "!services/**/__tests__/**", + "!__tests__/dist_assets.spec.ts", + "!core/message-manager/index.spec.ts" + ] + }, + "test: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" + ] + }, "bundle": { "dependsOn": ["^build", "@roo-code/vscode-webview#build"], "outputs": ["dist/**"] diff --git a/src/vitest.config.ts b/src/vitest.config.ts index 42986c454c..ed1c60f782 100644 --- a/src/vitest.config.ts +++ b/src/vitest.config.ts @@ -15,7 +15,9 @@ export default defineConfig({ testTimeout: 20_000, hookTimeout: 20_000, onConsoleLog, - maxWorkers: isWindowsCI ? 1 : undefined, + // Turbo runs two test lanes concurrently, so two workers per lane + // fill the four runner vCPUs on Windows CI. + maxWorkers: isWindowsCI ? 2 : undefined, coverage: { provider: "v8", reporter: ["text", "lcov"],