mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 15:35:39 +02:00
#1429 (reported by @TrickShotMLG02, confirmed by @Mape6 on a flat single-LAN that rules out subnet / mDNS-reflector theories): with the physical printer off the slicer's "Send" landed in Bambuddy's archive; once the printer powered on every subsequent "Send" went straight to the printer's SD card and bypassed Bambuddy. Bundle analysis: mape6-before showed clean FTP receive + archive lines, mape6-after had zero FTP attempts to Bambuddy once the printer was online. Cause: mqtt_bridge.py::_resolve_client encoded _target_ip_uint32_le / _vp_ip_uint32_le ONLY on client-identity change and early-returned on every refresh tick when the same client object was still bound. If target_client.ip_address was empty at first bind (DB row stale, or client constructed before SSDP refresh filled it in), the encoding stayed None, the net.info[*].ip rewrite block was skipped, the cache filled with the real printer IP, sticky-key preservation kept the poisoned net value alive across every subsequent incremental push, and the slicer followed the leaked IP. Only Bambuddy-restart-with-printer-off cleared it — the workaround both reporters independently arrived at. Same shape on multi-NIC printers (X1C, H2D Pro): the rewrite only matched entries whose ip equalled _target_ip_uint32_le, so a secondary interface IP Bambuddy never saw would leak through unchanged. Bridge fix: - _resolve_client calls a new _refresh_ip_encoding() on every refresh tick, even when client identity is unchanged; self-heals once ip_address becomes valid. - _refresh_ip_encoding() sweeps the existing _latest_print_state when encoding becomes valid for the first time. Without the sweep, sticky-key preservation keeps the pre-arm poisoned cache alive forever — incremental pushes that don't include net carry the bad value forward. - _rewrite_net_info_ips() rewrites EVERY non-zero net.info[].ip entry that doesn't already equal the VP IP, not just entries matching _target_ip_uint32_le. Multi-NIC printers stop leaking secondary interfaces. Zero-IP placeholders are left alone so "active interface" detection still works. - INFO logging on encoding arm/update and on cache sweep so future bundles directly answer "did the rewrite fire?". Mode wire-value rename (#1429 follow-up, separate confusion source): - Both reporters' support bundles showed mode: immediate while the UI said "Archive"; @TrickShotMLG02 quoted: "I have no idea why it says immediate in the support-info.json file. In the webui the printer is set to archive". UI button "Archive" had always saved immediate, and "Queue" had always saved print_queue. Canonical wire values are now archive / review / queue / proxy, matching the button labels 1:1. - New normalize_vp_mode() + VP_MODE_* constants in models/virtual_printer.py; manager.py normalises on construction so a legacy row read pre-migration still dispatches correctly. - core/database.py::run_migrations rewrites existing virtual_printers and settings rows; idempotent (re-runs are no-ops); identical SQL under SQLite and Postgres. - API routes accept both legacy and canonical on input, normalise before storage. GET /settings/virtual-printer normalises on read so the frontend's mode-button highlight works for stale legacy values. - Three frontend VP components (VirtualPrinterSettings, VirtualPrinterCard, VirtualPrinterAddDialog) switched click handlers and type aliases to canonical; each got its own normalizeMode() helper so a stale-cached settings payload still highlights the right button. Two pre-existing `printer.mode === 'queue' ? 'review'` legacy mappings in VirtualPrinterCard were the source of a test failure caught mid-implementation where the new canonical 'queue' was being mis-aliased back to 'review' and hiding the auto-dispatch + force-color-match toggles. mode handler is NOT the dispatch bug: manager.py::_archive_file (the handler for archive mode) doesn't dispatch to the physical printer. The "files end up on the printer's SD card" symptom was the IP-leak from the bridge cache. The mode rename is purely clarity / support- bundle accuracy.
76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
/**
|
|
* Tests for the VirtualPrinterDiagnosticModal component.
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { server } from '../mocks/server';
|
|
import { render } from '../utils';
|
|
import { VirtualPrinterDiagnosticModal } from '../../components/VirtualPrinterDiagnosticModal';
|
|
import type { VPDiagnosticResult } from '../../api/client';
|
|
|
|
const problemResult: VPDiagnosticResult = {
|
|
vp_id: 3,
|
|
vp_name: 'Garage VP',
|
|
mode: 'archive',
|
|
overall: 'problems',
|
|
checks: [
|
|
{ id: 'enabled', status: 'pass', params: {} },
|
|
{ id: 'running', status: 'fail', params: {} },
|
|
{ id: 'port_mqtt', status: 'fail', params: { port: 8883 } },
|
|
],
|
|
};
|
|
|
|
/** Stub the diagnostic endpoint and count how often it is hit. */
|
|
function setupDiagnostic(result: VPDiagnosticResult): { calls: () => number } {
|
|
let count = 0;
|
|
server.use(
|
|
http.get('*/virtual-printers/:id/diagnostic', () => {
|
|
count += 1;
|
|
return HttpResponse.json(result);
|
|
}),
|
|
);
|
|
return { calls: () => count };
|
|
}
|
|
|
|
describe('VirtualPrinterDiagnosticModal', () => {
|
|
it('runs the diagnostic on mount and renders the checks', async () => {
|
|
const probe = setupDiagnostic(problemResult);
|
|
|
|
render(<VirtualPrinterDiagnosticModal vpId={3} vpName="Garage VP" onClose={() => {}} />);
|
|
|
|
expect(await screen.findByText(/Found problems that explain/)).toBeInTheDocument();
|
|
expect(probe.calls()).toBe(1);
|
|
// Per-check titles render; the port param is interpolated into the title.
|
|
expect(screen.getByText('Services running')).toBeInTheDocument();
|
|
expect(screen.getByText('Control service (port 8883)')).toBeInTheDocument();
|
|
});
|
|
|
|
it('re-runs the diagnostic when "Run again" is clicked', async () => {
|
|
const probe = setupDiagnostic(problemResult);
|
|
const user = userEvent.setup();
|
|
|
|
render(<VirtualPrinterDiagnosticModal vpId={3} vpName="Garage VP" onClose={() => {}} />);
|
|
|
|
await screen.findByText(/Found problems that explain/);
|
|
expect(probe.calls()).toBe(1);
|
|
|
|
await user.click(screen.getByText('Run again'));
|
|
await waitFor(() => expect(probe.calls()).toBe(2));
|
|
});
|
|
|
|
it('calls onClose when the Close button is clicked', async () => {
|
|
setupDiagnostic({ ...problemResult, overall: 'ok' });
|
|
const onClose = vi.fn();
|
|
const user = userEvent.setup();
|
|
|
|
render(<VirtualPrinterDiagnosticModal vpId={3} vpName="Garage VP" onClose={onClose} />);
|
|
|
|
await screen.findByText(/set up correctly/);
|
|
await user.click(screen.getByText('Close'));
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
});
|