/** * Anthropic system cache_control 断言测试(v0.7.3 P1-1) * * Anthropic 缓存按"内容块前缀"命中:system 必须以块数组传递并在块上打 * cache_control 才可缓存。本文件锁定: * C1 非空 system → 块数组 + {type:'ephemeral'}; * C2 空 system → 保持空字符串(不发空块); * C3 thinking 模式下断言仍然存在(cache 与 thinking 不互斥); * C4 system 块文本为四分区完整拼接(roleDefinition/outputConstraints/ * safetyGuidelines/dynamicReminders)。 */ import { describe, it, expect, vi, afterEach } from 'vitest'; vi.mock('electron-log', () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); import { AnthropicAdapter } from '../anthropic.adapter'; import type { MetonaRequest } from '../../types'; function captureFetch(): { bodies: Array> } { const bodies: Array> = []; const genericBody = { content: [{ type: 'text', text: 'ok' }], usage: { input_tokens: 3, output_tokens: 2 }, stop_reason: 'end_turn', }; const fetchMock = vi.fn(async (_url: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body ?? '{}')) as Record); return new Response(JSON.stringify(genericBody), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }); vi.stubGlobal('fetch', fetchMock); return { bodies }; } afterEach(() => { vi.unstubAllGlobals(); }); function makeAdapter(): AnthropicAdapter { return new AnthropicAdapter({ provider: 'anthropic', baseURL: 'http://a.test', apiKey: 'k', defaultModel: 'claude-sonnet-4-5', }); } function makeRequest(overrides?: Partial): MetonaRequest { return { meta: { sessionId: 's1', iteration: 1, requestId: 'r1', timestamp: Date.now(), agentVersion: 'test', }, systemPrompt: { roleDefinition: 'You are Metona.', outputConstraints: 'Be concise.', safetyGuidelines: 'Stay safe.', dynamicReminders: '## Current Workspace\n`/ws`', }, messages: [{ role: 'user', content: 'hi', timestamp: Date.now() }], params: { maxTokens: 63_488, temperature: 0, stream: false }, ...overrides, }; } describe('AnthropicAdapter — system cache_control(P1-1)', () => { it('C1: 非空 system → 单 text 块 + cache_control ephemeral', async () => { const adapter = makeAdapter(); const { bodies } = captureFetch(); await adapter.send(makeRequest()); const system = bodies[0].system as Array<{ type: string; text: string; cache_control: { type: string }; }>; expect(Array.isArray(system)).toBe(true); expect(system).toHaveLength(1); expect(system[0].type).toBe('text'); expect(system[0].cache_control).toEqual({ type: 'ephemeral' }); }); it('C2: 空 system → 保持空字符串(不发空块)', async () => { const adapter = makeAdapter(); const { bodies } = captureFetch(); await adapter.send( makeRequest({ systemPrompt: { roleDefinition: '', outputConstraints: '', safetyGuidelines: '' }, }), ); expect(bodies[0].system).toBe(''); }); it('C3: thinking 模式下 cache_control 断言仍然存在', async () => { const adapter = makeAdapter(); const { bodies } = captureFetch(); await adapter.send( makeRequest({ params: { maxTokens: 8192, temperature: 0, stream: false, thinkingEnabled: true, thinkingEffort: 'high', }, }), ); const system = bodies[0].system as Array<{ cache_control: { type: string } }>; expect(system[0].cache_control).toEqual({ type: 'ephemeral' }); // thinking 与 cache 共存:thinking 块也在请求体中 expect(bodies[0].thinking).toMatchObject({ type: 'enabled' }); }); it('C4: system 块文本为四分区完整拼接', async () => { const adapter = makeAdapter(); const { bodies } = captureFetch(); await adapter.send(makeRequest()); const system = bodies[0].system as Array<{ text: string }>; expect(system[0].text).toContain('You are Metona.'); expect(system[0].text).toContain('Be concise.'); expect(system[0].text).toContain('Stay safe.'); expect(system[0].text).toContain('## Current Workspace'); }); });