slicerPrinterMatch.test.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. import { describe, it, expect } from 'vitest';
  2. import {
  3. buildCompatibilityIndex,
  4. matchesPrinterModelSuffix,
  5. presetCompatibility,
  6. EMPTY_COMPATIBILITY_INDEX,
  7. } from '../../utils/slicerPrinterMatch';
  8. const X1C = 'Bambu Lab X1 Carbon 0.4 nozzle';
  9. const P2S = 'Bambu Lab P2S 0.4 nozzle';
  10. // Mirror of backend/app/utils/printer_models.py PRINTER_MODEL_MAP, fetched
  11. // from /slicer/printer-models at runtime (#1325 follow-up). Listed in tests
  12. // to exercise the @BBL name fallback against the same registry the real
  13. // app sees.
  14. const PRINTER_MODELS: Record<string, string> = {
  15. 'Bambu Lab X1 Carbon': 'X1C',
  16. 'Bambu Lab X1': 'X1',
  17. 'Bambu Lab X1E': 'X1E',
  18. 'Bambu Lab P1S': 'P1S',
  19. 'Bambu Lab P1P': 'P1P',
  20. 'Bambu Lab P2S': 'P2S',
  21. 'Bambu Lab A1': 'A1',
  22. 'Bambu Lab A1 Mini': 'A1 Mini',
  23. 'Bambu Lab A1 mini': 'A1 Mini',
  24. 'Bambu Lab H2D': 'H2D',
  25. 'Bambu Lab H2D Pro': 'H2D Pro',
  26. 'Bambu Lab H2C': 'H2C',
  27. 'Bambu Lab H2S': 'H2S',
  28. 'Bambu Lab X2D': 'X2D',
  29. };
  30. describe('buildCompatibilityIndex', () => {
  31. it('inverts the printer-model registry into short-code → display fragment', () => {
  32. const index = buildCompatibilityIndex(PRINTER_MODELS);
  33. expect(index.bambuModelByShortCode.X1C).toBe('X1 Carbon');
  34. expect(index.bambuModelByShortCode.P2S).toBe('P2S');
  35. expect(index.bambuModelByShortCode['A1 Mini']).toBe('A1 Mini');
  36. expect(index.bambuModelByShortCode['H2D Pro']).toBe('H2D Pro');
  37. });
  38. it('tolerates an empty printer-model registry (model fetch hasn\'t resolved yet)', () => {
  39. const index = buildCompatibilityIndex();
  40. expect(index.bambuModelByShortCode).toEqual({});
  41. });
  42. });
  43. describe('presetCompatibility', () => {
  44. const index = buildCompatibilityIndex(PRINTER_MODELS);
  45. it('uses compatible_printers exactly when present (imported / local tier)', () => {
  46. const preset = { name: 'My Process', compatible_printers: [X1C] };
  47. expect(presetCompatibility(preset, 'process', X1C, EMPTY_COMPATIBILITY_INDEX)).toBe('match');
  48. expect(presetCompatibility(preset, 'process', P2S, EMPTY_COMPATIBILITY_INDEX)).toBe('mismatch');
  49. });
  50. it('is unknown when compatible_printers is set but no printer is selected', () => {
  51. expect(
  52. presetCompatibility({ name: 'P', compatible_printers: [X1C] }, 'process', null, index),
  53. ).toBe('unknown');
  54. });
  55. it('is unknown when no printer is selected', () => {
  56. expect(
  57. presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', null, index),
  58. ).toBe('unknown');
  59. });
  60. it('compatible_printers wins over @BBL even when the name suggests a different printer', () => {
  61. // Authoritative slicer declaration: this @BBL P2S preset has been
  62. // manually reassigned to X1C. The compatible_printers list must win.
  63. expect(
  64. presetCompatibility(
  65. { name: '0.20mm Standard @BBL P2S', compatible_printers: [X1C] },
  66. 'process',
  67. X1C,
  68. index,
  69. ),
  70. ).toBe('match');
  71. expect(
  72. presetCompatibility(
  73. { name: '0.20mm Standard @BBL P2S', compatible_printers: [X1C] },
  74. 'process',
  75. P2S,
  76. index,
  77. ),
  78. ).toBe('mismatch');
  79. });
  80. });
  81. // ─── #1325 follow-up: @BBL name fallback ──────────────────────────────────
  82. describe('presetCompatibility — @BBL name fallback', () => {
  83. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  84. // Bambu's short codes vs the long forms in printer-preset names: the
  85. // entire reason the fallback needs a registry to consult.
  86. it.each<[string, string, 'match' | 'mismatch']>([
  87. // @BBL X1C → "X1 Carbon" (the case the old hardcoded list got right)
  88. ['0.20mm Standard @BBL X1C', X1C, 'match'],
  89. ['0.20mm Standard @BBL X1C', 'Bambu Lab P1S 0.4 nozzle', 'mismatch'],
  90. // @BBL X1 must NOT match X1 Carbon (X1 and X1C are physically different printers)
  91. ['0.20mm Standard @BBL X1', 'Bambu Lab X1 0.4 nozzle', 'match'],
  92. ['0.20mm Standard @BBL X1', X1C, 'mismatch'],
  93. // @BBL A1 must NOT match A1 mini (case the original hardcoded list got wrong)
  94. ['0.20mm Standard @BBL A1', 'Bambu Lab A1 0.4 nozzle', 'match'],
  95. ['0.20mm Standard @BBL A1', 'Bambu Lab A1 mini 0.4 nozzle', 'mismatch'],
  96. // @BBL "A1 Mini" — multi-word token
  97. ['0.20mm Standard @BBL A1 Mini', 'Bambu Lab A1 mini 0.4 nozzle', 'match'],
  98. // @BBL H2D vs H2D Pro disambiguation
  99. ['0.20mm Standard @BBL H2D', 'Bambu Lab H2D 0.4 nozzle', 'match'],
  100. ['0.20mm Standard @BBL H2D', 'Bambu Lab H2D Pro 0.4 nozzle', 'mismatch'],
  101. ['0.20mm Standard @BBL H2D Pro', 'Bambu Lab H2D Pro 0.4 nozzle', 'match'],
  102. // Models the original hardcoded list missed, now resolved via the
  103. // backend registry.
  104. ['Bambu PLA Basic @BBL P2S', P2S, 'match'],
  105. ['Bambu PLA Basic @BBL P2S', X1C, 'mismatch'],
  106. ['0.20mm Standard @BBL X2D', 'Bambu Lab X2D 0.4 nozzle', 'match'],
  107. ['0.20mm Standard @BBL H2C', 'Bambu Lab H2C 0.4 nozzle', 'match'],
  108. ['0.20mm Standard @BBL H2S', 'Bambu Lab H2S 0.4 nozzle', 'match'],
  109. ])('classifies %s against %s as %s', (presetName, printerName, expected) => {
  110. expect(presetCompatibility({ name: presetName }, 'process', printerName, idx)).toBe(expected);
  111. });
  112. it('handles a trailing nozzle-size suffix on the @BBL tag', () => {
  113. // An explicit "0.4 nozzle" suffix matches the 0.4 printer (Bambu's
  114. // convention is to omit it for 0.4, but some cloud presets write it).
  115. expect(
  116. presetCompatibility(
  117. { name: '0.20mm Standard @BBL X1C 0.4 nozzle' },
  118. 'process',
  119. X1C,
  120. idx,
  121. ),
  122. ).toBe('match');
  123. // #1325 follow-up #2 (IndividualGhost1905, 2026-05-23): a different
  124. // nozzle size IS a mismatch — a 0.6-nozzle process is unusable on a
  125. // 0.4-nozzle printer. The dedicated "nozzle filtering" describe
  126. // block below covers the full matrix; this case stays here as the
  127. // counterpart to the matching-suffix case above.
  128. expect(
  129. presetCompatibility(
  130. { name: '0.20mm Standard @BBL X1C 0.6 nozzle' },
  131. 'process',
  132. X1C,
  133. idx,
  134. ),
  135. ).toBe('mismatch');
  136. });
  137. it('is unknown when the preset has no @BBL tag at all (custom name, no other signal)', () => {
  138. expect(presetCompatibility({ name: 'My Custom Process' }, 'process', X1C, idx)).toBe('unknown');
  139. });
  140. it('is unknown for a Bambu preset against a non-Bambu printer (can\'t parse the printer name)', () => {
  141. expect(
  142. presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', 'CustomBuild 0.4', idx),
  143. ).toBe('unknown');
  144. });
  145. it('falls back to raw-token comparison for a model not yet in the registry', () => {
  146. // A future "Q1" printer with cloud presets named "@BBL Q1" should
  147. // match without any code change to the registry — both names resolve
  148. // to "Q1" directly.
  149. expect(
  150. presetCompatibility(
  151. { name: '0.20mm Standard @BBL Q1' },
  152. 'process',
  153. 'Bambu Lab Q1 0.4 nozzle',
  154. idx,
  155. ),
  156. ).toBe('match');
  157. // And mismatch against a different printer.
  158. expect(
  159. presetCompatibility(
  160. { name: '0.20mm Standard @BBL Q1' },
  161. 'process',
  162. X1C,
  163. idx,
  164. ),
  165. ).toBe('mismatch');
  166. });
  167. it('still resolves @BBL when the registry has not loaded yet (raw-token only)', () => {
  168. // EMPTY_COMPATIBILITY_INDEX = no models — first paint of the SliceModal
  169. // before the /slicer/printer-models fetch resolves. Short codes that
  170. // match their printer-name fragment directly (P2S, H2D, etc.) still
  171. // work; codes that differ in form (X1C vs "X1 Carbon") gracefully
  172. // fall through to 'mismatch'.
  173. expect(
  174. presetCompatibility(
  175. { name: '0.20mm Standard @BBL P2S' },
  176. 'process',
  177. P2S,
  178. EMPTY_COMPATIBILITY_INDEX,
  179. ),
  180. ).toBe('match');
  181. expect(
  182. presetCompatibility(
  183. { name: '0.20mm Standard @BBL X1C' },
  184. 'process',
  185. X1C,
  186. EMPTY_COMPATIBILITY_INDEX,
  187. ),
  188. ).toBe('mismatch'); // X1C ≠ "X1 Carbon" without the registry
  189. });
  190. });
  191. // #1325 follow-up #2 (IndividualGhost1905, 2026-05-23): the @BBL name
  192. // fallback must also filter by nozzle diameter. Bambu ships per-nozzle
  193. // variants of process / filament presets — 0.2 / 0.4 / 0.6 / 0.8 —
  194. // and a 0.6-nozzle process is unusable on a 0.4-nozzle printer. Bambu's
  195. // naming convention: 0.4 is the default and DROPS the suffix; 0.2 / 0.6
  196. // / 0.8 carry an explicit "<size> nozzle" segment. So an empty suffix
  197. // means 0.4, not "any nozzle".
  198. describe('presetCompatibility — nozzle filtering on @BBL name fallback', () => {
  199. const X1C_04 = 'Bambu Lab X1 Carbon 0.4 nozzle';
  200. const X1C_06 = 'Bambu Lab X1 Carbon 0.6 nozzle';
  201. const X1C_08 = 'Bambu Lab X1 Carbon 0.8 nozzle';
  202. const index = buildCompatibilityIndex(PRINTER_MODELS);
  203. it('treats a no-suffix process as 0.4 (Bambu default) and matches a 0.4 printer', () => {
  204. expect(
  205. presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', X1C_04, index),
  206. ).toBe('match');
  207. });
  208. it('flags a 0.6-nozzle process as mismatch against a 0.4 printer', () => {
  209. expect(
  210. presetCompatibility({ name: '0.30mm @BBL X1C 0.6 nozzle' }, 'process', X1C_04, index),
  211. ).toBe('mismatch');
  212. });
  213. it('flags an 0.8-nozzle process as mismatch against a 0.4 printer', () => {
  214. expect(
  215. presetCompatibility({ name: '0.40mm Strength @BBL X1C 0.8 nozzle' }, 'process', X1C_04, index),
  216. ).toBe('mismatch');
  217. });
  218. it('matches a 0.6-nozzle process against a 0.6 printer', () => {
  219. expect(
  220. presetCompatibility({ name: '0.30mm @BBL X1C 0.6 nozzle' }, 'process', X1C_06, index),
  221. ).toBe('match');
  222. });
  223. it('flags a no-suffix process (=0.4) as mismatch against a 0.6 printer', () => {
  224. expect(
  225. presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', X1C_06, index),
  226. ).toBe('mismatch');
  227. });
  228. it('applies the same rule to filament presets', () => {
  229. // Bambu's bundled filament presets follow the same per-nozzle naming.
  230. expect(
  231. presetCompatibility(
  232. { name: 'Bambu PLA Basic @BBL X1C 0.6 nozzle' },
  233. 'filament',
  234. X1C_04,
  235. index,
  236. ),
  237. ).toBe('mismatch');
  238. expect(
  239. presetCompatibility({ name: 'Bambu PLA Basic @BBL X1C' }, 'filament', X1C_04, index),
  240. ).toBe('match');
  241. });
  242. it('keeps a 0.4 process matching a 0.4 printer when the preset DOES carry an explicit "0.4 nozzle" suffix', () => {
  243. // Some cloud presets write the 0.4 suffix explicitly even though
  244. // Bambu's bundled convention omits it. Both forms must compare equal.
  245. expect(
  246. presetCompatibility(
  247. { name: '0.20mm Standard @BBL X1C 0.4 nozzle' },
  248. 'process',
  249. X1C_04,
  250. index,
  251. ),
  252. ).toBe('match');
  253. });
  254. it('still flags a wrong-MODEL process even when the nozzle matches', () => {
  255. // The model filter must continue to dominate over the nozzle filter:
  256. // a 0.4 A1 process isn't usable on an X1C 0.4 just because both are 0.4.
  257. expect(
  258. presetCompatibility({ name: '0.20mm Standard @BBL A1' }, 'process', X1C_04, index),
  259. ).toBe('mismatch');
  260. });
  261. it('falls back to model-only when the selected printer name has no parseable nozzle', () => {
  262. // Defensive degrade for non-Bambu / hand-typed names that happen to
  263. // match the model. Real Bambu printer presets always carry a nozzle,
  264. // so this path is rare; the assertion pins the intentional behaviour.
  265. const noNozzle = 'Bambu Lab X1 Carbon'; // no "0.4 nozzle" suffix
  266. expect(
  267. presetCompatibility({ name: '0.30mm @BBL X1C 0.6 nozzle' }, 'process', noNozzle, index),
  268. ).toBe('match');
  269. });
  270. it('flags 0.4 process on 0.8 printer (sanity check across the third common size)', () => {
  271. expect(
  272. presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', X1C_08, index),
  273. ).toBe('mismatch');
  274. });
  275. });
  276. describe('matchesPrinterModelSuffix (#1649)', () => {
  277. it('matches the canonical short code against itself', () => {
  278. expect(matchesPrinterModelSuffix('X1C', 'X1C')).toBe(true);
  279. });
  280. it('is case-insensitive on both sides', () => {
  281. expect(matchesPrinterModelSuffix('x1c', 'X1C')).toBe(true);
  282. expect(matchesPrinterModelSuffix('a1 mini', 'A1 Mini')).toBe(true);
  283. });
  284. it('matches Bambu cloud rename A1M against the long form A1 Mini', () => {
  285. expect(matchesPrinterModelSuffix('A1M', 'A1 Mini')).toBe(true);
  286. });
  287. it('matches the long form A1 Mini against the short Bambu cloud code A1M', () => {
  288. expect(matchesPrinterModelSuffix('A1 Mini', 'A1M')).toBe(true);
  289. });
  290. it('does NOT match A1M against A1 (different printer, must not collapse)', () => {
  291. expect(matchesPrinterModelSuffix('A1M', 'A1')).toBe(false);
  292. });
  293. it('does NOT match A1 against A1 Mini', () => {
  294. expect(matchesPrinterModelSuffix('A1', 'A1 Mini')).toBe(false);
  295. });
  296. it('does NOT match unrelated models', () => {
  297. expect(matchesPrinterModelSuffix('X1C', 'P1S')).toBe(false);
  298. });
  299. });
  300. describe('H2D Pro spelled H2DP in preset names (#2982)', () => {
  301. const H2D_PRO = 'Bambu Lab H2D Pro 0.4 nozzle';
  302. const H2D = 'Bambu Lab H2D 0.4 nozzle';
  303. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  304. it('matches the preset-name spelling against the printer-name spelling', () => {
  305. // The bundle names every H2D Pro process and filament "@BBL H2DP"; the
  306. // printer preset, and PRINTER_MODEL_MAP with it, says "H2D Pro". Without
  307. // the alias an H2D Pro read all 198 bundled processes as another
  308. // printer's, and got an A1 process auto-picked.
  309. expect(matchesPrinterModelSuffix('H2DP', 'H2D Pro')).toBe(true);
  310. expect(matchesPrinterModelSuffix('H2D Pro', 'H2DP')).toBe(true);
  311. });
  312. it('does NOT collapse H2DP into the plain H2D', () => {
  313. // Different machines. The nozzle-count and build-volume differences make
  314. // their presets genuinely non-interchangeable.
  315. expect(matchesPrinterModelSuffix('H2DP', 'H2D')).toBe(false);
  316. expect(matchesPrinterModelSuffix('H2D', 'H2D Pro')).toBe(false);
  317. });
  318. it('classifies an @BBL H2DP process as compatible with an H2D Pro', () => {
  319. expect(
  320. presetCompatibility(
  321. { name: '0.20mm Balanced Strength @BBL H2DP' },
  322. 'process',
  323. H2D_PRO,
  324. idx,
  325. ),
  326. ).toBe('match');
  327. });
  328. it('still classifies an @BBL H2DP process as a mismatch for a plain H2D', () => {
  329. expect(
  330. presetCompatibility(
  331. { name: '0.20mm Balanced Strength @BBL H2DP' },
  332. 'process',
  333. H2D,
  334. idx,
  335. ),
  336. ).toBe('mismatch');
  337. });
  338. });
  339. describe('presetCompatibility with Bambu cloud A1M rename (#1649)', () => {
  340. const A1_MINI = 'Bambu Lab A1 mini 0.4 nozzle';
  341. const A1 = 'Bambu Lab A1 0.4 nozzle';
  342. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  343. it('matches a cloud preset using the new @BBL A1M suffix against an A1 Mini printer', () => {
  344. // The slicer-mirrored case: technopaw's report — A1 Mini cloud presets
  345. // newly ship as "Bambu PLA Basic @BBL A1M ..." and used to be filtered
  346. // out by the model check that compared "A1M" vs "A1 mini" verbatim.
  347. expect(
  348. presetCompatibility({ name: 'Bambu PLA Basic @BBL A1M' }, 'filament', A1_MINI, idx),
  349. ).toBe('match');
  350. });
  351. it('matches a 0.4-nozzle process with @BBL A1M against an A1 Mini printer', () => {
  352. expect(
  353. presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1_MINI, idx),
  354. ).toBe('match');
  355. });
  356. it('does NOT match @BBL A1M against an A1 (non-mini) printer', () => {
  357. // The alias must not collapse two physically different printers — A1
  358. // and A1 Mini ship distinct profile sets.
  359. expect(
  360. presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1, idx),
  361. ).toBe('mismatch');
  362. });
  363. });
  364. describe('presetCompatibility — long-form @Bambu Lab printer tag (#2628)', () => {
  365. const A1 = 'Bambu Lab A1 0.4 nozzle';
  366. const A1_MINI = 'Bambu Lab A1 mini 0.4 nozzle';
  367. const H2D = 'Bambu Lab H2D 0.4 nozzle';
  368. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  369. it('flags a user-saved H2D filament preset as a mismatch on an A1', () => {
  370. // michaelklos's report: this exact preset sat in an unused filament slot
  371. // of a multi-plate 3MF. Classified 'unknown' it was treated as usable,
  372. // auto-picked, and the CLI rejected the slice with "filament preset
  373. // (slot 1) is not compatible with printer Bambu Lab A1 0.4 nozzle".
  374. expect(
  375. presetCompatibility({ name: 'SUNLU TPU 95A @Bambu Lab H2D 0.4 nozzle' }, 'filament', A1, idx),
  376. ).toBe('mismatch');
  377. });
  378. it('matches the same preset against its own printer', () => {
  379. expect(
  380. presetCompatibility({ name: 'SUNLU TPU 95A @Bambu Lab H2D 0.4 nozzle' }, 'filament', H2D, idx),
  381. ).toBe('match');
  382. });
  383. it('resolves a long-form model whose display name differs from its short code', () => {
  384. const name = 'My PETG @Bambu Lab X1 Carbon 0.4 nozzle';
  385. expect(presetCompatibility({ name }, 'filament', X1C, idx)).toBe('match');
  386. expect(presetCompatibility({ name }, 'filament', A1, idx)).toBe('mismatch');
  387. });
  388. it('applies the nozzle filter to the long form too', () => {
  389. expect(
  390. presetCompatibility({ name: 'My PETG @Bambu Lab A1 0.6 nozzle' }, 'filament', A1, idx),
  391. ).toBe('mismatch');
  392. });
  393. it('ignores a trailing "(Custom)" suffix rather than mangling the model token', () => {
  394. // The slicer appends this to user-saved presets. Parsed naively the tag
  395. // becomes "H2D 0.4 nozzle (Custom)" — which would brand the preset a
  396. // mismatch against its own printer and hide it from the dropdown.
  397. const name = 'Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)';
  398. expect(presetCompatibility({ name }, 'filament', H2D, idx)).toBe('match');
  399. expect(presetCompatibility({ name }, 'filament', A1, idx)).toBe('mismatch');
  400. });
  401. it('keeps the A1M alias working through the long form', () => {
  402. expect(
  403. presetCompatibility({ name: 'My PLA @Bambu Lab A1 mini 0.4 nozzle' }, 'filament', A1_MINI, idx),
  404. ).toBe('match');
  405. expect(
  406. presetCompatibility({ name: 'My PLA @Bambu Lab A1 mini 0.4 nozzle' }, 'filament', A1, idx),
  407. ).toBe('mismatch');
  408. });
  409. it('stays unknown for an @-tag that names no recognisable printer', () => {
  410. // "@Voron 0.4 nozzle" is not a Bambu printer preset — the matcher must
  411. // not guess, so the preset keeps its place in the main dropdown list.
  412. expect(
  413. presetCompatibility({ name: 'My PLA @Voron 0.4 nozzle' }, 'filament', A1, idx),
  414. ).toBe('unknown');
  415. });
  416. it('reads the tag from the last @ so a stray earlier one cannot swallow it', () => {
  417. expect(
  418. presetCompatibility({ name: 'My @work PLA @Bambu Lab H2D 0.4 nozzle' }, 'filament', A1, idx),
  419. ).toBe('mismatch');
  420. expect(
  421. presetCompatibility({ name: 'My @work PLA @Bambu Lab H2D 0.4 nozzle' }, 'filament', H2D, idx),
  422. ).toBe('match');
  423. });
  424. });
  425. describe('presetCompatibility — nozzle-only @<size> tag (#2628 follow-up)', () => {
  426. const P2S_04 = 'Bambu Lab P2S 0.4 nozzle';
  427. const X1C_02 = 'Bambu Lab X1 Carbon 0.2 nozzle';
  428. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  429. it('rules out a 0.2-nozzle profile on a 0.4-nozzle printer', () => {
  430. // The live case: "Overture PLA Matte @0.2" inherits from the X1C 0.2
  431. // nozzle system profile, so the slicer rejected a P2S 0.4 slice with
  432. // "not compatible with printer Bambu Lab P2S 0.4 nozzle". The name
  433. // carries no model, so this size is the only signal available.
  434. expect(
  435. presetCompatibility({ name: 'Overture PLA Matte @0.2' }, 'filament', P2S_04, idx),
  436. ).toBe('mismatch');
  437. });
  438. it('stays unknown when the size agrees — a size is not a model', () => {
  439. // The profile might belong to a different printer with the same nozzle;
  440. // promoting this to 'match' would claim knowledge we don't have.
  441. expect(
  442. presetCompatibility({ name: 'Overture PLA Matte @0.2' }, 'filament', X1C_02, idx),
  443. ).toBe('unknown');
  444. });
  445. it('accepts the "0.2 nozzle" and "0.2mm" spellings of the same tag', () => {
  446. for (const name of ['My PLA @0.2 nozzle', 'My PLA @0.2mm']) {
  447. expect(presetCompatibility({ name }, 'filament', P2S_04, idx)).toBe('mismatch');
  448. }
  449. });
  450. it('compares sizes numerically so 0.20 and 0.2 are one size', () => {
  451. expect(
  452. presetCompatibility({ name: 'My PLA @0.20' }, 'filament', X1C_02, idx),
  453. ).toBe('unknown');
  454. });
  455. it('ignores a numeric tag that cannot be a nozzle', () => {
  456. // "@2026" is a year, not a 2026 mm nozzle — guessing here would brand
  457. // the profile incompatible with every printer that exists.
  458. expect(presetCompatibility({ name: 'My PLA @2026' }, 'filament', P2S_04, idx)).toBe('unknown');
  459. expect(presetCompatibility({ name: 'My PLA @0.05' }, 'filament', P2S_04, idx)).toBe('unknown');
  460. });
  461. it('leaves a model-bearing tag on the model path', () => {
  462. // Regression guard: the nozzle-only branch must not swallow the forms
  463. // that carry a model — those still resolve to match/mismatch.
  464. expect(
  465. presetCompatibility({ name: 'Bambu PLA Basic @BBL P2S' }, 'filament', P2S_04, idx),
  466. ).toBe('match');
  467. expect(
  468. presetCompatibility({ name: 'My PLA @Bambu Lab X1 Carbon 0.4 nozzle' }, 'filament', P2S_04, idx),
  469. ).toBe('mismatch');
  470. });
  471. });
  472. describe("presetCompatibility — BambuStudio's \"# \" user-clone prefix", () => {
  473. const index = buildCompatibilityIndex(PRINTER_MODELS);
  474. // Editing a system preset saves a copy under this name; .bbscfg bundle
  475. // exports use the same convention. The backend already normalises it in
  476. // _canonical_printer_model.
  477. const CLONED_X1C = '# Bambu Lab X1 Carbon 0.4 nozzle';
  478. it('still matches a printer-tagged preset when the printer is a clone', () => {
  479. // Regression: the prefix failed the "Bambu Lab …" test, so every preset
  480. // came back 'unknown' and the dropdown filter silently did nothing.
  481. expect(
  482. presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', CLONED_X1C, index),
  483. ).toBe('match');
  484. });
  485. it('still rules out another printer when the selected printer is a clone', () => {
  486. expect(
  487. presetCompatibility({ name: '0.20mm Standard @BBL H2D' }, 'process', CLONED_X1C, index),
  488. ).toBe('mismatch');
  489. });
  490. it('matches a cloned preset against an unprefixed printer', () => {
  491. expect(
  492. presetCompatibility({ name: '# 0.20mm Standard @BBL X1C' }, 'process', X1C, index),
  493. ).toBe('match');
  494. });
  495. it('still compares the nozzle size through the prefix', () => {
  496. expect(
  497. presetCompatibility({ name: '0.20mm Standard @BBL X1C 0.6 nozzle' }, 'process', CLONED_X1C, index),
  498. ).toBe('mismatch');
  499. });
  500. it('matches compatible_printers with the prefix on either side', () => {
  501. // A preset cloned from a system printer lists the *unprefixed* name; a raw
  502. // comparison against the "# " form reads as a mismatch, which now hides
  503. // the preset rather than merely demoting it.
  504. expect(
  505. presetCompatibility({ name: 'My Process', compatible_printers: [X1C] }, 'process', CLONED_X1C, index),
  506. ).toBe('match');
  507. expect(
  508. presetCompatibility({ name: 'My Process', compatible_printers: [CLONED_X1C] }, 'process', X1C, index),
  509. ).toBe('match');
  510. });
  511. it('does not let the prefix turn a genuine mismatch into a match', () => {
  512. expect(
  513. presetCompatibility({ name: 'My Process', compatible_printers: [P2S] }, 'process', CLONED_X1C, index),
  514. ).toBe('mismatch');
  515. });
  516. it('leaves an untagged clone unknown rather than guessing', () => {
  517. expect(
  518. presetCompatibility({ name: '# My own profile' }, 'process', CLONED_X1C, index),
  519. ).toBe('unknown');
  520. });
  521. });