import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { createToolGateway, extractToolInstruction, hardenAiderLaunchPlan, prepareAiderReceiptFile, resolveAiderReceiptPath, } from './tool-gateway.mjs'; test('tool gateway extracts text instructions from H5 message content', () => { assert.equal( extractToolInstruction({ content: [ { type: 'text', text: '第一步' }, { type: 'image_url', image_url: { url: 'https://example.test/a.png' } }, { type: 'text', text: '第二步' }, ], }), '第一步\n第二步', ); }); test('tool gateway is disabled by default and reports protocol', () => { const gateway = createToolGateway({ env: {} }); assert.deepEqual(gateway.getStatus(), { enabled: false, dryRun: false, protocol: 'agent-run-v1', executors: ['aider', 'openhands'], defaultExecutor: 'aider', openhandsTaskTypes: ['repo_refactor', 'multi_file', 'complex_repo', 'page_data_dev_complex'], }); }); test('tool gateway selects openhands for configured task types', () => { const gateway = createToolGateway({ env: { MEMIND_TOOL_GATEWAY_DEFAULT_EXECUTOR: 'aider', MEMIND_TOOL_GATEWAY_OPENHANDS_TASK_TYPES: 'repo_refactor', }, }); assert.equal(gateway.selectExecutor({ taskType: 'repo_refactor' }), 'openhands'); assert.equal(gateway.selectExecutor({ taskType: 'small_patch' }), 'aider'); }); test('tool gateway selects openhands for page_data_dev_complex by default', () => { const gateway = createToolGateway({ env: {} }); assert.equal(gateway.selectExecutor({ taskType: 'page_data_dev_complex' }), 'openhands'); }); test('tool gateway honors an explicitly required Aider executor over task defaults', () => { const gateway = createToolGateway({ env: {} }); assert.equal(gateway.selectExecutor({ taskType: 'page_data_dev_complex', userMessage: { metadata: { memindRun: { executor: 'aider', selectedChatSkill: 'aider-development', }, }, }, }), 'aider'); }); test('tool gateway resolves and prepares the required Aider receipt without pre-validating it', async () => { const requestId = 'req-receipt'; const relativePath = `.memind/agent-runs/${requestId}.json`; const userMessage = { metadata: { memindRun: { validation: { expectedFile: { path: relativePath, contains: requestId }, }, }, }, }; assert.equal(resolveAiderReceiptPath(userMessage, requestId), relativePath); const cwd = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-aider-receipt-')); try { const target = await prepareAiderReceiptFile(cwd, relativePath); const pending = JSON.parse(await fs.readFile(target, 'utf8')); assert.deepEqual(pending, { status: 'pending', executor: 'aider' }); assert.equal((await fs.readFile(target, 'utf8')).includes(requestId), false); } finally { await fs.rm(cwd, { recursive: true, force: true }); } }); test('tool gateway hardens Aider against repository commits and includes the receipt', () => { const plan = hardenAiderLaunchPlan( { ok: true, args: ['--model', 'test-model'] }, '/tmp/work/.memind/agent-runs/req.json', ['/tmp/work/public/order.html'], ); assert.ok(plan.args.includes('--no-git')); assert.ok(plan.args.includes('--no-auto-commits')); assert.ok(plan.args.includes('--no-dirty-commits')); assert.deepEqual( plan.args.slice(-2), ['--file', '/tmp/work/.memind/agent-runs/req.json'], ); assert.ok(plan.args.includes('/tmp/work/public/order.html')); }); test('tool gateway dry run builds executor launch plan without spawning', async () => { const plans = []; const gateway = createToolGateway({ env: { MEMIND_TOOL_GATEWAY_ENABLED: '1', MEMIND_TOOL_GATEWAY_DRY_RUN: '1', MEMIND_TOOL_GATEWAY_DEFAULT_EXECUTOR: 'aider', }, llmProviderService: { async getExecutorLaunchPlan(executor, options) { plans.push({ executor, options }); return { ok: true, executor, cwd: options.cwd, command: '/usr/bin/true', args: ['--noop'], }; }, }, }); const result = await gateway.executeJob({ runId: 'run-1', requestId: 'req-1', userId: 'user-1', cwd: '/tmp/work', taskType: 'small_patch', userMessage: { content: [{ type: 'text', text: 'fix it' }] }, }); assert.equal(result.dryRun, true); assert.equal(result.executor, 'aider'); assert.equal(result.command, '/usr/bin/true'); assert.equal(plans.length, 1); assert.equal(plans[0].options.instruction, 'fix it'); });