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:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user