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.
260 lines
11 KiB
JavaScript
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');
|
|
});
|