PrintersPageBucketing.test.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. /**
  2. * Regression tests for the printer-status bucketing logic in PrintersPage.tsx.
  3. *
  4. * The bug: a printer in gcode_state="FAILED" with no active HMS errors was
  5. * being counted as a "problem" in the header badge — this is the post-cancel
  6. * terminal state, not a real fault. After cancelling a print on h2d-1 the
  7. * printer card kept showing "1 problem" forever even after the HMS list was
  8. * empty, until the next print started.
  9. *
  10. * The fix: FAILED-without-HMS is bucketed as "finished" (same operator
  11. * meaning: print ended, plate may need clearing). FAILED-with-HMS still
  12. * counts as a problem because there's a real fault to investigate.
  13. *
  14. * Mirrors the logic at PrintersPage.tsx:917-948 and the classifyPrinterStatus
  15. * helper at PrintersPage.tsx:1028 — kept as inline copies so this test
  16. * doesn't need the helpers to be exported.
  17. */
  18. import { describe, it, expect } from 'vitest';
  19. type Status = {
  20. connected: boolean;
  21. state: string | null;
  22. hms_errors?: { code: string; attr: number; severity: number }[];
  23. };
  24. type Bucket = 'printing' | 'paused' | 'finished' | 'idle' | 'offline' | 'error';
  25. const KNOWN_HMS_CODES = new Set(['0300_4057', '0500_4038']);
  26. function filterKnownHMSErrors(errors: Status['hms_errors']): NonNullable<Status['hms_errors']> {
  27. return (errors ?? []).filter((e) => {
  28. const codeNum = parseInt(e.code.replace('0x', ''), 16) || 0;
  29. const module = ((e.attr >> 16) & 0xFFFF).toString(16).padStart(4, '0').toUpperCase();
  30. const code = (codeNum & 0xFFFF).toString(16).padStart(4, '0').toUpperCase();
  31. return KNOWN_HMS_CODES.has(`${module}_${code}`);
  32. });
  33. }
  34. function classifyPrinterStatus(status: Status | undefined): Bucket {
  35. if (!status?.connected) return 'offline';
  36. const knownHms = filterKnownHMSErrors(status.hms_errors);
  37. if (knownHms.length > 0) return 'error';
  38. switch (status.state) {
  39. case 'RUNNING': return 'printing';
  40. case 'PAUSE': return 'paused';
  41. case 'FINISH': return 'finished';
  42. case 'FAILED': return 'finished';
  43. default: return 'idle';
  44. }
  45. }
  46. describe('FAILED-without-HMS bucketing', () => {
  47. it('classifies FAILED with no HMS errors as "finished" (post-cancel terminal state, not a problem)', () => {
  48. const cancelledPrinter: Status = {
  49. connected: true,
  50. state: 'FAILED',
  51. hms_errors: [],
  52. };
  53. expect(classifyPrinterStatus(cancelledPrinter)).toBe('finished');
  54. });
  55. it('classifies FAILED + active known HMS as "error"', () => {
  56. const reallyFailedPrinter: Status = {
  57. connected: true,
  58. state: 'FAILED',
  59. hms_errors: [{ code: '0x4057', attr: 0x0300_0000, severity: 1 }],
  60. };
  61. expect(classifyPrinterStatus(reallyFailedPrinter)).toBe('error');
  62. });
  63. it('classifies FAILED + only unknown HMS as "finished" (unknown codes are not "real" problems by our taxonomy)', () => {
  64. const cancelEcho: Status = {
  65. connected: true,
  66. state: 'FAILED',
  67. hms_errors: [{ code: '0x2001b', attr: 0x0C00_0C00, severity: 1 }], // 0C00_001B not in known set
  68. };
  69. expect(classifyPrinterStatus(cancelEcho)).toBe('finished');
  70. });
  71. it('classifies FINISH as "finished" (unchanged baseline)', () => {
  72. const completedPrinter: Status = { connected: true, state: 'FINISH' };
  73. expect(classifyPrinterStatus(completedPrinter)).toBe('finished');
  74. });
  75. it('classifies disconnected printer as "offline" (HMS / state irrelevant)', () => {
  76. const offline: Status = {
  77. connected: false,
  78. state: 'FAILED',
  79. hms_errors: [{ code: '0x4057', attr: 0x0300_0000, severity: 1 }],
  80. };
  81. expect(classifyPrinterStatus(offline)).toBe('offline');
  82. });
  83. });