diff --git a/apps/cli/src/commands/eval/commands/run.ts b/apps/cli/src/commands/eval/commands/run.ts index 38cabbb35..fedd959d5 100644 --- a/apps/cli/src/commands/eval/commands/run.ts +++ b/apps/cli/src/commands/eval/commands/run.ts @@ -108,9 +108,14 @@ export const evalRunCommand = command({ long: 'cache', description: 'Enable provider response cache (persisted to disk)', }), + cachePath: option({ + type: optional(string), + long: 'cache-path', + description: 'Enable provider response cache at the given directory', + }), noCache: flag({ long: 'no-cache', - description: 'Disable caching (overrides YAML execution.cache)', + description: 'Disable response caching (overrides --cache, --cache-path, config, and YAML)', }), verbose: flag({ long: 'verbose', @@ -262,6 +267,7 @@ export const evalRunCommand = command({ agentTimeout: args.agentTimeout, maxRetries: args.maxRetries, cache: args.cache, + cachePath: args.cachePath, noCache: args.noCache, verbose: args.verbose, workspaceMode: args.workspaceMode, diff --git a/apps/cli/src/commands/eval/run-eval.ts b/apps/cli/src/commands/eval/run-eval.ts index 139d47804..41b038906 100644 --- a/apps/cli/src/commands/eval/run-eval.ts +++ b/apps/cli/src/commands/eval/run-eval.ts @@ -96,7 +96,10 @@ interface NormalizedOptions { readonly agentTimeoutSeconds?: number; readonly maxRetries: number; readonly cache: boolean; + readonly cachePath?: string; readonly noCache: boolean; + readonly tsConfigCache?: boolean; + readonly tsConfigCachePath?: string; readonly verbose: boolean; readonly otelFile?: string; readonly exportOtel: boolean; @@ -313,13 +316,12 @@ function normalizeOptions( const cliMaxRetries = normalizeOptionalNumber(rawOptions.maxRetries); const configMaxRetries = config?.execution?.maxRetries; - // Cache: CLI flags take priority, then config file, then default (true via shouldEnableCache) - const cliCache = normalizeBoolean(rawOptions.cache); + // Response cache: CLI request/path, then eval YAML, then TypeScript config, then default off. + const cliCachePath = normalizeString(rawOptions.cachePath); + const cliCache = normalizeBoolean(rawOptions.cache) || cliCachePath !== undefined; const cliNoCache = normalizeBoolean(rawOptions.noCache); const configCacheEnabled = config?.cache?.enabled; - // If neither --cache nor --no-cache was passed, use config value - const resolvedCache = cliCache || (!cliNoCache && configCacheEnabled === true); - const resolvedNoCache = cliNoCache; + const configCachePath = normalizeString(config?.cache?.path); // Output dir: CLI --out > config output.dir > auto-generated const cliOut = normalizeString(rawOptions.out); @@ -352,8 +354,11 @@ function normalizeOptions( dryRunDelayMax: normalizeNumber(rawOptions.dryRunDelayMax, 0), agentTimeoutSeconds: cliAgentTimeout ?? configAgentTimeoutSeconds, maxRetries: cliMaxRetries ?? configMaxRetries ?? 2, - cache: resolvedCache, - noCache: resolvedNoCache, + cache: cliCache, + cachePath: cliCachePath, + noCache: cliNoCache, + tsConfigCache: configCacheEnabled, + tsConfigCachePath: configCachePath, // Boolean OR: config `true` cannot be overridden to `false` from CLI. // Intentional — there are no --no-verbose / --no-keep-workspaces flags. // Precedence: CLI > YAML config > TS config @@ -1311,13 +1316,15 @@ export async function runEvalCommand( cliCache: options.cache, cliNoCache: options.noCache, yamlCache: yamlCacheEnabled, + tsConfigCache: options.tsConfigCache, }); + const activeCachePath = options.cachePath ?? yamlCachePath ?? options.tsConfigCachePath; const cache = cacheEnabled - ? new ResponseCache(yamlCachePath ? path.resolve(yamlCachePath) : undefined) + ? new ResponseCache(activeCachePath ? path.resolve(activeCachePath) : undefined) : undefined; - if (cacheEnabled) { - console.log(`Response cache: enabled${yamlCachePath ? ` (${yamlCachePath})` : ''}`); + if (cache) { + console.log(`Response cache: enabled (${cache.cachePath})`); } // Resolve suite-level threshold: CLI --threshold takes precedence over YAML execution.threshold. diff --git a/apps/cli/test/eval.integration.test.ts b/apps/cli/test/eval.integration.test.ts index 8db576c6e..ab6382935 100644 --- a/apps/cli/test/eval.integration.test.ts +++ b/apps/cli/test/eval.integration.test.ts @@ -199,6 +199,23 @@ async function readDiagnostics(fixture: EvalFixture): Promise { + await writeFile( + path.join(fixture.suiteDir, 'agentv.config.ts'), + `export default { cache: { enabled: true, path: ${JSON.stringify(cachePath)} } };\n`, + 'utf8', + ); +} + +async function prependYamlCacheConfig(fixture: EvalFixture, cachePath: string): Promise { + const original = await readFile(fixture.testFilePath, 'utf8'); + await writeFile( + fixture.testFilePath, + `execution:\n cache: true\n cache_path: ${cachePath}\n\n${original}`, + 'utf8', + ); +} + describe('agentv eval CLI', () => { it('writes results, summary, and prompt dumps using default directories', async () => { const fixture = await createFixture(); @@ -285,4 +302,117 @@ describe('agentv eval CLI', () => { await rm(fixture.baseDir, { recursive: true, force: true }); } }, 30_000); + + it('honors agentv.config.ts cache.path when response cache is enabled there', async () => { + const fixture = await createFixture(); + try { + const cachePath = '.agentv/ts-response-cache'; + await writeTsCacheConfig(fixture, cachePath); + + const { stdout } = await runCli(fixture, ['eval', fixture.testFilePath]); + + const resolvedCachePath = path.resolve(fixture.suiteDir, cachePath); + expect(stdout).toContain(`Response cache: enabled (${resolvedCachePath})`); + const diagnostics = await readDiagnostics(fixture); + expect(diagnostics).toMatchObject({ + hasCache: true, + cachePath: resolvedCachePath, + useCache: true, + }); + } finally { + await rm(fixture.baseDir, { recursive: true, force: true }); + } + }, 30_000); + + it('honors eval YAML execution.cache_path', async () => { + const fixture = await createFixture(); + try { + const cachePath = '.agentv/yaml-response-cache'; + await prependYamlCacheConfig(fixture, cachePath); + + const { stdout } = await runCli(fixture, ['eval', fixture.testFilePath]); + + const resolvedCachePath = path.resolve(fixture.suiteDir, cachePath); + expect(stdout).toContain(`Response cache: enabled (${resolvedCachePath})`); + const diagnostics = await readDiagnostics(fixture); + expect(diagnostics).toMatchObject({ + hasCache: true, + cachePath: resolvedCachePath, + useCache: true, + }); + } finally { + await rm(fixture.baseDir, { recursive: true, force: true }); + } + }, 30_000); + + it('honors CLI --cache-path as an explicit response cache opt-in', async () => { + const fixture = await createFixture(); + try { + const cachePath = '.agentv/cli-response-cache'; + + const { stdout } = await runCli(fixture, [ + 'eval', + fixture.testFilePath, + '--cache-path', + cachePath, + ]); + + const resolvedCachePath = path.resolve(fixture.suiteDir, cachePath); + expect(stdout).toContain(`Response cache: enabled (${resolvedCachePath})`); + const diagnostics = await readDiagnostics(fixture); + expect(diagnostics).toMatchObject({ + hasCache: true, + cachePath: resolvedCachePath, + useCache: true, + }); + } finally { + await rm(fixture.baseDir, { recursive: true, force: true }); + } + }, 30_000); + + it('lets --no-cache override config-driven response cache settings', async () => { + const fixture = await createFixture(); + try { + await writeTsCacheConfig(fixture, '.agentv/disabled-response-cache'); + + const { stdout } = await runCli(fixture, ['eval', fixture.testFilePath, '--no-cache']); + + expect(stdout).not.toContain('Response cache: enabled'); + const diagnostics = await readDiagnostics(fixture); + expect(diagnostics).toMatchObject({ + hasCache: false, + cachePath: null, + useCache: false, + }); + } finally { + await rm(fixture.baseDir, { recursive: true, force: true }); + } + }, 30_000); + + it('keeps response cache help separate from transcript replay terminology', async () => { + const result = await execa('bun', ['--no-env-file', CLI_ENTRY, 'eval', 'run', '--help'], { + cwd: projectRoot, + env: { ...process.env, CI: 'true' }, + reject: false, + }); + const helpText = `${result.stdout}\n${result.stderr}`; + expect(helpText).toContain('--cache'); + expect(helpText).toContain('--cache-path'); + expect(helpText).toContain('--transcript'); + + const cacheHelp = helpText + .split(/\r?\n/) + .filter((line) => line.includes('--cache')) + .join('\n') + .toLowerCase(); + expect(cacheHelp).toContain('response cache'); + expect(cacheHelp).not.toContain('replay'); + + const transcriptHelp = helpText + .split(/\r?\n/) + .filter((line) => line.includes('--transcript')) + .join('\n') + .toLowerCase(); + expect(transcriptHelp).not.toContain('cache'); + }, 30_000); }); diff --git a/apps/cli/test/fixtures/mock-run-evaluation.ts b/apps/cli/test/fixtures/mock-run-evaluation.ts index 91bc84080..da198883a 100644 --- a/apps/cli/test/fixtures/mock-run-evaluation.ts +++ b/apps/cli/test/fixtures/mock-run-evaluation.ts @@ -25,6 +25,14 @@ interface RunEvaluationOptionsLike { readonly onResult?: (result: EvaluationResultLike) => Promise | void; } +function getCachePath(cache: unknown): string | null { + if (!cache || typeof cache !== 'object') { + return null; + } + const maybeCachePath = (cache as { readonly cachePath?: unknown }).cachePath; + return typeof maybeCachePath === 'string' ? maybeCachePath : null; +} + interface EvaluationResultLike { readonly testId: string; readonly score: number; @@ -82,6 +90,8 @@ async function maybeWriteDiagnostics( agentTimeoutMs: options.agentTimeoutMs ?? null, promptDumpDir: options.promptDumpDir, filter: options.filter ?? null, + hasCache: options.cache !== undefined, + cachePath: getCachePath(options.cache), useCache: options.useCache ?? false, envSample: process.env.CLI_ENV_SAMPLE ?? null, envRootOnly: process.env.CLI_ENV_ROOT_ONLY ?? null, diff --git a/apps/web/src/content/docs/docs/evaluation/running-evals.mdx b/apps/web/src/content/docs/docs/evaluation/running-evals.mdx index 5e082211d..d041bd992 100644 --- a/apps/web/src/content/docs/docs/evaluation/running-evals.mdx +++ b/apps/web/src/content/docs/docs/evaluation/running-evals.mdx @@ -427,6 +427,40 @@ The `{timestamp}` placeholder is replaced with an ISO-like timestamp (e.g., `202 **Precedence:** CLI flags > project-local `.agentv/config.yaml` > home/global `$AGENTV_HOME/config.yaml` (or `~/.agentv/config.yaml`) > `agentv.config.ts` > built-in defaults. +## Response Cache + +AgentV's response cache stores exact provider responses on disk to reduce repeated live LLM calls while iterating on the same eval. It is disabled by default. Enable it from the CLI with `--cache`, or enable it with a custom directory using `--cache-path`: + +```bash +agentv eval evals/dataset.eval.yaml --cache +agentv eval evals/dataset.eval.yaml --cache-path .agentv/response-cache +``` + +Eval YAML can enable the same cache per suite: + +```yaml +execution: + cache: true + cache_path: .agentv/response-cache +``` + +Project TypeScript config can set the project default: + +```typescript +import { defineConfig } from '@agentv/core'; + +export default defineConfig({ + cache: { + enabled: true, + path: '.agentv/response-cache', + }, +}); +``` + +`--no-cache` disables response caching regardless of CLI, eval YAML, or TypeScript config. Cache path precedence is `--cache-path` > eval YAML `execution.cache_path` > TypeScript config `cache.path` > `.agentv/cache`. + +Response cache and replay are separate concepts. The response cache is an iteration aid for repeated live provider calls. Transcript or fixture replay is target substitution from curated artifacts, and graders still run fresh against the replayed output. + ## Environment Variables ### AGENTV_HOME diff --git a/packages/core/src/evaluation/cache/response-cache.ts b/packages/core/src/evaluation/cache/response-cache.ts index 6017f82fe..b55a555e2 100644 --- a/packages/core/src/evaluation/cache/response-cache.ts +++ b/packages/core/src/evaluation/cache/response-cache.ts @@ -13,7 +13,7 @@ const DEFAULT_CACHE_PATH = '.agentv/cache'; * Directory structure: //.json */ export class ResponseCache implements EvaluationCache { - private readonly cachePath: string; + readonly cachePath: string; constructor(cachePath?: string) { this.cachePath = cachePath ?? DEFAULT_CACHE_PATH; @@ -47,16 +47,21 @@ export class ResponseCache implements EvaluationCache { * * Precedence: * 1. --no-cache CLI flag → always disabled - * 2. --cache CLI flag OR execution.cache YAML → enabled - * 3. Default → disabled (safe for variability testing) + * 2. --cache CLI flag → enabled + * 3. execution.cache YAML → enabled/disabled for that eval file + * 4. agentv.config.ts cache.enabled → project default + * 5. Default → disabled (safe for variability testing) */ export function shouldEnableCache(params: { cliCache: boolean; cliNoCache: boolean; yamlCache?: boolean; + tsConfigCache?: boolean; }): boolean { if (params.cliNoCache) return false; - return params.cliCache || params.yamlCache === true; + if (params.cliCache) return true; + if (params.yamlCache !== undefined) return params.yamlCache; + return params.tsConfigCache === true; } /** diff --git a/packages/core/src/evaluation/config.ts b/packages/core/src/evaluation/config.ts index 77989b338..537fe3507 100644 --- a/packages/core/src/evaluation/config.ts +++ b/packages/core/src/evaluation/config.ts @@ -64,7 +64,7 @@ const AgentVConfigSchema = z.object({ .object({ /** Enable response caching */ enabled: z.boolean().optional(), - /** Cache file path */ + /** Response cache directory */ path: z.string().optional(), }) .optional(), diff --git a/packages/core/src/evaluation/evaluate.ts b/packages/core/src/evaluation/evaluate.ts index 328930b77..ae503dc91 100644 --- a/packages/core/src/evaluation/evaluate.ts +++ b/packages/core/src/evaluation/evaluate.ts @@ -62,6 +62,11 @@ import micromatch from 'micromatch'; import { buildDirectoryChain, findGitRoot } from './file-utils.js'; import type { AssertFn } from './assertions.js'; +import { + ResponseCache, + shouldEnableCache, + shouldSkipCacheForTemperature, +} from './cache/response-cache.js'; import { DEFAULT_THRESHOLD } from './graders/scoring.js'; import type { EvalMetadata } from './metadata.js'; import { runEvaluation } from './orchestrator.js'; @@ -185,6 +190,8 @@ export interface EvalConfig { readonly agentTimeoutMs?: number; /** Enable response caching */ readonly cache?: boolean; + /** Response cache directory. Requires cache to be enabled. */ + readonly cachePath?: string; /** Verbose logging */ readonly verbose?: boolean; /** Callback for each completed result */ @@ -202,6 +209,7 @@ export interface MaterializedEvalConfig { readonly tests: readonly EvalTest[]; readonly workers?: number; readonly cache?: boolean; + readonly cachePath?: string; readonly budgetUsd?: number; readonly threshold?: number; readonly metadata?: EvalMetadata; @@ -322,6 +330,14 @@ export async function evaluate(config: EvalConfig): Promise { } const collectedResults: EvaluationResult[] = []; + const cacheEnabled = shouldEnableCache({ + cliCache: config.cache === true, + cliNoCache: false, + yamlCache: config.cache === undefined ? materialized.cache : undefined, + }); + const cache = cacheEnabled + ? new ResponseCache(materialized.cachePath ? path.resolve(materialized.cachePath) : undefined) + : undefined; const results = await runEvaluation({ testFilePath, @@ -335,6 +351,10 @@ export async function evaluate(config: EvalConfig): Promise { filter: config.filter, threshold: config.threshold, evalCases: materialized.tests, + cache, + useCache: + !!cache && + !shouldSkipCacheForTemperature(resolvedTarget.config as unknown as Record), ...(materialized.budgetUsd !== undefined && { budgetUsd: materialized.budgetUsd }), onResult: async (result) => { collectedResults.push(result); @@ -379,6 +399,7 @@ export async function materializeEvalConfig( tests, workers: config.workers ?? suite.workers, cache: config.cache ?? suite.cacheConfig?.enabled, + cachePath: config.cachePath ?? suite.cacheConfig?.cachePath, budgetUsd: config.budgetUsd ?? suite.budgetUsd, threshold: config.threshold ?? suite.threshold, metadata: config.metadata ?? suite.metadata, @@ -399,6 +420,7 @@ export async function materializeEvalConfig( tests, workers: config.workers, cache: config.cache, + cachePath: config.cachePath, budgetUsd: config.budgetUsd, threshold: config.threshold, metadata: config.metadata, diff --git a/packages/core/src/evaluation/loaders/config-loader.ts b/packages/core/src/evaluation/loaders/config-loader.ts index 3089d08f2..02d8c28fc 100644 --- a/packages/core/src/evaluation/loaders/config-loader.ts +++ b/packages/core/src/evaluation/loaders/config-loader.ts @@ -406,7 +406,11 @@ export function extractCacheConfig(suite: JsonObject): CacheConfig | undefined { return undefined; } - const cachePath = executionObj.cache_path ?? executionObj.cachePath; + if (executionObj.cachePath !== undefined) { + logWarning('Invalid execution.cachePath: use snake_case execution.cache_path in YAML.'); + } + + const cachePath = executionObj.cache_path; const resolvedCachePath = typeof cachePath === 'string' && cachePath.trim().length > 0 ? cachePath.trim() : undefined; diff --git a/packages/core/src/evaluation/loaders/ts-eval-loader.ts b/packages/core/src/evaluation/loaders/ts-eval-loader.ts index d406b90ef..3556c8d8a 100644 --- a/packages/core/src/evaluation/loaders/ts-eval-loader.ts +++ b/packages/core/src/evaluation/loaders/ts-eval-loader.ts @@ -76,7 +76,12 @@ export async function loadTsEvalSuite( return { tests: materialized.tests, ...(materialized.workers !== undefined && { workers: materialized.workers }), - ...(materialized.cache !== undefined && { cacheConfig: { enabled: materialized.cache } }), + ...(materialized.cache !== undefined && { + cacheConfig: { + enabled: materialized.cache, + ...(materialized.cachePath !== undefined && { cachePath: materialized.cachePath }), + }, + }), ...(materialized.budgetUsd !== undefined && { budgetUsd: materialized.budgetUsd }), ...(materialized.threshold !== undefined && { threshold: materialized.threshold }), ...(materialized.metadata !== undefined && { metadata: materialized.metadata }), diff --git a/packages/core/test/evaluation/cache-config.test.ts b/packages/core/test/evaluation/cache-config.test.ts index 09f1a6d4f..74471498b 100644 --- a/packages/core/test/evaluation/cache-config.test.ts +++ b/packages/core/test/evaluation/cache-config.test.ts @@ -32,10 +32,10 @@ describe('extractCacheConfig', () => { expect(result).toEqual({ enabled: true, cachePath: '.agentv/my-cache' }); }); - it('should accept camelCase cachePath', () => { + it('should ignore camelCase cachePath on the YAML wire surface', () => { const suite: JsonObject = { execution: { cache: true, cachePath: 'custom/cache' } }; const result = extractCacheConfig(suite); - expect(result).toEqual({ enabled: true, cachePath: 'custom/cache' }); + expect(result).toEqual({ enabled: true, cachePath: undefined }); }); it('should return undefined for invalid cache value', () => { diff --git a/packages/core/test/evaluation/evaluate-programmatic-api.test.ts b/packages/core/test/evaluation/evaluate-programmatic-api.test.ts index 6918f56e7..0120a9975 100644 --- a/packages/core/test/evaluation/evaluate-programmatic-api.test.ts +++ b/packages/core/test/evaluation/evaluate-programmatic-api.test.ts @@ -6,6 +6,8 @@ */ import { describe, expect, it } from 'bun:test'; +import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import path from 'node:path'; import { evaluate } from '../../src/evaluation/evaluate.js'; @@ -35,6 +37,41 @@ describe('evaluate() — programmatic API extensions', () => { PROGRAMMATIC_API_TIMEOUT_MS, ); + // --------------------------------------------------------------------------- + // response cache + // --------------------------------------------------------------------------- + + it( + 'writes response cache entries to a custom programmatic cachePath', + async () => { + const cachePath = mkdtempSync(path.join(tmpdir(), 'agentv-programmatic-cache-')); + try { + const { summary } = await evaluate({ + tests: [ + { + id: 'programmatic-cache-path', + input: 'hello', + assert: [{ type: 'contains', value: 'cached' }], + }, + ], + target: { name: 'default', provider: 'mock', response: 'cached response' }, + cache: true, + cachePath, + }); + + expect(summary.passed).toBe(1); + const shardDirs = readdirSync(cachePath); + expect(shardDirs.length).toBeGreaterThan(0); + const firstShard = path.join(cachePath, shardDirs[0]); + expect(existsSync(firstShard)).toBe(true); + expect(readdirSync(firstShard).some((entry) => entry.endsWith('.json'))).toBe(true); + } finally { + rmSync(cachePath, { recursive: true, force: true }); + } + }, + PROGRAMMATIC_API_TIMEOUT_MS, + ); + // --------------------------------------------------------------------------- // turns + mode: 'conversation' // --------------------------------------------------------------------------- diff --git a/packages/core/test/evaluation/loaders/fixtures/default-export.eval.ts b/packages/core/test/evaluation/loaders/fixtures/default-export.eval.ts index df6bdcafd..10073ddb0 100644 --- a/packages/core/test/evaluation/loaders/fixtures/default-export.eval.ts +++ b/packages/core/test/evaluation/loaders/fixtures/default-export.eval.ts @@ -14,6 +14,7 @@ const config: EvalConfig = { ], workers: 2, cache: false, + cachePath: '.agentv/ts-eval-cache', budgetUsd: 1.5, threshold: 0.9, target: { name: 'inline-target', provider: 'mock', response: 'hello there' }, diff --git a/packages/core/test/evaluation/loaders/ts-eval-loader.test.ts b/packages/core/test/evaluation/loaders/ts-eval-loader.test.ts index 8abfc74bd..7a453ab3c 100644 --- a/packages/core/test/evaluation/loaders/ts-eval-loader.test.ts +++ b/packages/core/test/evaluation/loaders/ts-eval-loader.test.ts @@ -48,6 +48,7 @@ describe('loadTsEvalFile', () => { expect(suite.metadata?.tags).toEqual(['sdk', 'typescript']); expect(suite.workers).toBe(2); expect(suite.cacheConfig?.enabled).toBe(false); + expect(suite.cacheConfig?.cachePath).toBe('.agentv/ts-eval-cache'); expect(suite.budgetUsd).toBe(1.5); expect(suite.threshold).toBe(0.9); expect(suite.inlineTarget?.name).toBe('inline-target'); diff --git a/packages/core/test/evaluation/response-cache.test.ts b/packages/core/test/evaluation/response-cache.test.ts index b110bf0b7..6d80b31aa 100644 --- a/packages/core/test/evaluation/response-cache.test.ts +++ b/packages/core/test/evaluation/response-cache.test.ts @@ -93,6 +93,27 @@ describe('shouldEnableCache', () => { expect(shouldEnableCache({ cliCache: false, cliNoCache: false, yamlCache: true })).toBe(true); }); + it('should enable when TypeScript config cache.enabled is true', () => { + expect(shouldEnableCache({ cliCache: false, cliNoCache: false, tsConfigCache: true })).toBe( + true, + ); + }); + + it('should let YAML cache false override TypeScript config cache.enabled', () => { + expect( + shouldEnableCache({ + cliCache: false, + cliNoCache: false, + yamlCache: false, + tsConfigCache: true, + }), + ).toBe(false); + }); + + it('should let --cache override YAML cache false', () => { + expect(shouldEnableCache({ cliCache: true, cliNoCache: false, yamlCache: false })).toBe(true); + }); + it('should disable when --no-cache overrides --cache', () => { expect(shouldEnableCache({ cliCache: true, cliNoCache: true })).toBe(false); }); @@ -104,6 +125,12 @@ describe('shouldEnableCache', () => { it('should disable when YAML cache is false', () => { expect(shouldEnableCache({ cliCache: false, cliNoCache: false, yamlCache: false })).toBe(false); }); + + it('should disable when --no-cache overrides TypeScript config cache.enabled', () => { + expect(shouldEnableCache({ cliCache: false, cliNoCache: true, tsConfigCache: true })).toBe( + false, + ); + }); }); describe('shouldSkipCacheForTemperature', () => {