'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`); } });