Files
geo/apps/desktop-client/src/main/adapters/deepseek.test.ts
T

381 lines
11 KiB
TypeScript
Raw Normal View History

import { describe, expect, it, vi } from 'vitest'
vi.mock('electron/main', () => ({
ipcMain: {
on: vi.fn(),
},
}))
import { __deepseekTestUtils } from './deepseek'
import { getMonitorAdapter } from './index'
const {
extractQuestionText,
buildSourceItem,
buildDeepSeekPayload,
detectDeepSeekBusySignals,
dedupeSourceItems,
classifyDeepseekSources,
mergeDeepSeekRawSourceLinks,
isObservationComplete,
isDeepSeekToggleSelected,
isDeepSeekWebpagesTriggerText,
} = __deepseekTestUtils
describe('deepseek adapter helpers', () => {
it('extracts question text from monitoring payload', () => {
expect(extractQuestionText({ question_text: 'DeepSeek 怎么看 GEO' })).toBe(
'DeepSeek 怎么看 GEO',
)
})
it('dedupes sources by normalized url without hash fragments', () => {
const items = dedupeSourceItems([
{
url: 'https://example.com/a#top',
title: 'A',
normalized_url: 'https://example.com/a#top',
},
{
url: 'https://example.com/a#bottom',
title: 'A later',
normalized_url: 'https://example.com/a#bottom',
},
])
expect(items).toHaveLength(1)
expect(items[0]?.normalized_url).toBe('https://example.com/a')
expect(items[0]?.title).toBe('A')
})
it('treats revealed search cards as citation sources and inline links as content citation candidates', () => {
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: 'https://example.com/citation',
title: 'Citation',
normalized_url: 'https://example.com/citation',
},
],
sourcePanelLinks: [],
searchPanelLinks: [
{
url: 'https://example.com/search',
title: 'Search',
normalized_url: 'https://example.com/search',
},
],
})
expect(classified.citations).toHaveLength(1)
expect(classified.inlineCitationCandidates).toHaveLength(1)
expect(classified.searchResults).toHaveLength(1)
expect(classified.citations[0]?.url).toBe('https://example.com/search')
expect(classified.inlineCitationCandidates[0]?.url).toBe('https://example.com/citation')
expect(classified.searchResults[0]?.url).toBe('https://example.com/search')
})
it('enriches numeric inline citations with source metadata from revealed search cards', () => {
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: 'https://example.com/citation',
title: null,
text: '-1',
siteName: null,
kind: 'inline',
},
],
sourcePanelLinks: [],
searchPanelLinks: [
{
url: 'https://example.com/citation',
title: 'DeepSeek source article',
text: 'Snippet',
siteName: 'Example',
kind: 'search',
},
],
})
expect(classified.inlineCitationCandidates).toHaveLength(1)
expect(classified.inlineCitationCandidates[0]?.title).toBe('DeepSeek source article')
expect(classified.inlineCitationCandidates[0]?.site_name).toBe('Example')
expect(classified.citations).toHaveLength(1)
expect(classified.searchResults).toHaveLength(1)
})
it('keeps reasoning search pages as citation sources separate from answer content citations', () => {
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: 'https://example.com/body-citation',
title: 'Answer body citation',
text: '[15]',
siteName: 'Body Site',
kind: 'inline',
},
],
sourcePanelLinks: [],
searchPanelLinks: [
{
url: 'https://example.com/reasoning-search',
title: 'Reasoning searched page',
text: '浏览页面',
siteName: 'Search Site',
kind: 'search',
},
],
})
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
'https://example.com/body-citation',
])
expect(classified.citations.map((item) => item.url)).toEqual([
'https://example.com/reasoning-search',
])
expect(classified.searchResults.map((item) => item.url)).toEqual([
'https://example.com/reasoning-search',
])
})
it('retains all revealed DeepSeek search results instead of truncating after 32 items', () => {
const searchPanelLinks = Array.from({ length: 78 }, (_, index) => ({
url: `https://example.com/search-${index + 1}`,
title: `Search ${index + 1}`,
text: `Snippet ${index + 1}`,
siteName: 'Example',
kind: 'search' as const,
}))
const classified = classifyDeepseekSources({
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks,
})
expect(classified.citations).toHaveLength(78)
expect(classified.searchResults).toHaveLength(78)
})
it('merges revealed sidebar search links with the snapshot search links by normalized url', () => {
const merged = mergeDeepSeekRawSourceLinks(
[
{
url: 'https://example.com/report#top',
title: null,
text: 'Visible title',
siteName: null,
kind: 'search',
},
],
[
{
url: 'https://example.com/report#bottom',
title: 'Merged title',
text: 'Merged snippet',
siteName: 'Example',
kind: 'search',
},
{
url: 'https://example.com/extra',
title: 'Extra result',
text: 'Extra snippet',
siteName: 'Example',
kind: 'search',
},
],
)
expect(merged).toHaveLength(2)
expect(merged[0]).toEqual(
expect.objectContaining({
url: 'https://example.com/report',
title: 'Merged title',
text: 'Visible title',
siteName: 'Example',
}),
)
expect(merged[1]?.url).toBe('https://example.com/extra')
})
it('normalizes deepseek raw source links into monitoring source items', () => {
const item = buildSourceItem({
url: 'https://example.com/report#cite-1',
title: null,
text: 'DeepSeek source title',
siteName: 'Example',
kind: 'source',
})
expect(item).toEqual(
expect.objectContaining({
url: 'https://example.com/report',
normalized_url: 'https://example.com/report',
title: 'DeepSeek source title',
site_name: 'Example',
host: 'example.com',
}),
)
})
it('unwraps DeepSeek redirect URLs before storing source items', () => {
const item = buildSourceItem({
url: 'https://chat.deepseek.com/api/redirect?url=https%3A%2F%2Fexample.com%2Fsource%23ref',
title: 'Redirected source',
text: null,
siteName: null,
kind: 'search',
})
expect(item).toEqual(
expect.objectContaining({
url: 'https://example.com/source',
normalized_url: 'https://example.com/source',
host: 'example.com',
}),
)
})
it('does not use numeric citation markers as source titles', () => {
const item = buildSourceItem({
url: 'https://example.com/report#cite-1',
title: null,
text: '-1',
siteName: null,
kind: 'inline',
})
expect(item?.title).toBeNull()
expect(item?.site_name).toBe('example.com')
})
it('treats a stable answer as complete even without citations', () => {
expect(
isObservationComplete({
answer: '这是最终答案',
busy: false,
loginRequired: false,
challengeRequired: false,
signature: 'sig-1',
}),
).toBe(true)
})
it('ignores source-logo loading placeholders when computing busy signals', () => {
expect(
detectDeepSeekBusySignals({
bodyText: '已阅读 10 个网页',
descriptors: [
'site_logo_loading site_logo_fallback',
'site_logo_loading site_logo_fallback',
],
}),
).toEqual([])
})
it('recognizes deepseek toggle selected state from class names', () => {
expect(isDeepSeekToggleSelected('ds-toggle-button ds-toggle-button--selected')).toBe(true)
expect(isDeepSeekToggleSelected('ds-toggle-button')).toBe(false)
})
it('only treats the bottom 网页 pill as the DeepSeek source trigger', () => {
expect(isDeepSeekWebpagesTriggerText('57 个网页')).toBe(true)
expect(isDeepSeekWebpagesTriggerText('已阅读 57 个网页')).toBe(true)
expect(isDeepSeekWebpagesTriggerText('搜索到 39 个网页')).toBe(false)
})
it('normalizes DeepSeek model labels without mistaking search toggles for models', () => {
const snapshot = {
url: 'https://chat.deepseek.com/',
title: 'DeepSeek',
loginRequired: false,
loginReason: null,
challengeRequired: false,
challengeReason: null,
busy: false,
busySignals: [],
composerValue: null,
sendDisabled: false,
answer: 'Final answer',
answerText: 'Final answer',
reasoning: null,
signature: 'sig-1',
currentModelLabel: '深度思考 (R1)',
providerRequestID: null,
requestID: null,
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks: [],
}
const sources = {
citations: [],
inlineCitationCandidates: [],
searchResults: [],
}
const r1Payload = buildDeepSeekPayload(snapshot, sources, 'Final answer')
const searchPayload = buildDeepSeekPayload(
{
...snapshot,
currentModelLabel: '联网搜索',
},
sources,
'Final answer',
)
expect(r1Payload.provider_model).toBe('DeepSeek-R1')
expect(searchPayload.provider_model).toBeNull()
})
it('builds json-safe payloads with null placeholders instead of undefined', () => {
const payload = buildDeepSeekPayload(
{
url: 'https://chat.deepseek.com/',
title: 'DeepSeek',
loginRequired: false,
loginReason: null,
challengeRequired: false,
challengeReason: null,
busy: false,
busySignals: [],
composerValue: null,
sendDisabled: false,
answer: 'Final answer',
answerText: 'Final answer',
reasoning: null,
signature: 'sig-1',
currentModelLabel: null,
providerRequestID: null,
requestID: null,
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks: [],
},
{
citations: [],
inlineCitationCandidates: [],
searchResults: [],
},
'Final answer',
)
expect(payload.provider_model).toBeNull()
expect(payload.provider_request_id).toBeNull()
expect(payload.request_id).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).provider_model).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).provider_request_id).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).request_id).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).signature).toBe('sig-1')
expect(
(payload.raw_response_json as Record<string, unknown>).inline_citation_candidates,
).toEqual([])
})
})
describe('monitor adapter registry', () => {
it('registers deepseek as a monitor adapter', () => {
expect(getMonitorAdapter('deepseek')?.provider).toBe('deepseek')
})
})