test(13-01): add failing test for tenderFingerprint (SCHEMA-03)

RED: NULL-tolerant fingerprint (title+buyer+cpv dominant, value-bucket,
deadline-day), collision guard, umlaut normalization, deterministic
sha256. Implementation follows in the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 08:33:17 +02:00
parent f3cd702197
commit 063ba5b180
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { tenderFingerprint } from './tender-fingerprint';
/**
* SCHEMA-03 (D-04) — pure fingerprint function. NULL-tolerant because the
* live DB is 92.4% NULL estimatedValue / 15.7% NULL deadlineAt (13-RESEARCH
* Pattern 4) — a fingerprint that requires those fields would never match
* the overwhelming majority of real DÖE tenders.
*/
describe('tenderFingerprint', () => {
const base = {
buyerName: 'Stadt München',
title: 'Neubau einer Kindertagesstätte',
cpvDivisions: ['45', '71'],
deadlineAt: null as Date | null,
estimatedValue: null as number | null,
};
it('matches when title+buyer+cpv are equal and value+deadline are both NULL', () => {
const a = tenderFingerprint({ ...base });
const b = tenderFingerprint({ ...base });
expect(a).toBe(b);
});
it('differs for a different title (collision guard, Pitfall 1)', () => {
const a = tenderFingerprint(base);
const b = tenderFingerprint({ ...base, title: 'Sanierung eines Kindergartens' });
expect(a).not.toBe(b);
});
it('normalizes umlauts so "München" and "Muenchen" contribute the same text', () => {
const a = tenderFingerprint({ ...base, buyerName: 'Stadt München' });
const b = tenderFingerprint({ ...base, buyerName: 'Stadt Muenchen' });
expect(a).toBe(b);
});
it('is order-independent and deduplicated for cpvDivisions', () => {
const a = tenderFingerprint({ ...base, cpvDivisions: ['45', '71'] });
const b = tenderFingerprint({ ...base, cpvDivisions: ['71', '45', '71'] });
expect(a).toBe(b);
});
it('buckets estimatedValue by order of magnitude — 12000 and 15000 collide', () => {
const a = tenderFingerprint({ ...base, estimatedValue: 12000 });
const b = tenderFingerprint({ ...base, estimatedValue: 15000 });
expect(a).toBe(b);
});
it('separates estimatedValue across a magnitude boundary', () => {
const a = tenderFingerprint({ ...base, estimatedValue: 9000 });
const b = tenderFingerprint({ ...base, estimatedValue: 90000 });
expect(a).not.toBe(b);
});
it('returns a stable sha256 hex string (deterministic across runs)', () => {
const result = tenderFingerprint(base);
expect(result).toMatch(/^[a-f0-9]{64}$/);
});
it('treats a null buyerName as an empty segment, not a crash', () => {
expect(() => tenderFingerprint({ ...base, buyerName: null })).not.toThrow();
const a = tenderFingerprint({ ...base, buyerName: null });
const b = tenderFingerprint({ ...base, buyerName: null });
expect(a).toBe(b);
});
});