import assert from 'node:assert/strict'; import { tool } from 'ai'; import { z } from 'zod'; import { runZcodeCanvasTask } from '../../lib/ai/zcode-agent/task'; import { AgentToolRejectedError, newAgentRunSnapshot } from '../../lib/ai/canvas-agent-runtime/protocol'; import type { runNativeZcode } from '../../services/zcode-agent/runner.mjs'; type NativeOptions = Parameters[0]; function fixture(objective = '修改图片') { const snapshot = newAgentRunSnapshot(objective, []); let writes = 0; const saved: string[] = []; const schema = z.object({ nodeIds: z.array(z.string()).default([]) }); const definitions = { planCanvasTask: tool({ inputSchema: z.object({}) }), edit: tool({ inputSchema: schema }), readCanvas: tool({ inputSchema: z.object({}) }) }; const ports = { runId: crypto.randomUUID(), snapshot, definitions, instructions: '', context: {}, gateway: { modelId: 'fixture', apiType: 'openai-chat-completions', forward: async () => Response.json({}) }, signal: new AbortController().signal, checkpoint: async (_snapshot: unknown, status: string) => { saved.push(status); }, consumeInputs: async () => ({ cancelled: false }), isCancelled: async () => false, verify: async () => ({ passed: true, issues: [] as string[] }), tools: { planCanvasTask: { title: '计划', readOnly: true, concurrentSafe: true, execute: async () => ({ steps: ['修改图片'] }) }, readCanvas: { title: '读取', readOnly: true, concurrentSafe: true, execute: async () => ({ nodes: [] }) }, edit: { title: '修改', readOnly: false, concurrentSafe: false, execute: async () => { writes++; return { updatedNodeIds: ['a'] }; } } }, native: async (options: NativeOptions) => { await options.execute('planCanvasTask', {}, 'plan', options.signal!); await options.execute('edit', { nodeIds: ['a'] }, 'edit', options.signal!); return result; } }; return { ports, snapshot, saved, writes: () => writes }; } const result = { type: 'result', sessionId: 'sess_fixture', response: '已完成' }; async function main() { let checks = 0; { const h = fixture(); h.ports.native = async options => { await options.execute('planCanvasTask', {}, 'plan', options.signal!); await options.execute('edit', { nodeIds: ['a'] }, 'same', options.signal!); await options.execute('edit', { nodeIds: ['a'] }, 'same', options.signal!); return result; }; assert.equal(await runZcodeCanvasTask(h.ports), 'completed'); assert.equal(h.writes(), 1); checks++; } { const h = fixture(); h.ports.native = async options => { await assert.rejects(options.execute('edit', { nodeIds: ['a'] }, 'early', options.signal!), /计划/); await options.execute('planCanvasTask', {}, 'plan', options.signal!); await assert.rejects(options.execute('edit', { nodeIds: 42 }, 'invalid', options.signal!), /参数无效/); await options.execute('edit', { nodeIds: ['a'] }, 'valid', options.signal!); return result; }; assert.equal(await runZcodeCanvasTask(h.ports), 'completed'); assert.equal(h.writes(), 1); checks++; } { const h = fixture(); h.snapshot.calls.push({ id: 'unknown', name: 'edit', input: {}, status: 'started' }); assert.equal(await runZcodeCanvasTask(h.ports), 'needs_review'); assert.equal(h.writes(), 0); checks++; } { const h = fixture(); h.snapshot.verification = { passed: false, issues: ['bad b'], retryable: true, failedNodeIds: ['b'] }; h.ports.native = async options => { await options.execute('planCanvasTask', {}, 'plan', options.signal!); await assert.rejects(options.execute('edit', { nodeIds: ['a','b'] }, 'cross', options.signal!), /未通过/); await options.execute('edit', { nodeIds: ['b'] }, 'only-failed', options.signal!); return result; }; assert.equal(await runZcodeCanvasTask(h.ports), 'completed'); assert.equal(h.writes(), 1); checks++; } { const h = fixture('画布里有什么'); h.ports.native = async () => ({ ...result, response: '画布为空' }); assert.equal(await runZcodeCanvasTask(h.ports), 'completed'); assert.equal(h.snapshot.message, '画布为空'); checks++; } { const h = fixture('抠图'); h.ports.native = async () => result; assert.equal(await runZcodeCanvasTask(h.ports), 'needs_review'); assert.equal(h.writes(), 0); checks++; } { const h = fixture(); h.ports.isCancelled = async () => true; assert.equal(await runZcodeCanvasTask(h.ports), 'cancelled'); assert.equal(h.writes(), 1); checks++; } { const h = fixture(); let attempts = 0; h.ports.tools.edit.execute = async () => { attempts++; if (attempts === 1) throw new AgentToolRejectedError('参数未通过,未提交'); return { updatedNodeIds: ['a'] }; }; h.ports.native = async options => { await options.execute('planCanvasTask', {}, 'plan', options.signal!); await assert.rejects(options.execute('edit', { nodeIds: ['a'] }, 'rejected', options.signal!)); await options.execute('edit', { nodeIds: ['a'] }, 'corrected', options.signal!); return result; }; assert.equal(await runZcodeCanvasTask(h.ports), 'completed'); assert.equal(h.snapshot.calls.find(c=>c.id==='rejected')?.uncertain,false); checks++; } console.log('NATIVE_TASK_ADAPTER_PASSED', { checks }); } main().catch(error => { console.error(error); process.exitCode = 1; });