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