import { firstValueFrom, of } from 'rxjs'; import { filter, timeout } from 'rxjs/operators'; import { AiAnalysisService, AiStreamState } from './ai-analysis.service'; import { CloudApiService } from '../../../app/core/services/cloud-api.service'; describe('AiAnalysisService retry lifecycle', () => { it('keeps the outer stream alive while a transient failure is retried', async () => { const promptConfig = { ensureLoaded: () => of([]), getTextSnapshot: (_key: string, fallback: string) => fallback, getTextForDefaultTemplate: (template: string) => template, getPromptKeyForTemplateText: () => undefined, getDefaultAiModel: () => 'deepseek-v4-flash', getOutputStyleInstruction: () => '', }; const cloud = jasmine.createSpyObj('CloudApiService', ['runPromise']); cloud.runPromise.and.returnValues(Promise.reject(new Error('temporary upstream failure')), Promise.resolve({ choices: [{ message: { content: '重试成功' }, finish_reason: 'stop' }] })); const service = new AiAnalysisService(promptConfig as any, cloud); const finalState = await firstValueFrom(service.streamAnalysis('分析真实评论').pipe( filter((state: AiStreamState) => state.complete && !state.error), timeout(2_000), )); expect(cloud.runPromise).toHaveBeenCalledTimes(2); expect(finalState.text).toBe('重试成功'); }); }); describe('AiAnalysisService extractAiText reasoning isolation', () => { let service: AiAnalysisService; beforeEach(() => { const promptConfig = { ensureLoaded: () => of([]), getTextSnapshot: (_key: string, fallback: string) => fallback, getTextForDefaultTemplate: (template: string) => template, getPromptKeyForTemplateText: () => undefined, getDefaultAiModel: () => 'deepseek-v4-flash', getOutputStyleInstruction: () => '', }; service = new AiAnalysisService(promptConfig as any, jasmine.createSpyObj('CloudApiService', ['runPromise'])); }); it('only extracts content when both content and reasoning_content are present', () => { const json = { choices: [{ delta: { content: '正式内容', reasoning_content: '{"type":"metric-grid","metrics":[残缺草稿', }, }], }; expect((service as any).extractAiText(json)).toBe('正式内容'); }); it('returns an empty string when only reasoning_content exists (no content fallback)', () => { const json = { choices: [{ delta: { reasoning_content: '残缺JSON草稿,无正式内容' } }], }; expect((service as any).extractAiText(json)).toBe(''); }); });