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 verrauscht = { ...s64(), partNumber: 'M386A8K4OBM1-CRC4Y' }; const plan = proposeAssignment(session, verrauscht); 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 ohnePn = { ...s64(), partNumber: null }; const plan = proposeAssignment(session, ohnePn); 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 entfernt = undoLast(session); assert.equal(entfernt.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 stapelA = session.stacks.find((s) => s.id === 'A'); const stapelB = session.stacks.find((s) => s.id === 'B'); assert.equal(stapelA, undefined, 'leerer Stapel wird entfernt'); assert.equal(stapelB.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 unvollstaendig = { ...s64(), partNumber: null }; const entry1 = scan(session, unvollstaendig); 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 vollstaendiger = s64(); scan(session, vollstaendiger); 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 unvollstaendig = { ...s32(), partNumber: null }; const a = scan(session, unvollstaendig); moveEntry(session, a.entryId, 'Z'); const stapelZ = session.stacks.find((s) => s.id === 'Z'); assert.notEqual(stapelZ.spec, a.spec, 'Stapel-Spec und Eintrags-Spec duerfen kein gemeinsames Objekt sein'); stapelZ.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 erster = scan(session, emptySpec()); const zweiter = scan(session, emptySpec()); assert.notEqual(erster.stackId, zweiter.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'); }); 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 entfernt = undoLast(session); assert.equal(entfernt.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 stapelA = session.stacks.find((s) => s.id === 'A'); const stapelB = session.stacks.find((s) => s.id === 'B'); assert.equal(stapelA, undefined, 'Stapel A ist seit dem Umsortieren leer und wurde bereits entfernt'); assert.equal(stapelB.count, 1, 'Stapel B enthaelt weiterhin a, nur b wurde zurueckgenommen'); });