import { test } from 'node:test'; import assert from 'node:assert/strict'; import { recognize } from '../src/pipeline.js'; import { emptySpec } from '../src/spec.js'; import { createSession, proposeAssignment, commitAssignment } from '../src/session.js'; const OCR_TEXT = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908'; const deps = ({ codes = [], text = '' }) => ({ decodeBarcodes: async () => codes, runOcr: async () => text, }); test('Barcode mit bekannter Teilenummer ergibt gruen und ueberspringt OCR', async () => { let ocrCalled = false; const result = await recognize({}, { decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'], runOcr: async () => { ocrCalled = true; return ''; }, }); assert.equal(result.source, 'barcode'); assert.equal(result.confidence, 'green'); assert.equal(result.spec.capacityGb, 64); assert.equal(ocrCalled, false, 'OCR darf bei gruenem Barcode nicht laufen'); }); test('Barcode ohne bekannte Teilenummer faellt auf OCR zurueck', async () => { const result = await recognize({}, deps({ codes: ['7325773'], text: OCR_TEXT })); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.capacityGb, 64); }); test('OCR-Ergebnis behaelt die Teilenummer aus dem Barcode bei', async () => { const result = await recognize({}, deps({ codes: ['7325773'], text: '64GB 4DRx4 PC4-2400' })); assert.equal(result.spec.partNumber, '7325773'); }); test('ohne Barcode und ohne brauchbaren Text ergibt rot', async () => { const result = await recognize({}, deps({ codes: [], text: 'Made in Philippines' })); assert.equal(result.source, 'none'); assert.equal(result.confidence, 'red'); }); test('OCR-Fehler fuehrt zu rot statt zu einem Absturz', async () => { const result = await recognize({}, { decodeBarcodes: async () => [], runOcr: async () => { throw new Error('tesseract nicht geladen'); }, }); assert.equal(result.confidence, 'red'); assert.equal(result.source, 'none'); }); test('Barcode-Fehler fuehrt nicht zum Abbruch, OCR uebernimmt', async () => { const result = await recognize({}, { decodeBarcodes: async () => { throw new Error('zxing nicht geladen'); }, runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.spec.capacityGb, 64); }); // --- Beanstandung 1: unerwartete Rueckgabeform statt eines Fehlers --- test('decodeBarcodes liefert null statt einer Liste, kein Absturz', async () => { const result = await recognize({}, { decodeBarcodes: async () => null, runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.capacityGb, 64); }); test('decodeBarcodes liefert undefined statt einer Liste, kein Absturz', async () => { const result = await recognize({}, { decodeBarcodes: async () => undefined, runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.capacityGb, 64); }); test('decodeBarcodes liefert ein Objekt statt einer Liste, kein Absturz', async () => { const result = await recognize({}, { decodeBarcodes: async () => ({ notAList: true }), runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.capacityGb, 64); }); test('runOcr liefert keine Zeichenkette, wird wie leerer Text behandelt', async () => { const result = await recognize({}, { decodeBarcodes: async () => [], runOcr: async () => ({ unexpected: true }), }); assert.equal(result.source, 'none'); assert.equal(result.confidence, 'red'); }); // --- Beanstandung 2: mehrere verwertbare Barcodes im selben Bild --- test('mehrere unvertraegliche Barcodes ergeben rot statt eines ungeprueften ersten Treffers', async () => { let ocrCalled = false; const result = await recognize({}, { // Gleiche Bauform und Dichte, aber widersprechende Geschwindigkeit: // zwei unterschiedliche Module im selben Bild. decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K40BM1-CWE4Y'], runOcr: async () => { ocrCalled = true; return ''; }, }); assert.equal(result.source, 'none'); assert.equal(result.confidence, 'red'); assert.deepEqual(result.spec, emptySpec()); assert.equal(ocrCalled, false, 'bei mehrdeutigem Barcode-Ergebnis soll nicht geraten werden'); }); test('mehrere untereinander vertraegliche Barcodes ergeben gruen (der erste Treffer entscheidet, es gibt nichts zusammenzufuehren)', async () => { let ocrCalled = false; const result = await recognize({}, { // Zwei Lesungen desselben Etiketts (z.B. Mehrfachtreffer durch // Spiegelung) - inhaltlich identisch, nur die Schreibweise weicht // an einer verwechslungstoleranten Stelle ab (B/8). decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K408M1-CRC4Y'], runOcr: async () => { ocrCalled = true; return ''; }, }); assert.equal(result.source, 'barcode'); assert.equal(result.confidence, 'green'); assert.equal(result.spec.capacityGb, 64); assert.equal(result.spec.formFactor, 'LRDIMM'); assert.equal(result.spec.rank, '4DRx4'); assert.equal(result.spec.speed, 'PC4-2400'); assert.equal(ocrCalled, false, 'OCR darf bei gruenem Mehrfachtreffer nicht laufen'); }); // --- Beanstandung 1: haengender Adapter darf die Erkennung nicht dauerhaft blockieren --- test('haengender Barcode-Adapter blockiert nicht dauerhaft, OCR uebernimmt', { timeout: 500 }, async () => { const result = await recognize({}, { decodeBarcodes: () => new Promise(() => {}), // loest nie ein und wirft nie runOcr: async () => OCR_TEXT, barcodeTimeoutMs: 20, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.capacityGb, 64); }); test('haengender OCR-Adapter blockiert nicht dauerhaft, Erkennung ergibt rot', { timeout: 500 }, async () => { const result = await recognize({}, { decodeBarcodes: async () => [], runOcr: () => new Promise(() => {}), // loest nie ein und wirft nie ocrTimeoutMs: 20, }); assert.equal(result.source, 'none'); assert.equal(result.confidence, 'red'); }); // --- Beanstandung 1 (3. Runde): Mehrdeutigkeit bei unverwertbaren Barcodes --- test('zwei widersprechende unverwertbare Barcodes: keiner erhaelt Vorrang, gelesene Teilenummer bleibt', async () => { const result = await recognize({}, { // Beide ohne bekanntes Nummernschema, und sie widersprechen sich - // keiner darf die aus dem Klartext gelesene Teilenummer ueberschreiben. decodeBarcodes: async () => ['7325773', '9999999'], runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.partNumber, 'M386A8K40BM1-CRC4Y'); assert.equal(result.spec.capacityGb, 64); }); test('zwei unverwertbare Barcodes mit gleicher Teilenummer: Vorrang bleibt bestehen', async () => { const result = await recognize({}, { decodeBarcodes: async () => ['7325773', '7325773'], runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.partNumber, '7325773'); }); test('zwei unverwertbare Barcodes, die sich nur in einer Verwechslung unterscheiden, gelten als derselbe', async () => { const result = await recognize({}, { // 'O' und '0' sind eine typische Verwechslung (siehe CONFUSIONS in spec.js). decodeBarcodes: async () => ['732O773', '7320773'], runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.partNumber, '732O773'); }); // --- Erweiterung: unbekanntes Nummernschema mit gelesener Teilenummer genuegt fuer Gruen --- test('Barcode mit unbekanntem Nummernschema und gelesener Teilenummer ergibt gruen, auch ohne Kapazitaet', async () => { const result = await recognize({}, { // Hynix-Teilenummer: Schema unbekannt (nur Samsung ist hinterlegt), aber // exakt gelesen - und OCR findet auf dem Etikett ebenfalls nichts // Verwertbares. Trotzdem genuegt die gelesene Teilenummer fuer Gruen. decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'], runOcr: async () => '', }); assert.equal(result.source, 'barcode'); assert.equal(result.confidence, 'green'); assert.equal(result.spec.partNumber, 'HMA84GL7AFR4N-UH'); assert.equal(result.spec.capacityGb, null); }); test('findet OCR trotz unbekanntem Nummernschema eine Kapazitaet, wird sie uebernommen (weiterhin gelb, nicht gruen)', async () => { const result = await recognize({}, { decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'], runOcr: async () => OCR_TEXT, }); assert.equal(result.source, 'ocr'); assert.equal(result.confidence, 'yellow'); assert.equal(result.spec.capacityGb, 64); // Die Teilenummer aus dem Barcode ist exakter und bleibt erhalten. assert.equal(result.spec.partNumber, 'HMA84GL7AFR4N-UH'); }); test('widersprechende unverwertbare Barcodes ohne jede OCR-Kapazitaet: die Vorsicht bleibt erhalten, kein Gruen', () => recognize({}, { decodeBarcodes: async () => ['7325773', '9999999'], runOcr: async () => '', }).then((result) => { assert.equal(result.source, 'none'); assert.equal(result.confidence, 'red'); })); test('recognize liefert die gelesenen Rohcodes zur Weitergabe an die Stapelzuordnung', async () => { const result = await recognize({}, { decodeBarcodes: async () => ['HMA84GL7AFR4N-UH', 'SN-000123'], runOcr: async () => '', }); assert.deepEqual([...result.codes].sort(), ['HMA84GL7AFR4N-UH', 'SN-000123'].sort()); }); // --- Erweiterung: ein bekannter Code ueberspringt die Texterkennung --- test('bekannter Code ueberspringt die Texterkennung vollstaendig', async () => { let ocrCalled = false; const result = await recognize({}, { decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'], runOcr: async () => { ocrCalled = true; return ''; }, isKnownCode: (code) => code === 'HMA84GL7AFR4N-UH', }); assert.equal(ocrCalled, false, 'ein bekannter Code macht die Texterkennung ueberfluessig'); assert.equal(result.source, 'barcode'); assert.equal(result.confidence, 'green'); }); test('bekannter Code unter mehreren Codes ueberspringt die Texterkennung trotzdem (Seriennummer laeuft ins Leere)', async () => { let ocrCalled = false; const result = await recognize({}, { decodeBarcodes: async () => ['HMA84GL7AFR4N-UH', 'SN-000123'], runOcr: async () => { ocrCalled = true; return ''; }, isKnownCode: (code) => code === 'HMA84GL7AFR4N-UH', }); assert.equal(ocrCalled, false); assert.equal(result.confidence, 'green'); }); test('unbekannter Code (isKnownCode liefert stets false) ueberspringt die Texterkennung nicht', async () => { let ocrCalled = false; const result = await recognize({}, { decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'], runOcr: async () => { ocrCalled = true; return ''; }, isKnownCode: () => false, }); assert.equal(ocrCalled, true); assert.equal(result.confidence, 'green'); }); test('ohne isKnownCode-Abhaengigkeit bleibt die Pipeline ohne sie lauffaehig (Texterkennung laeuft wie bisher)', async () => { let ocrCalled = false; const result = await recognize({}, { decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'], runOcr: async () => { ocrCalled = true; return ''; }, }); assert.equal(ocrCalled, true); assert.equal(result.confidence, 'green'); }); // --- Erweiterung: Rohtext als additives Feld (Funktion "Text erkennen") --- // // Der Rohtext ist reine Anzeige-Information fuer den Nutzer - er verlaesst // die Erkennung, wird aber nirgends zur Stapelzuordnung herangezogen (siehe // den letzten Test unten, der recognize() mit session.js zusammenspielen // laesst). test('recognize liefert den Rohtext, wenn die Texterkennung gelaufen ist (gelbes Ergebnis)', async () => { const result = await recognize({}, deps({ codes: [], text: OCR_TEXT })); assert.equal(result.source, 'ocr'); assert.equal(result.rawText, OCR_TEXT); }); test('recognize liefert den Rohtext auch, wenn die Texterkennung lief, aber nichts Verwertbares fand (rot)', async () => { const result = await recognize({}, deps({ codes: [], text: 'Made in Philippines' })); assert.equal(result.confidence, 'red'); assert.equal(result.rawText, 'Made in Philippines'); }); test('recognize liefert den Rohtext, wenn OCR keine Kapazitaet findet, aber eine unverwertbare Barcode-Teilenummer bestaetigt (gruen)', async () => { // '7325773' hat kein bekanntes Nummernschema (decodePartNumber liefert // keine Kapazitaet) - die Texterkennung laeuft trotzdem und liefert Text // ohne jede Kapazitaetsangabe. Die Teilenummer aus dem Barcode traegt // (siehe recognize()), das Ergebnis bleibt gruen - der gelaufene Rohtext // wird trotzdem zurueckgegeben. const result = await recognize({}, { decodeBarcodes: async () => ['7325773'], runOcr: async () => 'Made in Philippines', }); assert.equal(result.source, 'barcode'); assert.equal(result.confidence, 'green'); assert.equal(result.spec.partNumber, '7325773'); assert.equal(result.rawText, 'Made in Philippines'); }); test('recognize liefert eine leere Zeichenkette als Rohtext, wenn die Texterkennung nicht lief (gruener Barcode-Treffer)', async () => { const result = await recognize({}, { decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'], runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); }, }); assert.equal(result.source, 'barcode'); assert.equal(result.rawText, ''); }); test('recognize liefert eine leere Zeichenkette als Rohtext, wenn ein bekannter Code die Texterkennung ueberfluessig macht', async () => { const result = await recognize({}, { decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'], runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); }, isKnownCode: (code) => code === 'HMA84GL7AFR4N-UH', }); assert.equal(result.rawText, ''); }); test('recognize liefert eine leere Zeichenkette als Rohtext bei mehrdeutigen verwertbaren Barcodes (rot, ohne OCR)', async () => { const result = await recognize({}, { decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K40BM1-CWE4Y'], runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); }, }); assert.equal(result.confidence, 'red'); assert.equal(result.rawText, ''); }); test('der Rohtext beeinflusst die Stapelzuordnung nicht: zwei Aufnahmen mit unterschiedlichem Rohtext, aber gleichen verstandenen Feldern, landen auf demselben Stapel', async () => { const firstText = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908'; // Anderer Rohtext (andere Reihenfolge, zusaetzliches "Made in Korea", // andere Gross-/Kleinschreibung) - dieselben verstandenen Felder. const secondText = 'made in korea m386a8k40bm1-crc4y 64gb 4drx4 PC4-2400 1908'; const first = await recognize({}, deps({ codes: [], text: firstText })); const second = await recognize({}, deps({ codes: [], text: secondText })); assert.notEqual(first.rawText, second.rawText, 'Vorbedingung: die Rohtexte unterscheiden sich tatsaechlich'); assert.deepEqual(first.spec, second.spec, 'Vorbedingung: dieselben verstandenen Felder'); const session = createSession(); const plan1 = proposeAssignment(session, first.spec, first.codes); const entry1 = commitAssignment(session, first.spec, first.source, plan1.stackId, first.codes); // Der Rohtext haengt (wie main.js es tut) rein additiv am Eintrag, ohne // dass session.js davon weiss - siehe main.js/processCapture(). entry1.rawText = first.rawText; const plan2 = proposeAssignment(session, second.spec, second.codes); assert.equal(plan2.kind, 'match', 'das zweite Modul muss denselben Stapel treffen'); const entry2 = commitAssignment(session, second.spec, second.source, plan2.stackId, second.codes); entry2.rawText = second.rawText; assert.equal(entry1.stackId, entry2.stackId, 'unterschiedlicher Rohtext darf keinen eigenen Stapel erzeugen'); assert.notEqual(entry1.rawText, entry2.rawText, 'die Eintraege fuehren trotzdem je ihren eigenen Rohtext'); });