const assert = require('assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { createMeetingKnowledgeService } = require('../mcp/src/dashboard/meeting-knowledge-service'); const { OpenAICompatibleClient, AnthropicCompatibleClient } = require('../mcp/src/core/agent-runtime'); async function testEmptyToolCompatibility() { const originalFetch = global.fetch; const requests = []; global.fetch = async (url, options) => { requests.push({ url: String(url), body: JSON.parse(options.body) }); if (String(url).includes('/chat/completions')) { return { ok: true, json: async () => ({ choices: [{ message: { content: '{}' } }] }) }; } return { ok: true, json: async () => ({ content: [{ type: 'text', text: '{}' }] }) }; }; try { await new OpenAICompatibleClient({ apiKey: 'test-only', baseUrl: 'https://model.test', model: 'test' }).complete([{ role: 'user', content: 'test' }], []); await new AnthropicCompatibleClient({ apiKey: 'test-only', baseUrl: 'https://model.test', model: 'test' }).complete([{ role: 'user', content: 'test' }], []); assert.equal(requests.length, 2); assert.equal('tools' in requests[0].body, false); assert.equal('tool_choice' in requests[0].body, false); assert.equal('tools' in requests[1].body, false); } finally { global.fetch = originalFetch; } } async function main() { await testEmptyToolCompatibility(); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-meeting-knowledge-')); const liveStore = path.join(tempRoot, 'live'); let round = 1; const officialStatus = async () => ({ installed: true, valid: true, authorized: true, ready: true, installedVersion: '0.1.9', }); const rpc = payload => ({ status: 'ok', data: { response: { jsonrpc: '2.0', id: 1, result: { content: [{ type: 'text', text: JSON.stringify(payload) }] } } } }); const officialCall = async ({ method, args }) => { if (method === 'list_user_meetings') { assert.match(args.begin_datetime, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); assert.match(args.end_datetime, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); return rpc({ meetingid_list: [round === 1 ? 'meeting-live-1' : 'meeting-live-2'] }); } assert.equal(method, 'get_meeting_info'); const suffix = args.meetingid.endsWith('1') ? '一' : '二'; return rpc({ meetingid: args.meetingid, title: `客户项目推进会${suffix}`, meeting_start_datetime: '2026-07-16 14:00', meeting_duration: 3600, description: '王刚负责在 7 月 20 日前整理客户演示清单。', location: '线上会议室', status: 3, meeting_type: 0, password: 'must-not-be-saved', host_key: 'host-secret-value', attendees: { member: [{ userid: 'wanggang', status: 1, phone_number: '13800000000' }], tmp_external_user: [], }, settings: { enable_waiting_room: true, credentials: 'private-value' }, }); }; const analyzer = async () => ({ status: 'completed', summary: '会议资料明确要求王刚整理客户演示清单。', topics: ['客户演示准备'], decisions: [], actionItems: [{ title: '整理客户演示清单', owner: '王刚', dueDate: '2026-07-20', priority: 'high', evidence: '会议描述明确写明负责人和截止日期。', confidence: 0.98, }], suggestedActions: ['人工确认演示清单范围'], risks: ['缺少会议转写,无法验证其他讨论内容'], knowledgeTags: ['客户演示', '项目推进'], analysisBasis: '仅基于企业微信官方会议元数据与会议描述。', requiresReview: true, analyzedAt: new Date().toISOString(), error: null, }); try { const service = createMeetingKnowledgeService({ storeDir: liveStore, officialStatus, officialCall, meetingCapability: async () => ({ available: true, reason: 'available', message: '会议权限可用' }), analyzer, initCommand: 'node qiwei-official-cli.js init', }); const firstSync = await service.sync(); assert.equal(firstSync.status, 'ok'); assert.equal(firstSync.summary.live, true); assert.equal(firstSync.summary.syncedCount, 1); assert.equal(firstSync.data.meetings[0].sourceKind, 'official-cli-live'); const recordPath = firstSync.data.meetings[0].files.json; const markdownPath = firstSync.data.meetings[0].files.markdown; assert.ok(fs.existsSync(recordPath)); assert.ok(fs.existsSync(markdownPath)); const persisted = fs.readFileSync(recordPath, 'utf8'); assert.doesNotMatch(persisted, /must-not-be-saved|host-secret-value|13800000000|private-value/); assert.doesNotMatch(persisted, /"password"|"host_key"|"phone_number"|"credentials"/); const analyzed = await service.analyze('meeting-live-1'); assert.equal(analyzed.status, 'ok'); assert.equal(analyzed.data.meeting.analysis.actionItems[0].owner, '王刚'); assert.match(fs.readFileSync(markdownPath, 'utf8'), /整理客户演示清单/); round = 2; const secondSync = await service.sync(); assert.equal(secondSync.summary.syncedCount, 1); assert.equal(secondSync.summary.meetingCount, 2); assert.equal(secondSync.data.meetings.find(item => item.id === 'meeting-live-1').analysis.status, 'completed'); const hub = await service.hub(); assert.equal(hub.status, 'ok'); assert.equal(hub.summary.meetingCount, 2); assert.equal(hub.summary.actionItemCount, 1); const unauthorizedStore = path.join(tempRoot, 'unauthorized'); const unauthorized = createMeetingKnowledgeService({ storeDir: unauthorizedStore, officialStatus: async () => ({ installed: true, valid: true, authorized: false, ready: false }), officialCall: async () => { throw new Error('未授权时不应调用官方会议接口'); }, initCommand: 'node qiwei-official-cli.js init', }); const blocked = await unauthorized.sync(); assert.equal(blocked.status, 'error'); assert.equal(blocked.summary.needsInitialization, true); assert.equal(fs.existsSync(path.join(unauthorizedStore, 'index.json')), false); const unauthorizedHub = await unauthorized.hub(); assert.equal(unauthorizedHub.data.meetings.length, 0); assert.equal(unauthorizedHub.data.cli.ready, false); assert.match(unauthorizedHub.data.cli.initCommand, /init/); let blockedOfficialCalls = 0; const policyBlocked = createMeetingKnowledgeService({ storeDir: path.join(tempRoot, 'policy-blocked'), officialStatus, officialCall: async () => { blockedOfficialCalls += 1; throw new Error('企业策略受限时不应继续请求会议列表'); }, meetingCapability: async () => ({ available: false, reason: 'enterprise-policy', message: '当前企业未开放会议 CLI' }), }); const blockedHub = await policyBlocked.hub(); assert.equal(blockedHub.summary.cliReady, true); assert.equal(blockedHub.summary.meetingReady, false); assert.equal(blockedHub.data.cli.capabilityReason, 'enterprise-policy'); const policySync = await policyBlocked.sync(); assert.equal(policySync.status, 'error'); assert.equal(policySync.summary.needsMeetingCapability, true); assert.equal(blockedOfficialCalls, 0); process.stdout.write(`${JSON.stringify({ status: 'ok', liveSyncContract: true, accumulatedMeetings: hub.summary.meetingCount, sensitiveFieldsRemoved: true, aiActionItemsPersisted: true, unauthorizedPathUsesNoMock: true, emptyToolModelCallsSupported: true, enterprisePolicyIsExplicit: true, jsonRpcEnvelopeSupported: true, }, null, 2)}\n`); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } } main().catch(error => { process.stderr.write(`${error.stack || error.message}\n`); process.exitCode = 1; });