feat(ci): add code coverage pipeline and E2E mocking with aimock#4
feat(ci): add code coverage pipeline and E2E mocking with aimock#4
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds V8 coverage collection and root test:coverage orchestration with Codecov upload; introduces LLMock-backed VS Code E2E record/replay harness with fixtures and a new E2E workflow; implements Changes
Sequence Diagram(s)sequenceDiagram
participant Runner as Test Runner
participant LLMock as LLMock Server
participant VSCode as VS Code Extension
participant Fixtures as Fixture Store
Runner->>Runner: Read env (AIMOCK_RECORD, USE_MOCK, OPENROUTER_API_KEY, AIMOCK_URL)
alt Record Mode
Runner->>LLMock: Start LLMock (record/proxy real API)
else Replay/Mock Mode
Runner->>Fixtures: Load fixtures/
Runner->>LLMock: Start LLMock (replay mode)
LLMock->>Fixtures: Load fixture mappings
end
Runner->>VSCode: Launch extension with AIMOCK_URL & API key override
VSCode->>LLMock: Send LLM request
alt Fixture matched
LLMock->>Fixtures: Match by substring/regex
Fixtures-->>LLMock: Return fixture response (toolCalls)
else No match & Record Mode
LLMock->>LLMock: Proxy to real OpenRouter and persist openai-*.json
end
LLMock-->>VSCode: Return mocked/recorded response
VSCode->>VSCode: Emit ask/completion events
Runner->>VSCode: Call approveCurrentAsk() to auto-approve ask
Runner->>LLMock: Shutdown and cleanup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
8866a06 to
dea2378
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
codecov.yml (1)
11-12: Patch gate is not strictly 80% with current threshold.With
target: 80%andthreshold: 5%, PRs can pass around ~75% patch coverage. If you want strict enforcement, set threshold to0%.Suggested tweak
patch: default: target: 80% - threshold: 5% + threshold: 0%🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@codecov.yml` around lines 11 - 12, The codecov patch gate allows ~75% patch coverage because target: 80% combined with threshold: 5% permits a 5% drop; change the threshold key in the codecov.yml from 5% to 0% so the patch coverage requirement is strictly enforced at the configured target (update the threshold: value to 0% next to the existing target: 80% entry)..github/workflows/e2e.yml (1)
8-13: Path filter is a bit narrow for E2E reliability.Dependency/infrastructure changes outside these folders can still break mocked E2E. Consider including root dependency/build files (
package.json,pnpm-lock.yaml,turbo.json, and possibly.github/actions/setup-node-pnpm/**) in trigger paths.Suggested path expansion
paths: - "src/**" - "webview-ui/**" - "apps/vscode-e2e/**" - "packages/core/**" + - "package.json" + - "pnpm-lock.yaml" + - "turbo.json" + - ".github/actions/setup-node-pnpm/**"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/e2e.yml around lines 8 - 13, Update the e2e workflow path filters to include root dependency/build files so infra changes trigger E2E: modify the paths array in .github/workflows/e2e.yml (the paths: block) to add entries for "package.json", "pnpm-lock.yaml", "turbo.json", and the actions directory ".github/actions/setup-node-pnpm/**" (in addition to the existing "src/**", "webview-ui/**", "apps/vscode-e2e/**", "packages/core/**").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/vscode-e2e/AGENTS.md`:
- Around line 20-21: The command examples in AGENTS.md use inconsistent
working-directory conventions (e.g., the git clean command with
apps/vscode-e2e/fixtures/ vs later examples using fixtures/... as if cwd is
apps/vscode-e2e/); pick one convention and make all examples consistent — either
prefix every path with apps/vscode-e2e/ (so commands like git clean -fx
apps/vscode-e2e/fixtures/) or change all to relative to apps/vscode-e2e/ (cd
apps/vscode-e2e && git clean -fx fixtures/); update the affected lines (the git
clean example and the later fixtures/... examples) so copy/paste onboarding
steps always work.
In `@apps/vscode-e2e/src/runTest.ts`:
- Around line 93-99: The catch in runTest.ts currently calls process.exit(1)
which aborts the event loop and prevents the awaited cleanup in the finally
block (await fs.rm and await mock?.stop()) from running; change the catch to set
process.exitCode = 1 instead of calling process.exit(1) so the process can
finish pending async teardown in the finally block (inside the same
try/catch/finally around the test-run logic and cleanup).
In `@apps/vscode-e2e/src/suite/index.ts`:
- Around line 20-28: The code may call api.setConfiguration with
openRouterApiKey set to process.env.OPENROUTER_API_KEY! which can be undefined
in record mode; before calling setConfiguration, add an explicit guard that
checks isRecord (the isRecord constant) and verifies
process.env.OPENROUTER_API_KEY is present, and if missing throw a clear error or
exit (with a message mentioning OPENROUTER_API_KEY and record mode) so the
failure is immediate and actionable; keep the rest of the setConfiguration logic
(including aimockUrl handling and openRouterBaseUrl) unchanged.
In `@packages/types/tsup.config.ts`:
- Around line 11-13: Remove the stray noExternal entry referencing
"ai-sdk-provider-poe" from the tsup config in packages/types (the noExternal
array), because `@roo-code/types` does not declare or import that package; either
delete that array element or remove the entire noExternal key if unused, and if
the package is actually required add it to this package's dependencies instead
of keeping it in noExternal.
---
Nitpick comments:
In @.github/workflows/e2e.yml:
- Around line 8-13: Update the e2e workflow path filters to include root
dependency/build files so infra changes trigger E2E: modify the paths array in
.github/workflows/e2e.yml (the paths: block) to add entries for "package.json",
"pnpm-lock.yaml", "turbo.json", and the actions directory
".github/actions/setup-node-pnpm/**" (in addition to the existing "src/**",
"webview-ui/**", "apps/vscode-e2e/**", "packages/core/**").
In `@codecov.yml`:
- Around line 11-12: The codecov patch gate allows ~75% patch coverage because
target: 80% combined with threshold: 5% permits a 5% drop; change the threshold
key in the codecov.yml from 5% to 0% so the patch coverage requirement is
strictly enforced at the configured target (update the threshold: value to 0%
next to the existing target: 80% entry).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f6facfb6-7de1-477d-afdc-242ba3b61259
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (29)
.github/workflows/code-qa.yml.github/workflows/e2e.ymlapps/cli/package.jsonapps/cli/vitest.config.tsapps/vscode-e2e/AGENTS.mdapps/vscode-e2e/fixtures/.gitignoreapps/vscode-e2e/fixtures/.gitkeepapps/vscode-e2e/fixtures/markdown-lists.jsonapps/vscode-e2e/fixtures/modes.jsonapps/vscode-e2e/fixtures/task-hello-world.jsonapps/vscode-e2e/package.jsonapps/vscode-e2e/src/runTest.tsapps/vscode-e2e/src/suite/index.tscodecov.ymlpackage.jsonpackages/cloud/package.jsonpackages/cloud/vitest.config.tspackages/core/package.jsonpackages/core/vitest.config.tspackages/telemetry/package.jsonpackages/telemetry/vitest.config.tspackages/types/src/api.tspackages/types/tsup.config.tssrc/extension/api.tssrc/package.jsonsrc/vitest.config.tsturbo.jsonwebview-ui/package.jsonwebview-ui/vitest.config.ts
| // ai-sdk-provider-poe is ESM-only (./code subpath has no "require" condition) | ||
| // so tsup must bundle it inline rather than emit a runtime require() call. | ||
| noExternal: ["ai-sdk-provider-poe"], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether `@roo-code/types` actually depends on or imports ai-sdk-provider-poe.
set -euo pipefail
echo "== packages/types/package.json dependency check =="
cat packages/types/package.json | jq '{dependencies, devDependencies, peerDependencies}'
echo
echo "== references to ai-sdk-provider-poe inside packages/types =="
rg -n -C2 --type=ts --type=js --type=json 'ai-sdk-provider-poe' packages/types || true
echo
echo "Expected:"
echo "- If no dependency/import exists in packages/types, remove noExternal from this file."
echo "- If imports exist, ensure dependency is declared in packages/types/package.json."Repository: Zoo-Code-Org/Zoo-Code
Length of output: 1086
🏁 Script executed:
# Search entire workspace for ai-sdk-provider-poe
rg -n 'ai-sdk-provider-poe' --type=ts --type=js --type=jsonRepository: Zoo-Code-Org/Zoo-Code
Length of output: 249
🏁 Script executed:
# Check all tsup.config files to see if this noExternal appears elsewhere
fd -e tsup.config.ts -e tsup.config.jsRepository: Zoo-Code-Org/Zoo-Code
Length of output: 47
🏁 Script executed:
# Check if ai-sdk-provider-poe is in workspace root package.json or any root dependencies
cat package.json | jq '{dependencies, devDependencies, peerDependencies}' || trueRepository: Zoo-Code-Org/Zoo-Code
Length of output: 701
🏁 Script executed:
# Check all source files (excluding node_modules and config files) for ai-sdk-provider-poe
find packages/types -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" | grep -v node_modules | xargs rg 'ai-sdk-provider-poe' || echo "No matches found"Repository: Zoo-Code-Org/Zoo-Code
Length of output: 243
🏁 Script executed:
# Let's also look at the entire packages/types structure to understand what this package actually exports
ls -la packages/types/src/ 2>/dev/null || echo "No src directory"
head -50 packages/types/tsup.config.tsRepository: Zoo-Code-Org/Zoo-Code
Length of output: 2817
Remove the noExternal entry for ai-sdk-provider-poe—this package is not a dependency of @roo-code/types.
The noExternal configuration on line 13 references a package that @roo-code/types neither declares as a dependency nor imports anywhere in its codebase. This config should be removed unless ai-sdk-provider-poe is actually used by this package and needs to be added as a dependency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/types/tsup.config.ts` around lines 11 - 13, Remove the stray
noExternal entry referencing "ai-sdk-provider-poe" from the tsup config in
packages/types (the noExternal array), because `@roo-code/types` does not declare
or import that package; either delete that array element or remove the entire
noExternal key if unused, and if the package is actually required add it to this
package's dependencies instead of keeping it in noExternal.
There was a problem hiding this comment.
The entry is needed. CodeRabbit's search ran against the PR diff only and didn't see packages/types/src/providers/poe.ts, which lives in zoo/main (this repo's Poe provider addition). That file re-exports poeDefaultModelId, POE_DEFAULT_BASE_URL, and getPoeDefaultModelInfo directly from ai-sdk-provider-poe/code. The package is declared as a dependency in packages/types/package.json (pinned at 2.0.18) and its ./code subpath is ESM-only — no require export condition. Without noExternal, tsup externalises it and the built dist/index.cjs emits a runtime require("ai-sdk-provider-poe/code") that Node can't resolve (confirmed by the previous CI failure: Package subpath './code' is not defined by "exports"). Keeping the entry as-is.
There was a problem hiding this comment.
Only users with a collaborator, contributor, member, or owner role can interact with CodeRabbit.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/vscode-e2e/src/runTest.ts`:
- Around line 9-12: When AIMOCK_RECORD is set (isRecord true) but
process.env.OPENROUTER_API_KEY is missing, add an early fail-fast check in
runTest.ts that throws or exits with a clear error message; locate the block
that defines isRecord and useMock and if isRecord &&
!process.env.OPENROUTER_API_KEY then call process.exit(1) or throw a descriptive
Error indicating OPENROUTER_API_KEY is required for recording (mention
AIMOCK_RECORD) so recording mode fails immediately rather than producing
downstream errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 12360661-eeaa-42af-8668-1c188d7ba9a5
📒 Files selected for processing (3)
apps/vscode-e2e/AGENTS.mdapps/vscode-e2e/src/runTest.tsapps/vscode-e2e/src/suite/index.ts
✅ Files skipped from review due to trivial changes (1)
- apps/vscode-e2e/AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/vscode-e2e/src/suite/index.ts
64396e9 to
f8d0666
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/vscode-e2e/src/runTest.ts`:
- Around line 59-73: Move resource acquisition into the existing try/finally so
cleanup always runs: call mock.start() and create the temporary workspace
(fs.mkdtemp(...) that assigns testWorkspace) inside the same try block that
already ensures mock?.stop() and workspace cleanup in the finally; ensure
references to mock.start(), mock?.stop(), and the testWorkspace variable (used
later for cleanup) are updated so both startup failures and normal runs trigger
the finally cleanup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 334a27dd-dc8d-4fd4-9e85-508ee6de55d8
📒 Files selected for processing (4)
apps/vscode-e2e/AGENTS.mdapps/vscode-e2e/src/runTest.tsapps/vscode-e2e/src/suite/index.tspackages/core/src/custom-tools/__tests__/custom-tool-registry.spec.ts
✅ Files skipped from review due to trivial changes (2)
- packages/core/src/custom-tools/tests/custom-tool-registry.spec.ts
- apps/vscode-e2e/AGENTS.md
There was a problem hiding this comment.
♻️ Duplicate comments (1)
apps/vscode-e2e/src/runTest.ts (1)
21-60:⚠️ Potential issue | 🟡 MinorMove LLMock startup under the existing
try/finally.
mock.start()still runs before Line 72, so a startup failure skips both thecatchlogging and thefinallycleanup path. That reopens the teardown gap for mock initialization errors.♻️ Proposed fix
let mock: InstanceType<typeof LLMock> | undefined +let testWorkspace: string | undefined - if (useMock) { - const fixturesDir = path.resolve(__dirname, "../fixtures") - - mock = new LLMock({ - port: 0, // random free port - ...(isRecord && { - record: { - providers: { openai: "https://openrouter.ai/api" }, - fixturePath: fixturesDir, - }, - }), - }) - - mock.loadFixtureDir(fixturesDir) - - if (!isRecord) { - mock.addFixture({ - match: { userMessage: /^<environment_details>/ }, - response: { - toolCalls: [ - { - name: "attempt_completion", - arguments: JSON.stringify({ result: "Switched to ❓ Ask mode as requested." }), - id: "call_modes_post_switch_001", - }, - ], - }, - }) - } - - await mock.start() - } - - let testWorkspace: string | undefined - try { + if (useMock) { + const fixturesDir = path.resolve(__dirname, "../fixtures") + + mock = new LLMock({ + port: 0, // random free port + ...(isRecord && { + record: { + providers: { openai: "https://openrouter.ai/api" }, + fixturePath: fixturesDir, + }, + }), + }) + + mock.loadFixtureDir(fixturesDir) + + if (!isRecord) { + mock.addFixture({ + match: { userMessage: /^<environment_details>/ }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: "Switched to ❓ Ask mode as requested." }), + id: "call_modes_post_switch_001", + }, + ], + }, + }) + } + + await mock.start() + } + // Create a temporary workspace folder for tests testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-workspace-"))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/vscode-e2e/src/runTest.ts` around lines 21 - 60, The LLMock is started before entering the existing try/finally so failures in mock.start() bypass the catch and finally cleanup; move the mock.start() call into the same try block that wraps the test run (so startup errors are handled by the existing catch and ensure mock.stop()/cleanup in finally), keeping mock.loadFixtureDir(...) and mock.addFixture(...) where they are but calling mock.start() after those within the try; reference LLMock instance "mock" and its start/stop methods (mock.start, mock.stop) to locate and move the call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@apps/vscode-e2e/src/runTest.ts`:
- Around line 21-60: The LLMock is started before entering the existing
try/finally so failures in mock.start() bypass the catch and finally cleanup;
move the mock.start() call into the same try block that wraps the test run (so
startup errors are handled by the existing catch and ensure mock.stop()/cleanup
in finally), keeping mock.loadFixtureDir(...) and mock.addFixture(...) where
they are but calling mock.start() after those within the try; reference LLMock
instance "mock" and its start/stop methods (mock.start, mock.stop) to locate and
move the call.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f2c7e9e-ac8f-40c8-8fe0-b7cd79a6fab6
📒 Files selected for processing (1)
apps/vscode-e2e/src/runTest.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
.github/workflows/code-qa.yml (1)
61-68: Cache key at Line 65 is too granular and reduces cache reuse.Including
${{ github.sha }}creates a new cache on every commit. Restore keys help, but this still increases churn and cache pressure unnecessarily.♻️ Suggested cache-key adjustment
- key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }} + key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}- ${{ runner.os }}-turbo-🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/code-qa.yml around lines 61 - 68, The cache key for the "Cache Turbo" step is too granular because it includes `${{ github.sha }}`, causing a new cache per commit; update the key used by the actions/cache step (the `key:` under the "Cache Turbo" step that references `runner.os` and `hashFiles('**/pnpm-lock.yaml')`) to remove `${{ github.sha }}` and rely on `runner.os` plus the lockfile hash (e.g., `${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}`) so caches are reused across commits, and adjust the `restore-keys` to match the less specific prefixes already present for `.turbo/cache`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In @.github/workflows/code-qa.yml:
- Around line 61-68: The cache key for the "Cache Turbo" step is too granular
because it includes `${{ github.sha }}`, causing a new cache per commit; update
the key used by the actions/cache step (the `key:` under the "Cache Turbo" step
that references `runner.os` and `hashFiles('**/pnpm-lock.yaml')`) to remove `${{
github.sha }}` and rely on `runner.os` plus the lockfile hash (e.g., `${{
runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}`) so caches are reused
across commits, and adjust the `restore-keys` to match the less specific
prefixes already present for `.turbo/cache`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 03caa32e-ff21-4369-aeb0-a8d30a04ed5d
📒 Files selected for processing (1)
.github/workflows/code-qa.yml
Summary
Unit test coverage (
code-qa.yml): Adds a matrixunit-testjob (ubuntu + windows) runningpnpm test:coverageacross 6 workspaces:src,webview-ui,packages/core,packages/cloud,packages/telemetry,apps/cli. Each workspace gets@vitest/coverage-v8and atest:coveragescript producingcoverage/lcov.info. Atest:coverageTurbo task is added withoutputs: ["coverage/**"]so unchanged packages are served from cache. CI caches.turbo/cacheviaactions/cacheto skip vitest on unchanged packages on subsequent runs.Codecov (
codecov.yml): Uploads lcov files from both platforms usingCODECOV_TOKEN. Project check usestarget: auto(never regress below current baseline, 1% threshold). Patch check enforces 80% coverage on new lines (5% threshold). RequiresCODECOV_TOKENsecret to be added to repo settings.E2E mocking (
e2e.yml): New workflow triggered on PRs touchingsrc/**,webview-ui/**,apps/vscode-e2e/**,packages/core/**. Runstest:ci:mockviaxvfb-run(headless VS Code, 30-minute timeout). Uses@copilotkit/aimock— a local HTTP server that replays committed fixture files. No OpenRouter API key needed. Fixtures are matched by substring on the last user message; three fixtures are committed covering the existing test suite.approveCurrentAsk()API: AddsapproveCurrentAsk()toRooCodeAPI(types + implementation). The E2E harness calls this to unblockcompletion_resultasks without going through webview button routing. The primary button forcompletion_resultroutes to "Start New Task" by design — correct UX for users, not useful in a test harness.packages/typesCJS fix:ai-sdk-provider-poeis ESM-only; its./codesubpath has norequireexport condition. tsup externalises it by default, leaving a runtimerequire("ai-sdk-provider-poe/code")in the CJS bundle that Node can't resolve. AddednoExternal: ["ai-sdk-provider-poe"]topackages/types/tsup.config.tsso esbuild bundles it inline. No-op in repos that don't importpoe.ts.runTest.tshardening: Four fixes to the E2E test runner: (1) fail-fast guard whenAIMOCK_RECORD=truebutOPENROUTER_API_KEYis missing; (2)process.exitCode = 1instead ofprocess.exit(1)so thefinallycleanup block runs on failure; (3)fs.mkdtemp()moved inside thetryblock so cleanup always runs even if workspace creation fails; (4)testWorkspaceguarded withif (testWorkspace)infinally.Test timeout bump:
custom-tool-registry.spec.tshas two tests that dynamically import TypeScript files at runtime. The first cold compilation exceeds 120s on Windows under V8 coverage instrumentation. Both timeouts bumped from 120s to 300s to match observed worst-case CI runner performance.Key implementation details
incoming_last_user_message. The<environment_details>block (which includes the current time) is always present — match strings must never include timestamps or paths. The modes test triggers a second API call whose last user message starts with<environment_details>directly; a programmatic regex fixture handles this inrunTest.ts.OPENROUTER_API_KEY=<key> pnpm --filter @roo-code/vscode-e2e test:recordproxies unmatched requests to OpenRouter and writesfixtures/openai-*.json. Copy the relevant block into a named fixture file, then delete the auto-recorded files (they are gitignored).actions/cachepersists.turbo/cachebetween runs keyed by lockfile hash + SHA. On a rerun with no source changes, all packages hit Turbo cache and vitest never starts.Test procedure
Coverage (local):
E2E mock replay (no API key):
Record new fixtures (requires OpenRouter key):