Files
WechatExplorer/tests/unit/ai-provider-search-consent.test.ts
Wxw-Gu 0b845db2e0 feat: 优化问问微信检索性能与分析交互
- 补充 Worker、WCDB、sender、IPC、序列化时间账
- 增加 Agent 增量覆盖统计和重复检索停止条件
- 补充性能与交互回归测试
2026-08-07 15:28:45 +08:00

131 lines
4.9 KiB
TypeScript

import { mkdtempSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterAll, describe, expect, it, vi } from 'vitest'
import type { AIProviderConfig } from '../../src/shared/ai-provider'
const root = mkdtempSync(join(tmpdir(), 'wxe-ai-search-consent-'))
vi.mock('electron', () => ({
app: { getPath: () => root },
safeStorage: {
isEncryptionAvailable: () => true,
encryptString: (value: string) => Buffer.from(value),
decryptString: (value: Buffer) => value.toString('utf8')
}
}))
import { AIProviderService } from '../../src/main/services/ai-provider-service'
const provider = (baseUrl: string): AIProviderConfig => ({
id: 'fixture-provider',
name: 'Fixture Provider',
type: 'custom' as const,
baseUrl,
auth: { type: 'none' as const },
models: [
{
id: 'fixture-model',
name: 'Fixture Model',
capabilities: { chat: true, vision: false, ocr: false, longContext: false }
}
],
defaultModel: 'fixture-model',
advanced: { timeoutMs: 1_000, extraHeaders: {} }
})
describe('AI Search provider identity', () => {
afterAll(() => rmSync(root, { recursive: true, force: true }))
it('classifies local providers by their URL rather than provider type', () => {
const service = new AIProviderService()
expect(service.save(provider('https://first.example.test/v1')).success).toBe(true)
expect(service.getAiSearchProviderStatus()).toMatchObject({
requiresConsent: true,
recipient: 'https://first.example.test/v1'
})
expect(
service.save({ ...provider('https://remote.example.test'), type: 'ollama' }).success
).toBe(true)
expect(service.getAiSearchProviderStatus()).toMatchObject({ requiresConsent: true })
expect(service.save(provider('http://localhost:11434/')).success).toBe(true)
expect(service.getAiSearchProviderStatus()).toMatchObject({
requiresConsent: false,
recipient: 'http://localhost:11434'
})
expect(service.save(provider('http://[::1]:11434')).success).toBe(true)
expect(service.getAiSearchProviderStatus()).toMatchObject({ requiresConsent: false })
expect(service.save(provider('http://127.0.0.1:11434')).success).toBe(true)
expect(service.getAiSearchProviderStatus()).toMatchObject({ requiresConsent: false })
})
it('normalizes a provider recipient without persisting any AI Search authorization', () => {
const service = new AIProviderService()
expect(service.save(provider('HTTPS://REMOTE.EXAMPLE.TEST:443/v1/')).success).toBe(true)
expect(service.getAiSearchProviderStatus()).toMatchObject({
requiresConsent: true,
recipient: 'https://remote.example.test/v1'
})
expect(service.list().providers[0]).not.toHaveProperty('aiSearchDataConsent')
})
it('serializes only the caller-provided content to a mocked provider payload', async () => {
const service = new AIProviderService()
service.save(provider('https://payload.example.test/v1'))
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ choices: [{ message: { content: 'ok' } }], usage: {} }), {
status: 200,
headers: { 'content-type': 'application/json' }
})
)
vi.stubGlobal('fetch', fetchMock)
const sent = await service.chat([
{ role: 'system', content: 'system instruction' },
{ role: 'user', content: 'minimal evidence only' }
])
expect(sent.success).toBe(true)
const request = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)) as {
messages: Array<{ content: string }>
}
expect(request.messages.map((message) => message.content)).toEqual([
'system instruction',
'minimal evidence only'
])
expect(JSON.stringify(request)).not.toContain('conversationId')
expect(JSON.stringify(request)).not.toContain('messageId')
vi.unstubAllGlobals()
})
it('aborts the provider fetch when the caller cancels an AI request', async () => {
const service = new AIProviderService()
service.save(provider('http://127.0.0.1:11434'))
let fetchSignal: AbortSignal | undefined
const fetchMock = vi.fn(
(_url: string, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
fetchSignal = init?.signal || undefined
fetchSignal?.addEventListener('abort', () => reject(fetchSignal?.reason), { once: true })
})
)
vi.stubGlobal('fetch', fetchMock)
const controller = new AbortController()
try {
const result = service.chat(
[{ role: 'user', content: 'cancel this request' }],
undefined,
controller.signal
)
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1))
controller.abort(new DOMException('cancelled by test', 'AbortError'))
await expect(result).rejects.toMatchObject({ name: 'AbortError' })
expect(fetchSignal?.aborted).toBe(true)
} finally {
vi.unstubAllGlobals()
}
})
})