Ein gelesener Rohcode ist nicht garantiert eine Teilenummer: teilen sich zwei technisch verschiedene Module z.B. denselben Los-Tag, hat der bisherige Codeabgleich sie ohne Rueckfrage auf denselben Stapel gezogen. proposeAssignment verlangt fuer einen Codekandidaten jetzt zusaetzlich specsCompatible; widerspricht der einzige Treffer, faellt die Zuordnung auf den Vergleich der technischen Angaben zurueck und im Zweifel auf einen eigenen Stapel. TDD: Test zuerst rot gesehen (Los-Tag-Fall aus der Beanstandung), dann die Filterung ergaenzt.
313 lines
14 KiB
JavaScript
313 lines
14 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { emptySpec, specsCompatible } 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']);
|
|
});
|
|
|
|
// --- Fehler 1: ein Codetreffer darf einen Widerspruch der Angaben nicht ueberstimmen ---
|
|
|
|
test('Codetreffer mit widersprechenden technischen Angaben ist kein Kandidat (geteilter Los-Tag statt Teilenummer)', () => {
|
|
const session = createSession();
|
|
// Modul 1: Samsung 64GB LRDIMM, Etikett traegt zusaetzlich einen Los-Tag.
|
|
const modul1 = { ...emptySpec(), capacityGb: 64, formFactor: 'LRDIMM' };
|
|
commitAssignment(session, modul1, 'barcode', 'A', ['LOS-2024-07']);
|
|
|
|
// Modul 2: Samsung 64GB RDIMM (andere Bauform!), aber derselbe Los-Tag,
|
|
// weil beide Etiketten aus derselben Fertigungscharge stammen.
|
|
const modul2 = { ...emptySpec(), capacityGb: 64, formFactor: 'RDIMM' };
|
|
assert.equal(specsCompatible(modul1, modul2), false, 'Vorbedingung: die Bauformen widersprechen sich');
|
|
|
|
const plan = proposeAssignment(session, modul2, ['LOS-2024-07']);
|
|
assert.notEqual(plan.stackId, 'A', 'ein RDIMM darf trotz geteiltem Los-Tag nicht auf den LRDIMM-Stapel A');
|
|
assert.equal(plan.kind, 'new', 'ohne vertraeglichen Kandidaten faellt das Modul auf einen eigenen Stapel zurueck');
|
|
});
|
|
|
|
// --- Fehler 2: eine Korrektur (Umsortieren/Entfernen) muss den Code am alten Stapel entfernen ---
|
|
|
|
test('nach moveEntry zieht der Code des umsortierten Eintrags den alten Stapel nicht mehr an', () => {
|
|
const session = createSession();
|
|
const a = commitAssignment(session, emptySpec(), 'barcode', 'A', ['CODE-X']);
|
|
// Ein zweiter Eintrag mit anderem Code haelt Stapel A am Leben, damit der
|
|
// Test tatsaechlich prueft, dass CODE-X verschwindet - nicht nur, dass ein
|
|
// leerer Stapel entfernt wurde.
|
|
commitAssignment(session, emptySpec(), 'barcode', 'A', ['CODE-Y']);
|
|
|
|
moveEntry(session, a.entryId, 'B');
|
|
|
|
const stackA = session.stacks.find((s) => s.id === 'A');
|
|
assert.ok(stackA, 'Stapel A bleibt bestehen (der zweite Eintrag mit CODE-Y haengt noch dort)');
|
|
assert.deepEqual(stackA.codes, ['CODE-Y'], 'CODE-X darf nach dem Umsortieren nicht mehr zu Stapel A gehoeren');
|
|
|
|
const plan = proposeAssignment(session, emptySpec(), ['CODE-X']);
|
|
assert.notEqual(plan.stackId, 'A', 'ein neues Modul mit CODE-X darf Stapel A nicht mehr an sich ziehen');
|
|
});
|
|
|
|
test('nach removeEntry zieht der Code des entfernten Eintrags den alten Stapel nicht mehr an', () => {
|
|
const session = createSession();
|
|
const a = commitAssignment(session, emptySpec(), 'barcode', 'A', ['CODE-X']);
|
|
commitAssignment(session, emptySpec(), 'barcode', 'A', ['CODE-Y']);
|
|
|
|
removeEntry(session, a.entryId);
|
|
|
|
const stackA = session.stacks.find((s) => s.id === 'A');
|
|
assert.ok(stackA, 'Stapel A bleibt bestehen (der zweite Eintrag mit CODE-Y haengt noch dort)');
|
|
assert.deepEqual(stackA.codes, ['CODE-Y'], 'CODE-X darf nach dem Entfernen nicht mehr zu Stapel A gehoeren');
|
|
|
|
const plan = proposeAssignment(session, emptySpec(), ['CODE-X']);
|
|
assert.notEqual(plan.stackId, 'A', 'ein neues Modul mit CODE-X darf Stapel A nicht mehr an sich ziehen');
|
|
});
|
|
|
|
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');
|
|
});
|