Files
cc-3-8-x-mcp/router/test/offline-tools.test.js
T
furao 14c5b00f14 Initial public release: cc-3-8-x-mcp
Cocos Creator 3.8.x MCP bridge extension with a built-in offline CLI.

Components:
- Editor extension: in-process MCP server exposing scene / asset-db /
  preview / local / editor-process-control tools
- stdio router: aggregates multiple editor instances on one machine,
  with shortName dedup
- offline CLI (cocos-mcp-cli): headless prefab read/write + a wrapper
  around the Cocos CLI build

Pure Node.js, zero third-party dependencies. Licensed under Apache-2.0.
2026-06-06 11:33:19 +08:00

219 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// ============================================================
// router/test/offline-tools.test.js
// T13 offline tool 测试
//
// 直接 require router/src/offline-tools.js,测:
// 1. prefab_query happy pathtree / node / find
// 2. prefab_edit happy pathset-active 写 tmp 文件)
// 3. prefab_batch happy pathopsJson 文件 → editPrefab
// 4. 相对路径 filePath 报错
// 5. prefab_batch opsJsonPath 相对路径报错
// ============================================================
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const os = require('os');
const {
isOfflineTool,
handleOfflineToolCall,
requireAbsolutePath,
OFFLINE_TOOLS,
} = require('../src/offline-tools.js');
// fixture: HomeUI.prefab(只读,在 cli/test/fixtures/
const FIXTURE_PATH = path.resolve(
__dirname,
'../../cli/test/fixtures/HomeUI.prefab'
);
// 复制 fixture 到 tmp 用于写操作
function makeTmp(tag) {
var dst = path.join(os.tmpdir(), 'HomeUI-router-' + tag + '-' + Date.now() + '.prefab');
fs.copyFileSync(FIXTURE_PATH, dst);
return dst;
}
// ── OFFLINE_TOOLS 定义完整性 ────────────────────────────────────
test('OFFLINE_TOOLS 导出 3 个 tool,名称正确', () => {
assert.equal(OFFLINE_TOOLS.length, 3);
var names = OFFLINE_TOOLS.map(function (t) { return t.name; });
assert.ok(names.includes('prefab_query'));
assert.ok(names.includes('prefab_edit'));
assert.ok(names.includes('prefab_batch'));
});
test('isOfflineTool 对已知 name 返回 true,未知 name 返回 false', () => {
assert.equal(isOfflineTool('prefab_query'), true);
assert.equal(isOfflineTool('prefab_edit'), true);
assert.equal(isOfflineTool('prefab_batch'), true);
assert.equal(isOfflineTool('router_list_editors'), false);
assert.equal(isOfflineTool('scene_set_property'), false);
assert.equal(isOfflineTool(''), false);
});
test('每个 offline tool description 包含 "[offline]" 标注', () => {
for (var t of OFFLINE_TOOLS) {
assert.ok(
t.description.includes('[offline]'),
'tool ' + t.name + ' description 应包含 "[offline]"'
);
}
});
// ── requireAbsolutePath ────────────────────────────────────────
test('requireAbsolutePath 相对路径抛错', () => {
assert.throws(
function () { requireAbsolutePath('relative/path.prefab', 'test'); },
/必须是绝对路径/
);
});
test('requireAbsolutePath 绝对路径不抛错', () => {
assert.doesNotThrow(function () {
requireAbsolutePath('/absolute/path.prefab', 'test');
});
});
// ── prefab_query happy path ─────────────────────────────────────
test('prefab_query type=tree 返回 MCP content,根节点名称为 HomeUI', async () => {
var result = await handleOfflineToolCall('prefab_query', {
filePath: FIXTURE_PATH,
selector: { type: 'tree' },
});
assert.ok(Array.isArray(result.content), 'result.content 应是数组');
assert.equal(result.content[0].type, 'text');
var data = JSON.parse(result.content[0].text);
assert.equal(data.name, 'HomeUI', '根节点 name 应为 HomeUI');
assert.ok(Array.isArray(data.children), 'children 应是数组');
});
test('prefab_query 无 selector 默认返回 tree', async () => {
var result = await handleOfflineToolCall('prefab_query', {
filePath: FIXTURE_PATH,
});
var data = JSON.parse(result.content[0].text);
assert.equal(data.name, 'HomeUI');
});
test('prefab_query type=find 返回 cc.Label id 列表', async () => {
var result = await handleOfflineToolCall('prefab_query', {
filePath: FIXTURE_PATH,
selector: { type: 'find', nodeType: 'cc.Label' },
});
var ids = JSON.parse(result.content[0].text);
assert.ok(Array.isArray(ids), 'find 结果应是数组');
assert.ok(ids.length > 0, '应找到至少一个 cc.Label');
ids.forEach(function (id) { assert.equal(typeof id, 'number'); });
});
// ── prefab_query 相对路径报错 ────────────────────────────────────
test('prefab_query 相对路径 filePath 抛错', async () => {
await assert.rejects(
function () {
return handleOfflineToolCall('prefab_query', {
filePath: 'relative/HomeUI.prefab',
});
},
/必须是绝对路径/
);
});
// ── prefab_edit happy path ──────────────────────────────────────
test('prefab_edit set-active 成功,返回 changed=true + opsApplied=1', async () => {
var tmp = makeTmp('edit');
try {
var result = await handleOfflineToolCall('prefab_edit', {
filePath: tmp,
ops: [
{ op: 'set-active', node: 'HomeUI', active: false },
],
});
var data = JSON.parse(result.content[0].text);
assert.equal(data.changed, true, 'changed 应为 true');
assert.equal(data.opsApplied, 1, 'opsApplied 应为 1');
assert.ok(Array.isArray(data.nodesAffected), 'nodesAffected 应是数组');
} finally {
try { fs.unlinkSync(tmp); } catch (_) {}
}
});
// ── prefab_edit 相对路径报错 ─────────────────────────────────────
test('prefab_edit 相对路径 filePath 抛错', async () => {
await assert.rejects(
function () {
return handleOfflineToolCall('prefab_edit', {
filePath: './relative.prefab',
ops: [{ op: 'set-active', node: 'HomeUI', active: false }],
});
},
/必须是绝对路径/
);
});
// ── prefab_batch happy path ─────────────────────────────────────
test('prefab_batch 从 JSON 文件读取 ops,成功写回', async () => {
var tmp = makeTmp('batch');
var opsJson = path.join(os.tmpdir(), 'router-batch-ops-' + Date.now() + '.json');
var ops = [
{ op: 'set-active', node: 'HomeUI', active: true },
];
fs.writeFileSync(opsJson, JSON.stringify(ops), 'utf-8');
try {
var result = await handleOfflineToolCall('prefab_batch', {
filePath: tmp,
opsJsonPath: opsJson,
});
var data = JSON.parse(result.content[0].text);
assert.equal(data.changed, true);
assert.equal(data.opsApplied, 1);
} finally {
try { fs.unlinkSync(tmp); } catch (_) {}
try { fs.unlinkSync(opsJson); } catch (_) {}
}
});
// ── prefab_batch 相对路径报错 ────────────────────────────────────
test('prefab_batch 相对路径 filePath 抛错', async () => {
await assert.rejects(
function () {
return handleOfflineToolCall('prefab_batch', {
filePath: 'relative.prefab',
opsJsonPath: '/absolute/ops.json',
});
},
/必须是绝对路径/
);
});
test('prefab_batch 相对路径 opsJsonPath 抛错', async () => {
await assert.rejects(
function () {
return handleOfflineToolCall('prefab_batch', {
filePath: FIXTURE_PATH,
opsJsonPath: 'relative/ops.json',
});
},
/必须是绝对路径/
);
});