Files
funplay-cocos-mcp/test/scene-management.test.js
T
mingyuansi b65a4f22c3 fix: Windows spawn, prefab path, preview API, asset deps, error messages; add execute-context resource, tool descriptions, scene/script/asset tools
Bug fixes (verified against Cocos Creator 3.8.8):
- diagnostics: shell:true for .cmd/.bat on Windows (T125/T126)
- prefabs: duplicatePrefab target resolves under assets/ (T420)
- cocos-project: add preview.open candidate for 3.8.8 (T444)
- assets-advanced: detect directory assets in inspectAssetDependencies (T110)
- scene: improve component-not-found errors with compilation hint (T429)

Documentation improvements:
- tool-registry: add enum and injected vars to execute_javascript, path format examples, asset ref limitation note
- resources: add cocos://mcp/execute-context resource with variables, patterns, pitfalls

New tools (core 37->38, full 101->110):
- scene-management: create_scene, query_scene_state (core), copy_paste_node, rename_node, reparent_node
- scripts: create_script with component/plain templates
- prefabs: create_prefab
- assets-advanced: batch_asset_ops, find_unused_assets
2026-06-30 21:53:06 +08:00

360 lines
12 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const { createSceneManagementTools, buildSceneContent } = require('../lib/tools/scene-management');
function makeTools(overrides = {}) {
const createSchema = (properties, required) => {
const schema = { type: 'object', properties };
if (required && required.length) {
schema.required = required;
}
return schema;
};
const calls = [];
const sceneBridge = {
call: async (method, args) => {
calls.push({ method, args });
return { ok: true, method, args };
},
};
const tools = createSceneManagementTools({ createSchema, sceneBridge, ...overrides });
const byName = {};
for (const tool of tools) {
byName[tool.name] = tool;
}
return { tools, byName, calls, sceneBridge, createSchema };
}
function mockEditor(requestImpl) {
global.Editor = {
Message: {
request: requestImpl,
},
};
}
function unmockEditor() {
delete global.Editor;
}
test('create_scene requires sceneName and savePath', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.create_scene.handler({ savePath: 'db://assets/x.scene' }), /sceneName is required/);
await assert.rejects(() => byName.create_scene.handler({ sceneName: 'X' }), /savePath is required/);
});
test('create_scene rejects non-.scene savePath', async () => {
const { byName } = makeTools();
await assert.rejects(
() => byName.create_scene.handler({ sceneName: 'X', savePath: 'db://assets/x.prefab' }),
/must end with \.scene/
);
});
test('buildSceneContent reads template and replaces scene name', () => {
const content = buildSceneContent('MyNewScene');
const json = JSON.parse(content);
assert.equal(json[0]._name, 'MyNewScene');
assert.equal(json[1]._name, 'MyNewScene');
assert.equal(json[0].__type__, 'cc.SceneAsset');
assert.equal(json[1].__type__, 'cc.Scene');
assert.ok(Array.isArray(json[1]._children), 'Scene should have _children array');
assert.equal(json[1]._children.length, 0, 'Template scene should have no children');
const globalTypes = json.slice(2).map((o) => o.__type__);
assert.ok(globalTypes.includes('cc.SceneGlobals'));
assert.ok(globalTypes.includes('cc.AmbientInfo'));
assert.ok(globalTypes.includes('cc.ShadowsInfo'));
assert.ok(globalTypes.includes('cc.SkyboxInfo'));
assert.ok(globalTypes.includes('cc.FogInfo'));
assert.ok(globalTypes.includes('cc.OctreeInfo'));
assert.ok(globalTypes.includes('cc.SkinInfo'));
assert.ok(globalTypes.includes('cc.LightProbeInfo'));
assert.ok(globalTypes.includes('cc.PostSettingsInfo'));
});
test('buildSceneContent throws on missing template file', () => {
assert.throws(
() => buildSceneContent('Test', '/nonexistent/path/template.scene'),
/Failed to read scene template/
);
});
test('create_scene creates asset with template content and returns result', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { uuid: 'scene-uuid-1', url: 'db://assets/scenes/NewScene.scene' };
});
try {
const { byName } = makeTools();
const result = await byName.create_scene.handler({
sceneName: 'NewScene',
savePath: 'db://assets/scenes/NewScene.scene',
open: false,
});
assert.equal(result.created, true);
assert.equal(result.sceneName, 'NewScene');
assert.equal(result.uuid, 'scene-uuid-1');
assert.equal(result.opened, false);
assert.equal(requests.length, 1);
assert.equal(requests[0].method, 'create-asset');
assert.equal(requests[0].args[0], 'db://assets/scenes/NewScene.scene');
const content = requests[0].args[1];
assert.ok(typeof content === 'string' && content.length > 0, 'content should be non-empty string');
const json = JSON.parse(content);
assert.equal(json[0]._name, 'NewScene');
assert.equal(json[1]._name, 'NewScene');
assert.equal(json[0].__type__, 'cc.SceneAsset');
assert.equal(json[1].__type__, 'cc.Scene');
} finally {
unmockEditor();
}
});
test('create_scene with overwrite passes option to create-asset', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { uuid: 'scene-uuid-2', url: 'db://assets/scenes/OverwriteScene.scene' };
});
try {
const { byName } = makeTools();
await byName.create_scene.handler({
sceneName: 'OverwriteScene',
savePath: 'db://assets/scenes/OverwriteScene.scene',
open: false,
overwrite: true,
});
assert.equal(requests[0].args.length, 3, 'should have 3 args: url, content, option');
assert.deepEqual(requests[0].args[2], { overwrite: true });
} finally {
unmockEditor();
}
});
test('query_scene_state is_dirty calls query-dirty', async () => {
const requests = [];
mockEditor(async (channel, method) => {
requests.push({ channel, method });
return true;
});
try {
const { byName } = makeTools();
const result = await byName.query_scene_state.handler({ action: 'is_dirty' });
assert.equal(result.action, 'is_dirty');
assert.equal(result.result, true);
assert.equal(requests[0].method, 'query-dirty');
} finally {
unmockEditor();
}
});
test('query_scene_state is_ready calls query-is-ready', async () => {
const requests = [];
mockEditor(async (channel, method) => {
requests.push({ channel, method });
return true;
});
try {
const { byName } = makeTools();
const result = await byName.query_scene_state.handler({ action: 'is_ready' });
assert.equal(result.action, 'is_ready');
assert.equal(requests[0].method, 'query-is-ready');
} finally {
unmockEditor();
}
});
test('query_scene_state soft_reload calls soft-reload', async () => {
const requests = [];
mockEditor(async (channel, method) => {
requests.push({ channel, method });
return null;
});
try {
const { byName } = makeTools();
const result = await byName.query_scene_state.handler({ action: 'soft_reload' });
assert.equal(result.action, 'soft_reload');
assert.equal(requests[0].method, 'soft-reload');
} finally {
unmockEditor();
}
});
test('query_scene_state rejects unknown action', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.query_scene_state.handler({ action: 'unknown' }), /Unknown action/);
});
test('copy_paste_node copy requires uuids', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'copy' }), /uuids is required/);
});
test('copy_paste_node copy calls copy-node', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'copy',
uuids: ['uuid-1', 'uuid-2'],
});
assert.equal(result.action, 'copy');
assert.deepEqual(result.uuids, ['uuid-1', 'uuid-2']);
assert.equal(requests[0].method, 'copy-node');
assert.deepEqual(requests[0].args[0], ['uuid-1', 'uuid-2']);
} finally {
unmockEditor();
}
});
test('copy_paste_node cut calls cut-node', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'cut',
uuids: ['uuid-1'],
});
assert.equal(result.action, 'cut');
assert.equal(requests[0].method, 'cut-node');
} finally {
unmockEditor();
}
});
test('copy_paste_node paste requires target', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'paste' }), /target is required/);
});
test('copy_paste_node paste calls paste-node with target and keepWorldTransform', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'paste',
target: 'parent-uuid',
keepWorldTransform: true,
});
assert.equal(result.action, 'paste');
assert.equal(result.target, 'parent-uuid');
assert.equal(result.keepWorldTransform, true);
assert.equal(requests[0].method, 'paste-node');
assert.deepEqual(requests[0].args[0], { target: 'parent-uuid', keepWorldTransform: true });
} finally {
unmockEditor();
}
});
test('copy_paste_node paste with uuids passes them in options', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return { ok: true };
});
try {
const { byName } = makeTools();
await byName.copy_paste_node.handler({
action: 'paste',
target: 'parent-uuid',
uuids: ['copied-uuid-1'],
});
assert.equal(requests[0].method, 'paste-node');
assert.deepEqual(requests[0].args[0], {
target: 'parent-uuid',
keepWorldTransform: false,
uuids: ['copied-uuid-1'],
});
} finally {
unmockEditor();
}
});
test('copy_paste_node duplicate calls duplicate-node', async () => {
const requests = [];
mockEditor(async (channel, method, ...args) => {
requests.push({ channel, method, args });
return ['new-uuid-1'];
});
try {
const { byName } = makeTools();
const result = await byName.copy_paste_node.handler({
action: 'duplicate',
uuids: ['uuid-1'],
});
assert.equal(result.action, 'duplicate');
assert.deepEqual(result.uuids, ['uuid-1']);
assert.equal(requests[0].method, 'duplicate-node');
assert.deepEqual(requests[0].args[0], ['uuid-1']);
} finally {
unmockEditor();
}
});
test('copy_paste_node duplicate requires uuids', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'duplicate' }), /uuids is required/);
});
test('copy_paste_node rejects unknown action', async () => {
const { byName } = makeTools();
await assert.rejects(() => byName.copy_paste_node.handler({ action: 'invalid' }), /Unknown action/);
});
test('rename_node calls sceneBridge with renameNode', async () => {
const { byName, calls } = makeTools();
await byName.rename_node.handler({ path: 'Canvas/TestNode', newName: 'RenamedNode' });
assert.equal(calls.length, 1);
assert.equal(calls[0].method, 'renameNode');
assert.equal(calls[0].args.path, 'Canvas/TestNode');
assert.equal(calls[0].args.newName, 'RenamedNode');
});
test('reparent_node calls sceneBridge with reparentNode', async () => {
const { byName, calls } = makeTools();
await byName.reparent_node.handler({
path: 'Canvas/TestNode',
targetPath: 'Canvas/OtherParent',
siblingIndex: 2,
keepWorldTransform: true,
});
assert.equal(calls.length, 1);
assert.equal(calls[0].method, 'reparentNode');
assert.equal(calls[0].args.path, 'Canvas/TestNode');
assert.equal(calls[0].args.targetPath, 'Canvas/OtherParent');
assert.equal(calls[0].args.siblingIndex, 2);
assert.equal(calls[0].args.keepWorldTransform, true);
});
test('all 5 tools have correct profiles', () => {
const { byName } = makeTools();
assert.equal(byName.create_scene.profile, 'full');
assert.equal(byName.query_scene_state.profile, 'core');
assert.equal(byName.copy_paste_node.profile, 'full');
assert.equal(byName.rename_node.profile, 'full');
assert.equal(byName.reparent_node.profile, 'full');
});
test('all 5 tools have inputSchema with type object', () => {
const { byName } = makeTools();
for (const name of Object.keys(byName)) {
assert.equal(byName[name].inputSchema.type, 'object', `${name} should have object schema`);
assert.ok(byName[name].inputSchema.properties, `${name} should have properties`);
}
});