From a3b016ea8e8e508298d6cf53f6adc0ac8fb409ac Mon Sep 17 00:00:00 2001 From: furao Date: Fri, 14 Aug 2026 12:09:20 +0800 Subject: [PATCH] =?UTF-8?q?[cc-3-8-x-mcp]=20=E4=BF=AE=E5=A4=8D:=20?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E7=A6=BB=E7=BA=BF=20CLI=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=B8=8E=E8=87=AA=E5=8C=85=E5=90=AB=E5=A4=B9?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改了什么:解析结果记录资源来源路径,并生成独立 Prefab、嵌套资源和脚本夹具。 为什么:测试依赖外部项目资源和临时目录时会导致嵌套解析与类 ID 结果不稳定。 影响范围:离线 CLI 解析、组件引用错误信息及 offline tools 测试。 --- cli/src/editor/ops/set-component-ref.js | 2 +- cli/src/parse.js | 3 + cli/test/api.test.js | 15 +- cli/test/cli.test.js | 5 +- cli/test/fixture.js | 225 ++++++++++++++++++++++++ cli/test/fixtures/.gitignore | 1 + cli/test/query.test.js | 2 +- cli/test/smoke.test.js | 9 +- router/test/offline-tools.test.js | 10 +- 9 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 cli/test/fixture.js diff --git a/cli/src/editor/ops/set-component-ref.js b/cli/src/editor/ops/set-component-ref.js index 8551ff4..a3fbbb3 100644 --- a/cli/src/editor/ops/set-component-ref.js +++ b/cli/src/editor/ops/set-component-ref.js @@ -92,7 +92,7 @@ function execSetComponentRef(prefabData, op) { const comp = findComponent(elements, node, componentType); if (!comp) { - throw new Error(`editPrefab [set-component-ref]: 节点 "${node._name}" 未挂 "${componentType}" 组件`); + throw new Error(`editPrefab [set-component-ref]: 节点 "${node._name}" 未挂 "${rawComponentType}" 组件`); } const { node: refNode, nodeId: refNodeId } = resolveNode(prefabData, refNodeSelector, 'set-component-ref'); diff --git a/cli/src/parse.js b/cli/src/parse.js index 1308b54..7875776 100644 --- a/cli/src/parse.js +++ b/cli/src/parse.js @@ -153,6 +153,9 @@ function parsePrefab(filePath) { raw, elements, rootId, + // 解析结果可直接交给 overrides/nested helpers 使用;调用方不应再被迫 + // 手工补一遍来源路径。editPrefab 仍可用 options.projectRoot 覆盖它。 + resolverStartPath: filePath, findNodeByName, findNodesByType, getRoot, diff --git a/cli/test/api.test.js b/cli/test/api.test.js index 6fb6213..a404aac 100644 --- a/cli/test/api.test.js +++ b/cli/test/api.test.js @@ -18,16 +18,16 @@ const { editPrefab } = require('../src/editor/index.js'); const { parsePrefab } = require('../src/parse.js'); const { listOverrides } = require('../src/overrides.js'); const { addRootTargetOverride, resolveLocalIdChain } = require('../src/editor/nested.js'); - -const FIXTURE_PATH = path.resolve(__dirname, 'fixtures/HomeUI.prefab'); +const fixture = require('./fixture.js'); +const FIXTURE_PATH = fixture.ensureHomeUiFixture(); // 项目根(含 assets/ + package.json),传给 editPrefab options.projectRoot // 让 UuidResolver 在 /tmp/ 临时文件场景下也能定位 assets/ 目录 -const PROJECT_ROOT = path.resolve(__dirname, '../../../../'); +const PROJECT_ROOT = fixture.FIXTURE_PROJECT_ROOT; // 每个测试独立 tmp 文件 function makeTmp(tag) { - return path.join(os.tmpdir(), `HomeUI-api-${tag}-${Date.now()}.prefab`); + return path.join(fixture.FIXTURE_TEMP_DIR, `HomeUI-api-${tag}-${process.pid}-${Date.now()}.prefab`); } // 把 fixture 复制到 tmp,返回 tmp 路径(让 editPrefab 可以写回) @@ -1236,7 +1236,8 @@ test('add-component: 普通节点挂自定义 ccclass 追加组件 + CompPrefabI assert.ok(typeof newCompRef.__id__ === 'number'); const comp = after.elements[newCompRef.__id__]; - assert.equal(comp.__type__, 'TaskBtn'); + const { compressUuid } = require('../src/id.js'); + assert.equal(comp.__type__, compressUuid(fixture.TASK_BTN_UUID)); assert.equal(comp.node.__id__, after.elements.findIndex((e) => e === node)); assert.equal(comp._enabled, true); assert.ok(comp.__prefab && typeof comp.__prefab.__id__ === 'number'); @@ -1358,7 +1359,8 @@ test('remove-component: 普通节点移除组件引用 + 保持其他 __id__ 稳 // 组件元素本身作为 orphan 保留:elements 长度不变、原槽位仍是 TaskBtn assert.equal(after.elements.length, beforeElementsLen); - assert.equal(after.elements[beforeCompId].__type__, 'TaskBtn'); + const { compressUuid } = require('../src/id.js'); + assert.equal(after.elements[beforeCompId].__type__, compressUuid(fixture.TASK_BTN_UUID)); }); test('remove-component: 节点上找不到对应组件抛错,不落盘', () => { @@ -1674,4 +1676,3 @@ test('FIX-2 addRootTargetOverride: 数组 propertyPath 各索引独立不被幂 assert.ok(paths.includes(JSON.stringify(['_items', 1])), '应含 ["_items", 1]'); assert.ok(paths.includes(JSON.stringify(['_items', 2])), '应含 ["_items", 2]'); }); - diff --git a/cli/test/cli.test.js b/cli/test/cli.test.js index 839e045..72d814f 100644 --- a/cli/test/cli.test.js +++ b/cli/test/cli.test.js @@ -19,7 +19,8 @@ const path = require('path'); const os = require('os'); const BIN = path.resolve(__dirname, '../bin/cocos-mcp-cli.js'); -const FIXTURE = path.resolve(__dirname, 'fixtures/HomeUI.prefab'); +const fixture = require('./fixture.js'); +const FIXTURE = fixture.ensureHomeUiFixture(); function run(args) { return spawnSync(process.execPath, [BIN, ...args], { @@ -29,7 +30,7 @@ function run(args) { } function tmpCopy() { - const dest = path.join(os.tmpdir(), `HomeUI-cli-test-${Date.now()}.prefab`); + const dest = path.join(fixture.FIXTURE_TEMP_DIR, `HomeUI-cli-test-${process.pid}-${Date.now()}.prefab`); fs.copyFileSync(FIXTURE, dest); return dest; } diff --git a/cli/test/fixture.js b/cli/test/fixture.js new file mode 100644 index 0000000..ec7abe6 --- /dev/null +++ b/cli/test/fixture.js @@ -0,0 +1,225 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { + makePrefabRoot, + makeNode, + makePrefabInfo, + makeCompPrefabInfo, + makeUITransform, + makeSprite, + makeLabel, +} = require('../src/primitives.js'); + +const FIXTURE_DIR = path.join(__dirname, 'fixtures'); +const FIXTURE_PROJECT_ROOT = path.join(FIXTURE_DIR, 'project'); +const FIXTURE_ASSETS_DIR = path.join(FIXTURE_PROJECT_ROOT, 'assets'); +const FIXTURE_TEMP_DIR = path.join(FIXTURE_ASSETS_DIR, '.tmp'); +const HOME_UI_FIXTURE = path.join(FIXTURE_ASSETS_DIR, 'HomeUI.prefab'); +const NESTED_PREFAB_FIXTURE = path.join(FIXTURE_ASSETS_DIR, 'TaskEntry.prefab'); +const TASK_BTN_UUID = '5f4d2de3-a7dc-4d47-a41f-f27a12549b20'; +const NESTED_PREFAB_UUID = 'addbe9a2-fd7e-5f3b-9164-ee44de8b1045'; +const NESTED_ROOT_FILE_ID = 'as0LdMaKxSWSLxrZB9u9KA'; + +/** + * 生成一个只覆盖 CLI/override 测试契约的最小 Cocos 3 Prefab。 + * stub 固定放在 id=10,保留旧测试对 mountedChildren/propertyOverrides 的覆盖。 + */ +function buildHomeUiFixture() { + const elements = new Array(28); + + elements[0] = makePrefabRoot({ name: 'HomeUI', rootId: 1 }); + elements[1] = makeNode({ + name: 'HomeUI', + childIds: [2, 4, 6, 8, 10, 24], + prefabId: 19, + }); + elements[2] = makeNode({ + name: 'touchArea', + pos: [0, 0, 0], + parentId: 1, + prefabId: 3, + }); + elements[3] = makePrefabInfo({ rootId: 1, fileId: 'fixtureTouchAreaFileId' }); + elements[4] = makeNode({ + name: 'left', + pos: [-243, 12, 0], + parentId: 1, + prefabId: 5, + }); + elements[5] = makePrefabInfo({ rootId: 1, fileId: 'fixtureLeftNodeFileId' }); + elements[6] = makeNode({ + name: 'n5', + parentId: 1, + componentIds: [7], + prefabId: 20, + }); + elements[7] = makeSprite({ + nodeId: 6, + spriteFrameUuid: '00000000-0000-0000-0000-000000000005@f9941', + prefabInfoId: 22, + }); + elements[8] = makeNode({ + name: 'n7', + parentId: 1, + componentIds: [9], + prefabId: 21, + }); + elements[9] = makeLabel({ + nodeId: 8, + string: '开始', + prefabInfoId: 23, + }); + + // nested prefab stub:结构与 Creator 3.8 序列化格式一致。 + elements[10] = { + __type__: 'cc.Node', + _objFlags: 0, + _parent: { __id__: 1 }, + _prefab: { __id__: 11 }, + __editorExtras__: {}, + }; + elements[11] = { + __type__: 'cc.PrefabInfo', + root: { __id__: 10 }, + asset: { + __uuid__: NESTED_PREFAB_UUID, + __expectedType__: 'cc.Prefab', + }, + fileId: NESTED_ROOT_FILE_ID, + instance: { __id__: 12 }, + targetOverrides: null, + }; + elements[12] = { + __type__: 'cc.PrefabInstance', + fileId: 'fixtureNestedInstanceId', + prefabRootNode: { __id__: 1 }, + mountedChildren: [], + mountedComponents: [], + propertyOverrides: [13, 15, 16, 17, 18].map((id) => ({ __id__: id })), + removedComponents: [], + }; + elements[13] = { + __type__: 'CCPropertyOverrideInfo', + targetInfo: { __id__: 14 }, + propertyPath: ['_name'], + value: 'taskEntry', + }; + elements[14] = { + __type__: 'cc.TargetInfo', + localID: [NESTED_ROOT_FILE_ID], + }; + elements[15] = { + __type__: 'CCPropertyOverrideInfo', + targetInfo: { __id__: 14 }, + propertyPath: ['_lpos'], + value: { __type__: 'cc.Vec3', x: -272, y: 53, z: 0 }, + }; + elements[16] = { + __type__: 'CCPropertyOverrideInfo', + targetInfo: { __id__: 14 }, + propertyPath: ['_lrot'], + value: { __type__: 'cc.Quat', x: 0, y: 0, z: 0, w: 1 }, + }; + elements[17] = { + __type__: 'CCPropertyOverrideInfo', + targetInfo: { __id__: 14 }, + propertyPath: ['_euler'], + value: { __type__: 'cc.Vec3', x: 0, y: 0, z: 0 }, + }; + elements[18] = { + __type__: 'CCPropertyOverrideInfo', + targetInfo: { __id__: 14 }, + propertyPath: ['_lscale'], + value: { __type__: 'cc.Vec3', x: 1, y: 1, z: 1 }, + }; + + elements[19] = makePrefabInfo({ + rootId: 1, + fileId: 'fixtureHomeRootFileId', + nestedPrefabInstanceRoots: [10], + }); + elements[20] = makePrefabInfo({ rootId: 1, fileId: 'fixtureSpriteNodeFileId' }); + elements[21] = makePrefabInfo({ rootId: 1, fileId: 'fixtureLabelNodeFileId' }); + elements[22] = makeCompPrefabInfo('fixtureSpriteCompId'); + elements[23] = makeCompPrefabInfo('fixtureLabelCompId'); + elements[24] = makeNode({ + name: 'btnMerge', + parentId: 1, + componentIds: [26], + prefabId: 25, + }); + elements[25] = makePrefabInfo({ rootId: 1, fileId: 'fixtureBtnMergeNodeFileId' }); + elements[26] = makeUITransform({ + nodeId: 24, + width: 120, + height: 60, + prefabInfoId: 27, + }); + elements[27] = makeCompPrefabInfo('fixtureBtnMergeUiTransformId'); + + return elements; +} + +function buildNestedPrefabFixture() { + return [ + makePrefabRoot({ name: 'TaskEntry', rootId: 1 }), + makeNode({ name: 'TaskEntry', prefabId: 2 }), + makePrefabInfo({ rootId: 1, fileId: NESTED_ROOT_FILE_ID }), + ]; +} + +function writeGeneratedFile(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) return; + + const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; + fs.writeFileSync(tempPath, content, 'utf8'); + try { + fs.renameSync(tempPath, filePath); + } catch (error) { + if (fs.existsSync(filePath)) fs.unlinkSync(tempPath); + else throw error; + } +} + +function ensureHomeUiFixture() { + fs.mkdirSync(FIXTURE_TEMP_DIR, { recursive: true }); + writeGeneratedFile( + path.join(FIXTURE_PROJECT_ROOT, 'package.json'), + JSON.stringify({ name: 'cc-mcp-test-project', private: true }, null, 2) + '\n' + ); + writeGeneratedFile( + NESTED_PREFAB_FIXTURE, + JSON.stringify(buildNestedPrefabFixture(), null, 2) + '\n' + ); + writeGeneratedFile( + `${NESTED_PREFAB_FIXTURE}.meta`, + JSON.stringify({ ver: '1.1.50', importer: 'prefab', imported: true, uuid: NESTED_PREFAB_UUID }, null, 2) + '\n' + ); + const taskBtnPath = path.join(FIXTURE_ASSETS_DIR, 'scripts', 'TaskBtn.ts'); + writeGeneratedFile( + taskBtnPath, + "import { _decorator, Component } from 'cc';\nconst { ccclass } = _decorator;\n@ccclass('TaskBtn')\nexport class TaskBtn extends Component {}\n" + ); + writeGeneratedFile( + `${taskBtnPath}.meta`, + JSON.stringify({ ver: '4.0.24', importer: 'typescript', imported: true, uuid: TASK_BTN_UUID }, null, 2) + '\n' + ); + writeGeneratedFile( + HOME_UI_FIXTURE, + JSON.stringify(buildHomeUiFixture(), null, 2) + '\n' + ); + return HOME_UI_FIXTURE; +} + +module.exports = { + HOME_UI_FIXTURE, + FIXTURE_PROJECT_ROOT, + FIXTURE_TEMP_DIR, + TASK_BTN_UUID, + buildHomeUiFixture, + buildNestedPrefabFixture, + ensureHomeUiFixture, +}; diff --git a/cli/test/fixtures/.gitignore b/cli/test/fixtures/.gitignore index f409415..6db3b5c 100644 --- a/cli/test/fixtures/.gitignore +++ b/cli/test/fixtures/.gitignore @@ -1,2 +1,3 @@ # smoke test fixtures(只读副本,不提交) *.prefab +project/ diff --git a/cli/test/query.test.js b/cli/test/query.test.js index 0d10719..ac50a1b 100644 --- a/cli/test/query.test.js +++ b/cli/test/query.test.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict'); const path = require('path'); const { queryPrefab } = require('../src/query/index.js'); -const FIXTURE = path.join(__dirname, 'fixtures', 'HomeUI.prefab'); +const FIXTURE = require('./fixture.js').ensureHomeUiFixture(); // ─── selector: tree ───────────────────────────────────────── diff --git a/cli/test/smoke.test.js b/cli/test/smoke.test.js index 90b48c4..4610db0 100644 --- a/cli/test/smoke.test.js +++ b/cli/test/smoke.test.js @@ -9,14 +9,14 @@ const { test, after } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); -const os = require('os'); +const fixture = require('./fixture.js'); const { parsePrefab } = require('../src/parse.js'); const { writePrefab, detectIndent, detectTrailingNewline } = require('../src/write.js'); const { setOverrideProperty, listOverrides } = require('../src/overrides.js'); -const FIXTURE_PATH = path.resolve(__dirname, 'fixtures/HomeUI.prefab'); -const TMP_PATH = path.join(os.tmpdir(), `HomeUI-smoke-${Date.now()}.prefab`); +const FIXTURE_PATH = fixture.ensureHomeUiFixture(); +const TMP_PATH = path.join(fixture.FIXTURE_TEMP_DIR, `HomeUI-smoke-${process.pid}-${Date.now()}.prefab`); // 清理临时文件 after(() => { @@ -140,6 +140,7 @@ test('端到端: 改普通节点 _lpos.x + 写回 + 验证', () => { test('端到端: 更新 stub 节点已有 override (_lpos) + 写回 + 验证', () => { const prefabData = parsePrefab(FIXTURE_PATH); + prefabData.resolverStartPath = FIXTURE_PATH; // stub 节点 index 10(PrefabInfo index 11, fileId='as0LdMaKxSWSLxrZB9u9KA') // 已有 _lpos override: {x: -272, y: 53, z: 0} @@ -166,6 +167,7 @@ test('端到端: 更新 stub 节点已有 override (_lpos) + 写回 + 验证', ( test('端到端: 新增 stub 节点 override (不存在的属性) + 写回 + 验证', () => { const prefabData = parsePrefab(FIXTURE_PATH); + prefabData.resolverStartPath = FIXTURE_PATH; const STUB_ID = 10; const overridesBefore = listOverrides(prefabData, STUB_ID); @@ -218,4 +220,3 @@ test('JSON diff 精确:只有目标字段变化', () => { const origParsed = parsePrefab(FIXTURE_PATH); assert.equal(reparsed.elements.length, origParsed.elements.length, 'element 总数应不变'); }); - diff --git a/router/test/offline-tools.test.js b/router/test/offline-tools.test.js index 54b2d86..7857a4a 100644 --- a/router/test/offline-tools.test.js +++ b/router/test/offline-tools.test.js @@ -26,14 +26,12 @@ const { } = require('../src/offline-tools.js'); // fixture: HomeUI.prefab(只读,在 cli/test/fixtures/) -const FIXTURE_PATH = path.resolve( - __dirname, - '../../cli/test/fixtures/HomeUI.prefab' -); +const fixture = require('../../cli/test/fixture.js'); +const FIXTURE_PATH = fixture.ensureHomeUiFixture(); // 复制 fixture 到 tmp 用于写操作 function makeTmp(tag) { - var dst = path.join(os.tmpdir(), 'HomeUI-router-' + tag + '-' + Date.now() + '.prefab'); + var dst = path.join(fixture.FIXTURE_TEMP_DIR, 'HomeUI-router-' + tag + '-' + process.pid + '-' + Date.now() + '.prefab'); fs.copyFileSync(FIXTURE_PATH, dst); return dst; } @@ -52,7 +50,7 @@ test('isOfflineTool 对已知 name 返回 true,未知 name 返回 false', () = assert.equal(isOfflineTool('prefab_query'), true); assert.equal(isOfflineTool('prefab_edit'), true); assert.equal(isOfflineTool('prefab_batch'), true); - assert.equal(isOfflineTool('router_list_editors'), false); + assert.equal(isOfflineTool('gateway_list_editors'), false); assert.equal(isOfflineTool('scene_set_property'), false); assert.equal(isOfflineTool(''), false); });