gongxiangp2/apps/detail-agent-pi/test/native-wire.test.mjs
WuYongJie 93fc28f1bb feat(client): add native template slots and safe cloud version restore
Support explicit current-output A/B fill, protected edited drafts, scoped cloud update offers and atomic native restore with stale-open CAS. Preserve original unknown requests, improve Pi conversation continuity and native DPI/navigation reads, and publish the authenticated template metadata contract.

Validated: native backend 172 passed/5 environment tests ignored, UI 110, Pi 53, template/rule API 24 and native cloud contract 6; strict Clippy, layering and wasm. Real S01 selected B, edited/undo-redo, PCraft reopen and layered PSD roundtrip pass with exact pixels. Full nine-output generation and Photoshop acceptance remain open.
2026-10-23 21:27:06 +08:00

134 lines
10 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 { ProjectStore } from '../src/store.mjs';
import { DetailPiAgent } from '../src/agent.mjs';
import { auditResponse } from '../src/response-audit.mjs';
import { projectContext } from '../src/context-budget.mjs';
import { cleanup } from './cleanup.mjs';
import { decodeToolReply } from '../src/json-tool-codec.mjs';
import { normalizeContext } from '@earendil-works/pi-ai/compat';
import { createAssistantMessageEventStream } from '@earendil-works/pi-ai/compat';
import { SessionManager } from '@earendil-works/pi-coding-agent';
import { atomic } from '../src/store.mjs';
const sse = (delta, finish) => `data: ${JSON.stringify({ id: 'wire', model: 'qa', choices: [{ index: 0, delta, finish_reason: finish }] })}\n\ndata: [DONE]\n\n`;
async function setup(reply) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-recovery-'));
const scope = 'f'.repeat(64), store = await ProjectStore.open(dir, { scope, project: randomUUID() }, { brandId: 'qa', shopId: 'A', categoryId: 'desk', platform: 'qa', version: '1', source: 'qa', rules: [] });
const requests = [];
const broker = { info: async () => ({ accountScope: scope, model: { id: 'qa', protocol: 'openai-chat' }, pending: [] }), round() {
return { id: randomUUID(), fetch: async (_url, init) => {
const p = JSON.parse(init.body); requests.push(p);
return new Response(reply(p, requests.length), { headers: { 'content-type': 'text/event-stream' } });
}, finish: async valid => { assert.equal(valid, true); return { phase: 'complete' }; } };
} };
const host = await DetailPiAgent.open({ store, broker, callBudget: 3 });
return { host, store, requests, close: async () => { await host.dispose(); await cleanup(dir); } };
}
test('actual native Pi wire sends function schemas, executes typed call and accepts a plain final reply', async () => {
const x = await setup((_p, n) => n === 1 ? sse({ tool_calls: [{ index: 0, id: 'call-memory', type: 'function', function: { name: 'memory_set', arguments: JSON.stringify({ key: 'color', value: '胡桃色', evidence: '测试颜色是胡桃色' }) } }] }, 'tool_calls') : sse({ content: '已记录胡桃色' }, 'stop'));
try {
const r = await x.host.prompt('测试颜色是胡桃色,请记住。', { requiredTool: 'memory_set' });
assert.equal(r.text, '已记录胡桃色');
assert.equal((await x.store.context()).confirmedMemory.color.value, '胡桃色');
assert.equal(x.requests.length, 2);
assert.deepEqual(x.requests[0].tool_choice, { type: 'function', function: { name: 'memory_set' } });
assert.equal(x.requests[0].parallel_tool_calls, false);
assert.ok(x.requests[0].tools.some(t => t.type === 'function' && t.function.name === 'memory_set'));
assert.ok(x.requests[1].messages.some(m => m.role === 'tool' && m.tool_call_id === 'call-memory'));
assert.equal(x.requests[0].response_format, undefined);
} finally { await x.close(); }
});
test('native partial arguments and length-stopped valid prefixes never execute memory writes', async () => {
for (const [args, finish] of [['{"key":"color","value":"红色","evidence":"测试颜色是红色"', 'tool_calls'], ['{"key":"color","value":"红色","evidence":"测试颜色是红色"}', 'length']]) {
const x = await setup(() => sse({ tool_calls: [{ index: 0, id: 'partial', type: 'function', function: { name: 'memory_set', arguments: args } }] }, finish));
try {
await assert.rejects(x.host.prompt('测试颜色是红色'), /未完整|输出上限/);
assert.deepEqual((await x.store.context()).confirmedMemory, {});
assert.equal(x.requests.length, 1);
} finally { await x.close(); }
}
});
test('required tool returned as prose is a known protocol failure with one POST and no execution', async () => {
const x = await setup(() => sse({ content: 'invoke tool memory_set with color red' }, 'stop'));
try {
await assert.rejects(x.host.prompt('测试颜色是红色', { requiredTool: 'memory_set' }), /未返回要求的标准函数调用/);
assert.equal(x.requests.length, 1); assert.deepEqual((await x.store.context()).confirmedMemory, {});
} finally { await x.close(); }
});
test('a literal marker in a normal explanation is ordinary text, never a command or a repair trigger', async () => {
const x = await setup(() => sse({ content: '结束标记的示例是 <|eos|>。' }, 'stop'));
try { const r = await x.host.prompt('解释一个结束标记,不要调用工具'); assert.match(r.text, /结束标记/); assert.equal(x.requests.length, 1); }
finally { await x.close(); }
});
test('projection bounds stale canvas bodies without deleting original facts, errors or evidence', () => {
const large = { revision: 1, context: { layers: Array.from({ length: 200 }, (_, i) => ({ id: i, name: '完整画布数据'.repeat(20) })) } };
const m = { role: 'toolResult', toolName: 'canvas_read', toolCallId: 'old', content: [{ type: 'text', text: JSON.stringify(large) }], details: large };
const messages = [{ role: 'user', content: '原始用户事实保留' }, m, { ...m, toolCallId: 'new' }];
const before = JSON.stringify(messages), out = projectContext({ messages });
assert.equal(JSON.stringify(messages), before); assert.equal(out.messages[0], messages[0]);
assert.ok(out.messages[1].content[0].text.length < 300); assert.equal(out.messages[1].details, undefined);
assert.equal(out.messages[2].content[0].text, m.content[0].text);
});
test('a continuous conversation archives a previous rule domain rather than reinjecting it as the latest rules',()=>{
const old={role:'toolResult',toolName:'context_read',toolCallId:'old-domain',isError:false,content:[{type:'text',text:JSON.stringify({bindingHash:'old',snapshot:{rules:['旧店铺专属规范']}})}]};
const original=JSON.stringify(old),projected=projectContext({messages:[old]},{bindingHash:'current'});
assert.equal(JSON.stringify(old),original);
assert.ok(!JSON.stringify(projected).includes('旧店铺专属规范'));
assert.ok(JSON.stringify(projected).includes('旧规则域历史'));
});
test('wire audit detects complete SSE with a literal early EOS instead of blaming the browser', () => {
const a = auditResponse(sse({ content: '{"reply":"未结束<|eos|>' }, 'stop'), 'openai-chat');
assert.equal(a.literalEos, true); assert.deepEqual(a.finishes, ['stop']);
});
test('a terminal marker without a completion reason remains unproven and cannot execute a tool', () => {
assert.throws(() => auditResponse(sse({ content: '只有前缀' }, null), 'openai-chat'), /缺少完成原因/);
});
test('JSON compatibility errors distinguish complete prose from a broken JSON envelope', () => {
const c = normalizeContext({ messages: [] });
assert.throws(() => decodeToolReply('产品编号与颜色核对正确。', c, 'id'), /文字而非约定的JSON/);
assert.throws(() => decodeToolReply('{"reply":"只返回一半<|eos|>', c, 'id'), /提前结束标记/);
assert.throws(() => decodeToolReply('{"reply":"未闭合', c, 'id'), /JSON指令未完整/);
});
async function compactionFixture({ withCanvas, callBudget = 4, large = false }) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-recovery-')), scope = '9'.repeat(64);
const store = await ProjectStore.open(dir, { scope, project: randomUUID() }, { brandId: 'qa', shopId: 'qa', categoryId: 'qa', platform: 'qa', source: 'qa', version: '1', rules: [] });
const manager = SessionManager.create(store.dir, path.join(store.dir, 'sessions'));
for (let i = 0; i < (large ? 30 : 12); i++) {
manager.appendMessage({ role: 'user', content: `archive-marker-${i}: ` + 'x'.repeat(large ? 1400 : 500), timestamp: Date.now() });
manager.appendMessage({ role: 'assistant', api: 'openai-completions', provider: 'jiansun-native-98k', model: 'qa', content: [{ type: 'text', text: 'Synthetic, not a live model reply. ' + 'y'.repeat(large ? 1400 : 500) }], stopReason: 'stop', timestamp: Date.now(), usage: { input: 5000, output: 500, cacheRead: 0, cacheWrite: 0, totalTokens: 5500, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } });
}
await atomic(path.join(store.dir, 'session.json'), { file: manager.getSessionFile() });
let calls = 0, canvasCalls = 0;
const canvas = withCanvas ? { read() { canvasCalls++; throw Error('no canvas edit run during compaction'); }, delta() { canvasCalls++; throw Error('must not show internal summary'); } } : undefined;
const transport = { info: { accountScope: scope, model: { id: 'qa', protocol: 'openai-chat' }, pending: [] }, stream(model) {
calls++;
const stream = createAssistantMessageEventStream();
const message = { role: 'assistant', api: model.api, provider: model.provider, model: model.id, content: [{ type: 'text', text: 'Summary: original evidence remains archived.' }], stopReason: 'stop', timestamp: Date.now(), usage: { input: 5000, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 5020, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } };
queueMicrotask(() => { stream.push({ type: 'start', partial: message }); stream.push({ type: 'text_delta', contentIndex: 0, delta: message.content[0].text, partial: message }); stream.push({ type: 'done', reason: 'stop', message }); stream.end(message); });
return stream;
} };
const host = await DetailPiAgent.open({ store, transport, canvas, callBudget });
return { host, get calls() { return calls; }, get canvasCalls() { return canvasCalls; }, close: async () => { await host.dispose(); await cleanup(dir); } };
}
test('compaction with a connected canvas does not call an inactive edit run or show the internal summary', async () => {
const x = await compactionFixture({ withCanvas: true });
try {
const result = await x.host.compact();
assert.match(result.summary, /original evidence/); assert.ok(x.calls > 0); assert.equal(x.canvasCalls, 0);
assert.ok(x.host.manager.getEntries().some(e => e.type === 'message' && e.message.role === 'user' && e.message.content.includes('archive-marker-0')));
} finally { await x.close(); }
});
test('automatic compaction and subsequent inference share one user-turn call budget', async () => {
const x = await compactionFixture({ withCanvas: false, callBudget: 1, large: true });
try {
await assert.rejects(x.host.prompt('继续处理,但不能绕过预算'), /预算/);
assert.equal(x.calls, 1);
} finally { await x.close(); }
});