const test = require('node:test'); const assert = require('node:assert/strict'); const { groupDuplicates, signature, recommendKeep } = require('../lib/duplicates'); const { geometryKey } = require('../lib/fingerprint'); const file = (id, model_id, sha256, geometry_hash = null) => ({ id, model_id, sha256, geometry_hash }); test('identical content in two models is an exact group', () => { const groups = groupDuplicates([file(1, 10, 'aaa'), file(2, 20, 'aaa'), file(3, 30, 'zzz')]); assert.equal(groups.length, 1); assert.deepEqual(groups[0].modelIds, [10, 20]); assert.equal(groups[0].exact, true); assert.deepEqual(groups[0].matchedFiles, { 10: 1, 20: 1 }); }); test('same geometry with different bytes is flagged for review', () => { const [group] = groupDuplicates([file(1, 10, 'aaa', 'g1'), file(2, 20, 'bbb', 'g1')]); assert.deepEqual(group.modelIds, [10, 20]); assert.equal(group.exact, false); }); test('same geometry and same bytes stays exact', () => { const [group] = groupDuplicates([file(1, 10, 'aaa', 'g1'), file(2, 20, 'aaa', 'g1')]); assert.equal(group.exact, true); }); test('a file repeated inside one model is not a duplicate group', () => { assert.deepEqual(groupDuplicates([file(1, 10, 'aaa'), file(2, 10, 'aaa')]), []); }); test('links are transitive, so packs collapse into one group', () => { const groups = groupDuplicates([ file(1, 10, 'a'), file(2, 10, 'b'), file(3, 20, 'a'), file(4, 20, 'b', 'g'), file(5, 30, 'c', 'g'), ]); assert.equal(groups.length, 1); assert.deepEqual(groups[0].modelIds, [10, 20, 30]); assert.equal(groups[0].exact, false); assert.deepEqual(groups[0].matchedFiles, { 10: 2, 20: 2, 30: 1 }); }); test('signature ignores order and repeats', () => { assert.equal(signature([3, 1, 2, 1]), '1,2,3'); assert.equal(signature(['20', '10']), '10,20'); }); test('recommendKeep prefers the more complete model, then the older one', () => { const bare = { id: 1, date_added: '2026-01-01', files: [{}], tags: [], projects: [] }; const sourced = { id: 2, date_added: '2026-02-01', source_url: 'https://x', license: 'CC-BY', files: [{}], tags: ['a'], projects: [] }; assert.equal(recommendKeep([bare, sourced]), 2); const older = { ...bare, id: 3, date_added: '2025-12-01' }; assert.equal(recommendKeep([bare, older]), 3); }); test('geometryKey ignores axis order, position and float noise', () => { const a = { faces: 1200, area: 5321.4567, volume: -18342.12, extents: [20, 40.02, 10] }; const b = { faces: 1200, area: 5321.4571, volume: 18342.09, extents: [40.019, 10.001, 20] }; assert.equal(geometryKey(a), geometryKey(b)); assert.notEqual(geometryKey(a), geometryKey({ ...a, faces: 1202 })); assert.notEqual(geometryKey(a), geometryKey({ ...a, extents: [20, 40.02, 12] })); assert.equal(geometryKey({ faces: 0 }), null); });