Files
WechatExplorer/tests/unit/message-ref.test.ts
T
Wxw-Gu ab3b3731b7 feat: 重构问问微信并完善知识库增量检索
统一问问微信与 Agent Hub 的查询链路
完善查询覆盖度、新鲜度和部分结果表达,避免索引滞后产生错误结论
基于会话实现真正的增量追新与历史补齐
支持后台同步、取消恢复、重启续传以及同步期间继续查询
优化知识库跨会话检索、同步状态、进度展示和侧栏布局
2026-09-11 17:44:48 +08:00

122 lines
4.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
decodeMessageRef,
encodeMessageRef,
normalizeMessageIdentity
} from '../../src/shared/local-query-api'
import {
MESSAGES_AROUND_MAX_WINDOW,
messagesAroundRadii,
normalizeRadiusSeconds,
sliceMessagesAroundWindow,
widenRadiusSeconds
} from '../../src/main/services/messages-around'
/**
* 「跳到原聊天」的稳定身份。
*
* 只靠「会话 + 秒级时间戳」定位不到**这一条**消息:同一秒可能有多条,
* 时间戳也只能定位到"附近"。messageRef 是唯一跨层可用的稳定身份,
* 所以它的编解码必须与 Buffer / Node API 解耦(renderer 在 contextIsolation 下没有 Buffer)。
*/
describe('messageRef codec', () => {
it('round-trips conversation and message identity', () => {
const ref = encodeMessageRef('conversation-md5', '17078')
expect(decodeMessageRef(ref)).toEqual({
conversationId: 'conversation-md5',
messageId: '17078'
})
})
it('strips the synthetic local: prefix so both sides normalize to the same identity', () => {
const ref = encodeMessageRef('conversation-md5', 'local:17078')
expect(decodeMessageRef(ref)).toEqual({
conversationId: 'conversation-md5',
messageId: '17078'
})
// 两条不同的入参(带/不带前缀)必须归一到同一个身份。
expect(encodeMessageRef('conversation-md5', '17078')).toBe(ref)
})
it('produces an opaque, URL-safe token that does not leak raw ids', () => {
const ref = encodeMessageRef('conversation-md5', '17078')
expect(ref).not.toContain('conversation-md5')
expect(ref).not.toContain('17078')
expect(ref).toMatch(/^[A-Za-z0-9_-]+$/)
expect(ref).not.toMatch(/[+/=]/)
})
it('returns null instead of throwing for garbage, tampered or empty references', () => {
expect(decodeMessageRef('')).toBeNull()
expect(decodeMessageRef('not-base64!!')).toBeNull()
expect(decodeMessageRef(encodeMessageRef('conversation-md5', '17078') + 'x')).toBeNull()
expect(decodeMessageRef(undefined)).toBeNull()
expect(decodeMessageRef(42)).toBeNull()
expect(decodeMessageRef({ c: 'a', m: 'b' })).toBeNull()
// 合法 base64 但不是我们的结构。
expect(decodeMessageRef(btoa('{"x":1}').replace(/=+$/, ''))).toBeNull()
})
it('rejects identities missing either half', () => {
expect(normalizeMessageIdentity('', '17078')).toBeNull()
expect(normalizeMessageIdentity('conversation-md5', ' ')).toBeNull()
expect(normalizeMessageIdentity('conversation-md5', '17078')).toEqual({
conversationId: 'conversation-md5',
messageId: '17078'
})
})
})
/**
* 锚点窗口规则:加载必须围绕这条消息,**不能**退化成整段历史。
*/
describe('messages around anchor window', () => {
it('never plans an unbounded read when the anchor time is missing', () => {
const base = normalizeRadiusSeconds(undefined)
// 没有锚点 = 没有半径可试:调用方必须走「已打开会话但无法定位原消息」的降级文案。
expect(messagesAroundRadii(undefined, base, widenRadiusSeconds(base))).toEqual([])
expect(messagesAroundRadii(0, base, widenRadiusSeconds(base))).toEqual([])
expect(messagesAroundRadii(-5, base, widenRadiusSeconds(base))).toEqual([])
})
it('tries the narrow window first, then one bounded widening', () => {
const base = normalizeRadiusSeconds(undefined)
const widened = widenRadiusSeconds(base)
expect(messagesAroundRadii(1_785_900_000, base, widened)).toEqual([base, widened])
// 放宽有上限:一次点击不许变成全库扫描。
expect(widened).toBeLessThanOrEqual(3 * 24 * 3600)
expect(widened).toBeGreaterThanOrEqual(base)
})
it('clamps the requested radius into a sane band', () => {
expect(normalizeRadiusSeconds(1)).toBe(60)
expect(normalizeRadiusSeconds(6 * 3600)).toBe(6 * 3600)
expect(normalizeRadiusSeconds(365 * 24 * 3600)).toBe(24 * 3600)
})
it('caps the returned window so a huge conversation cannot flood the IPC payload', () => {
const messages = Array.from({ length: 5000 }, (_, index) => ({ id: `local:${index}` }))
const { window, index, truncated } = sliceMessagesAroundWindow(
messages,
'conversation-md5',
'4999'
)
expect(truncated).toBe(true)
expect(window).toHaveLength(MESSAGES_AROUND_MAX_WINDOW)
// 目标落在被截断的部分之外 → 如实报告"没找到",而不是伪装成找到。
expect(index).toBe(-1)
})
it('locates the exact message by normalized identity, not by position', () => {
const messages = [{ id: 'local:17076' }, { id: '17077' }, { id: 'local:17078' }]
const { window, index, truncated } = sliceMessagesAroundWindow(
messages,
'conversation-md5',
'17078'
)
expect(truncated).toBe(false)
expect(index).toBe(2)
expect(window[index]).toEqual({ id: 'local:17078' })
})
})