'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const compat = require('../js/compound-compat'); const utils = require('../js/spine-utils'); const pluginPath = path.join(__dirname, '..', 'js', 'plugin.js'); const pluginSource = fs.readFileSync(pluginPath, 'utf8'); const pageSource = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); const releaseRoot = path.join(__dirname, '..', '..', 'release', 'UNIVERSAL_SpineThumbnailGenerateServer'); const planned = compat.planChange( { entryName: 'hero.atlas.txt' }, { name: 'hero.atlas', ext: 'txt' }, utils.splitCompoundName, ); assert.deepEqual(planned, { sourceName: 'hero.atlas.txt', currentName: 'hero.atlas', currentExt: 'txt', currentFileName: 'hero.atlas.txt', newName: 'hero', newExt: 'atlas.txt', newFileName: 'hero.atlas.txt', }); assert.deepEqual(compat.planChange( { entryName: 'hero.skel.bytes' }, { name: 'hero.skel', ext: 'bytes' }, utils.splitCompoundName, ), { sourceName: 'hero.skel.bytes', currentName: 'hero.skel', currentExt: 'bytes', currentFileName: 'hero.skel.bytes', newName: 'hero', newExt: 'skel.bytes', newFileName: 'hero.skel.bytes', }); assert.equal(compat.planChange( { entryName: 'hero.atlas.txt' }, { name: 'hero', ext: 'atlas.txt' }, utils.splitCompoundName, ), null); assert.equal(compat.planChange( { entryName: 'hero.atlas' }, { name: 'hero', ext: 'atlas' }, utils.splitCompoundName, ), null); assert.equal(compat.planChange( { entryName: 'hero.atlas.txt' }, { name: 'renamed', ext: 'txt' }, utils.splitCompoundName, ).reason, 'sourceMismatch'); assert.deepEqual(utils.splitCompoundName('Hero.ATLAS.TXT'), { name: 'Hero', ext: 'ATLAS.TXT', }); assert.ok( pluginSource.indexOf('eagleItem.customThumbnail === true') < pluginSource.indexOf('const plan = compoundCompat.planChange'), 'custom thumbnails must be skipped before compatibility changes', ); assert.match(pluginSource, /await prepareCompoundCompatibility\(ready\)[\s\S]*queue\.push/); assert.match(pluginSource, /rollbackCompoundChange/); assert.match(pluginSource, /name=.*ext=/); assert.match(pluginSource, /function showCompoundConfirmation[\s\S]*input\.checked = true[\s\S]*function fieldsText/); assert.match(pluginSource, /const selectedIds = new Set\(await showCompoundConfirmation\(changes\)\)/); assert.match(pluginSource, /if \(location\.textContent\) content\.append\(location\)/); assert.match(pluginSource, /if \(location\.textContent\) details\.append\(location\)/); assert.match(pageSource, /id="compatDialog"/); assert.match(pageSource, /id="compatConfirm"/); assert.match(pageSource, /id="compatSkip"/); assert.match(pageSource, /id="restoreNames"/); assert.match(pageSource, /class="toolbar-actions"[\s\S]*id="clear"[\s\S]*id="restoreNames"/); assert.match(pageSource, /id="restoreDialog"/); assert.doesNotMatch(pageSource, /]*disabled/); assert.match(pluginSource, /input\.checked = canRestore/); assert.match(pluginSource, /compoundCompat\.inspectBackups/); assert.match(pluginSource, /compoundCompat\.restoreBackups\([\s\S]*selected,/); assert.doesNotMatch(pluginSource, /window\.confirm\(t\('thumb\.restoreConfirm'/); for (const relative of [ 'index.html', 'js/compound-compat.js', 'js/instance-coordinator.js', 'js/spine-utils.js', ]) { assert.equal( fs.readFileSync(path.join(__dirname, '..', relative), 'utf8'), fs.readFileSync(path.join(releaseRoot, relative), 'utf8'), relative + ' must match the release copy', ); } for (const file of fs.readdirSync(path.join(__dirname, '..', '_locales'))) { const relative = path.join('_locales', file); assert.equal( fs.readFileSync(path.join(__dirname, '..', relative), 'utf8'), fs.readFileSync(path.join(releaseRoot, relative), 'utf8'), relative + ' must match the release copy', ); } assert.equal( pluginSource.replace('const SHOW_DEV_VERSION = true;', 'const SHOW_DEV_VERSION = false;'), fs.readFileSync(path.join(releaseRoot, 'js', 'plugin.js'), 'utf8'), 'plugin.js may differ only by the development-version switch', ); (async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'spine-compound-compat-')); try { const userData = path.join(root, '用户 data'); fs.mkdirSync(userData); const store = compat.createStore(userData); assert.equal( store.file, path.join(userData, 'SpineThumbnailGenerateServer', 'compound-name-backups.json'), ); const original = { itemId: 'one', sourceName: 'hero.atlas.txt', folderPath: 'Assets/Heroes', currentName: 'hero.atlas', currentExt: 'txt', newName: 'hero', newExt: 'atlas.txt', }; store.remember(original); store.remember({ ...original, newName: 'changed-again', newExt: 'txt' }); assert.equal(store.list().length, 1); assert.equal(store.list()[0].originalName, 'hero.atlas'); const items = new Map([ ['one', { name: 'hero', ext: 'atlas.txt' }], ['changed', { name: 'user-renamed', ext: 'atlas.txt' }], ['failed', { id: 'failed', name: 'failed', ext: 'atlas.txt' }], ]); for (const itemId of ['changed', 'failed']) { store.remember({ itemId, sourceName: itemId + '.atlas.txt', currentName: itemId + '.atlas', currentExt: 'txt', newName: itemId, newExt: 'atlas.txt', }); } for (const itemId of ['ondisk', 'gone']) { store.remember({ itemId, sourceName: itemId + '.skel.bytes', currentName: itemId + '.skel', currentExt: 'bytes', newName: itemId, newExt: 'skel.bytes', }); } const inspected = await compat.inspectBackups( store, async (itemId) => items.get(itemId), async (entry) => entry.itemId === 'ondisk', ); assert.deepEqual(Object.fromEntries(inspected.map((row) => [row.entry.itemId, row.status])), { one: 'ready', changed: 'changed', failed: 'ready', ondisk: 'pluginApplied', gone: 'missing', }); const restored = await compat.restoreBackups( store, ['one', 'changed', 'failed'], async (itemId) => items.get(itemId), async (item) => { if (item.id === 'failed') throw new Error('save failed'); }, ); assert.deepEqual(restored, { total: 3, success: 1, skipped: 1, failed: 1 }); assert.deepEqual(items.get('one'), { name: 'hero.atlas', ext: 'txt' }); assert.deepEqual(items.get('changed'), { name: 'user-renamed', ext: 'atlas.txt' }); assert.equal(store.list().length, 4); } finally { fs.rmSync(root, { recursive: true, force: true }); } console.log('PASS: compound suffix changes require reviewable plans and preserve restorable originals'); })().catch((error) => { console.error(error); process.exitCode = 1; });