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:
mingyuansi
2026-06-30 21:53:06 +08:00
parent a9b539ca9c
commit b65a4f22c3
21 changed files with 5273 additions and 53 deletions
+136
View File
@@ -0,0 +1,136 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { findUnusedAssets, collectUuidReferences } = require('../lib/tools/assets-advanced');
function createMockAssetsDir() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-unused-test-'));
const assetsDir = path.join(tmpDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
return { tmpDir, assetsDir };
}
function writeFile(dir, relPath, content) {
const fullPath = path.join(dir, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, 'utf8');
}
test('findUnusedAssets returns assets not referenced by any serialized file', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const usedUuid = 'aaa11111-bbbb-cccc-dddd-eeeeeeee1111';
writeFile(assetsDir, 'textures/used.png', 'fake-png');
writeFile(assetsDir, 'textures/used.png.meta', JSON.stringify({ uuid: usedUuid }));
const unusedUuid = 'aaa22222-bbbb-cccc-dddd-eeeeeeee2222';
writeFile(assetsDir, 'textures/unused.png', 'fake-png');
writeFile(assetsDir, 'textures/unused.png.meta', JSON.stringify({ uuid: unusedUuid }));
writeFile(assetsDir, 'scenes/game.scene', JSON.stringify({
__type__: 'cc.SceneAsset',
scene: { __uuid__: usedUuid },
}));
const mockListAssets = async () => [
{ uuid: usedUuid, url: 'db://assets/textures/used.png', type: 'cc.Texture2D' },
{ uuid: unusedUuid, url: 'db://assets/textures/unused.png', type: 'cc.Texture2D' },
];
const result = await findUnusedAssets(tmpDir, {}, mockListAssets);
assert.equal(result.totalScanned, 2);
assert.equal(result.unusedCount, 1);
assert.equal(result.unused[0].uuid, unusedUuid);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('findUnusedAssets skips .ts and .scene files from unused list', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const scriptUuid = 'bbb11111-bbbb-cccc-dddd-eeeeeeee3333';
const sceneUuid = 'bbb22222-bbbb-cccc-dddd-eeeeeeee4444';
writeFile(assetsDir, 'scripts/MyScript.ts', 'export class MyScript {}');
writeFile(assetsDir, 'scripts/MyScript.ts.meta', JSON.stringify({ uuid: scriptUuid }));
writeFile(assetsDir, 'scenes/level.scene', '{}');
writeFile(assetsDir, 'scenes/level.scene.meta', JSON.stringify({ uuid: sceneUuid }));
const mockListAssets = async () => [
{ uuid: scriptUuid, url: 'db://assets/scripts/MyScript.ts', type: 'cc.ScriptAsset' },
{ uuid: sceneUuid, url: 'db://assets/scenes/level.scene', type: 'cc.SceneAsset' },
];
const result = await findUnusedAssets(tmpDir, {}, mockListAssets);
assert.equal(result.totalScanned, 2);
assert.equal(result.unusedCount, 0);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('findUnusedAssets respects excludeDirectories', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const referencedUuid = 'ccc11111-bbbb-cccc-dddd-eeeeeeee5555';
const unreferencedUuid = 'ccc22222-bbbb-cccc-dddd-eeeeeeee6666';
writeFile(assetsDir, 'textures/a.png', 'fake');
writeFile(assetsDir, 'textures/a.png.meta', JSON.stringify({ uuid: referencedUuid }));
writeFile(assetsDir, 'internal/b.png', 'fake');
writeFile(assetsDir, 'internal/b.png.meta', JSON.stringify({ uuid: unreferencedUuid }));
writeFile(assetsDir, 'internal/hidden.scene', JSON.stringify({
__type__: 'cc.SceneAsset',
tex: { __uuid__: referencedUuid },
}));
const mockListAssets = async () => [
{ uuid: referencedUuid, url: 'db://assets/textures/a.png', type: 'cc.Texture2D' },
{ uuid: unreferencedUuid, url: 'db://assets/internal/b.png', type: 'cc.Texture2D' },
];
const result = await findUnusedAssets(tmpDir, {
excludeDirectories: ['internal'],
}, mockListAssets);
assert.equal(result.totalScanned, 2);
assert.equal(result.unusedCount, 2);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('findUnusedAssets respects limit option', async () => {
const { tmpDir, assetsDir } = createMockAssetsDir();
try {
const assets = [];
for (let i = 0; i < 5; i++) {
const uuid = `ddd${i}1111-bbbb-cccc-dddd-eeeeeeee777${i}`;
writeFile(assetsDir, `textures/tex${i}.png`, 'fake');
writeFile(assetsDir, `textures/tex${i}.png.meta`, JSON.stringify({ uuid }));
assets.push({ uuid, url: `db://assets/textures/tex${i}.png`, type: 'cc.Texture2D' });
}
const mockListAssets = async () => assets;
const result = await findUnusedAssets(tmpDir, { limit: 3 }, mockListAssets);
assert.equal(result.totalScanned, 5);
assert.equal(result.unusedCount, 5);
assert.equal(result.unused.length, 3);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('collectUuidReferences is re-exported and works', () => {
const refs = collectUuidReferences(JSON.stringify({
__uuid__: 'abc12345-def6-7890-abcd-ef1234567890',
}));
assert.ok(refs.some((r) => r.uuid === 'abc12345-def6-7890-abcd-ef1234567890'));
});
+359
View File
@@ -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`);
}
});
+139
View File
@@ -0,0 +1,139 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const {
createScriptTools,
generateComponentTemplate,
generatePlainTemplate,
} = require('../lib/tools/scripts');
test('generateComponentTemplate produces valid component with start and update', () => {
const code = generateComponentTemplate('PlayerController');
assert.ok(code.includes("import { _decorator, Component } from 'cc'"));
assert.ok(code.includes("@ccclass('PlayerController')"));
assert.ok(code.includes('export class PlayerController extends Component'));
assert.ok(code.includes('start()'));
assert.ok(code.includes('update(deltaTime: number)'));
});
test('generateComponentTemplate includes primitive @property', () => {
const code = generateComponentTemplate('Enemy', [
{ name: 'speed', type: 'Number', default: 5 },
{ name: 'label', type: 'String', default: "'Enemy'" },
]);
assert.ok(code.includes('@property'));
assert.ok(code.includes('speed: number = 5'));
assert.ok(code.includes("label: string = 'Enemy'"));
assert.ok(!code.includes('Label'));
});
test('generateComponentTemplate includes CC type @property', () => {
const code = generateComponentTemplate('Weapon', [
{ name: 'target', type: 'Label' },
{ name: 'sprite', type: 'Sprite' },
]);
assert.ok(code.includes('@property(Label)'));
assert.ok(code.includes('target: Label | null = null'));
assert.ok(code.includes('@property(Sprite)'));
assert.ok(code.includes('sprite: Sprite | null = null'));
assert.ok(code.includes('import { _decorator, Component, Label, Sprite }'));
});
test('generateComponentTemplate uses defaults when property default is omitted', () => {
const code = generateComponentTemplate('Item', [
{ name: 'count', type: 'Number' },
{ name: 'flag', type: 'Boolean' },
]);
assert.ok(code.includes('count: number = 0'));
assert.ok(code.includes('flag: boolean = false'));
});
test('generateComponentTemplate with no properties has placeholder comment', () => {
const code = generateComponentTemplate('Empty');
assert.ok(code.includes('// Add properties here'));
});
test('generatePlainTemplate produces minimal class', () => {
const code = generatePlainTemplate('Utils');
assert.ok(code.includes('export class Utils'));
assert.ok(!code.includes('@ccclass'));
assert.ok(!code.includes('@property'));
});
test('create_script writes file and returns metadata', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-script-test-'));
try {
const createSchema = (properties, required) => {
const schema = { type: 'object', properties };
if (required && required.length) {
schema.required = required;
}
return schema;
};
const getRuntimeContext = () => ({ projectPath: tmpDir });
const tools = createScriptTools({ createSchema, getRuntimeContext });
const tool = tools[0];
const result = await tool.handler({
scriptName: 'TestComp',
savePath: 'assets/scripts/TestComp.ts',
template: 'component',
properties: [{ name: 'speed', type: 'Number', default: 10 }],
});
assert.equal(result.created, true);
assert.equal(result.scriptName, 'TestComp');
assert.equal(result.template, 'component');
assert.equal(result.className, 'TestComp');
const written = fs.readFileSync(path.join(tmpDir, 'assets', 'scripts', 'TestComp.ts'), 'utf8');
assert.ok(written.includes('export class TestComp extends Component'));
assert.ok(written.includes('speed: number = 10'));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test('create_script rejects non-.ts savePath', async () => {
const createSchema = () => ({ type: 'object', properties: {} });
const getRuntimeContext = () => ({ projectPath: '/tmp' });
const tools = createScriptTools({ createSchema, getRuntimeContext });
await assert.rejects(
() => tools[0].handler({ scriptName: 'X', savePath: 'assets/scripts/X.js' }),
/must end with \.ts/
);
});
test('create_script requires scriptName and savePath', async () => {
const createSchema = () => ({ type: 'object', properties: {} });
const getRuntimeContext = () => ({ projectPath: '/tmp' });
const tools = createScriptTools({ createSchema, getRuntimeContext });
await assert.rejects(() => tools[0].handler({ savePath: 'X.ts' }), /scriptName is required/);
await assert.rejects(() => tools[0].handler({ scriptName: 'X' }), /savePath is required/);
});
test('create_script plain template writes minimal class', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-script-test-'));
try {
const createSchema = () => ({ type: 'object', properties: {} });
const getRuntimeContext = () => ({ projectPath: tmpDir });
const tools = createScriptTools({ createSchema, getRuntimeContext });
const result = await tools[0].handler({
scriptName: 'Helper',
savePath: 'assets/scripts/Helper.ts',
template: 'plain',
});
assert.equal(result.template, 'plain');
const written = fs.readFileSync(path.join(tmpDir, 'assets', 'scripts', 'Helper.ts'), 'utf8');
assert.ok(written.includes('export class Helper'));
assert.ok(!written.includes('@ccclass'));
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
+3 -2
View File
@@ -21,7 +21,7 @@ function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-
test('core profile exposes the documented focused tool set', () => {
const tools = createRegistry('core').listTools();
assert.equal(tools.length, 37);
assert.equal(tools.length, 38);
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
@@ -31,12 +31,13 @@ test('core profile exposes the documented focused tool set', () => {
assert.equal(tools.some((tool) => tool.name === 'get_performance_snapshot'), true);
assert.equal(tools.some((tool) => tool.name === 'list_project_instructions'), true);
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
assert.equal(tools.some((tool) => tool.name === 'query_scene_state'), true);
assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
});
test('full profile exposes all built-in tools', () => {
const tools = createRegistry('full').listTools();
assert.equal(tools.length, 101);
assert.equal(tools.length, 110);
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
assert.equal(tools.some((tool) => tool.name === 'edit_prefab_json'), true);
assert.equal(tools.some((tool) => tool.name === 'create_project_skill'), true);