Files
TanerUslu b903b1c7aa OCR-Rohtext additiv zurückgeben, je Eintrag speichern und anzeigen
recognize() (pipeline.js) liefert zusätzlich den ungefilterten OCR-Rohtext
(rawText) zurück - leer, wenn keine Texterkennung lief. Rein additiv: die
Stapelzuordnung (session.js/spec.js, unangetastet) stützt sich weiterhin
ausschließlich auf die daraus abgeleiteten, verstandenen Spec-Felder.

main.js hängt den Rohtext nach dem Buchen als zusätzliche Eigenschaft an den
Eintrag (analog zu den bereits vorhandenen Barcode-Inhalten je Eintrag).
storage.js sichert/prüft/stellt entry.rawText nach demselben optionalen
Muster wie entry.codes wieder her - ein gesicherter Stand ohne dieses Feld
wird nicht verworfen.

In Funktion "Text erkennen" bleibt die Treffer-Rückmeldung jetzt stehen, bis
der Nutzer sie wegtippt, und zeigt den vollen erkannten Text (lesbar über
dasselbe Scroll-Muster wie die übrigen Vollbild-Overlays); in den beiden
Barcode-Funktionen bleibt es beim automatischen Ausblenden. Das
zurückgegebene Versprechen von showResult löst in jedem Fall - Zeitgeber,
Wegtippen, verdrängende neue Rückmeldung - genau einmal ein, damit die
Bedienung nie dauerhaft gesperrt bleibt. Die Sitzungsliste zeigt den Rohtext
zusätzlich je Eintrag zum Nachlesen.

Tests (pipeline.test.js, storage.test.js): rawText nur bei tatsächlich
gelaufener Texterkennung, unterschiedlicher Rohtext beeinflusst die
Stapelzuordnung nachweislich nicht, Rohtext übersteht Sichern/Laden inkl.
Abwärtskompatibilität zu einem Stand ohne dieses Feld.
2026-07-29 15:19:13 +02:00

375 lines
16 KiB
JavaScript

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');
});