Files
ocr_scanner/test/session.test.js
T
vchuser 61dde3eb08 Stapel merken sich gelesene Barcode-Inhalte, entscheiden die Zuordnung
Jeder Stapel fuehrt jetzt die Menge der Barcode-Zeichenketten, die bei
seinen Modulen gelesen wurden (stack.codes). Traegt ein gescanntes Modul
einen Code, den ein Stapel bereits kennt, hat das Vorrang vor dem Vergleich
der technischen Angaben - ein Barcode ist exakt gelesen, abgeleitete Angaben
koennen fehlerbehaftet sein. Genau ein Stapel ueber einen bekannten Code
-> Zuweisung, auch bei unvollstaendigen Angaben; mehrere -> weiterhin
mehrdeutig, der Nutzer entscheidet; kein passender Code -> unveraendert die
bisherige Regel. Eine Seriennummer, die bei jedem Modul anders ist, laeuft
dabei einfach ins Leere, ohne die Zuordnung ueber die Teilenummer zu
verhindern.

proposeAssignment und commitAssignment bekommen dafuer einen neuen, optional
en codes-Parameter (Vorgabe []); bestehende Aufrufe ohne diesen Parameter
verhalten sich unveraendert.

storage.js sichert und prueft stack.codes jetzt mit: fehlt das Feld (Stand
aus einer aelteren Fassung), gilt der Stapel als ohne bekannte Codes statt
den ganzen Stand zu verwerfen; ist es vorhanden, muss es eine Liste von
Zeichenketten sein wie jedes andere Feld auch.
2026-07-29 09:05:23 +02:00

260 lines
11 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { emptySpec } from '../src/spec.js';
import {
createSession, proposeAssignment, commitAssignment,
undoLast, moveEntry, removeEntry,
} from '../src/session.js';
const s64 = () => ({ ...emptySpec(), capacityGb: 64, formFactor: 'LRDIMM', rank: '4DRx4', speed: 'PC4-2400', partNumber: 'M386A8K40BM1-CRC4Y' });
const s32 = () => ({ ...emptySpec(), capacityGb: 32, formFactor: 'RDIMM', rank: '2Rx4', speed: 'PC4-2666', partNumber: 'M393A4K40BB1-CTD' });
function scan(session, spec, source = 'barcode') {
const plan = proposeAssignment(session, spec);
return commitAssignment(session, spec, source, plan.stackId);
}
test('erster Scan legt Stapel A an', () => {
const session = createSession();
const plan = proposeAssignment(session, s64());
assert.equal(plan.kind, 'new');
assert.equal(plan.stackId, 'A');
});
test('gleiches Modul landet auf demselben Stapel', () => {
const session = createSession();
scan(session, s64());
const plan = proposeAssignment(session, s64());
assert.equal(plan.kind, 'match');
assert.equal(plan.stackId, 'A');
});
test('anderes Modul legt Stapel B an', () => {
const session = createSession();
scan(session, s64());
const plan = proposeAssignment(session, s32());
assert.equal(plan.kind, 'new');
assert.equal(plan.stackId, 'B');
});
test('eine Verwechslung erzeugt keinen Fast-Duplikat-Stapel', () => {
const session = createSession();
scan(session, s64());
const noisy = { ...s64(), partNumber: 'M386A8K4OBM1-CRC4Y' };
const plan = proposeAssignment(session, noisy);
assert.equal(plan.kind, 'match');
assert.equal(plan.stackId, 'A');
});
test('Scan ohne Teilenummer bei zwei passenden Stapeln ist mehrdeutig', () => {
const session = createSession();
scan(session, { ...s64(), partNumber: 'M386A8K40BM1-CRC4Y' });
scan(session, { ...s64(), partNumber: 'HMAA8GL7AMR4N-UH' });
const withoutPartNumber = { ...s64(), partNumber: null };
const plan = proposeAssignment(session, withoutPartNumber);
assert.equal(plan.kind, 'ambiguous');
assert.deepEqual(plan.candidates, ['A', 'B']);
});
test('commitAssignment zaehlt den Stapel hoch und fuehrt die Liste', () => {
const session = createSession();
scan(session, s64());
scan(session, s64());
assert.equal(session.stacks.length, 1);
assert.equal(session.stacks[0].count, 2);
assert.equal(session.entries.length, 2);
});
test('undoLast nimmt den letzten Eintrag zurueck', () => {
const session = createSession();
scan(session, s64());
scan(session, s64());
const removed = undoLast(session);
assert.equal(removed.stackId, 'A');
assert.equal(session.stacks[0].count, 1);
assert.equal(session.entries.length, 1);
});
test('undoLast entfernt einen leer gewordenen Stapel', () => {
const session = createSession();
scan(session, s64());
undoLast(session);
assert.equal(session.stacks.length, 0);
assert.equal(undoLast(session), null);
});
test('moveEntry sortiert einen Eintrag um', () => {
const session = createSession();
const a = scan(session, s64());
scan(session, s32());
moveEntry(session, a.entryId, 'B');
const stackA = session.stacks.find((s) => s.id === 'A');
const stackB = session.stacks.find((s) => s.id === 'B');
assert.equal(stackA, undefined, 'leerer Stapel wird entfernt');
assert.equal(stackB.count, 2);
});
test('removeEntry entfernt einen einzelnen Eintrag', () => {
const session = createSession();
const a = scan(session, s64());
scan(session, s64());
removeEntry(session, a.entryId);
assert.equal(session.entries.length, 1);
assert.equal(session.stacks[0].count, 1);
});
test('Ergaenzung des Stapel-Specs veraendert nicht das Spec eines frueheren Eintrags', () => {
const session = createSession();
const incomplete = { ...s64(), partNumber: null };
const entry1 = scan(session, incomplete);
const stack = session.stacks.find((s) => s.id === entry1.stackId);
assert.notEqual(stack.spec, entry1.spec, 'Stapel-Spec und Eintrags-Spec duerfen kein gemeinsames Objekt sein');
assert.deepEqual(stack.spec, entry1.spec, 'inhaltlich sind beide Specs zunaechst gleich');
assert.equal(entry1.spec.partNumber, null);
const complete = s64();
scan(session, complete);
assert.equal(entry1.spec.partNumber, null, 'das Spec des ersten Eintrags darf durch eine spaetere Ergaenzung des Stapel-Specs nicht veraendert werden');
assert.equal(stack.spec.partNumber, 'M386A8K40BM1-CRC4Y');
});
test('moveEntry legt fuer einen neuen Stapel eine eigene Kopie des Specs an', () => {
const session = createSession();
const incomplete = { ...s32(), partNumber: null };
const a = scan(session, incomplete);
moveEntry(session, a.entryId, 'Z');
const stackZ = session.stacks.find((s) => s.id === 'Z');
assert.notEqual(stackZ.spec, a.spec, 'Stapel-Spec und Eintrags-Spec duerfen kein gemeinsames Objekt sein');
stackZ.spec.partNumber = 'M393A4K40BB1-CTD';
assert.equal(a.spec.partNumber, null, 'eine Ergaenzung des neu angelegten Stapel-Specs darf das Eintrags-Spec nicht veraendern');
});
test('ein Stapel ohne jede erkannte Angabe ist fuer kein Modul Kandidat', () => {
const session = createSession();
scan(session, emptySpec()); // Stapel A, vollstaendig leeres Spec (Rot-Fall)
const plan = proposeAssignment(session, s64());
assert.equal(plan.kind, 'new', 'ein leerer Stapel darf keine Kandidatur begruenden');
assert.notEqual(plan.stackId, 'A');
});
test('ein Stapel mit nur der Kapazitaet ist Kandidat fuer ein Modul gleicher Kapazitaet', () => {
const session = createSession();
scan(session, { ...emptySpec(), capacityGb: 64 });
const plan = proposeAssignment(session, { ...emptySpec(), capacityGb: 64 });
assert.equal(plan.kind, 'match');
assert.equal(plan.stackId, 'A');
});
test('ein voellig unerkanntes Modul erzeugt einen eigenen Stapel und zieht keine weiteren an sich', () => {
const session = createSession();
const first = scan(session, emptySpec());
const second = scan(session, emptySpec());
assert.notEqual(first.stackId, second.stackId, 'zwei unerkannte Module ohne gemeinsame Grundlage teilen sich keinen Stapel');
assert.equal(session.stacks.length, 2);
const plan = proposeAssignment(session, s64());
assert.equal(plan.kind, 'new', 'der leere Stapel darf ein erkanntes Modul nicht an sich ziehen');
});
// --- Erweiterung: bekannte Barcode-Inhalte entscheiden die Zuordnung ---
test('bekannter Barcode-Inhalt entscheidet die Zuordnung, auch bei unvollstaendigen Angaben', () => {
const session = createSession();
const unknownScheme = { ...emptySpec(), partNumber: 'HMA84GL7AFR4N-UH' };
commitAssignment(session, unknownScheme, 'barcode', 'A', ['HMA84GL7AFR4N-UH']);
// Angaben diesmal komplett leer - ohne Codeabgleich waere das mangels
// gemeinsamen Merkmals (sharesSetField) niemals ein Treffer.
const plan = proposeAssignment(session, emptySpec(), ['HMA84GL7AFR4N-UH']);
assert.equal(plan.kind, 'match');
assert.equal(plan.stackId, 'A');
});
test('anderer Barcode-Inhalt erzeugt einen eigenen Stapel', () => {
const session = createSession();
commitAssignment(session, { ...emptySpec(), partNumber: 'HMA84GL7AFR4N-UH' }, 'barcode', 'A', ['HMA84GL7AFR4N-UH']);
const plan = proposeAssignment(session, { ...emptySpec(), partNumber: 'ANDERE-PN' }, ['ANDERE-PN']);
assert.equal(plan.kind, 'new');
assert.notEqual(plan.stackId, 'A');
});
test('eine bei jedem Modul wechselnde Seriennummer verhindert die Zuordnung ueber die stabile Teilenummer nicht', () => {
const session = createSession();
commitAssignment(
session,
{ ...emptySpec(), partNumber: 'HMA84GL7AFR4N-UH' },
'barcode',
'A',
['HMA84GL7AFR4N-UH', 'SN-AAA111'],
);
// Gleiche Teilenummer, aber eine andere Seriennummer als beim ersten Mal -
// die Seriennummer laeuft ins Leere, die Teilenummer traegt trotzdem.
const plan = proposeAssignment(session, emptySpec(), ['HMA84GL7AFR4N-UH', 'SN-BBB222']);
assert.equal(plan.kind, 'match');
assert.equal(plan.stackId, 'A');
});
test('Codeabgleich hat Vorrang vor dem Vergleich der technischen Angaben', () => {
const session = createSession();
// B ist technisch identisch zu A (waere ohne Codeabgleich ebenfalls
// Kandidat und wuerde damit sogar Mehrdeutigkeit ausloesen), kennt den
// gescannten Code aber nicht.
commitAssignment(session, s64(), 'barcode', 'A', ['CODE-A']);
commitAssignment(session, s64(), 'barcode', 'B');
const plan = proposeAssignment(session, s64(), ['CODE-A']);
assert.equal(plan.kind, 'match');
assert.equal(plan.stackId, 'A');
});
test('mehrere Stapel mit passendem Code sind mehrdeutig, der Nutzer entscheidet', () => {
const session = createSession();
commitAssignment(session, emptySpec(), 'barcode', 'A', ['CODE-X']);
commitAssignment(session, emptySpec(), 'barcode', 'B', ['CODE-X']);
const plan = proposeAssignment(session, emptySpec(), ['CODE-X']);
assert.equal(plan.kind, 'ambiguous');
assert.deepEqual(plan.candidates.sort(), ['A', 'B']);
});
test('passt kein Code, bleibt die bisherige Regel (Vergleich der Angaben) unveraendert', () => {
const session = createSession();
commitAssignment(session, s64(), 'barcode', 'A', ['CODE-A']);
const plan = proposeAssignment(session, s64(), ['VOELLIG-ANDERER-CODE']);
assert.equal(plan.kind, 'match');
assert.equal(plan.stackId, 'A');
});
test('proposeAssignment und commitAssignment funktionieren ohne codes-Parameter weiterhin (Abwaertskompatibilitaet)', () => {
const session = createSession();
const plan = proposeAssignment(session, s64());
const entry = commitAssignment(session, s64(), 'barcode', plan.stackId);
assert.equal(entry.stackId, 'A');
assert.deepEqual(session.stacks[0].codes, []);
});
test('ein Stapel sammelt unterschiedliche Codes ueber mehrere Scans hinweg', () => {
const session = createSession();
commitAssignment(session, emptySpec(), 'barcode', 'A', ['CODE-1']);
commitAssignment(session, emptySpec(), 'barcode', 'A', ['CODE-1', 'CODE-2']);
const stackA = session.stacks.find((s) => s.id === 'A');
assert.deepEqual([...stackA.codes].sort(), ['CODE-1', 'CODE-2']);
});
test('undoLast-Vertrag: nach einem Umsortieren wird weiterhin der zuletzt erfasste Eintrag zurueckgenommen', () => {
const session = createSession();
const a = scan(session, s64());
const b = scan(session, s32());
moveEntry(session, a.entryId, 'B');
// Zuletzt ausgefuehrte Handlung war das Umsortieren von a; zuletzt erfasst wurde jedoch b.
const removed = undoLast(session);
assert.equal(removed.entryId, b.entryId, 'undoLast nimmt den zuletzt erfassten Eintrag zurueck, nicht die zuletzt ausgefuehrte Handlung (das Umsortieren)');
assert.equal(session.entries.length, 1);
assert.equal(session.entries[0].entryId, a.entryId);
assert.equal(session.entries[0].stackId, 'B', 'das Umsortieren von a bleibt von der Rueckname des unbeteiligten Eintrags b unberuehrt');
const stackA = session.stacks.find((s) => s.id === 'A');
const stackB = session.stacks.find((s) => s.id === 'B');
assert.equal(stackA, undefined, 'Stapel A ist seit dem Umsortieren leer und wurde bereits entfernt');
assert.equal(stackB.count, 1, 'Stapel B enthaelt weiterhin a, nur b wurde zurueckgenommen');
});