import { describe, expect, it } from 'vitest' import { __yuanbaoTestUtils } from './yuanbao' const { appendUniqueSourceItems, buildOrderedDomSourceItems, buildSourceItem, extractCitationMarkerIndexes, extractQuestionText, findUnresolvedCitationIndexes, mergeText, normalizeCitationMarkers, normalizeSourceUrl, parseYuanbaoCaptures, resolveCitationsFromMarkers, } = __yuanbaoTestUtils describe('yuanbao adapter helpers', () => { it('unwraps redirect URLs and reads Yuanbao document URL fields', () => { expect( normalizeSourceUrl( 'https://yuanbao.tencent.com/link?target=https%3A%2F%2Fexample.com%2Fa%23ref', ), ).toBe('https://example.com/a') expect( buildSourceItem({ docUrl: 'https://example.com/doc?from=yuanbao#source', title: '引用文章', siteName: 'Example', }), ).toMatchObject({ url: 'https://example.com/doc?from=yuanbao', normalized_url: 'https://example.com/doc?from=yuanbao', title: '引用文章', site_name: 'Example', host: 'example.com', }) }) it('rejects browser and build diagnostics as monitoring questions', () => { expect(extractQuestionText({ question_text: '合肥全屋定制推荐' })).toBe('合肥全屋定制推荐') expect(() => extractQuestionText({ title: '监控任务 · 混元 / 元宝' })).toThrow( 'yuanbao_question_text_missing', ) expect(() => extractQuestionText({ question_text: "The resource http://localhost:8000/static/js/runtime.c632b2fe.js was preloaded using link preload but not used within a few seconds from the window's load event.", }), ).toThrow('yuanbao_question_text_runtime_diagnostic') expect(() => extractQuestionText({ question_text: 'failed to solve: process "/bin/sh -c go build" did not complete successfully: exit code: 1', }), ).toThrow('yuanbao_question_text_runtime_diagnostic') }) it('merges streaming answer fragments without duplicating a repeated prefix', () => { const first = '通过三轮搜索,我已经掌握了合肥全屋定制市场的高性价比品牌。一定要合同中的定标准确和责任。' const repeated = '通过三轮搜索,我已经掌握了合肥全屋定制市场的高性价比品牌。结合你的预算,我建议优先看本土老牌。' expect(mergeText(first, repeated)).toBe(repeated) }) it('resolves citation markers against the best available source pool', () => { const sources = [ { url: 'https://example.com/1', normalized_url: 'https://example.com/1' }, { url: 'https://example.com/2', normalized_url: 'https://example.com/2' }, { url: 'https://example.com/3', normalized_url: 'https://example.com/3' }, ] expect(extractCitationMarkerIndexes('正文 [citation:1] 和 [citation:3]')).toEqual([1, 3]) expect( resolveCitationsFromMarkers('正文 [citation:1] 和 [citation:3]', [[], sources]).map( (item) => item.url, ), ).toEqual(['https://example.com/1', 'https://example.com/3']) }) it('preserves indexed DOM citation slots even when URLs repeat', () => { const domSources = buildOrderedDomSourceItems([ { url: 'https://example.com/repeated', title: '来源 1', text: null, siteName: 'Example', sourceIndex: 1, }, { url: 'https://example.com/repeated', title: '来源 2', text: null, siteName: 'Example', sourceIndex: 2, }, { url: 'https://example.com/third', title: '来源 3', text: null, siteName: 'Example', sourceIndex: 3, }, ]) expect(domSources).toHaveLength(3) expect(domSources.map((item) => item.title)).toEqual(['来源 1', '来源 2', '来源 3']) expect(findUnresolvedCitationIndexes('正文 [citation:2] [citation:3]', domSources)).toEqual([]) }) it('keeps primary DOM citation order when appending parsed extras', () => { const primary = [ { url: 'https://example.com/1', normalized_url: 'https://example.com/1' }, { url: 'https://example.com/2', normalized_url: 'https://example.com/2' }, ] const appended = appendUniqueSourceItems(primary, [ { url: 'https://example.com/2', normalized_url: 'https://example.com/2', title: 'duplicate' }, { url: 'https://example.com/3', normalized_url: 'https://example.com/3' }, ]) expect(appended.map((item) => item.url)).toEqual([ 'https://example.com/1', 'https://example.com/2', 'https://example.com/3', ]) }) it('does not report incomplete citations when the answer has no markers', () => { expect(findUnresolvedCitationIndexes('正文没有引用小图标', [])).toEqual([]) }) it("recognises both '[N]' and '[citation:N]' inline markers", () => { // The new yuanbao model lowers
placeholders to "[1][2][10]"; // older SSE frames may still emit "[citation:N]". Both must extract the same indexes. expect(extractCitationMarkerIndexes('合肥老牌选 [1][2] 性价比 [10]。')).toEqual([1, 2, 10]) expect(extractCitationMarkerIndexes('混合两种格式 [citation:3] 还有 [4]')).toEqual([3, 4]) const sources = [ { url: 'https://example.com/1', normalized_url: 'https://example.com/1' }, { url: 'https://example.com/2', normalized_url: 'https://example.com/2' }, ] expect( resolveCitationsFromMarkers('正文 [1] 然后 [2]', [[], sources]).map((item) => item.url), ).toEqual(['https://example.com/1', 'https://example.com/2']) }) it('rewrites legacy [citation:N] markers to bare [N] for SaaS rendering', () => { expect(normalizeCitationMarkers('正文 [citation:1] 和 [citation:10]')).toBe('正文 [1] 和 [10]') // Already-normalised text passes through. expect(normalizeCitationMarkers('已经是 [1][2] 格式')).toBe('已经是 [1][2] 格式') // Null/empty pass through. expect(normalizeCitationMarkers(null)).toBeNull() expect(normalizeCitationMarkers('')).toBe('') }) it('does not invent citations for fast-cache answers (no searchGuid frame, no [N] markers)', () => { // hunyuan_t1 sometimes returns a cached / non-search answer ("已深度思考(用时1秒)" // with no "找到了 N 篇相关资料" panel). The SSE then carries only `text` // frames and no `searchGuid`. The adapter must still surface that answer to // the SaaS pipeline — see the top-level query path: as long as `answer` is // non-empty and there are no [N] markers, neither the empty-response nor // the incomplete-citations branches fire, so we fall through to status=succeeded. const sseFrames = [ JSON.stringify({ type: 'text', msg: '在合肥,' }), JSON.stringify({ type: 'text', msg: '全屋定制市场非常成熟,' }), JSON.stringify({ type: 'text', msg: '没有绝对"最好"的商家。' }), ] .map((line) => `data: ${line}`) .join('\n\n') const summary = parseYuanbaoCaptures([ { url: 'https://yuanbao.tencent.com/api/chat/cache-hit', status: 200, contentType: 'text/event-stream', body: sseFrames, }, ]) expect(summary.answer).toBe('在合肥,全屋定制市场非常成熟,没有绝对"最好"的商家。') expect(summary.citations).toEqual([]) expect(summary.searchResults).toEqual([]) // No marker → no unresolved indexes, so the adapter does not gate on citations. expect(extractCitationMarkerIndexes(summary.answer)).toEqual([]) expect(findUnresolvedCitationIndexes(summary.answer, summary.citations)).toEqual([]) }) it('aligns citations[index-1] with answer markers when SSE places refs in searchGuid.docs (new hunyuan_t1 protocol)', () => { // Captured against the live yuanbao endpoint: deep-search now ships every // reference inside `searchGuid.docs[]` with a 1-based `index`, and the // legacy `citations` field is always null in this frame. Whatever the // adapter normalises to must preserve idx alignment so that the // unresolved-citations check passes. const sseFrames = [ JSON.stringify({ type: 'searchGuid', title: '引用 3 篇资料作为参考', docs: [ { index: 1, title: '志邦家居官网', url: 'https://www.zbom.com/about', web_url: 'https://www.zbom.com/about', web_site_name: '志邦家居', }, { index: 2, title: '合肥本地装修评测', url: 'https://example.com/hefei-review', web_site_name: '今日头条', }, { index: 3, title: '客来福官方介绍', url: 'https://example.com/kelaifu', web_site_name: '客来福', }, ], citations: null, }), JSON.stringify({ type: 'text', msg: '志邦家居 [1] 与客来福 [3] 都是合肥本土头部 [2]。', }), ] .map((line) => `data: ${line}`) .join('\n\n') const summary = parseYuanbaoCaptures([ { url: 'https://yuanbao.tencent.com/api/chat/abc', status: 200, contentType: 'text/event-stream', body: sseFrames, }, ]) expect(summary.citations.map((item) => item.url)).toEqual([ 'https://www.zbom.com/about', 'https://example.com/hefei-review', 'https://example.com/kelaifu', ]) expect(findUnresolvedCitationIndexes(summary.answer, summary.citations)).toEqual([]) }) it('collects source candidates from deep search thinking fields', () => { const summary = parseYuanbaoCaptures([ { url: 'https://yuanbao.tencent.com/api/chat', status: 200, contentType: 'text/event-stream', body: [ 'data: {"type":"deepSearch","contents":[{"type":"step","webPages":[{"targetUrl":"https://example.com/a#ref","title":"来源 A","siteName":"Example"}],"references":[{"sourceUrl":"https://example.com/b","title":"来源 B"}]}]}', 'data: {"type":"text","msg":"正文 [citation:1] [citation:2]"}', ].join('\n\n'), }, ]) expect(summary.searchResults.map((item) => item.url)).toContain('https://example.com/a') expect(summary.citations.map((item) => item.url)).toContain('https://example.com/b') expect( resolveCitationsFromMarkers(summary.answer, [ summary.citations, summary.searchResults, [...summary.citations, ...summary.searchResults], ]).map((item) => item.url), ).toEqual(['https://example.com/b', 'https://example.com/a']) }) })