feat: Implement VFX management, Undo/Redo, Find in File; docs: Update README and plans
This commit is contained in:
309
test/run_tests.js
Normal file
309
test/run_tests.js
Normal file
@@ -0,0 +1,309 @@
|
||||
const http = require('http');
|
||||
|
||||
// 配置
|
||||
const CONFIG = {
|
||||
host: '127.0.0.1',
|
||||
port: 3456,
|
||||
timeout: 5000
|
||||
};
|
||||
|
||||
// 控制台输出颜色
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
cyan: "\x1b[36m",
|
||||
gray: "\x1b[90m"
|
||||
};
|
||||
|
||||
function log(type, msg) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
switch (type) {
|
||||
case 'info': console.log(`${colors.cyan}[INFO]${colors.reset} ${msg}`); break;
|
||||
case 'success': console.log(`${colors.green}[PASS]${colors.reset} ${msg}`); break;
|
||||
case 'error': console.log(`${colors.red}[FAIL]${colors.reset} ${msg}`); break;
|
||||
case 'warn': console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`); break;
|
||||
case 'group': console.log(`\n${colors.gray}=== ${msg} ===${colors.reset}`); break;
|
||||
default: console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP 辅助函数
|
||||
function request(method, path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: CONFIG.host,
|
||||
port: CONFIG.port,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: CONFIG.timeout
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
// MCP 返回 { content: [{ type: 'text', text: "..." }] }
|
||||
resolve(parsed);
|
||||
} catch (e) {
|
||||
// 某些接口可能返回纯文本或非标准 JSON
|
||||
resolve(body);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => reject(new Error(`连接失败: ${e.message}`)));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
if (data) {
|
||||
req.write(JSON.stringify(data));
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// MCP 工具调用封装
|
||||
async function callTool(name, args = {}) {
|
||||
const payload = {
|
||||
name: name,
|
||||
arguments: args
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await request('POST', '/call-tool', payload);
|
||||
|
||||
// 解析复杂的 MCP 响应结构
|
||||
// 预期: { content: [ { type: 'text', text: "..." } ] }
|
||||
if (response && response.content && Array.isArray(response.content)) {
|
||||
const textContent = response.content.find(c => c.type === 'text');
|
||||
if (textContent) {
|
||||
// 工具结果本身可能是 JSON 字符串,尝试解析它
|
||||
try {
|
||||
return JSON.parse(textContent.text);
|
||||
} catch {
|
||||
return textContent.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw new Error(`工具 [${name}] 调用失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 断言辅助函数
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message || "断言失败");
|
||||
}
|
||||
}
|
||||
|
||||
// --- 测试套件 ---
|
||||
const tests = {
|
||||
async setup() {
|
||||
log('group', '连接性检查');
|
||||
try {
|
||||
const tools = await request('POST', '/list-tools');
|
||||
assert(tools && tools.tools && tools.tools.length > 0, "无法获取工具列表");
|
||||
log('success', `已连接到 MCP 服务器。发现 ${tools.tools.length} 个工具。`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log('error', `无法连接服务器。插件是否正在运行? (${e.message})`);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async testNodeLifecycle() {
|
||||
log('group', '节点生命周期测试');
|
||||
const nodeName = `TestNode_${Date.now()}`;
|
||||
|
||||
try {
|
||||
// 1. 创建节点
|
||||
log('info', `尝试创建节点: ${nodeName}`);
|
||||
const newNodeId = await callTool('create_node', { name: nodeName, type: 'empty' });
|
||||
log('info', `create_node 响应: ${JSON.stringify(newNodeId)}`);
|
||||
assert(typeof newNodeId === 'string' && newNodeId.length > 0, `create_node 没有返回 UUID。实际返回: ${JSON.stringify(newNodeId)}`);
|
||||
log('success', `已创建节点: ${nodeName} (${newNodeId})`);
|
||||
|
||||
// 2. 查找节点
|
||||
log('info', `尝试查找节点: ${nodeName}`);
|
||||
const findResult = await callTool('find_gameobjects', { conditions: { name: nodeName } });
|
||||
log('info', `find_gameobjects 响应: ${JSON.stringify(findResult)}`);
|
||||
assert(Array.isArray(findResult), `find_gameobjects 没有返回数组。实际返回: ${JSON.stringify(findResult)}`);
|
||||
assert(findResult.length >= 1, "find_gameobjects 未能找到已创建的节点");
|
||||
|
||||
// 查找特定节点(防止重名,虽然这里名字包含时间戳)
|
||||
const targetNode = findResult.find(n => n.name === nodeName);
|
||||
assert(targetNode, "找到节点但名称不匹配?");
|
||||
assert(targetNode.uuid === newNodeId, `找到的节点 UUID 不匹配。预期 ${newNodeId}, 实际 ${targetNode.uuid}`);
|
||||
log('success', `通过 find_gameobjects 找到节点: ${targetNode.name}`);
|
||||
|
||||
// 3. 更新变换 (Transform)
|
||||
log('info', `尝试更新变换信息`);
|
||||
await callTool('update_node_transform', { id: newNodeId, x: 100, y: 200 });
|
||||
// 通过查找验证(因为查找会返回位置信息)
|
||||
const updatedResult = await callTool('find_gameobjects', { conditions: { name: nodeName } });
|
||||
const updatedNode = updatedResult.find(n => n.uuid === newNodeId);
|
||||
log('info', `变换更新验证: x=${updatedNode.position.x}, y=${updatedNode.position.y}`);
|
||||
assert(updatedNode.position.x === 100 && updatedNode.position.y === 200, `节点位置更新失败。实际: (${updatedNode.position.x}, ${updatedNode.position.y})`);
|
||||
log('success', `节点变换已更新至 (100, 200)`);
|
||||
|
||||
return newNodeId; // 返回以供后续测试使用
|
||||
} catch (e) {
|
||||
log('error', `节点生命周期测试失败: ${e.message}`);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async testComponents(nodeId) {
|
||||
log('group', '组件管理测试');
|
||||
|
||||
// 1. 添加组件
|
||||
// 使用 cc.Sprite 因为它最常用
|
||||
log('info', `向 ${nodeId} 添加组件 cc.Sprite`);
|
||||
const addResult = await callTool('manage_components', {
|
||||
nodeId: nodeId,
|
||||
action: 'add',
|
||||
componentType: 'cc.Sprite'
|
||||
});
|
||||
log('success', `已添加 cc.Sprite 组件。响应: ${JSON.stringify(addResult)}`);
|
||||
|
||||
// 2. 获取组件
|
||||
log('info', `列出 ${nodeId} 的组件`);
|
||||
const components = await callTool('manage_components', { nodeId: nodeId, action: 'get' });
|
||||
log('info', `manage_components (get) 响应: ${JSON.stringify(components)}`);
|
||||
|
||||
assert(Array.isArray(components), `无法获取组件列表。实际返回: ${JSON.stringify(components)}`);
|
||||
// 宽松匹配:验证逻辑匹配(检查 type 或 properties.name 中是否包含 Sprite)
|
||||
const spriteComp = components.find(c => (c.type && c.type.includes('Sprite')) || (c.properties && c.properties.name && c.properties.name.includes('Sprite')));
|
||||
assert(spriteComp, "节点上未找到 cc.Sprite 组件");
|
||||
log('success', `验证组件存在: ${spriteComp.uuid} (${spriteComp.type || 'Unknown'})`);
|
||||
|
||||
// 3. 移除组件
|
||||
log('info', `移除组件 ${spriteComp.uuid}`);
|
||||
const removeResult = await callTool('manage_components', {
|
||||
nodeId: nodeId,
|
||||
action: 'remove',
|
||||
componentId: spriteComp.uuid
|
||||
});
|
||||
log('info', `移除结果: ${JSON.stringify(removeResult)}`);
|
||||
|
||||
// 等待引擎处理移除(异步过程)
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
// 验证移除
|
||||
const componentsAfter = await callTool('manage_components', { nodeId: nodeId, action: 'get' });
|
||||
log('info', `移除后的组件列表: ${JSON.stringify(componentsAfter)}`);
|
||||
|
||||
assert(!componentsAfter.find(c => (c.type && c.type.includes('Sprite')) || (c.uuid === spriteComp.uuid)), "组件未被移除");
|
||||
log('success', `组件移除成功`);
|
||||
},
|
||||
|
||||
async testEditorSelection(nodeId) {
|
||||
log('group', '编辑器选中测试');
|
||||
|
||||
// 1. 设置选中
|
||||
await callTool('manage_editor', {
|
||||
action: 'set_selection',
|
||||
target: 'node',
|
||||
properties: { nodes: [nodeId] }
|
||||
});
|
||||
|
||||
// 2. 获取选中
|
||||
const selection = await callTool('manage_editor', { action: 'get_selection' });
|
||||
// 预期: { nodes: [...], assets: [...] }
|
||||
assert(selection.nodes && selection.nodes.includes(nodeId), "选中状态更新失败");
|
||||
log('success', `编辑器选中状态已更新为节点 ${nodeId}`);
|
||||
},
|
||||
|
||||
async testAssetManagement() {
|
||||
log('group', '资源管理测试');
|
||||
const scriptPath = 'db://assets/temp_auto_test.js';
|
||||
|
||||
// 1. 创建脚本
|
||||
try {
|
||||
await callTool('manage_script', {
|
||||
action: 'create',
|
||||
path: scriptPath,
|
||||
content: 'cc.log("Test Script");'
|
||||
});
|
||||
log('success', `已创建临时资源: ${scriptPath}`);
|
||||
} catch (e) {
|
||||
if (e.message.includes('exists')) {
|
||||
log('warn', `资源已存在,正在尝试先删除...`);
|
||||
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
||||
// 重试创建
|
||||
await callTool('manage_script', { action: 'create', path: scriptPath, content: 'cc.log("Test Script");' });
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取信息
|
||||
// 等待 AssetDB 刷新 (导入需要时间)
|
||||
log('info', '等待 3 秒以进行资源导入...');
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
log('info', `获取资源信息: ${scriptPath}`);
|
||||
const info = await callTool('manage_asset', { action: 'get_info', path: scriptPath });
|
||||
log('info', `资源信息: ${JSON.stringify(info)}`);
|
||||
|
||||
assert(info && info.url === scriptPath, "无法获取资源信息");
|
||||
log('success', `已验证资源信息`);
|
||||
|
||||
// 3. 删除资源
|
||||
await callTool('manage_asset', { action: 'delete', path: scriptPath });
|
||||
|
||||
// 验证删除 (get_info 应该失败或返回 null/报错,但我们检查工具响应)
|
||||
try {
|
||||
const infoDeleted = await callTool('manage_asset', { action: 'get_info', path: scriptPath });
|
||||
// 如果返回了信息且 exists 为 true,说明没删掉
|
||||
assert(!(infoDeleted && infoDeleted.exists), "资源本应被删除,但仍然存在");
|
||||
} catch (e) {
|
||||
// 如果报错(如 Asset not found),则符合预期
|
||||
log('success', `已验证资源删除`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function run() {
|
||||
console.log(`\n${colors.cyan}正在启动 MCP Bridge 自动化测试...${colors.reset}`);
|
||||
console.log(`目标: http://${CONFIG.host}:${CONFIG.port}\n`);
|
||||
|
||||
const isConnected = await tests.setup();
|
||||
if (!isConnected) process.exit(1);
|
||||
|
||||
try {
|
||||
const nodeId = await tests.testNodeLifecycle();
|
||||
|
||||
await tests.testComponents(nodeId);
|
||||
|
||||
await tests.testEditorSelection(nodeId);
|
||||
|
||||
await tests.testAssetManagement();
|
||||
|
||||
// 清理:我们在测试中已经尽可能清理了,但保留节点可能有助于观察结果
|
||||
// 这里只是打印完成消息
|
||||
|
||||
console.log(`\n${colors.green}所有测试已成功完成!${colors.reset}\n`);
|
||||
} catch (e) {
|
||||
console.error(`\n${colors.red}[FATAL ERROR]${colors.reset} 测试套件出错:`);
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
107
test/test_find_file.js
Normal file
107
test/test_find_file.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const http = require('http');
|
||||
|
||||
const CONFIG = {
|
||||
host: '127.0.0.1',
|
||||
port: 3456,
|
||||
timeout: 5000
|
||||
};
|
||||
|
||||
// HTTP Helper
|
||||
function request(method, path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: CONFIG.host,
|
||||
port: CONFIG.port,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: CONFIG.timeout
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try { resolve(JSON.parse(body)); } catch (e) { resolve(body); }
|
||||
} else {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => reject(new Error(`Connection failed: ${e.message}`)));
|
||||
req.end(data ? JSON.stringify(data) : undefined);
|
||||
});
|
||||
}
|
||||
|
||||
async function callTool(name, args = {}) {
|
||||
const payload = { name: name, arguments: args };
|
||||
const response = await request('POST', '/call-tool', payload);
|
||||
if (response && response.content && Array.isArray(response.content)) {
|
||||
const textContent = response.content.find(c => c.type === 'text');
|
||||
if (textContent) {
|
||||
try { return JSON.parse(textContent.text); } catch { return textContent.text; }
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log("Testing find_in_file...");
|
||||
|
||||
try {
|
||||
// 1. Check tools
|
||||
const tools = await request('POST', '/list-tools');
|
||||
const findTool = tools.tools.find(t => t.name === 'find_in_file');
|
||||
if (!findTool) {
|
||||
console.error("FAILED: find_in_file tool not found in list.");
|
||||
return;
|
||||
}
|
||||
console.log("PASS: find_in_file exists in tool list.");
|
||||
|
||||
// 2. Create a temp file to search for
|
||||
const tempFilePath = "db://assets/test_find_me.txt";
|
||||
const uniqueString = "UniqueStringToFind_" + Date.now();
|
||||
console.log(`Creating temp file with content "${uniqueString}"...`);
|
||||
|
||||
await callTool('manage_asset', {
|
||||
action: 'create',
|
||||
path: tempFilePath,
|
||||
content: `This is a test file.\nIt contains ${uniqueString} here.`
|
||||
});
|
||||
|
||||
// Wait a bit for assetdb to refresh
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// 3. Call find_in_file
|
||||
console.log(`Searching for "${uniqueString}"...`);
|
||||
|
||||
const results = await callTool('find_in_file', { query: uniqueString });
|
||||
|
||||
if (!Array.isArray(results)) {
|
||||
console.error("FAILED: Result is not an array:", results);
|
||||
// Cleanup
|
||||
await callTool('manage_asset', { action: 'delete', path: tempFilePath });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${results.length} matches.`);
|
||||
const match = results.find(r => r.content.includes(uniqueString));
|
||||
|
||||
if (match) {
|
||||
console.log("PASS: Found match in created file.");
|
||||
console.log("Match Details:", match);
|
||||
} else {
|
||||
console.error("FAILED: Did not find match. Results:", results);
|
||||
}
|
||||
|
||||
// 4. Cleanup
|
||||
await callTool('manage_asset', { action: 'delete', path: tempFilePath });
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
146
test/test_undo.js
Normal file
146
test/test_undo.js
Normal file
@@ -0,0 +1,146 @@
|
||||
const http = require('http');
|
||||
|
||||
const CONFIG = {
|
||||
host: '127.0.0.1',
|
||||
port: 3456,
|
||||
timeout: 5000
|
||||
};
|
||||
|
||||
// HTTP Helper
|
||||
function request(method, path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: CONFIG.host,
|
||||
port: CONFIG.port,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: CONFIG.timeout
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try { resolve(JSON.parse(body)); } catch (e) { resolve(body); }
|
||||
} else {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => reject(new Error(`Connection failed: ${e.message}`)));
|
||||
req.end(data ? JSON.stringify(data) : undefined);
|
||||
});
|
||||
}
|
||||
|
||||
async function callTool(name, args = {}) {
|
||||
const payload = { name: name, arguments: args };
|
||||
const response = await request('POST', '/call-tool', payload);
|
||||
if (response && response.content && Array.isArray(response.content)) {
|
||||
const textContent = response.content.find(c => c.type === 'text');
|
||||
if (textContent) {
|
||||
try { return JSON.parse(textContent.text); } catch { return textContent.text; }
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Helper to wait
|
||||
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
async function run() {
|
||||
console.log("Testing manage_undo...");
|
||||
|
||||
try {
|
||||
// 1. Create a node
|
||||
const nodeName = "UndoTestNode_" + Date.now();
|
||||
console.log(`Creating node: ${nodeName}`);
|
||||
const nodeId = await callTool('create_node', { name: nodeName, type: 'empty' });
|
||||
|
||||
if (!nodeId || typeof nodeId !== 'string') {
|
||||
console.error("FAILED: Could not create node.", nodeId);
|
||||
return;
|
||||
}
|
||||
console.log(`Node created: ${nodeId}`);
|
||||
|
||||
// Wait to ensure creation is fully processed
|
||||
await wait(500);
|
||||
|
||||
// 2. Modify node (Change Name)
|
||||
console.log("Modifying node name (Action to undo)...");
|
||||
const newName = "RenamedNode_" + Date.now();
|
||||
await callTool('set_node_name', { id: nodeId, newName: newName });
|
||||
|
||||
await wait(2000);
|
||||
|
||||
// Verify modification
|
||||
let nodes = await callTool('find_gameobjects', { conditions: { name: newName } });
|
||||
let node = nodes.find(n => n.uuid === nodeId);
|
||||
|
||||
if (!node) {
|
||||
console.error(`FAILED: Node not found with new name ${newName}. Name update failed.`);
|
||||
// Try to read console logs to see why
|
||||
const logs = await callTool('read_console', { limit: 10, type: 'error' });
|
||||
console.log("Recent Error Logs:", JSON.stringify(logs, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(`Node renamed to ${node.name}.`);
|
||||
|
||||
// 3. Perform UNDO
|
||||
console.log("Executing UNDO...");
|
||||
await callTool('manage_undo', { action: 'undo' });
|
||||
|
||||
await wait(2000);
|
||||
|
||||
// Verify UNDO
|
||||
nodes = await callTool('find_gameobjects', { conditions: { name: nodeName } });
|
||||
// The original name was in variable nodeName
|
||||
node = nodes.find(n => n.uuid === nodeId);
|
||||
|
||||
if (node && node.name === nodeName) {
|
||||
console.log(`PASS: Undo successful. Node name returned to ${nodeName}.`);
|
||||
} else {
|
||||
console.error(`FAILED: Undo failed? Node name is ${node ? node.name : 'Unknown'}`);
|
||||
}
|
||||
|
||||
// 4. Perform REDO
|
||||
console.log("Executing REDO...");
|
||||
await callTool('manage_undo', { action: 'redo' });
|
||||
|
||||
await wait(2000);
|
||||
|
||||
// Verify REDO
|
||||
nodes = await callTool('find_gameobjects', { conditions: { name: newName } });
|
||||
node = nodes.find(n => n.uuid === nodeId);
|
||||
|
||||
if (node && node.name === newName) {
|
||||
console.log("PASS: Redo successful. Node name returned to " + newName + ".");
|
||||
} else {
|
||||
console.error(`FAILED: Redo failed? Node name is ${node ? node.name : 'Unknown'}`);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
// await callTool('manage_undo', { action: 'begin_group', description: 'Delete Node' }); // Optional
|
||||
// Node deletion tool... wait, we don't have delete_node tool exposed yet?
|
||||
// Ah, 'scene:delete-nodes' is internal.
|
||||
// We can use 'batch_execute' if we had a delete tool.
|
||||
// Checking available tools... we assume we can manually delete or leave it.
|
||||
// Actually, let's construct a delete call if possible via existing tools?
|
||||
// create_node, manage_components...
|
||||
// Wait, DEVELOPMENT_PLAN says 'batch_execute' exists.
|
||||
// But we don't have a direct 'delete_node' in getToolsList().
|
||||
// Oh, we missed implementing 'delete_node' in the previous phases?
|
||||
// Let's check main.js getToolsList again.
|
||||
// ... It has 'create_node', 'manage_components', ... 'scene_management'...
|
||||
// 'scene_management' has 'delete'? -> "场景管理" -> create, delete (scene file), duplicate.
|
||||
// It seems we lack `delete_node`.
|
||||
// Nevermind, letting the test node stay is fine for observation, or user can delete manually.
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
175
test/test_vfx.js
Normal file
175
test/test_vfx.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const http = require('http');
|
||||
|
||||
const CONFIG = {
|
||||
host: '127.0.0.1',
|
||||
port: 3456, // Ideally read from profile or keep dynamic, but fixed for test
|
||||
timeout: 5000
|
||||
};
|
||||
|
||||
// HTTP Helper
|
||||
function request(method, path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: CONFIG.host,
|
||||
port: CONFIG.port,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: CONFIG.timeout
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try { resolve(JSON.parse(body)); } catch (e) { resolve(body); }
|
||||
} else {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => reject(new Error(`Connection failed: ${e.message}`)));
|
||||
req.end(data ? JSON.stringify(data) : undefined);
|
||||
});
|
||||
}
|
||||
|
||||
async function callTool(name, args = {}) {
|
||||
const payload = { name: name, arguments: args };
|
||||
const response = await request('POST', '/call-tool', payload);
|
||||
if (response && response.content && Array.isArray(response.content)) {
|
||||
const textContent = response.content.find(c => c.type === 'text');
|
||||
if (textContent) {
|
||||
try { return JSON.parse(textContent.text); } catch { return textContent.text; }
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Helper to wait
|
||||
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
async function run() {
|
||||
console.log("Testing manage_vfx...");
|
||||
|
||||
try {
|
||||
// 1. Create a Particle Node
|
||||
const nodeName = "VFX_Test_" + Date.now();
|
||||
console.log(`Creating particle node: ${nodeName}`);
|
||||
|
||||
const createResult = await callTool('manage_vfx', {
|
||||
action: 'create',
|
||||
name: nodeName,
|
||||
properties: {
|
||||
duration: 5,
|
||||
emissionRate: 50,
|
||||
startColor: "#FF0000",
|
||||
endColor: "#0000FF"
|
||||
}
|
||||
});
|
||||
|
||||
let nodeId = createResult;
|
||||
// Check if result is UUID string or object
|
||||
if (typeof createResult === 'object') {
|
||||
// Sometimes mcp-bridge returns object? No, scene-script returns uuid or error.
|
||||
// But checking just in case
|
||||
nodeId = createResult.uuid || createResult;
|
||||
}
|
||||
|
||||
if (!nodeId || typeof nodeId !== 'string') {
|
||||
console.error("FAILED: Could not create VFX node.", createResult);
|
||||
return;
|
||||
}
|
||||
console.log(`VFX Node created: ${nodeId}`);
|
||||
|
||||
await wait(1000);
|
||||
|
||||
// 2. Perform Undo (Verify creation undo)
|
||||
// ... Optional, let's focus on Update first.
|
||||
|
||||
// 3. Update Particle Properties
|
||||
console.log("Updating particle properties...");
|
||||
const updateResult = await callTool('manage_vfx', {
|
||||
action: 'update',
|
||||
nodeId: nodeId,
|
||||
properties: {
|
||||
emissionRate: 100,
|
||||
startSize: 50,
|
||||
speed: 200
|
||||
}
|
||||
});
|
||||
console.log("Update result:", updateResult);
|
||||
|
||||
await wait(1000);
|
||||
|
||||
// 4. Get Info to Verify
|
||||
console.log("Verifying properties...");
|
||||
const info = await callTool('manage_vfx', { action: 'get_info', nodeId: nodeId });
|
||||
|
||||
if (!info) {
|
||||
console.error("FAILED: Could not get info.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Particle Info:", JSON.stringify(info, null, 2));
|
||||
|
||||
if (info.emissionRate === 100 && info.speed === 200) {
|
||||
console.log("PASS: Properties updated and verified.");
|
||||
} else {
|
||||
console.error("FAILED: Properties mismatch.");
|
||||
}
|
||||
|
||||
// 5. Verify 'custom' property using manage_components
|
||||
// We need to ensure custom is true for properties to take effect visually
|
||||
console.log("Verifying 'custom' property...");
|
||||
const components = await callTool('manage_components', {
|
||||
nodeId: nodeId,
|
||||
action: 'get'
|
||||
});
|
||||
|
||||
let particleComp = null;
|
||||
if (components && Array.isArray(components)) {
|
||||
particleComp = components.find(c => c.type === 'cc.ParticleSystem' || c.type === 'ParticleSystem');
|
||||
}
|
||||
|
||||
if (particleComp && particleComp.properties) {
|
||||
if (particleComp.properties.custom === true) {
|
||||
console.log("PASS: ParticleSystem.custom is TRUE.");
|
||||
} else {
|
||||
console.error("FAILED: ParticleSystem.custom is FALSE or Undefined.", particleComp.properties.custom);
|
||||
}
|
||||
|
||||
// Check texture/file if possible
|
||||
if (particleComp.properties.file || particleComp.properties.texture) {
|
||||
console.log("PASS: ParticleSystem has file/texture.");
|
||||
} else {
|
||||
console.warn("WARNING: ParticleSystem might not have a texture/file set.");
|
||||
}
|
||||
} else {
|
||||
console.error("FAILED: Could not retrieve component details.");
|
||||
}
|
||||
|
||||
await wait(1000);
|
||||
|
||||
// 6. Fetch Logs to debug texture loading
|
||||
console.log("Fetching recent Editor Logs...");
|
||||
const logs = await callTool('read_console', { limit: 20 });
|
||||
if (logs && Array.isArray(logs)) {
|
||||
logs.forEach(log => {
|
||||
const msg = log.message || JSON.stringify(log);
|
||||
const type = log.type || 'info';
|
||||
|
||||
// Filter for our debug logs or errors
|
||||
if (typeof msg === 'string' && (msg.includes("[mcp-bridge]") || type === 'error' || type === 'warn')) {
|
||||
console.log(`[Editor Log] [${type}] ${msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user