/** * assignMarkerId.test.js * ====================== * Integration-Test für server/editRobot.js → assignMarkerId(). * * Testet insbesondere den Guard für fehlende position_mm (Marker ohne * triangulierte Position, z.B. Einzelkamera-Marker nach Schritt 5). * * Technisch: editRobot.js ist ein ES-Modul — es wird über den dünnen Runner * test/fixtures/runAssignMarkerId.mjs per spawnSync aufgerufen (gleiche * Strategie wie yAxisRotation.test.js für das Python-Skript). * Datei-I/O läuft gegen echte Temp-Dateien (os.tmpdir()). */ const { spawnSync } = require('child_process'); const os = require('os'); const fs = require('fs'); const path = require('path'); const RUNNER = path.join(__dirname, 'fixtures', 'runAssignMarkerId.mjs'); // ── Hilfsfunktionen ─────────────────────────────────────────────────────────── function callAssignMarkerId(robotPath, params) { const proc = spawnSync('node', [RUNNER, robotPath, JSON.stringify(params)], { encoding: 'utf-8', }); if (proc.error) throw proc.error; const stdout = (proc.stdout ?? '').trim(); if (!stdout) throw new Error(`Kein Output.\nstderr: ${proc.stderr}`); return JSON.parse(stdout); } function makeTempRobot(content) { const p = path.join( os.tmpdir(), `robot_test_${Date.now()}_${Math.random().toString(36).slice(2)}.json` ); fs.writeFileSync(p, JSON.stringify(content, null, 2), 'utf8'); return p; } const ROBOT_WITH_42 = () => ({ links: { Arm1: { markers: [{ id: 42, set: 'A0', position: [100, 200, 300] }] }, }, }); const ROBOT_EMPTY = () => ({ links: { Arm1: { markers: [] } }, }); // ── Eingabe-Validierung ─────────────────────────────────────────────────────── describe('assignMarkerId – Eingabe-Validierung', () => { test('ungültige Marker-ID → changed: false', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: -1, link: 'Arm1' }); expect(r.changed).toBe(false); expect(r.error).toMatch(/Ungültige Marker-ID/); } finally { fs.unlinkSync(p); } }); test('kein link für neuen Marker → changed: false', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: 99, extraMarkers: [{ marker_id: 99, position_mm: [10, 20, 30] }], }); expect(r.changed).toBe(false); expect(r.error).toMatch(/Link/); } finally { fs.unlinkSync(p); } }); }); // ── Guard: fehlende position_mm ─────────────────────────────────────────────── describe('assignMarkerId – Guard: fehlende position_mm (z.B. Einzelkamera-Marker)', () => { test('extraMarker ohne position_mm → changed: false, kein Crash', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: 55, link: 'Arm1', extraMarkers: [{ marker_id: 55 }], // kein position_mm }); expect(r.changed).toBe(false); expect(r.error).toMatch(/position_mm fehlt/); } finally { fs.unlinkSync(p); } }); test('extraMarker mit position_mm: null → changed: false, kein Crash', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: 56, link: 'Arm1', extraMarkers: [{ marker_id: 56, position_mm: null }], }); expect(r.changed).toBe(false); expect(r.error).toMatch(/position_mm fehlt/); } finally { fs.unlinkSync(p); } }); test('extraMarker mit position_mm als String → changed: false, kein Crash', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: 57, link: 'Arm1', extraMarkers: [{ marker_id: 57, position_mm: '[1,2,3]' }], }); expect(r.changed).toBe(false); expect(r.error).toMatch(/position_mm fehlt/); } finally { fs.unlinkSync(p); } }); }); // ── Normalfall: Marker hinzufügen ───────────────────────────────────────────── describe('assignMarkerId – Normalfall: neuen Marker hinzufügen', () => { test('gültige position_mm → changed: true, action: added, Datei geschrieben', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: 77, link: 'Arm1', extraMarkers: [{ marker_id: 77, position_mm: [10.1, 20.22, 30.333] }], }); expect(r.changed).toBe(true); expect(r.change.action).toBe('added'); expect(r.change.markerId).toBe(77); expect(r.change.newLink).toBe('Arm1'); const saved = JSON.parse(fs.readFileSync(p, 'utf8')); const added = saved.links.Arm1.markers.find(m => m.id === 77); expect(added).toBeDefined(); expect(Array.isArray(added.position)).toBe(true); expect(added.position).toHaveLength(3); } finally { fs.unlinkSync(p); } }); test('Marker nicht in extraMarkers → changed: false', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: 99, link: 'Arm1', extraMarkers: [] }); expect(r.changed).toBe(false); expect(r.error).toMatch(/nicht.*vorhanden/i); } finally { fs.unlinkSync(p); } }); test('mit set → Marker hat set-Wert in der gespeicherten Datei', () => { const p = makeTempRobot(ROBOT_EMPTY()); try { const r = callAssignMarkerId(p, { markerId: 78, link: 'Arm1', set: 'A0', extraMarkers: [{ marker_id: 78, position_mm: [1, 2, 3] }], }); expect(r.changed).toBe(true); const saved = JSON.parse(fs.readFileSync(p, 'utf8')); const added = saved.links.Arm1.markers.find(m => m.id === 78); expect(added.set).toBe('A0'); } finally { fs.unlinkSync(p); } }); }); // ── Normalfall: bestehenden Marker aktualisieren ────────────────────────────── describe('assignMarkerId – Normalfall: bestehenden Marker aktualisieren', () => { test('Set ändern → changed: true, action: updated, Datei geändert', () => { const p = makeTempRobot(ROBOT_WITH_42()); try { const r = callAssignMarkerId(p, { markerId: 42, set: 'B0' }); expect(r.changed).toBe(true); expect(r.change.action).toBe('updated'); expect(r.change.oldSet).toBe('A0'); expect(r.change.newSet).toBe('B0'); const saved = JSON.parse(fs.readFileSync(p, 'utf8')); expect(saved.links.Arm1.markers.find(m => m.id === 42).set).toBe('B0'); } finally { fs.unlinkSync(p); } }); test('in anderen Link verschieben → oldLink / newLink korrekt, Datei geändert', () => { const p = makeTempRobot(ROBOT_WITH_42()); try { const r = callAssignMarkerId(p, { markerId: 42, link: 'Arm2' }); expect(r.changed).toBe(true); expect(r.change.oldLink).toBe('Arm1'); expect(r.change.newLink).toBe('Arm2'); const saved = JSON.parse(fs.readFileSync(p, 'utf8')); expect(saved.links.Arm1.markers.find(m => m.id === 42)).toBeUndefined(); expect(saved.links.Arm2.markers.find(m => m.id === 42)).toBeDefined(); } finally { fs.unlinkSync(p); } }); test('bestehender Marker: fehlende position_mm in extraMarkers ist irrelevant', () => { const p = makeTempRobot(ROBOT_WITH_42()); try { // Marker 42 ist in robot.json → position_mm-Guard darf nicht zuschlagen const r = callAssignMarkerId(p, { markerId: 42, set: 'C0', extraMarkers: [{ marker_id: 42 }], // kein position_mm – aber irrelevant }); expect(r.changed).toBe(true); expect(r.change.action).toBe('updated'); } finally { fs.unlinkSync(p); } }); });