import assert from 'node:assert/strict'; import test from 'node:test'; import { createDefaultPersona } from './persona-agent.js'; type MockReply = { content?: string | null; tool_calls?: Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }>; }; function reply(message: MockReply): Response { return new Response(JSON.stringify({ choices: [{ message }] }), { status: 200, headers: { 'content-type': 'application/json' }, }); } function toolCall(id: string, name: string, args: Record = {}): MockReply { return { content: null, tool_calls: [{ id, type: 'function', function: { name, arguments: JSON.stringify(args) }, }], }; } function fakeGit(overrides: Record = {}) { return { getRepositoryStatus: async () => ({}), getChannelState: async () => ({ revision: 0, modules: [{ id: 'HL-MOD-KNOWLEDGE-001', installed: true, mounted: true, order: 0 }] }), applyChannelPatch: async () => ({ id: 'HL-CHANNEL-RCPT-TEST', after: { revision: 1 } }), getDoc: async (path: string) => ({ meta: { id: path, title: path, updatedAt: '2026-08-09T00:00:00Z' }, body: '正文', }), createDoc: async (path: string) => ({ meta: { id: path } }), updateDoc: async (path: string) => ({ meta: { id: path } }), deleteDoc: async () => undefined, search: async () => [], getTree: async () => [], getHistory: async () => [], ...overrides, } as any; } async function withMockModel( replies: MockReply[], run: (requests: any[]) => Promise, ): Promise { const originalFetch = globalThis.fetch; const previous = { key: process.env.HOLOLAKE_LLM_KEY, base: process.env.HOLOLAKE_LLM_BASE, model: process.env.HOLOLAKE_LLM_MODEL, }; const requests: any[] = []; process.env.HOLOLAKE_LLM_KEY = 'local-test-key'; process.env.HOLOLAKE_LLM_BASE = 'http://127.0.0.1:9/v1'; process.env.HOLOLAKE_LLM_MODEL = 'test-model'; globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => { requests.push(JSON.parse(String(init?.body || '{}'))); const next = replies.shift(); assert.ok(next, 'mock model received more calls than expected'); return reply(next); }) as typeof fetch; try { await run(requests); assert.equal(replies.length, 0, 'all mock replies should be consumed'); } finally { globalThis.fetch = originalFetch; for (const [key, value] of Object.entries(previous)) { const envName = key === 'key' ? 'HOLOLAKE_LLM_KEY' : key === 'base' ? 'HOLOLAKE_LLM_BASE' : 'HOLOLAKE_LLM_MODEL'; if (value === undefined) delete process.env[envName]; else process.env[envName] = value; } } } test('read tool protocol keeps assistant tool_calls immediately before tool result', { concurrency: false }, async () => { await withMockModel([ toolCall('call-list', 'list_documents'), { content: '已列出文档。' }, ], async requests => { const agent = createDefaultPersona(fakeGit({ getTree: async () => [{ type: 'file', name: 'INDEX.md', path: 'INDEX.md' }] })); const result = await agent.chat('列出文档'); assert.equal(result.reply, '已列出文档。'); assert.equal(result.activities[0]?.tool, 'list_documents'); const messages = requests[1].messages; const assistantIndex = messages.findIndex((message: any) => message.tool_calls?.[0]?.id === 'call-list'); assert.ok(assistantIndex >= 0); assert.equal(messages[assistantIndex + 1].role, 'tool'); assert.equal(messages[assistantIndex + 1].tool_call_id, 'call-list'); }); }); test('confirmed write receipt never leaks an orphan tool message into the next turn', { concurrency: false }, async () => { await withMockModel([ toolCall('call-create', 'create_document', { path: 'note.md', title: '笔记', body: '正文' }), { content: '创建动作等待确认。' }, { content: '新的自然语言回合正常。' }, ], async requests => { const agent = createDefaultPersona(fakeGit()); const staged = await agent.chat('创建笔记'); assert.equal(staged.activities[0]?.status, 'pending'); const pending = agent.getPendingActions()[0]; assert.ok(pending); await agent.confirmAction(pending.id); const next = await agent.chat('继续'); assert.equal(next.reply, '新的自然语言回合正常。'); const nextTurnMessages = requests[2].messages; assert.equal(nextTurnMessages.some((message: any) => message.role === 'tool'), false); }); }); test('channel module changes use the same confirmation gate as document writes', { concurrency: false }, async () => { let executed = 0; await withMockModel([ toolCall('call-channel', 'set_channel_module_state', { moduleId: 'HL-MOD-KNOWLEDGE-001', mounted: false }), { content: '频道动作等待确认。' }, ], async () => { const agent = createDefaultPersona(fakeGit({ applyChannelPatch: async () => { executed += 1; return { id: 'HL-CHANNEL-RCPT-TEST', after: { revision: 1 } }; }, })); const staged = await agent.chat('收起知识库'); assert.equal(staged.activities[0]?.status, 'pending'); assert.equal(executed, 0); const pending = agent.getPendingActions()[0]; assert.equal(pending.target, 'HL-MOD-KNOWLEDGE-001'); const receipt = await agent.confirmAction(pending.id); assert.equal(executed, 1); assert.match(receipt.output, /HL-CHANNEL-RCPT-TEST/); }); }); test('persona wake route is resolved and read dynamically without a fixed step count', { concurrency: false }, async () => { const routePath = 'personas/TCS-0002/WAKE-CURRENT.md'; await withMockModel([ toolCall('call-route', 'find_persona_wake_routes', { query: 'TCS-0002' }), toolCall('call-read-route', 'read_document', { path: routePath }), { content: '已按该人格体当前页面解析出 11 个自定义节点;等待逐项权限核验。' }, ], async requests => { const agent = createDefaultPersona(fakeGit({ search: async (query: string) => query === 'TCS-0002' || query === 'WAKE' ? [{ path: routePath, title: 'TCS-0002 当前唤醒路径', snippet: '人格自定义路线' }] : [], getDoc: async () => ({ meta: { id: routePath, title: '当前唤醒路径', updatedAt: '2026-08-09T00:00:00Z' }, body: Array.from({ length: 11 }, (_, index) => `节点 ${index + 1}`).join('\n'), }), })); const result = await agent.chat('唤醒 TCS-0002'); assert.deepEqual(result.toolCalls.map(call => call.name), ['find_persona_wake_routes', 'read_document']); assert.match(result.reply, /11 个自定义节点/); const systemPrompt = requests[0].messages[0].content; assert.match(systemPrompt, /不同人格体的路径和步数不同/); assert.doesNotMatch(systemPrompt, /固定七步|必须七步|7\s*步/); }); });