import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' import { createRequire } from 'node:module' const requireWeb = createRequire(new URL('../../unreal_tran_web/package.json', import.meta.url)) const ts = requireWeb('typescript') const sourceRoot = new URL('../../unreal_tran_web/src/features/scene-legacy/', import.meta.url) const dataUrl = source => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}` const compile = file => ts.transpileModule(readFileSync(new URL(file, sourceRoot), 'utf8'), { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext }, }).outputText const clientUrl = dataUrl(compile('contentApiClient.ts')) const { LegacySceneOperationQueue, toPublishedModelAsset } = await import(clientUrl) const storeSource = compile('serverAssetStore.ts').replace(/from ['"]\.\/contentApiClient['"]/g, `from '${clientUrl}'`) const { SceneServerAssetStore } = await import(dataUrl(storeSource)) const asset = (id, code, overrides = {}) => ({ id, code, versionId: '201', name: `${code}.glb`, type: 'MODEL_FILE', status: 'READY', storageUri: `fsvc://test/${code}.glb`, mimeType: 'model/gltf-binary', sizeBytes: 12, sha256: 'a'.repeat(64), metadata: {}, sortOrder: 0, ...overrides, }) const foxReference = (patch = {}) => ({ targetProjectId: '157', targetVersionId: '201', sourceAssetId: '2030', assetCode: 'FOX', resourceUrl: 'content://asset/model/157/201/2030', ...patch }) function fixture(options = {}) { const calls = { versions: [], downloads: [], fallback: [], revoked: [], blobs: [] } const version = { id: '201', projectId: '157', versionNumber: 2, status: 'PUBLISHED', content: { modelResource: { assetCode: 'FOX' } }, assets: [asset('2030', 'FOX'), asset('2031', 'BOX')], ...options.version } const project = { id: '10', version: 1, assets: [asset('81', 'LOCAL', { versionId: '11' })], dependencies: [{ relationType: 'SCENE_MODEL', targetProjectId: '157', targetVersionId: '201', required: true }], ...options.project } const client = { projectId: project.id, loadVersion: async (...args) => { calls.versions.push(args) return options.loadVersion ? options.loadVersion(...args) : structuredClone(version) }, downloadAsset: async (...args) => { calls.downloads.push(args) return options.downloadAsset ? options.downloadAsset(...args) : new Blob([args.join('/')]) }, } const fallback = { put: async () => { throw new Error('Unexpected local upload') }, hasReferenceSync: value => { calls.fallback.push(['has', value]); return true }, acquireObjectUrl: async value => { calls.fallback.push(['acquire', value]); return 'blob:stale-local-cache' }, releaseObjectUrl: value => calls.fallback.push(['release', value]), validateReferences: async value => { calls.fallback.push(['validate', value]); return { valid: true, missing: [] } }, } const store = new SceneServerAssetStore({ client, project: () => project, fallback, operationQueue: new LegacySceneOperationQueue(), commitUploadedAsset: async () => {}, urlApi: { createObjectURL: blob => { calls.blobs.push(blob); return `blob:server-${calls.blobs.length}` }, revokeObjectURL: url => calls.revoked.push(url), }, }) return { store, project, version, calls } } test('fixed model reads the exact version and downloads through its visible scene dependency', async () => { const f = fixture() const reference = foxReference() const url = await f.store.acquireObjectUrl(reference) assert.equal(url, 'blob:server-1') assert.deepEqual(f.calls.versions, [['10', '201']]) assert.deepEqual(f.calls.downloads, [['10', '2030']]) assert.equal(await f.calls.blobs[0].text(), '10/2030') assert.deepEqual(f.calls.fallback, []) f.store.releaseObjectUrl(reference) assert.deepEqual(f.calls.revoked, [url]) }) test('an explicit missing code cannot fall back to the first READY asset', async () => { const f = fixture() await assert.rejects(f.store.acquireObjectUrl(foxReference({ assetCode: 'MISSING', sourceAssetId: '', resourceUrl: '' }))) assert.deepEqual(f.calls.downloads, []) assert.deepEqual(f.calls.fallback, []) }) test('asset code and id must refer to the same model file', async () => { const f = fixture() await assert.rejects(f.store.acquireObjectUrl(foxReference({ assetCode: 'BOX' }))) assert.deepEqual(f.calls.downloads, []) assert.deepEqual(f.calls.fallback, []) }) test('model URI project, version and asset id must agree with the descriptor', async () => { for (const resourceUrl of ['content://asset/model/158/201/2030', 'content://asset/model/157/202/2030', 'content://asset/model/157/201/2031']) { const f = fixture() await assert.rejects(f.store.acquireObjectUrl(foxReference({ resourceUrl })), undefined, resourceUrl) assert.deepEqual(f.calls.downloads, []) assert.deepEqual(f.calls.fallback, []) } }) test('a non-pseudo resource URI must still match the selected fixed model storage URI', async () => { const f = fixture() await assert.rejects(f.store.acquireObjectUrl(foxReference({ resourceUrl: 'fsvc://test/OTHER.glb' }))) assert.deepEqual(f.calls.downloads, []) assert.deepEqual(f.calls.fallback, []) }) test('fixed model references cannot bypass identity checks with an unrelated built-in path', async () => { const f = fixture() await assert.rejects(f.store.acquireObjectUrl(foxReference({ resourceUrl: '/models/other.glb' }))) assert.deepEqual(f.calls.fallback, []) }) test('a valid fixed built-in file is verified against its version before using static delivery', async () => { const f = fixture({ version: { assets: [asset('2030', 'FOX', { storageUri: '/models/fox.glb' })] } }) const reference = foxReference({ resourceUrl: '/models/fox.glb' }) assert.equal((await f.store.validateReferences([reference])).valid, true) await f.store.acquireObjectUrl(reference) assert.deepEqual(f.calls.versions, [['10', '201']]) assert.deepEqual(f.calls.downloads, [], 'Built-in files do not use the controlled-file endpoint') assert.equal(f.calls.fallback.filter(call => call[0] === 'acquire').length, 1) }) test('a matching controlled storage URI is accepted for the fixed model file', async () => { const f = fixture() assert.equal(await f.store.acquireObjectUrl(foxReference({ resourceUrl: 'fsvc://test/FOX.glb' })), 'blob:server-1') assert.deepEqual(f.calls.downloads, [['10', '2030']]) }) test('non-READY files and READY images are not accepted as model files', async () => { for (const patch of [{ status: 'UPLOADING' }, { type: 'IMAGE' }]) { const f = fixture({ version: { assets: [asset('2030', 'FOX', patch)] } }) await assert.rejects(f.store.acquireObjectUrl(foxReference())) assert.deepEqual(f.calls.downloads, []) assert.deepEqual(f.calls.fallback, []) } }) test('a version response from a different project or version cannot supply a matching asset id', async () => { for (const patch of [{ id: '202' }, { projectId: '158' }]) { const f = fixture({ version: patch }) await assert.rejects(f.store.acquireObjectUrl(foxReference())) assert.deepEqual(f.calls.downloads, []) } }) test('a failed fixed-version lookup is evicted and the same reference can be retried', async () => { let attempt = 0 const f = fixture({ loadVersion: async () => { if (++attempt === 1) throw new Error('Injected version timeout') return structuredClone(f.version) } }) await assert.rejects(f.store.acquireObjectUrl(foxReference()), /Injected version timeout/) assert.equal(await f.store.acquireObjectUrl(foxReference()), 'blob:server-1') assert.equal(f.calls.versions.length, 2) assert.equal(f.calls.downloads.length, 1) assert.deepEqual(f.calls.fallback, []) }) test('a controlled download failure does not return local cached bytes and can retry', async () => { let attempt = 0 const f = fixture({ downloadAsset: async () => { if (++attempt === 1) throw new Error('Injected controlled download failure') return new Blob(['fresh controlled GLB']) } }) await assert.rejects(f.store.acquireObjectUrl(foxReference({ assetKey: 'old-cached-fox' })), /controlled download failure/) assert.equal(await f.store.acquireObjectUrl(foxReference({ assetKey: 'old-cached-fox' })), 'blob:server-1') assert.equal(await f.calls.blobs[0].text(), 'fresh controlled GLB') assert.deepEqual(f.calls.fallback, []) }) test('scene-owned code resolves only against the current scene assets', async () => { const f = fixture() assert.equal(await f.store.acquireObjectUrl({ assetCode: 'LOCAL', resourceUrl: 'content://asset/scene/10/LOCAL' }), 'blob:server-1') assert.deepEqual(f.calls.versions, []) assert.deepEqual(f.calls.downloads, [['10', '81']]) }) test('scene-owned code does not override a conflicting controlled storage URI', async () => { const f = fixture() await assert.rejects(f.store.acquireObjectUrl({ assetCode: 'LOCAL', resourceUrl: 'fsvc://test/OTHER.glb' })) assert.deepEqual(f.calls.downloads, []) assert.deepEqual(f.calls.fallback, []) }) test('copied scene keeps its original pseudo URI but reads its own copied asset by stable code', async () => { const f = fixture() assert.equal(await f.store.acquireObjectUrl({ assetCode: 'LOCAL', resourceUrl: 'content://asset/scene/9/LOCAL' }), 'blob:server-1') assert.deepEqual(f.calls.downloads, [['10', '81']]) assert.deepEqual(f.calls.fallback, []) }) test('string content URIs are resolved for fixed models and scene-owned uploads', async () => { const f = fixture() await f.store.acquireObjectUrl('content://asset/model/157/201/2030') await f.store.acquireObjectUrl('content://asset/scene/10/LOCAL') assert.deepEqual(f.calls.downloads, [['10', '2030'], ['10', '81']]) assert.deepEqual(f.calls.fallback, []) }) test('a pooled fixed model URL does not validate another descriptor with a conflicting URI', async () => { const f = fixture() await f.store.acquireObjectUrl(foxReference()) await assert.rejects(f.store.acquireObjectUrl(foxReference({ resourceUrl: 'content://asset/model/157/202/2030' }))) assert.equal(f.calls.downloads.length, 1) }) test('a pooled scene-owned code does not validate another descriptor with a conflicting asset id', async () => { const f = fixture() await f.store.acquireObjectUrl({ assetCode: 'LOCAL', sourceAssetId: '81' }) await assert.rejects(f.store.acquireObjectUrl({ assetCode: 'LOCAL', sourceAssetId: '999' })) assert.equal(f.calls.downloads.length, 1) }) test('scene-owned ID-only references isolate cached files and reject a missing ID', async () => { for (const idField of ['sourceAssetId', 'assetId']) { const f = fixture({ project: { assets: [asset('81', 'LOCAL'), asset('82', 'OTHER')] } }) const first = { [idField]: '81' }, second = { [idField]: '82' } assert.equal(await f.store.acquireObjectUrl(first), 'blob:server-1') await assert.rejects(f.store.acquireObjectUrl({ [idField]: '999' }), /服务端模型资源不存在/) assert.equal(await f.store.acquireObjectUrl(second), 'blob:server-2') assert.deepEqual(f.calls.downloads, [['10', '81'], ['10', '82']]) assert.deepEqual(f.calls.fallback, []) f.store.releaseObjectUrl(first) assert.deepEqual(f.calls.revoked, ['blob:server-1']) assert.equal(await f.store.acquireObjectUrl(second), 'blob:server-2') assert.equal(f.calls.downloads.length, 2) f.store.dispose() } }) test('ID-only validation retains a missing reference when a valid reference follows it', async () => { for (const idField of ['sourceAssetId', 'assetId']) { const f = fixture() const result = await f.store.validateReferences([ { [idField]: '999', sourceName: 'missing-file' }, { [idField]: '81', sourceName: 'valid-file' }, ]) assert.equal(result.valid, false) assert.deepEqual(result.missing, ['missing-file']) assert.deepEqual(f.calls.fallback, []) assert.deepEqual(f.calls.downloads, []) f.store.dispose() } }) test('validation does not deduplicate conflicting scene-owned descriptors into a valid one', async () => { const f = fixture() const result = await f.store.validateReferences([{ assetCode: 'LOCAL', sourceAssetId: '999' }, { assetCode: 'LOCAL', sourceAssetId: '81' }]) assert.equal(result.valid, false) assert.equal(result.missing.length, 1) assert.deepEqual(f.calls.fallback, []) }) test('server-managed invalid references cannot pass using a populated fallback cache', async () => { const f = fixture() const result = await f.store.validateReferences([{ assetCode: 'MISSING', assetKey: 'old-cached-fox' }]) assert.equal(result.valid, false) assert.deepEqual(f.calls.fallback, []) }) test('plain local legacy references retain their fallback behavior', async () => { const f = fixture() assert.equal(await f.store.acquireObjectUrl({ assetKey: 'local-only-legacy' }), 'blob:stale-local-cache') assert.equal(f.calls.fallback.length, 1) assert.deepEqual(f.calls.downloads, []) }) test('published model catalogue refuses an explicit missing code instead of choosing the first GLB', () => { const f = fixture({ version: { content: { modelResource: { assetCode: 'MISSING' } } } }) assert.equal(toPublishedModelAsset({ projectId: '157', versionId: '201' }, f.version), null) }) test('published model catalogue selects only a READY MODEL_FILE and preserves its fixed identity', () => { const f = fixture({ version: { assets: [asset('99', 'FOX', { type: 'IMAGE' }), asset('100', 'FOX', { status: 'UPLOADING' }), asset('2030', 'FOX')] } }) const result = toPublishedModelAsset({ projectId: '157', versionId: '201' }, f.version) assert.equal(result.assetCode, 'FOX') assert.equal(result.sourceAssetId, '2030') assert.equal(result.targetVersionId, '201') assert.equal(result.resourceUrl, 'content://asset/model/157/201/2030') })