import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const { visualFingerprint, hammingDistance } = createRequire(import.meta.url)('../packages/core/visual-hash.cjs'); function image(width = 64, height = 64, layout = 0, brightness = 0) { const rgba = new Uint8Array(width * height * 4); for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) { const nx = x / width, ny = y / height; let value = 220; if (!layout) { if (ny < 0.2) value = 45; if (nx > 0.2 && nx < 0.8 && ny > 0.35 && ny < 0.8) value = 140; if (nx > 0.3 && nx < 0.7 && ny > 0.5 && ny < 0.6) value = 65; } else { if (nx < 0.25) value = 65; if (nx > 0.4 && nx < 0.9 && ny > 0.2 && ny < 0.45) value = 120; if (nx > 0.6 && ny > 0.7) value = 45; } const index = (y * width + x) * 4; rgba[index] = rgba[index + 1] = rgba[index + 2] = value + brightness; rgba[index + 3] = 255; } return { rgba, width, height }; } test('DCT fingerprints are deterministic, brightness stable and resize tolerant', () => { const original = visualFingerprint(image()); assert.equal(original.algorithm, 'phash-dct32-v1'); assert.match(original.hash, /^[0-9a-f]{16}$/); assert.equal(original.usable, true); assert.deepEqual(visualFingerprint(image()), original); assert.equal(hammingDistance(original.hash, visualFingerprint(image(64, 64, 0, 12)).hash), 0); assert.ok(hammingDistance(original.hash, visualFingerprint(image(128, 128)).hash) <= 6); }); test('blank low contrast and transparent images are unusable', () => { for (const value of [0, 127, 255]) { const rgba = new Uint8Array(64 * 64 * 4); for (let index = 0; index < rgba.length; index += 4) { rgba[index] = rgba[index + 1] = rgba[index + 2] = value; rgba[index + 3] = 255; } const result = visualFingerprint({ rgba, width: 64, height: 64 }); assert.equal(result.usable, false); assert.equal(result.quality.contrast, 0); } const transparent = image(); for (let index = 3; index < transparent.rgba.length; index += 4) transparent.rgba[index] = 0; assert.equal(visualFingerprint(transparent).usable, false); }); test('different layouts exceed the conservative similarity threshold', () => { const distance = hammingDistance(visualFingerprint(image()).hash, visualFingerprint(image(64, 64, 1)).hash); assert.ok(distance > 10, `distance ${distance}`); }); test('crop normalizes selected region and clamps safe partial intersections', () => { const input = image(); assert.deepEqual(visualFingerprint({ ...input, region: { x: -10, y: -10, width: 74, height: 74 } }), visualFingerprint(input)); const cropped = visualFingerprint({ ...input, region: { x: 15, y: 15, width: 40, height: 40 } }); assert.notEqual(cropped.hash, visualFingerprint(input).hash); }); test('malformed dimensions buffers crop and hash formats are rejected', () => { const input = image(); for (const change of [{ width: -1 }, { width: 1.5 }, { width: 9000 }, { height: 9000 }, { rgba: [1, 2, 3] }, { rgba: new Uint8Array(4) }, { region: { x: 100, y: 100, width: 3, height: 3 } }, { region: { x: 0, y: 0, width: 0, height: 4 } }, { region: { x: Infinity, y: 0, width: 4, height: 4 } }]) assert.throws(() => visualFingerprint({ ...input, ...change })); assert.throws(() => visualFingerprint({ rgba: new Uint8Array(4), width: 8192, height: 8192 })); assert.throws(() => hammingDistance('A'.repeat(16), '0'.repeat(16))); assert.throws(() => hammingDistance('0', '0'.repeat(16))); assert.equal(hammingDistance('0'.repeat(16), 'f'.repeat(16)), 64); });