mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
feat: 拆分代码
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useState } from 'react'
|
||||
import { useSearchHistory } from '../../src/renderer/src/components/search/hooks/useSearchHistory'
|
||||
import type { AiSearchTimeRange } from '../../src/shared/ai-search'
|
||||
import type {
|
||||
AISearchCacheRecord,
|
||||
EvidenceItem,
|
||||
SearchRange,
|
||||
SearchScope,
|
||||
SearchStage
|
||||
} from '../../src/renderer/src/components/search/searchTypes'
|
||||
import {
|
||||
SEARCH_ACTIVE_RESULT_KEY,
|
||||
SEARCH_CACHE_KEY,
|
||||
SEARCH_HISTORY_KEY,
|
||||
buildSearchCacheKey
|
||||
} from '../../src/renderer/src/components/search/searchUtils'
|
||||
import {
|
||||
aiSearchContact,
|
||||
aiSearchGroup,
|
||||
makeCacheRecord,
|
||||
makePipelineEvidence
|
||||
} from './support/ai-search-fixtures'
|
||||
|
||||
const onNotice = vi.fn()
|
||||
|
||||
const useHistoryHarness = () => {
|
||||
const [query, setQuery] = useState('当前问题')
|
||||
const [scope, setScope] = useState<SearchScope>('global')
|
||||
const [scopeContactMd5, setScopeContactMd5] = useState('')
|
||||
const [range, setRange] = useState<SearchRange>('30d')
|
||||
const [timeRangeOverride, setTimeRangeOverride] = useState<AiSearchTimeRange | undefined>()
|
||||
const [resultQuery, setResultQuery] = useState('')
|
||||
const [answer, setAnswer] = useState('')
|
||||
const [evidence, setEvidence] = useState<EvidenceItem[]>([])
|
||||
const [evidenceCollection, setEvidenceCollection] = useState<EvidenceItem[]>([])
|
||||
const [visibleEvidenceCount, setVisibleEvidenceCount] = useState(0)
|
||||
const [senderNames, setSenderNames] = useState<Record<string, string>>({})
|
||||
const [messageCount, setMessageCount] = useState(0)
|
||||
const [cachedAt, setCachedAt] = useState(0)
|
||||
const [analysisError, setAnalysisError] = useState('')
|
||||
const [stage, setStage] = useState<SearchStage>('idle')
|
||||
const [selectedEvidence, setSelectedEvidence] = useState(0)
|
||||
const [historyOpen, setHistoryOpen] = useState(true)
|
||||
const history = useSearchHistory({
|
||||
query,
|
||||
scope,
|
||||
range,
|
||||
conversationContactMd5: scopeContactMd5 || aiSearchContact.md5,
|
||||
setQuery,
|
||||
setScope,
|
||||
setScopeContactMd5,
|
||||
setRange,
|
||||
setTimeRangeOverride,
|
||||
setResultQuery,
|
||||
setAnswer,
|
||||
setEvidence,
|
||||
setEvidenceCollection,
|
||||
setVisibleEvidenceCount,
|
||||
setSenderNames,
|
||||
setMessageCount,
|
||||
setCachedAt,
|
||||
setAnalysisError,
|
||||
setStage,
|
||||
setSelectedEvidence,
|
||||
setHistoryOpen,
|
||||
onNotice
|
||||
})
|
||||
|
||||
return {
|
||||
...history,
|
||||
state: {
|
||||
query,
|
||||
scope,
|
||||
scopeContactMd5,
|
||||
range,
|
||||
timeRangeOverride,
|
||||
resultQuery,
|
||||
answer,
|
||||
evidence,
|
||||
evidenceCollection,
|
||||
visibleEvidenceCount,
|
||||
senderNames,
|
||||
messageCount,
|
||||
cachedAt,
|
||||
analysisError,
|
||||
stage,
|
||||
selectedEvidence,
|
||||
historyOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const readCacheRecords = (): AISearchCacheRecord[] =>
|
||||
JSON.parse(localStorage.getItem(SEARCH_CACHE_KEY) || '[]') as AISearchCacheRecord[]
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('useSearchHistory', () => {
|
||||
it('loads and filters the persisted history list', () => {
|
||||
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(['问题 A', 42, null, '问题 B']))
|
||||
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
|
||||
expect(result.current.history).toEqual(['问题 A', '问题 B'])
|
||||
})
|
||||
|
||||
it('restores a history cache with its original scope, range and time override', () => {
|
||||
const query = '恢复历史问题'
|
||||
const cached = makeCacheRecord({
|
||||
query,
|
||||
scope: 'conversation',
|
||||
contactMd5: aiSearchGroup.md5,
|
||||
range: '7d',
|
||||
answer: '历史缓存答案',
|
||||
evidence: [makePipelineEvidence(1, aiSearchGroup)]
|
||||
}) as AISearchCacheRecord
|
||||
localStorage.setItem(SEARCH_CACHE_KEY, JSON.stringify([cached]))
|
||||
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
act(() => result.current.restoreHistoryQuery(query))
|
||||
|
||||
expect(result.current.state.query).toBe(query)
|
||||
expect(result.current.state.scope).toBe('conversation')
|
||||
expect(result.current.state.scopeContactMd5).toBe(aiSearchGroup.md5)
|
||||
expect(result.current.state.range).toBe('7d')
|
||||
expect(result.current.state.timeRangeOverride).toMatchObject({
|
||||
label: '近 7 天',
|
||||
reason: '恢复历史搜索的时间范围',
|
||||
source: 'user_selected'
|
||||
})
|
||||
expect(result.current.state.answer).toBe('历史缓存答案')
|
||||
expect(result.current.state.stage).toBe('result')
|
||||
expect(result.current.state.historyOpen).toBe(false)
|
||||
expect(onNotice).toHaveBeenCalledWith('已恢复这条历史问题的最近结果')
|
||||
})
|
||||
|
||||
it('deletes a history item and all cache records for its normalized query', () => {
|
||||
const deleted = makeCacheRecord({ query: '待删除问题', answer: '应删除' })
|
||||
const retained = makeCacheRecord({ query: '保留问题', answer: '应保留' })
|
||||
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(['待删除问题', '保留问题']))
|
||||
localStorage.setItem(SEARCH_CACHE_KEY, JSON.stringify([deleted, retained]))
|
||||
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
act(() => result.current.removeHistoryQuery(' 待删除问题 '))
|
||||
|
||||
expect(result.current.history).toEqual(['待删除问题', '保留问题'])
|
||||
expect(readCacheRecords()).toEqual([retained])
|
||||
|
||||
act(() => result.current.removeHistoryQuery('待删除问题'))
|
||||
expect(result.current.history).toEqual(['保留问题'])
|
||||
expect(readCacheRecords()).toEqual([retained])
|
||||
})
|
||||
|
||||
it('reads and applies a current cache hit while marking the active session result', () => {
|
||||
const cached = makeCacheRecord({
|
||||
query: '缓存命中问题',
|
||||
answer: '缓存命中答案',
|
||||
evidence: [makePipelineEvidence(1)]
|
||||
}) as AISearchCacheRecord
|
||||
localStorage.setItem(SEARCH_CACHE_KEY, JSON.stringify([cached]))
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
|
||||
const found = result.current.readCachedResult(cached.key)
|
||||
expect(found).toEqual(cached)
|
||||
act(() => result.current.applyCachedResult(cached, '缓存命中问题'))
|
||||
|
||||
expect(result.current.state.answer).toBe('缓存命中答案')
|
||||
expect(result.current.state.resultQuery).toBe('缓存命中问题')
|
||||
expect(result.current.state.evidence).toHaveLength(1)
|
||||
expect(sessionStorage.getItem(SEARCH_ACTIVE_RESULT_KEY)).toBe(cached.key)
|
||||
})
|
||||
|
||||
it('preserves an intentionally incomplete cache collection instead of rebuilding it', () => {
|
||||
const evidence = makeCacheRecord({
|
||||
query: '不完整缓存',
|
||||
evidence: [makePipelineEvidence(1)]
|
||||
}).evidence as EvidenceItem[]
|
||||
const cached: AISearchCacheRecord = {
|
||||
version: 3,
|
||||
key: buildSearchCacheKey('global', '', '30d', '不完整缓存'),
|
||||
createdAt: 10,
|
||||
answer: '不完整',
|
||||
evidence,
|
||||
evidenceCollection: [],
|
||||
senderNames: {},
|
||||
messageCount: 1
|
||||
}
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
|
||||
act(() => result.current.applyCachedResult(cached, '不完整缓存'))
|
||||
|
||||
expect(result.current.state.evidence).toHaveLength(1)
|
||||
expect(result.current.state.evidenceCollection).toEqual([])
|
||||
expect(result.current.state.visibleEvidenceCount).toBe(0)
|
||||
})
|
||||
|
||||
it('falls back to legacy evidence when evidenceCollection is absent', () => {
|
||||
const cached = makeCacheRecord({
|
||||
query: '旧缓存问题',
|
||||
evidence: [makePipelineEvidence(1)]
|
||||
}) as AISearchCacheRecord
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
|
||||
act(() => result.current.applyCachedResult(cached, '旧缓存问题'))
|
||||
|
||||
expect(result.current.state.evidenceCollection).toBe(result.current.state.evidence)
|
||||
expect(result.current.state.visibleEvidenceCount).toBe(1)
|
||||
})
|
||||
|
||||
it('does not reuse an old cache after the caller marks a new search as bypassing cache', () => {
|
||||
const cached = makeCacheRecord({ query: '旧结果', answer: '旧答案' }) as AISearchCacheRecord
|
||||
localStorage.setItem(SEARCH_CACHE_KEY, JSON.stringify([cached]))
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
|
||||
act(() => result.current.skipNextCache())
|
||||
const shouldBypass = result.current.consumeCacheBypass()
|
||||
|
||||
expect(shouldBypass).toBe(true)
|
||||
expect(result.current.state.answer).toBe('')
|
||||
expect(result.current.consumeCacheBypass()).toBe(false)
|
||||
expect(result.current.readCachedResult(cached.key)).toEqual(cached)
|
||||
})
|
||||
|
||||
it('persists a new compact cache record and its active session key', () => {
|
||||
const { result } = renderHook(() => useHistoryHarness())
|
||||
const evidence = makeCacheRecord({
|
||||
query: '新搜索问题',
|
||||
evidence: [makePipelineEvidence(1)]
|
||||
}).evidence as EvidenceItem[]
|
||||
|
||||
act(() =>
|
||||
result.current.persistSearchResult({
|
||||
key: buildSearchCacheKey('global', '', '30d', '新搜索问题'),
|
||||
answer: '新答案',
|
||||
evidence,
|
||||
evidenceCollection: evidence,
|
||||
senderNames: { 'sender-1': '发送者 1' },
|
||||
messageCount: 20
|
||||
})
|
||||
)
|
||||
|
||||
const records = readCacheRecords()
|
||||
expect(records).toHaveLength(1)
|
||||
expect(records[0].version).toBe(3)
|
||||
expect(records[0].answer).toBe('新答案')
|
||||
expect(sessionStorage.getItem(SEARCH_ACTIVE_RESULT_KEY)).toBe(records[0].key)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,421 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AiSearchFinalEvidence, AiSearchPipelineResult } from '../../src/shared/ai-search'
|
||||
import type { KnowledgeRuntimeStatus } from '../../src/shared/knowledge'
|
||||
import type { Contact } from '../../src/shared/types'
|
||||
import {
|
||||
contactLabel,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatEvidenceTimestamp,
|
||||
formatMeasuredDuration,
|
||||
formatSearchTraceOverview,
|
||||
knowledgeStateLabel
|
||||
} from '../../src/renderer/src/components/search/searchFormatters'
|
||||
import {
|
||||
createSearchCacheRecord,
|
||||
mapCacheRecordToResult,
|
||||
mapEvidenceSenderNames,
|
||||
mapPipelineEvidence,
|
||||
mapPipelineEvidenceItem,
|
||||
mapSearchResultToTrace
|
||||
} from '../../src/renderer/src/components/search/searchMappers'
|
||||
import { createSearchResultResetState } from '../../src/renderer/src/components/search/searchState'
|
||||
import type {
|
||||
AISearchCacheRecord,
|
||||
EvidenceItem,
|
||||
SearchTrace
|
||||
} from '../../src/renderer/src/components/search/searchTypes'
|
||||
import {
|
||||
aiSearchContact,
|
||||
aiSearchGroup,
|
||||
makeSearchResult
|
||||
} from '../component/support/ai-search-fixtures'
|
||||
|
||||
const makeFinalEvidence = (
|
||||
index: number,
|
||||
overrides: Partial<AiSearchFinalEvidence> = {}
|
||||
): AiSearchFinalEvidence => ({
|
||||
id: `E${index}`,
|
||||
chunkId: `chunk-${index}`,
|
||||
conversationId: aiSearchContact.md5,
|
||||
conversationName: aiSearchContact.m_nsNickName,
|
||||
conversationType: aiSearchContact.type,
|
||||
startTime: 1_700_000_000_000 + index * 1_000,
|
||||
endTime: 1_700_000_000_000 + index * 1_000,
|
||||
messageId: `message-${index}`,
|
||||
senderId: `sender-${index}`,
|
||||
sender: `发送者 ${index}`,
|
||||
timestamp: 1_700_000_000_000 + index * 1_000,
|
||||
messageIds: [`message-${index}`],
|
||||
sourceKind: 'text',
|
||||
text: `证据 ${index}`,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const makeKnowledgeStatus = (state: KnowledgeRuntimeStatus['state']): KnowledgeRuntimeStatus => ({
|
||||
accountId: 'account-1',
|
||||
state,
|
||||
indexedMessageCount: 0,
|
||||
indexedChunkCount: 0,
|
||||
sourceMessageCount: null,
|
||||
processedMessages: 0,
|
||||
totalMessages: null,
|
||||
estimatedRemainingMs: null,
|
||||
databaseBytes: 0,
|
||||
walBytes: 0,
|
||||
shmBytes: 0
|
||||
})
|
||||
|
||||
const makeTrace = (overrides: Partial<SearchTrace> = {}): SearchTrace => ({
|
||||
knowledgeMessages: 20,
|
||||
retrievedEvidence: 10,
|
||||
finalEvidence: 8,
|
||||
timings: {
|
||||
totalMs: 1_250,
|
||||
knowledgeSearchMs: 250,
|
||||
aiGenerationMs: 1_000
|
||||
} as SearchTrace['timings'],
|
||||
contextEvidence: 6,
|
||||
inputTokensEstimated: false,
|
||||
aggregation: {
|
||||
messageCount: 8,
|
||||
peopleCount: 1,
|
||||
conversationCount: 1,
|
||||
people: [],
|
||||
conversations: []
|
||||
},
|
||||
invalidCitationIds: [],
|
||||
agent: { mode: 'agent', toolCalls: 0, trace: [] },
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('AI Search workspace pure formatters', () => {
|
||||
it('formats byte counts exactly as the workspace did', () => {
|
||||
expect(formatBytes(0)).toBe('0 B')
|
||||
expect(formatBytes(512)).toBe('512 B')
|
||||
expect(formatBytes(1_536)).toBe('1.5 KB')
|
||||
expect(formatBytes(2 * 1024 ** 3)).toBe('2.0 GB')
|
||||
})
|
||||
|
||||
it('formats measured and unmeasured durations exactly as the workspace did', () => {
|
||||
expect(formatDuration(999)).toBe('999ms')
|
||||
expect(formatDuration(1_250)).toBe('1.3s')
|
||||
expect(formatMeasuredDuration(undefined)).toBe('未测量')
|
||||
expect(formatMeasuredDuration(1_000)).toBe('1.0s')
|
||||
})
|
||||
|
||||
it('formats evidence timestamps with the existing zh-CN locale options', () => {
|
||||
const timestamp = 1_700_000_001_000
|
||||
expect(formatEvidenceTimestamp(timestamp)).toBe(
|
||||
new Date(timestamp).toLocaleString('zh-CN', { hour12: false })
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps every knowledge runtime state label unchanged', () => {
|
||||
expect(knowledgeStateLabel(null)).toBe('读取中')
|
||||
expect(knowledgeStateLabel(makeKnowledgeStatus('unavailable'))).toBe('未建立')
|
||||
expect(knowledgeStateLabel(makeKnowledgeStatus('building'))).toBe('建立中')
|
||||
expect(knowledgeStateLabel(makeKnowledgeStatus('syncing'))).toBe('增量同步')
|
||||
expect(knowledgeStateLabel(makeKnowledgeStatus('ready'))).toBe('已同步')
|
||||
expect(knowledgeStateLabel(makeKnowledgeStatus('error'))).toBe('异常')
|
||||
})
|
||||
|
||||
it('keeps the existing contact label fallback order', () => {
|
||||
expect(contactLabel(aiSearchContact)).toBe('测试会话')
|
||||
expect(contactLabel({ ...aiSearchContact, m_nsNickName: '' })).toBe('测试联系人')
|
||||
expect(
|
||||
contactLabel({ ...aiSearchContact, m_nsNickName: '', remark: '', wechatNickname: '' })
|
||||
).toBe('wxid_fixture')
|
||||
expect(contactLabel(null)).toBe('未选择会话')
|
||||
})
|
||||
|
||||
it('formats the compact search trace overview without changing labels or units', () => {
|
||||
expect(formatSearchTraceOverview(makeTrace())).toEqual({
|
||||
totalDuration: '1.3s',
|
||||
knowledgeDuration: '250ms',
|
||||
aiDuration: '1.0s',
|
||||
contextEvidence: '6 条'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AI Search pipeline evidence mapping', () => {
|
||||
it('reuses the existing renderer contact object when the conversation is loaded', () => {
|
||||
const item = makeFinalEvidence(1)
|
||||
const mapped = mapPipelineEvidenceItem(item, new Map([[aiSearchContact.md5, aiSearchContact]]))
|
||||
|
||||
expect(mapped.contact).toBe(aiSearchContact)
|
||||
expect(mapped).toEqual({
|
||||
evidenceId: 'E1',
|
||||
sourceKind: 'text',
|
||||
contact: aiSearchContact,
|
||||
message: {
|
||||
id: 'message-1',
|
||||
from: 'sender-1',
|
||||
type: '检索消息',
|
||||
datetime: formatEvidenceTimestamp(item.timestamp),
|
||||
content: '证据 1',
|
||||
isSender: false,
|
||||
name: '发送者 1',
|
||||
senderId: 'sender-1',
|
||||
createTime: Math.floor(item.timestamp / 1_000)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('creates the existing group fallback and voice presentation for an unloaded conversation', () => {
|
||||
const item = makeFinalEvidence(2, {
|
||||
conversationId: 'missing@chatroom',
|
||||
conversationName: '未加载群',
|
||||
conversationType: 'group',
|
||||
sourceKind: 'voice',
|
||||
sender: '我'
|
||||
})
|
||||
const mapped = mapPipelineEvidenceItem(item, new Map())
|
||||
|
||||
expect(mapped.contact).toEqual({
|
||||
md5: 'missing@chatroom',
|
||||
m_nsUsrName: 'missing@chatroom',
|
||||
m_nsNickName: '未加载群',
|
||||
type: 'group'
|
||||
})
|
||||
expect(mapped.message.type).toBe('语音转写')
|
||||
expect(mapped.message.isSender).toBe(true)
|
||||
})
|
||||
|
||||
it('creates the existing user fallback and sender default when senderId is absent', () => {
|
||||
const item = makeFinalEvidence(3, {
|
||||
conversationId: 'missing-user',
|
||||
conversationName: '未加载联系人',
|
||||
conversationType: 'user',
|
||||
senderId: undefined
|
||||
})
|
||||
const mapped = mapPipelineEvidenceItem(item, new Map())
|
||||
|
||||
expect(mapped.contact.type).toBe('user')
|
||||
expect(mapped.message.from).toBe('user')
|
||||
expect(mapped.message.senderId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('maps a collection in order and builds sender names with the existing overwrite behavior', () => {
|
||||
const items = [
|
||||
makeFinalEvidence(1, { senderId: 'same-sender', sender: '旧名称' }),
|
||||
makeFinalEvidence(2, { senderId: 'same-sender', sender: '新名称' }),
|
||||
makeFinalEvidence(3, { senderId: undefined, sender: '无 ID' })
|
||||
]
|
||||
const mapped = mapPipelineEvidence(items, [aiSearchContact])
|
||||
|
||||
expect(mapped.map((item) => item.evidenceId)).toEqual(['E1', 'E2', 'E3'])
|
||||
expect(mapEvidenceSenderNames(mapped)).toEqual({ 'same-sender': '新名称' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('AI Search trace and cache mapping', () => {
|
||||
it('maps pipeline trace fields and preserves the original default values', () => {
|
||||
const result: AiSearchPipelineResult = makeSearchResult()
|
||||
|
||||
expect(mapSearchResultToTrace(result, 4)).toEqual({
|
||||
knowledgeMessages: 20,
|
||||
retrievedEvidence: 0,
|
||||
finalEvidence: 4,
|
||||
timings: result.timings,
|
||||
contextEvidence: 0,
|
||||
inputTokens: undefined,
|
||||
inputTokensEstimated: false,
|
||||
aggregation: result.aggregation,
|
||||
invalidCitationIds: [],
|
||||
agent: result.agent,
|
||||
voiceCoverage: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('maps AI token, citation, and voice coverage details without transforming them', () => {
|
||||
const result: AiSearchPipelineResult = makeSearchResult()
|
||||
result.ai = {
|
||||
providerName: 'provider',
|
||||
modelName: 'model',
|
||||
inputTokens: 321,
|
||||
inputTokensEstimated: true
|
||||
}
|
||||
result.citationValidation = { status: 'sanitized', invalidCitationIds: ['E9'] }
|
||||
result.knowledge.voiceCoverage = {
|
||||
voiceMessageCount: 3,
|
||||
transcribedVoiceCount: 2,
|
||||
failedVoiceCount: 1,
|
||||
voiceCoverageComplete: true
|
||||
}
|
||||
|
||||
const trace = mapSearchResultToTrace(result, 2)
|
||||
expect(trace.inputTokens).toBe(321)
|
||||
expect(trace.inputTokensEstimated).toBe(true)
|
||||
expect(trace.invalidCitationIds).toEqual(['E9'])
|
||||
expect(trace.voiceCoverage).toBe(result.knowledge.voiceCoverage)
|
||||
})
|
||||
|
||||
it('maps a current cache record and limits only the visible collection to one page', () => {
|
||||
const evidence = mapPipelineEvidence([makeFinalEvidence(1)], [aiSearchContact])
|
||||
const evidenceCollection = mapPipelineEvidence(
|
||||
Array.from({ length: 10 }, (_, index) => makeFinalEvidence(index + 1)),
|
||||
[aiSearchContact]
|
||||
)
|
||||
const cached: AISearchCacheRecord = {
|
||||
version: 3,
|
||||
key: 'cache-key',
|
||||
createdAt: 123,
|
||||
answer: '缓存答案',
|
||||
evidence,
|
||||
evidenceCollection,
|
||||
senderNames: { 'sender-1': '发送者 1' },
|
||||
messageCount: 99
|
||||
}
|
||||
|
||||
const mapped = mapCacheRecordToResult(cached, '恢复问题', 8)
|
||||
expect(mapped).toEqual({
|
||||
resultQuery: '恢复问题',
|
||||
answer: '缓存答案',
|
||||
evidence,
|
||||
evidenceCollection,
|
||||
visibleEvidenceCount: 8,
|
||||
senderNames: { 'sender-1': '发送者 1' },
|
||||
messageCount: 99,
|
||||
cachedAt: 123
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to legacy cache evidence when evidenceCollection is absent', () => {
|
||||
const evidence = mapPipelineEvidence([makeFinalEvidence(1)], [aiSearchContact])
|
||||
const cached: AISearchCacheRecord = {
|
||||
version: 3,
|
||||
key: 'legacy-cache-key',
|
||||
createdAt: 456,
|
||||
answer: '旧缓存',
|
||||
evidence,
|
||||
senderNames: {},
|
||||
messageCount: 1
|
||||
}
|
||||
|
||||
const mapped = mapCacheRecordToResult(cached, '旧问题', 8)
|
||||
expect(mapped.evidenceCollection).toBe(evidence)
|
||||
expect(mapped.visibleEvidenceCount).toBe(1)
|
||||
})
|
||||
|
||||
it('creates the same compact version 3 cache record as the workspace did', () => {
|
||||
const fullContact: Contact = { ...aiSearchGroup, avatar: 'avatar', remark: '不应写入缓存' }
|
||||
const evidence: EvidenceItem[] = [
|
||||
{
|
||||
evidenceId: 'E1',
|
||||
sourceKind: 'voice',
|
||||
contact: fullContact,
|
||||
message: {
|
||||
id: 'message-1',
|
||||
from: 'sender-1',
|
||||
type: '语音转写',
|
||||
datetime: '2023/11/14 22:13:21',
|
||||
content: '证据 1',
|
||||
isSender: false,
|
||||
name: '发送者 1',
|
||||
senderId: 'sender-1',
|
||||
createTime: 1_700_000_001,
|
||||
sessionId: '不应写入缓存'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
expect(
|
||||
createSearchCacheRecord({
|
||||
key: 'cache-key',
|
||||
createdAt: 789,
|
||||
answer: '答案',
|
||||
evidence,
|
||||
evidenceCollection: evidence,
|
||||
senderNames: { 'sender-1': '发送者 1' },
|
||||
messageCount: 20
|
||||
})
|
||||
).toEqual({
|
||||
version: 3,
|
||||
key: 'cache-key',
|
||||
createdAt: 789,
|
||||
answer: '答案',
|
||||
evidence: [
|
||||
{
|
||||
evidenceId: 'E1',
|
||||
contact: {
|
||||
md5: fullContact.md5,
|
||||
m_nsUsrName: fullContact.m_nsUsrName,
|
||||
m_nsNickName: fullContact.m_nsNickName,
|
||||
type: 'group',
|
||||
avatar: 'avatar'
|
||||
},
|
||||
message: {
|
||||
id: 'message-1',
|
||||
from: 'sender-1',
|
||||
type: '语音转写',
|
||||
datetime: '2023/11/14 22:13:21',
|
||||
content: '证据 1',
|
||||
isSender: false,
|
||||
name: '发送者 1',
|
||||
senderId: 'sender-1',
|
||||
localId: undefined,
|
||||
serverId: undefined,
|
||||
createTime: 1_700_000_001
|
||||
}
|
||||
}
|
||||
],
|
||||
evidenceCollection: [
|
||||
{
|
||||
evidenceId: 'E1',
|
||||
contact: {
|
||||
md5: fullContact.md5,
|
||||
m_nsUsrName: fullContact.m_nsUsrName,
|
||||
m_nsNickName: fullContact.m_nsNickName,
|
||||
type: 'group',
|
||||
avatar: 'avatar'
|
||||
},
|
||||
message: {
|
||||
id: 'message-1',
|
||||
from: 'sender-1',
|
||||
type: '语音转写',
|
||||
datetime: '2023/11/14 22:13:21',
|
||||
content: '证据 1',
|
||||
isSender: false,
|
||||
name: '发送者 1',
|
||||
senderId: 'sender-1',
|
||||
localId: undefined,
|
||||
serverId: undefined,
|
||||
createTime: 1_700_000_001
|
||||
}
|
||||
}
|
||||
],
|
||||
senderNames: { 'sender-1': '发送者 1' },
|
||||
messageCount: 20
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AI Search result reset state', () => {
|
||||
it('returns every existing search result reset value', () => {
|
||||
expect(createSearchResultResetState()).toEqual({
|
||||
analysisError: '',
|
||||
answer: '',
|
||||
evidence: [],
|
||||
evidenceCollection: [],
|
||||
visibleEvidenceCount: 0,
|
||||
selectedEvidence: 0,
|
||||
cachedAt: 0,
|
||||
searchTrace: null,
|
||||
searchProgress: {},
|
||||
agentTrace: [],
|
||||
searchDetailsOpen: false
|
||||
})
|
||||
})
|
||||
|
||||
it('returns independent arrays and progress objects for consecutive resets', () => {
|
||||
const first = createSearchResultResetState()
|
||||
const second = createSearchResultResetState()
|
||||
|
||||
expect(first.evidence).not.toBe(second.evidence)
|
||||
expect(first.evidenceCollection).not.toBe(second.evidenceCollection)
|
||||
expect(first.searchProgress).not.toBe(second.searchProgress)
|
||||
expect(first.agentTrace).not.toBe(second.agentTrace)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user