82 lines
7.0 KiB
JavaScript
82 lines
7.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { mkdtemp, readFile } from 'node:fs/promises';
|
|
import { join, resolve } from 'node:path';
|
|
import { runNativeZcode } from '../../services/zcode-agent/runner.mjs';
|
|
|
|
const stateDir = await mkdtemp('/var/tmp/yedir-native-fixture-');
|
|
const calls = []; const events = []; let requests = 0;
|
|
function completion(body, message, finish) {
|
|
const base = { id: 'fixture', created: 1, model: 'canvas-proof' };
|
|
const usage = { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 };
|
|
if (!body.stream) return Response.json({ ...base, object: 'chat.completion', usage,
|
|
choices: [{ index: 0, message: { role: 'assistant', ...message }, finish_reason: finish }] });
|
|
let delta = message;
|
|
if (message.tool_calls) delta = { tool_calls: message.tool_calls.map((call, index) => ({ index, ...call })) };
|
|
const chunks = [{ ...base, object: 'chat.completion.chunk', choices: [{ index: 0, delta: { role: 'assistant', ...delta }, finish_reason: null }] },
|
|
{ ...base, object: 'chat.completion.chunk', usage, choices: [{ index: 0, delta: {}, finish_reason: finish }] }];
|
|
return new Response(chunks.map(value => `data: ${JSON.stringify(value)}\n\n`).join('') + 'data: [DONE]\n\n',
|
|
{ headers: { 'content-type': 'text/event-stream' } });
|
|
}
|
|
try {
|
|
const result = await runNativeZcode({ runId: 'native-mcp-proof', stateDir,
|
|
runtimeDir: process.env.ZCODE_FIXTURE_RUNTIME || process.env.CANVAS_ZCODE_RUNTIME_DIR || resolve('.local/zcode-runtime'),
|
|
nodePath: process.env.ZCODE_FIXTURE_NODE || join(process.env.ZCODE_FIXTURE_RUNTIME || process.env.CANVAS_ZCODE_RUNTIME_DIR || resolve('.local/zcode-runtime'), 'bin/node'),
|
|
modelId: 'canvas-proof', instructions: 'Use the canvas MCP tools to read the canvas and then rename node a. Do not use Bash. Respond in Chinese.',
|
|
prompt: '读取画布,然后把节点 a 改名为原生核心测试,实际执行后回复完成。', timeoutMs: 90_000,
|
|
tools: [{ name: 'readCanvas', description: 'Read actual canvas', readOnly: true, inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
{ name: 'renameNode', description: 'Rename actual canvas node', readOnly: false,
|
|
inputSchema: { type: 'object', properties: { nodeId: { type: 'string' }, title: { type: 'string' } }, required: ['nodeId', 'title'] } }],
|
|
execute: async (name, input, id) => { calls.push({ name, input, id }); return name === 'readCanvas'
|
|
? { nodes: [{ id: 'a', title: 'before' }] } : { updatedNodeIds: ['a'], title: input.title }; },
|
|
onEvent: async event => { events.push(event.type); },
|
|
onSandbox: async sandbox => console.log('SANDBOX', JSON.stringify(sandbox)),
|
|
model: async body => {
|
|
requests++; if (requests > 12) return Response.json({ error: { message: 'fixture budget exceeded' } }, { status: 429 });
|
|
const names = (body.tools || []).map(tool => tool.function?.name || tool.name || '');
|
|
console.log('MODEL', requests, 'tools', names.filter(name => /readCanvas|renameNode/.test(name)));
|
|
const hasRead = body.messages?.some(message => message.role === 'tool' && String(message.content).includes('before'));
|
|
const hasRename = body.messages?.some(message => message.role === 'tool' && String(message.content).includes('updatedNodeIds'));
|
|
if (!names.some(name => name.includes('readCanvas'))) return completion(body, { content: '模型配置辅助调用完成' }, 'stop');
|
|
if (!hasRead || !hasRename) {
|
|
const name = names.find(name => name.includes(hasRead ? 'renameNode' : 'readCanvas'));
|
|
return completion(body, { tool_calls: [{ id: hasRead ? 'native_rename' : 'native_read', type: 'function',
|
|
function: { name, arguments: JSON.stringify(hasRead ? { nodeId: 'a', title: '原生核心测试' } : {}) } }] }, 'tool_calls');
|
|
}
|
|
return completion(body, { content: '已完成节点改名。' }, 'stop');
|
|
},
|
|
});
|
|
assert.deepEqual(calls.map(call => call.name), ['readCanvas', 'renameNode']);
|
|
assert.match(result.response, /完成/); assert.ok(result.sessionId);
|
|
const beforeResume = calls.length;
|
|
const resumed = await runNativeZcode({ runId: 'native-mcp-proof', stateDir,
|
|
runtimeDir: process.env.ZCODE_FIXTURE_RUNTIME || process.env.CANVAS_ZCODE_RUNTIME_DIR || resolve('.local/zcode-runtime'),
|
|
nodePath: process.env.ZCODE_FIXTURE_NODE || join(process.env.ZCODE_FIXTURE_RUNTIME || process.env.CANVAS_ZCODE_RUNTIME_DIR || resolve('.local/zcode-runtime'), 'bin/node'),
|
|
modelId: 'canvas-proof', instructions: 'Reply to the user without changing the canvas.', prompt: '继续会话,确认刚才的修改。',
|
|
sessionId: result.sessionId, timeoutMs: 60000, tools: [], execute: async () => { throw new Error('must not repeat'); },
|
|
model: async body => { assert.ok(JSON.stringify(body.messages).includes('原生核心测试'), 'native session must contain previous tool history'); return completion(body, { content: '已恢复原生会话。' }, 'stop'); }
|
|
});
|
|
assert.equal(resumed.sessionId, result.sessionId); assert.equal(calls.length, beforeResume);
|
|
console.log('NATIVE_RESUME_PASSED', resumed.sessionId);
|
|
const controller = new AbortController(); let paidStarted = 0; let paidSettled = 0;
|
|
await assert.rejects(runNativeZcode({ runId: 'native-cancel-proof', stateDir,
|
|
runtimeDir: process.env.ZCODE_FIXTURE_RUNTIME || process.env.CANVAS_ZCODE_RUNTIME_DIR || resolve('.local/zcode-runtime'),
|
|
nodePath: process.env.ZCODE_FIXTURE_NODE || join(process.env.ZCODE_FIXTURE_RUNTIME || process.env.CANVAS_ZCODE_RUNTIME_DIR || resolve('.local/zcode-runtime'), 'bin/node'),
|
|
modelId: 'canvas-proof', instructions: 'Use only the paidProof MCP tool.', prompt: '执行工具。', timeoutMs: 60000,
|
|
signal: controller.signal, tools: [{ name: 'paidProof', description: 'Synthetic accepted operation', readOnly: false, inputSchema: { type: 'object', properties: {} } }],
|
|
execute: async () => { paidStarted++; controller.abort(); await new Promise(resolve => setTimeout(resolve, 300)); paidSettled++; return { createdNodeIds: ['synthetic-proof'] }; },
|
|
model: async body => {
|
|
const name = body.tools?.map(tool => tool.function?.name).find(name => name?.includes('paidProof'));
|
|
return completion(body, name ? { tool_calls: [{ id: 'accepted_paid_proof', type: 'function', function: { name, arguments: '{}' } }] } : { content: '辅助调用完成' }, name ? 'tool_calls' : 'stop');
|
|
}
|
|
}), /停止/);
|
|
assert.equal(paidStarted, 1); assert.equal(paidSettled, 1);
|
|
console.log('NATIVE_CANCEL_DRAIN_PASSED', { paidStarted, paidSettled });
|
|
console.log('NATIVE_FIXTURE_PASSED' , JSON.stringify({ calls: calls.map(call => call.name), sessionId: result.sessionId,
|
|
requests, eventTypes: [...new Set(events)], stateDir }));
|
|
} catch (error) {
|
|
console.error('NATIVE_FIXTURE_FAILED', error.message, stateDir);
|
|
try { const exit = JSON.parse(await readFile(join(stateDir, 'native-mcp-proof/last-exit.json'), 'utf8'));
|
|
console.error('EXIT', JSON.stringify(exit).replace(/Bearer\s+[\w.-]+|sk-[\w-]+/g, '[redacted]')); } catch {}
|
|
process.exitCode = 1;
|
|
}
|