import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { randomUUID } from 'node:crypto'; import { normalizeContext } from '@earendil-works/pi-ai/compat'; import { decodeToolReply, jsonToolContext } from '../src/json-tool-codec.mjs'; import { ProjectStore } from '../src/store.mjs'; import { DetailPiAgent } from '../src/agent.mjs'; import { cleanup } from './cleanup.mjs'; test('strict JSON never executes prose, unknown tools, extra fields or malformed args', () => { const context = normalizeContext({ systemPrompt: 'QA', tools: [{ name: 'memory_set', description: '', parameters: {} }], messages: [] }); assert.throws(() => decodeToolReply('invoke tool memory_set', context, 'a')); assert.throws(() => decodeToolReply('{"reply":"","toolCalls":[{"name":"bash","arguments":{}}]}', context, 'a')); assert.throws(() => decodeToolReply('{"reply":"","toolCalls":[],"execute":"x"}', context, 'a')); assert.throws(() => decodeToolReply('{"reply":"","toolCalls":[{"name":"memory_set","arguments":"bad"}]}', context, 'a')); assert.equal(decodeToolReply('{"reply":"已记录","toolCalls":[]}', context, 'a')[0].text, '已记录'); assert.deepEqual(decodeToolReply('{"reply":"只读回复"}', context, 'a'), [{ type: 'text', text: '只读回复' }]); assert.equal(decodeToolReply('{"toolCalls":[{"name":"memory_set","arguments":{}}]}', context, 'a')[0].type, 'toolCall'); assert.throws(() => decodeToolReply('{}', context, 'a')); assert.ok(jsonToolContext(context).messages[0].content.includes('JSON Schema')); }); test('actual Pi SDK consumes JSON codec through real OpenAI parser and validates/executes tools', async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-json-sdk-')); const scope = 'c'.repeat(64), store = await ProjectStore.open(dir, { scope, project: randomUUID() }, { brandId: 'qa', shopId: 'qa', categoryId: 'qa', platform: 'qa', version: '1', source: 'qa-fixture', rules: [] }); let calls = 0, finishes = 0; const broker = { info: async () => ({ accountScope: scope, model: { id: 'qa-model', protocol: 'openai-chat' }, pending: [] }), round() { const id = randomUUID(); return { id, fetch: async (_url, init) => { calls++; const payload = JSON.parse(init.body); assert.equal(payload.tools, undefined); assert.equal(payload.response_format.type, 'json_object'); assert.equal(payload.max_tokens, 8192); assert.equal(payload.max_completion_tokens, undefined); const answer = calls === 1 ? { reply: '正在记录', toolCalls: [{ name: 'memory_set', arguments: { key: 'color', value: '浅灰色', evidence: '测试颜色是浅灰色' } }] } : { reply: '浅灰色已记录', toolCalls: [] }; const chunk = { id, model: 'qa-model', choices: [{ index: 0, delta: { role: 'assistant', content: JSON.stringify(answer) }, finish_reason: null }] }; const finish = { id, model: 'qa-model', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] }; return new Response(`data: ${JSON.stringify(chunk)}\n\ndata: ${JSON.stringify(finish)}\n\ndata: [DONE]\n\n`, { headers: { 'content-type': 'text/event-stream' } }); }, finish: async valid => { assert.equal(valid, true); finishes++; return { phase: 'complete' }; } }; }, }; const host = await DetailPiAgent.open({ store, broker, protocolMode: 'structured-json' }); const result = await host.prompt('这个测试颜色是浅灰色,请记住。'); assert.equal((await store.context()).confirmedMemory.color.value, '浅灰色'); assert.equal(result.text, '浅灰色已记录'); assert.equal(calls, 2); assert.equal(finishes, 2); await host.dispose(); await cleanup(dir); }); test('output length stop never executes even a syntactically complete JSON tool prefix', async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-length-')); const scope = 'e'.repeat(64), store = await ProjectStore.open(dir, { scope, project: randomUUID() }, { brandId: 'qa', shopId: 'qa', categoryId: 'qa', platform: 'qa', version: '1', source: 'qa', rules: [] }); let posts = 0; const broker = { info: async () => ({ accountScope: scope, model: { id: 'qa', protocol: 'openai-chat' }, pending: [] }), round() { return { id: randomUUID(), fetch: async () => { posts++; const content = JSON.stringify({ toolCalls: [{ name: 'memory_set', arguments: { key: 'color', value: '红色', evidence: '测试颜色是红色' } }] }); const raw = `data: ${JSON.stringify({ id: 'length', model: 'qa', choices: [{ index: 0, delta: { content }, finish_reason: 'length' }] })}\n\ndata: [DONE]\n\n`; return new Response(raw, { headers: { 'content-type': 'text/event-stream' } }); }, finish: async () => ({ phase: 'complete' }) }; } }; const host = await DetailPiAgent.open({ store, broker, protocolMode: 'structured-json' }); await assert.rejects(host.prompt('测试颜色是红色'), /输出上限/); assert.deepEqual((await store.context()).confirmedMemory, {}); assert.equal(posts, 1); await host.dispose(); await cleanup(dir); });