import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import ts from 'typescript' const source = readFileSync(new URL('../utils/libraryBrowse.ts', import.meta.url), 'utf8') const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText const { buildLibraryEntries, chronologicalStills } = await import(`data:text/javascript;base64,${Buffer.from(js).toString('base64')}`) const image = (id, extra = {}) => ({ id, folderId: 'inbox', filename: `${id}.png`, role: 'output', createdAt: 1, width: 512, height: 512, ...extra }) const entries = stills => buildLibraryEntries({ stills, clips: [], folderId: 'all', filter: 'images', search: '', sort: 'date-desc' }) test('manual grouping joins unrelated images without changing iteration ancestry', () => { const a = image('a', { familyId: 'iteration-a', chainIndex: 8, manualGroupId: 'manual', manualGroupName: 'Favorites', manualGroupIndex: 0 }) const b = image('b', { familyId: 'iteration-b', chainIndex: 0, manualGroupId: 'manual', manualGroupName: 'Favorites', manualGroupIndex: 1 }) const grouped = entries([b, a]) assert.equal(grouped.length, 1) assert.equal(grouped[0].name, 'Favorites') assert.deepEqual(chronologicalStills(grouped[0].stillGroup.parts).map(x => x.id), ['a', 'b']) assert.equal(a.familyId, 'iteration-a') assert.equal(b.familyId, 'iteration-b') assert.equal(entries([a, b].map(({ manualGroupId, manualGroupName, manualGroupIndex, ...rest }) => rest)).length, 2) }) test('groups remain folder scoped and do not absorb future iterations', () => { const a = image('a', { familyId: 'iteration', manualGroupId: 'manual' }) const b = image('b', { familyId: 'iteration' }) const c = image('c', { folderId: 'private', manualGroupId: 'manual' }) assert.equal(entries([a, b, c]).length, 3) }) const librarySource = readFileSync(new URL('../server/utils/library.ts', import.meta.url), 'utf8') const ast = ts.createSourceFile('library.ts', librarySource, ts.ScriptTarget.Latest, true) const names = ['stillsInSameFamily', 'groupLibraryStills', 'moveStillFamily', 'renameStillFamily'] const functions = ast.statements.filter(node => ts.isFunctionDeclaration(node) && names.includes(node.name?.text)).map(node => node.getText(ast).replace(/^export /, '')).join('\n') const serverJs = ts.transpileModule(functions, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText function server(catalog) { return new Function('mutate', 'stillRole', 'createError', 'familyBaseName', 'stillChainName', `${serverJs}; return { ${names.join(',')} };`)( async (_, fn) => fn(catalog), item => item.role, error => Object.assign(new Error(error.statusMessage), error), name => name, (name, index) => `${name} ${index}`) } test('group, rename, move and ungroup preserve original generation links and names', async () => { const a = image('a', { familyId: 'first', name: 'Original A' }) const b = image('b', { familyId: 'second', name: 'Original B' }) const catalog = { stills: [a, b], folders: [{ id: 'inbox' }, { id: 'other' }] } const api = server(catalog) await api.groupLibraryStills('owner', ['a', 'b']) assert.equal(a.manualGroupId, b.manualGroupId) await api.renameStillFamily('owner', 'a', 'Favorites') await api.moveStillFamily('owner', 'a', 'other') assert.equal(b.folderId, 'other') assert.equal(b.manualGroupName, 'Favorites') assert.equal(a.name, 'Original A') assert.equal(b.familyId, 'second') await api.groupLibraryStills('owner', ['a'], true) assert.equal(a.manualGroupId, undefined) assert.equal(b.manualGroupId, undefined) assert.equal(a.familyId, 'first') }) test('invalid grouping is rejected before modifying any images', async () => { const a = image('a') const b = image('b', { folderId: 'private' }) const api = server({ stills: [a, b] }) await assert.rejects(api.groupLibraryStills('owner', ['a', 'b']), /same folder/) await assert.rejects(api.groupLibraryStills('owner', ['a', 'missing']), /generated images/) assert.equal(a.manualGroupId, undefined) })