import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { PROJECT_SCHEMA_VERSION, type Project } from '../../shared/types'; import type { AssetId } from '../../shared/types/ids'; import { collectReferencedAssetIds, reconcileAssetFiles } from './assetPrune'; void test('collectReferencedAssetIds: превью, видео и аудио', () => { const p = { scenes: { s1: { previewAssetId: 'pr' as AssetId, media: { videos: ['v1' as AssetId], audios: [{ assetId: 'a1' as AssetId, autoplay: true, loop: true }], }, }, }, } as unknown as Project; const s = collectReferencedAssetIds(p); assert.deepEqual([...s].sort(), ['a1', 'pr', 'v1'].sort()); }); void test('reconcileAssetFiles: снимает осиротевшие assets и удаляет файлы', async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dnd-asset-prune-')); const relPath = 'assets/orphan.bin'; await fs.mkdir(path.join(tmp, 'assets'), { recursive: true }); await fs.writeFile(path.join(tmp, relPath), Buffer.from([1, 2, 3])); const asset = { id: 'orph' as AssetId, type: 'audio' as const, mime: 'audio/wav', originalName: 'x.wav', relPath, sha256: 'a', sizeBytes: 3, createdAt: new Date().toISOString(), }; const base = { id: 'p1', meta: { name: 't', fileBaseName: 't', createdAt: '', updatedAt: '', createdWithAppVersion: '1', appVersion: '1', schemaVersion: PROJECT_SCHEMA_VERSION, }, currentSceneId: null, currentGraphNodeId: null, sceneGraphNodes: [], sceneGraphEdges: [], } as unknown as Project; const prev: Project = { ...base, scenes: {}, assets: { orphan: asset } as Project['assets'], }; const next: Project = { ...base, scenes: {}, assets: { orphan: asset } as Project['assets'], }; const out = await reconcileAssetFiles(prev, next, tmp); assert.ok(!('orphan' in out.assets)); await assert.rejects(() => fs.stat(path.join(tmp, relPath))); }); void test('reconcileAssetFiles: удаляет файл при исключении id из assets', async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dnd-asset-prune-')); const relPath = 'assets/gone.bin'; await fs.mkdir(path.join(tmp, 'assets'), { recursive: true }); await fs.writeFile(path.join(tmp, relPath), Buffer.from([9])); const asset = { id: 'gone' as AssetId, type: 'audio' as const, mime: 'audio/wav', originalName: 'x.wav', relPath, sha256: 'b', sizeBytes: 1, createdAt: new Date().toISOString(), }; const base = { id: 'p1', meta: { name: 't', fileBaseName: 't', createdAt: '', updatedAt: '', createdWithAppVersion: '1', appVersion: '1', schemaVersion: PROJECT_SCHEMA_VERSION, }, scenes: {}, currentSceneId: null, currentGraphNodeId: null, sceneGraphNodes: [], sceneGraphEdges: [], } as unknown as Project; const prev: Project = { ...base, assets: { gone: asset } as Project['assets'] }; const next: Project = { ...base, assets: {} as Project['assets'] }; const out = await reconcileAssetFiles(prev, next, tmp); assert.deepEqual(out.assets, {}); await assert.rejects(() => fs.stat(path.join(tmp, relPath))); });