printhive/test/spoolSync.test.js
dlawler489 48f564c648 Add spools automatically from what the printer has loaded
Every Bambu spool reports an RFID identity — a spool id and a tag id, both
unique — so a printer's report can now keep the Filament page in step: a new
tagged spool is created, named and coded from the bundled filament list; one
already known is moved to its current slot; one you added by hand before the
printer was connected is adopted rather than duplicated; and one the printer no
longer reports is marked as taken out. It runs on "Save & test connection" and
every ten minutes in the background, still only reading from the printer.

Untagged spools are listed, not stored: without a tag, one spool of a colour is
indistinguishable from the next, so guessing would mean duplicates.

The planning is a pure function, tested against the cases that would cause
duplicates. The database side was exercised separately against a real
Postgres: three successive reports created, moved and unloaded spools as
intended, and a deliberate duplicate was refused by the partial unique index.

The amount left isn't taken from the printer yet: an AMS lite only reports full
or empty, so it's left blank until printers can say which kind of AMS they have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:39:30 +10:00

84 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const test = require('node:test');
const assert = require('node:assert/strict');
const { planSync, slotKey } = require('../lib/spoolSync');
const { paletteMatch } = require('../lib/filaments');
const palette = require('../db/bambu-filaments.json').filaments;
const SERIAL = 'PRINTER1';
const tray = (slot, hex, uuid, extra = {}) => ({
unit: 0, slot, empty: false, material: 'PLA', type: 'Basic', colour: hex, brand: 'PLA Basic',
uuid, tagUid: uuid ? `TAG${slot}` : null, idName: null, weight: 1000, ...extra,
});
test('a tagged spool the shelf has never seen is created and named from the filament list', () => {
const { actions, untracked } = planSync([], [tray(0, '#C12E1F', 'U-RED', { idName: 'A00-R0' })], SERIAL, palette);
assert.equal(untracked.length, 0);
assert.equal(actions.length, 1);
assert.equal(actions[0].op, 'create');
assert.equal(actions[0].slot, 'PRINTER1/0/0');
assert.equal(actions[0].spool.colour, 'Red');
assert.equal(actions[0].spool.code, '10200');
assert.equal(actions[0].spool.colour_hex, '#C12E1F');
assert.equal(actions[0].spool.product_code, 'A00-R0');
assert.equal(actions[0].spool.total_g, 1000);
assert.equal(actions[0].spool.remaining_g, null, 'an AMS lite can’t say how much is left');
});
test('a spool the shelf already knows is moved, not added again', () => {
const existing = [{ id: 7, material: 'PLA', colour_hex: '#000000', rfid_uuid: 'U-BLACK',
printer_serial: SERIAL, ams_slot: 'PRINTER1/0/3' }];
const { actions } = planSync(existing, [tray(1, '#000000', 'U-BLACK')], SERIAL, palette);
assert.deepEqual(actions, [{ op: 'update', id: 7, slot: 'PRINTER1/0/1', printerSerial: SERIAL }]);
});
test('a spool added by hand is adopted by the printer rather than duplicated', () => {
// The Jade White spool added before the printer was connected.
const existing = [{ id: 1, material: 'PLA', colour_hex: '#ffffff', rfid_uuid: null, printer_serial: null, ams_slot: null }];
const { actions } = planSync(existing, [tray(3, '#FFFFFF', 'U-WHITE', { idName: 'A00-W1' })], SERIAL, palette);
assert.equal(actions.length, 1);
assert.equal(actions[0].op, 'adopt');
assert.equal(actions[0].id, 1);
assert.equal(actions[0].uuid, 'U-WHITE');
assert.equal(actions[0].code, '10100');
});
test('one hand-added spool is adopted once, even with two matching trays', () => {
const existing = [{ id: 1, material: 'PLA', colour_hex: '#FFFFFF', rfid_uuid: null }];
const { actions } = planSync(existing, [tray(0, '#FFFFFF', 'U-W1'), tray(1, '#FFFFFF', 'U-W2')], SERIAL, palette);
assert.deepEqual(actions.map((a) => a.op), ['adopt', 'create']);
});
test('a hand-added spool of another colour or material is left alone', () => {
const existing = [
{ id: 1, material: 'PLA', colour_hex: '#FF0000', rfid_uuid: null }, // wrong colour
{ id: 2, material: 'PETG', colour_hex: '#FFFFFF', rfid_uuid: null }, // wrong material
];
const { actions } = planSync(existing, [tray(0, '#FFFFFF', 'U-W')], SERIAL, palette);
assert.deepEqual(actions.map((a) => a.op), ['create']);
});
test('an untagged spool is listed, not guessed at', () => {
const { actions, untracked } = planSync([], [tray(2, '#123456', null)], SERIAL, palette);
assert.deepEqual(actions, []);
assert.deepEqual(untracked, [{ slot: 'PRINTER1/0/2', material: 'PLA', colour: '#123456' }]);
});
test('a spool this printer no longer reports is unloaded; other printers’ spools are not', () => {
const existing = [
{ id: 5, material: 'PLA', colour_hex: '#000000', rfid_uuid: 'U-GONE', printer_serial: SERIAL, ams_slot: 'PRINTER1/0/0' },
{ id: 6, material: 'PLA', colour_hex: '#000000', rfid_uuid: 'U-ELSEWHERE', printer_serial: 'OTHER', ams_slot: 'OTHER/0/0' },
];
const { actions } = planSync(existing, [], SERIAL, palette);
assert.deepEqual(actions, [{ op: 'unload', id: 5 }]);
});
test('empty trays are ignored, and the external holder has its own slot key', () => {
assert.deepEqual(planSync([], [{ empty: true, unit: 0, slot: 0 }], SERIAL, palette).actions, []);
assert.equal(slotKey(SERIAL, { unit: null, slot: 254 }), 'PRINTER1/ext/254');
});
test('palette matching names a colour within its product line, and doesn’t guess', () => {
assert.deepEqual(paletteMatch(palette, 'PLA', 'Basic', '#FFFFFF'), { name: 'Jade White', code: '10100', type: 'Basic' });
assert.equal(paletteMatch(palette, 'PLA', 'Matte', '#FFFFFF').name, 'Ivory White');
assert.equal(paletteMatch(palette, 'PLA', 'Basic', '#123456'), null);
});