gongxiangp2/apps/detail-agent-pi/test/json-codec.test.mjs
WuYongJie 1b6425b552 feat(desktop): connect native Pi sessions with scoped rules and recovery
Track the fixed PhotoCraft source baseline with the existing native client integration. Use the actual Pi SDK for normal sends, durable session recovery, demand-loaded enterprise rules and checked undoable canvas tools. Retain request identities and never resend unknown inference. Include local packaging and real-call acceptance evidence.
2026-10-22 10:49:58 +08:00

67 lines
5.0 KiB
JavaScript

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);
});