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.
67 lines
5.0 KiB
JavaScript
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);
|
|
});
|