#!/usr/bin/env node 'use strict'; const assert = require('assert/strict'); const fs = require('fs'); const path = require('path'); const REPO_DIR = path.resolve(__dirname, '..'); const SERVER_PATH = path.join(REPO_DIR, 'server.js'); const mcpServer = require(path.join(REPO_DIR, 'lib', 'ccweb-mcp-server.js')); const serverSource = fs.readFileSync(SERVER_PATH, 'utf8'); function check(label, fn) { try { fn(); } catch (err) { err.message = `${label}: ${err.message}`; throw err; } } function toolNames(tools) { return tools.map((tool) => tool && tool.name).filter(Boolean); } function findTool(tools, name) { return tools.find((tool) => tool && tool.name === name); } function replyModeValues(value) { if (Array.isArray(value)) return value; if (value && typeof value === 'object') return Object.values(value); return []; } function withoutNamespace(tool) { const clone = { ...tool }; delete clone.namespace; return clone; } function assertExactReplyModeEnum(actual, label) { assert.deepEqual( actual, ['one_way', 'return_and_continue'], `${label} must be exactly ["one_way", "return_and_continue"]`, ); } check('public TOOLS expose only the unified send communication tool', () => { assert.ok(Array.isArray(mcpServer.TOOLS), 'TOOLS must be exported as an array'); const names = toolNames(mcpServer.TOOLS); assert.equal( names.filter((name) => name === 'ccweb_send_message').length, 1, `expected exactly one ccweb_send_message, got: ${names.join(', ')}`, ); assert.equal( names.includes('ccweb_request_reply'), false, `ccweb_request_reply must be hidden from public TOOLS; got: ${names.join(', ')}`, ); }); const publicSendTool = findTool(mcpServer.TOOLS, 'ccweb_send_message'); check('ccweb_send_message schema requires replyMode with the exact enum', () => { assert.ok(publicSendTool, 'ccweb_send_message must exist in public TOOLS'); const schema = publicSendTool.inputSchema || {}; const required = schema.required || []; assert.ok(required.includes('targetConversationId'), 'required must include targetConversationId'); assert.ok(required.includes('content'), 'required must include content'); assert.ok(required.includes('replyMode'), 'required must include replyMode'); assertExactReplyModeEnum(schema.properties?.replyMode?.enum, 'replyMode enum'); }); check('ccweb_send_message description states strong selection and non-blocking rules', () => { const description = String(publicSendTool.description || ''); assert.match(description, /one_way/, 'description must mention one_way'); assert.match(description, /return_and_continue/, 'description must mention return_and_continue'); assert.match(description, /立即返回/, 'description must say calls return immediately'); assert.match(description, /(不阻塞|不会阻塞|不等待)/, 'description must state the call does not block/wait'); assert.match( description, /(仅当[\s\S]*来源[\s\S]*不需要[\s\S]*结果[\s\S]*one_way|one_way[\s\S]*仅当[\s\S]*来源[\s\S]*不需要[\s\S]*结果)/, 'description must say one_way is only for cases where the source does not need the result', ); assert.match( description, /(分析|实现|测试|验收|完成后汇报|后续依赖)[\s\S]*必须[\s\S]*return_and_continue|return_and_continue[\s\S]*必须[\s\S]*(分析|实现|测试|验收|完成后汇报|后续依赖)/, 'description must say result-dependent work must use return_and_continue', ); }); check('future compatibility exports are present and keep hidden legacy callable', () => { assertExactReplyModeEnum( replyModeValues(mcpServer.CCWEB_REPLY_MODES).sort(), 'CCWEB_REPLY_MODES values', ); assert.equal(typeof mcpServer.isCallableToolName, 'function', 'isCallableToolName must be exported'); assert.equal( mcpServer.isCallableToolName('ccweb_send_message'), true, 'ccweb_send_message must be callable', ); assert.equal( mcpServer.isCallableToolName('ccweb_request_reply'), true, 'hidden legacy ccweb_request_reply must remain callable', ); assert.equal( typeof mcpServer.codexAppCommunicationTools, 'function', 'codexAppCommunicationTools must be exported', ); }); check('Codex App fallback communication tools reuse the public send contract', () => { const fallbackTools = mcpServer.codexAppCommunicationTools(); assert.ok(Array.isArray(fallbackTools), 'codexAppCommunicationTools() must return an array'); const names = toolNames(fallbackTools); assert.equal( names.includes('ccweb_request_reply'), false, `fallback tools must not expose ccweb_request_reply; got: ${names.join(', ')}`, ); assert.equal( names.includes('ccweb_display_image'), false, 'fallback tools must not include ccweb_display_image', ); assert.equal( names.includes('ccweb_prompt_user'), false, 'fallback tools must not include ccweb_prompt_user', ); assert.equal( names.some((name) => name.startsWith('ccweb_task_')), false, `fallback tools must not include task-board tools; got: ${names.join(', ')}`, ); const fallbackSendTool = findTool(fallbackTools, 'ccweb_send_message'); assert.ok(fallbackSendTool, 'fallback tools must include ccweb_send_message'); assert.deepEqual( withoutNamespace(fallbackSendTool), publicSendTool, 'fallback ccweb_send_message must match public definition except namespace', ); }); check('server.js dynamic tools and shared call gates reuse the shared contract', () => { assert.match( serverSource, /function\s+codexAppCommunicationDynamicTools\s*\([^)]*\)\s*{[\s\S]{0,600}codexAppCommunicationTools\s*\(/, 'codexAppCommunicationDynamicTools() must delegate to codexAppCommunicationTools()', ); assert.match( serverSource, /case ['"]tools\/call['"]\s*:[\s\S]{0,1400}isCallableToolName\s*\(/, 'shared MCP tools/call gate must use isCallableToolName()', ); assert.match( serverSource, /case ['"]ccweb_request_reply['"]\s*:/, 'internal dispatcher must keep the hidden legacy ccweb_request_reply case', ); }); check('unified runtime persists the original request and maps the legacy reply alias', () => { const sendStart = serverSource.indexOf('function sendCrossConversationMessage'); const sendEnd = serverSource.indexOf('function requestCrossConversationReply', sendStart); const sendSource = serverSource.slice(sendStart, sendEnd); assert.ok(sendStart >= 0 && sendEnd > sendStart, 'sendCrossConversationMessage() source must be present'); assert.match( sendSource, /(args\.replyMode|normalizeCrossConversationReplyMode)/, 'ccweb_send_message runtime must read or normalize replyMode', ); assert.match( sendSource, /originalRequest:\s*content/, 'return_and_continue pending state must persist the original request', ); const legacyStart = serverSource.indexOf('function requestCrossConversationReply'); const legacyEnd = serverSource.indexOf('function listPendingCrossConversationReplies', legacyStart); const legacySource = serverSource.slice(legacyStart, legacyEnd); assert.match( legacySource, /(RETURN_AND_CONTINUE|return_and_continue)/, 'legacy ccweb_request_reply must map explicitly to return_and_continue', ); }); check('target runtime prompt explains both reply modes and prevents duplicate return', () => { const promptStart = serverSource.indexOf('function buildCrossConversationRuntimeText'); const promptEnd = serverSource.indexOf('function buildCrossConversationReplyContent', promptStart); const promptSource = serverSource.slice(promptStart, promptEnd); assert.match(promptSource, /one_way/, 'target prompt must identify one_way mode'); assert.match(promptSource, /return_and_continue/, 'target prompt must identify return_and_continue mode'); assert.match( promptSource, /(不会自动回传|不会自动写回来源)/, 'one_way target prompt must state that no automatic return occurs', ); assert.match( promptSource, /(自动回传|自动写回来源)/, 'return_and_continue target prompt must state that the system returns the result automatically', ); assert.match( promptSource, /(不要|禁止)[\s\S]*(手工|手动)[\s\S]*(回传|发送)/, 'return_and_continue target prompt must prohibit duplicate manual return', ); }); check('source auto-run prompt carries correlation context and completion guardrail', () => { const promptStart = serverSource.indexOf('function buildCrossConversationReplyAutoRunText'); const promptEnd = serverSource.indexOf('function startCrossConversationReplyAutoRun', promptStart); const promptSource = serverSource.slice(promptStart, promptEnd); assert.match(promptSource, /requestId/, 'source auto-run prompt must include requestId'); assert.match(promptSource, /targetSession\?\.id|targetSession\.id/, 'source auto-run prompt must include target conversation id'); assert.match(promptSource, /originalRequest/, 'source auto-run prompt must include the original request'); assert.match(promptSource, /已返回不等于已完成/, 'source auto-run prompt must distinguish returned from completed'); assert.match(promptSource, /完整/, 'source auto-run prompt must require a completeness check'); }); console.log('ccweb message reply unit checks passed.');