mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-03 18:33:14 +08:00
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { AgentHubConversationStore } from '../../src/main/services/agent-hub-conversation-store'
|
|
|
|
describe('AgentHubConversationStore', () => {
|
|
const roots: string[] = []
|
|
|
|
afterEach(() => {
|
|
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
})
|
|
|
|
function createStore(options: { maxMessages?: number; maxConversations?: number } = {}): {
|
|
store: AgentHubConversationStore
|
|
filePath: string
|
|
} {
|
|
const root = mkdtempSync(join(tmpdir(), 'tracememo-conv-'))
|
|
roots.push(root)
|
|
const filePath = join(root, 'agent-hub', 'conversations.json')
|
|
let sequence = 0
|
|
const store = new AgentHubConversationStore({
|
|
filePath: () => filePath,
|
|
createId: () => `m-${(sequence += 1)}`,
|
|
now: () => 1_700_000_000_000,
|
|
...(options.maxMessages !== undefined
|
|
? { maxMessagesPerConversation: options.maxMessages }
|
|
: {}),
|
|
...(options.maxConversations !== undefined
|
|
? { maxConversations: options.maxConversations }
|
|
: {})
|
|
})
|
|
return { store, filePath }
|
|
}
|
|
|
|
it('记录一次收发并给出会话摘要', () => {
|
|
const { store, filePath } = createStore()
|
|
|
|
store.append({ userId: 'user-a', direction: 'in', kind: 'text', text: '你好' })
|
|
const result = store.append({
|
|
userId: 'user-a',
|
|
direction: 'out',
|
|
kind: 'text',
|
|
text: '这是回答',
|
|
status: 'sent'
|
|
})
|
|
|
|
expect(result?.summary).toMatchObject({
|
|
userId: 'user-a',
|
|
messageCount: 2,
|
|
lastPreview: '这是回答',
|
|
lastDirection: 'out'
|
|
})
|
|
expect(result?.message).toMatchObject({ direction: 'out', status: 'sent', text: '这是回答' })
|
|
|
|
const conversation = store.get('user-a')
|
|
expect(conversation?.messages.map((item) => item.direction)).toEqual(['in', 'out'])
|
|
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
|
|
if (process.platform !== 'win32') {
|
|
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
|
}
|
|
})
|
|
|
|
it('保留发送失败的记录与错误码,便于在界面上对账', () => {
|
|
const { store } = createStore()
|
|
store.append({
|
|
userId: 'user-a',
|
|
direction: 'out',
|
|
kind: 'text',
|
|
text: '发不出去的内容',
|
|
status: 'failed',
|
|
errorCode: 'STALE_TOKEN'
|
|
})
|
|
|
|
const message = store.get('user-a')?.messages[0]
|
|
expect(message).toMatchObject({ status: 'failed', errorCode: 'STALE_TOKEN' })
|
|
})
|
|
|
|
it('媒体消息只保留类型占位,不写入路径细节以外的内容', () => {
|
|
const { store } = createStore()
|
|
const result = store.append({
|
|
userId: 'user-a',
|
|
direction: 'out',
|
|
kind: 'image',
|
|
text: '群日报.png',
|
|
status: 'sent'
|
|
})
|
|
|
|
expect(result?.summary.lastPreview).toBe('群日报.png')
|
|
// 空文本的媒体消息在列表里退化为中文占位。
|
|
store.append({ userId: 'user-a', direction: 'in', kind: 'image', text: '' })
|
|
expect(store.listSummaries()[0].lastPreview).toBe('[图片]')
|
|
})
|
|
|
|
it('单个会话超出上限时丢弃最旧的消息', () => {
|
|
const { store } = createStore({ maxMessages: 3 })
|
|
for (let index = 0; index < 5; index += 1) {
|
|
store.append({ userId: 'user-a', direction: 'in', kind: 'text', text: `第 ${index} 条` })
|
|
}
|
|
|
|
const messages = store.get('user-a')?.messages ?? []
|
|
expect(messages).toHaveLength(3)
|
|
expect(messages.map((item) => item.text)).toEqual(['第 2 条', '第 3 条', '第 4 条'])
|
|
})
|
|
|
|
it('会话数超出上限时只保留最近活跃的', () => {
|
|
const { store } = createStore({ maxConversations: 2 })
|
|
store.append({
|
|
userId: 'old',
|
|
direction: 'in',
|
|
kind: 'text',
|
|
text: '旧会话',
|
|
createdAt: 1_000
|
|
})
|
|
store.append({
|
|
userId: 'mid',
|
|
direction: 'in',
|
|
kind: 'text',
|
|
text: '中间会话',
|
|
createdAt: 2_000
|
|
})
|
|
store.append({
|
|
userId: 'new',
|
|
direction: 'in',
|
|
kind: 'text',
|
|
text: '新会话',
|
|
createdAt: 3_000
|
|
})
|
|
|
|
const summaries = store.listSummaries()
|
|
expect(summaries.map((item) => item.userId)).toEqual(['new', 'mid'])
|
|
expect(store.get('old')).toBeNull()
|
|
})
|
|
|
|
it('会话列表按最近活跃排序,并带上条数', () => {
|
|
const { store } = createStore()
|
|
store.append({ userId: 'a', direction: 'in', kind: 'text', text: '1', createdAt: 1_000 })
|
|
store.append({ userId: 'b', direction: 'in', kind: 'text', text: '2', createdAt: 2_000 })
|
|
store.append({ userId: 'a', direction: 'in', kind: 'text', text: '3', createdAt: 3_000 })
|
|
|
|
expect(store.listSummaries().map((item) => [item.userId, item.messageCount])).toEqual([
|
|
['a', 2],
|
|
['b', 1]
|
|
])
|
|
})
|
|
|
|
it('get 返回副本,外部改动不会污染缓存', () => {
|
|
const { store } = createStore()
|
|
store.append({ userId: 'a', direction: 'in', kind: 'text', text: '原始' })
|
|
|
|
const first = store.get('a')
|
|
first!.messages[0].text = '被改过'
|
|
expect(store.get('a')?.messages[0].text).toBe('原始')
|
|
})
|
|
|
|
it('清空后文件与内存都为空', () => {
|
|
const { store, filePath } = createStore()
|
|
store.append({ userId: 'a', direction: 'in', kind: 'text', text: 'x' })
|
|
|
|
store.clear()
|
|
|
|
expect(store.listSummaries()).toEqual([])
|
|
expect(store.get('a')).toBeNull()
|
|
expect(JSON.parse(readFileSync(filePath, 'utf8'))).toEqual({ version: 1, conversations: [] })
|
|
})
|
|
|
|
it('忽略空会话标识,不产生垃圾条目', () => {
|
|
const { store } = createStore()
|
|
expect(store.append({ userId: ' ', direction: 'in', kind: 'text', text: 'x' })).toBeNull()
|
|
expect(store.listSummaries()).toEqual([])
|
|
})
|
|
|
|
it('文件损坏时按空库处理,不抛错', () => {
|
|
const { store, filePath } = createStore()
|
|
store.append({ userId: 'a', direction: 'in', kind: 'text', text: 'x' })
|
|
rmSync(filePath)
|
|
// 重新实例化,读一个不存在的文件
|
|
const fresh = new AgentHubConversationStore({ filePath: () => filePath })
|
|
expect(fresh.listSummaries()).toEqual([])
|
|
})
|
|
})
|