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
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
'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`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user