slicerPrinterMatch.test.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  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('presetCompatibility with Bambu cloud A1M rename (#1649)', () => {
  301. const A1_MINI = 'Bambu Lab A1 mini 0.4 nozzle';
  302. const A1 = 'Bambu Lab A1 0.4 nozzle';
  303. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  304. it('matches a cloud preset using the new @BBL A1M suffix against an A1 Mini printer', () => {
  305. // The slicer-mirrored case: technopaw's report — A1 Mini cloud presets
  306. // newly ship as "Bambu PLA Basic @BBL A1M ..." and used to be filtered
  307. // out by the model check that compared "A1M" vs "A1 mini" verbatim.
  308. expect(
  309. presetCompatibility({ name: 'Bambu PLA Basic @BBL A1M' }, 'filament', A1_MINI, idx),
  310. ).toBe('match');
  311. });
  312. it('matches a 0.4-nozzle process with @BBL A1M against an A1 Mini printer', () => {
  313. expect(
  314. presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1_MINI, idx),
  315. ).toBe('match');
  316. });
  317. it('does NOT match @BBL A1M against an A1 (non-mini) printer', () => {
  318. // The alias must not collapse two physically different printers — A1
  319. // and A1 Mini ship distinct profile sets.
  320. expect(
  321. presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1, idx),
  322. ).toBe('mismatch');
  323. });
  324. });
  325. describe('presetCompatibility — long-form @Bambu Lab printer tag (#2628)', () => {
  326. const A1 = 'Bambu Lab A1 0.4 nozzle';
  327. const A1_MINI = 'Bambu Lab A1 mini 0.4 nozzle';
  328. const H2D = 'Bambu Lab H2D 0.4 nozzle';
  329. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  330. it('flags a user-saved H2D filament preset as a mismatch on an A1', () => {
  331. // michaelklos's report: this exact preset sat in an unused filament slot
  332. // of a multi-plate 3MF. Classified 'unknown' it was treated as usable,
  333. // auto-picked, and the CLI rejected the slice with "filament preset
  334. // (slot 1) is not compatible with printer Bambu Lab A1 0.4 nozzle".
  335. expect(
  336. presetCompatibility({ name: 'SUNLU TPU 95A @Bambu Lab H2D 0.4 nozzle' }, 'filament', A1, idx),
  337. ).toBe('mismatch');
  338. });
  339. it('matches the same preset against its own printer', () => {
  340. expect(
  341. presetCompatibility({ name: 'SUNLU TPU 95A @Bambu Lab H2D 0.4 nozzle' }, 'filament', H2D, idx),
  342. ).toBe('match');
  343. });
  344. it('resolves a long-form model whose display name differs from its short code', () => {
  345. const name = 'My PETG @Bambu Lab X1 Carbon 0.4 nozzle';
  346. expect(presetCompatibility({ name }, 'filament', X1C, idx)).toBe('match');
  347. expect(presetCompatibility({ name }, 'filament', A1, idx)).toBe('mismatch');
  348. });
  349. it('applies the nozzle filter to the long form too', () => {
  350. expect(
  351. presetCompatibility({ name: 'My PETG @Bambu Lab A1 0.6 nozzle' }, 'filament', A1, idx),
  352. ).toBe('mismatch');
  353. });
  354. it('ignores a trailing "(Custom)" suffix rather than mangling the model token', () => {
  355. // The slicer appends this to user-saved presets. Parsed naively the tag
  356. // becomes "H2D 0.4 nozzle (Custom)" — which would brand the preset a
  357. // mismatch against its own printer and hide it from the dropdown.
  358. const name = 'Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)';
  359. expect(presetCompatibility({ name }, 'filament', H2D, idx)).toBe('match');
  360. expect(presetCompatibility({ name }, 'filament', A1, idx)).toBe('mismatch');
  361. });
  362. it('keeps the A1M alias working through the long form', () => {
  363. expect(
  364. presetCompatibility({ name: 'My PLA @Bambu Lab A1 mini 0.4 nozzle' }, 'filament', A1_MINI, idx),
  365. ).toBe('match');
  366. expect(
  367. presetCompatibility({ name: 'My PLA @Bambu Lab A1 mini 0.4 nozzle' }, 'filament', A1, idx),
  368. ).toBe('mismatch');
  369. });
  370. it('stays unknown for an @-tag that names no recognisable printer', () => {
  371. // "@Voron 0.4 nozzle" is not a Bambu printer preset — the matcher must
  372. // not guess, so the preset keeps its place in the main dropdown list.
  373. expect(
  374. presetCompatibility({ name: 'My PLA @Voron 0.4 nozzle' }, 'filament', A1, idx),
  375. ).toBe('unknown');
  376. });
  377. it('reads the tag from the last @ so a stray earlier one cannot swallow it', () => {
  378. expect(
  379. presetCompatibility({ name: 'My @work PLA @Bambu Lab H2D 0.4 nozzle' }, 'filament', A1, idx),
  380. ).toBe('mismatch');
  381. expect(
  382. presetCompatibility({ name: 'My @work PLA @Bambu Lab H2D 0.4 nozzle' }, 'filament', H2D, idx),
  383. ).toBe('match');
  384. });
  385. });
  386. describe('presetCompatibility — nozzle-only @<size> tag (#2628 follow-up)', () => {
  387. const P2S_04 = 'Bambu Lab P2S 0.4 nozzle';
  388. const X1C_02 = 'Bambu Lab X1 Carbon 0.2 nozzle';
  389. const idx = buildCompatibilityIndex(PRINTER_MODELS);
  390. it('rules out a 0.2-nozzle profile on a 0.4-nozzle printer', () => {
  391. // The live case: "Overture PLA Matte @0.2" inherits from the X1C 0.2
  392. // nozzle system profile, so the slicer rejected a P2S 0.4 slice with
  393. // "not compatible with printer Bambu Lab P2S 0.4 nozzle". The name
  394. // carries no model, so this size is the only signal available.
  395. expect(
  396. presetCompatibility({ name: 'Overture PLA Matte @0.2' }, 'filament', P2S_04, idx),
  397. ).toBe('mismatch');
  398. });
  399. it('stays unknown when the size agrees — a size is not a model', () => {
  400. // The profile might belong to a different printer with the same nozzle;
  401. // promoting this to 'match' would claim knowledge we don't have.
  402. expect(
  403. presetCompatibility({ name: 'Overture PLA Matte @0.2' }, 'filament', X1C_02, idx),
  404. ).toBe('unknown');
  405. });
  406. it('accepts the "0.2 nozzle" and "0.2mm" spellings of the same tag', () => {
  407. for (const name of ['My PLA @0.2 nozzle', 'My PLA @0.2mm']) {
  408. expect(presetCompatibility({ name }, 'filament', P2S_04, idx)).toBe('mismatch');
  409. }
  410. });
  411. it('compares sizes numerically so 0.20 and 0.2 are one size', () => {
  412. expect(
  413. presetCompatibility({ name: 'My PLA @0.20' }, 'filament', X1C_02, idx),
  414. ).toBe('unknown');
  415. });
  416. it('ignores a numeric tag that cannot be a nozzle', () => {
  417. // "@2026" is a year, not a 2026 mm nozzle — guessing here would brand
  418. // the profile incompatible with every printer that exists.
  419. expect(presetCompatibility({ name: 'My PLA @2026' }, 'filament', P2S_04, idx)).toBe('unknown');
  420. expect(presetCompatibility({ name: 'My PLA @0.05' }, 'filament', P2S_04, idx)).toBe('unknown');
  421. });
  422. it('leaves a model-bearing tag on the model path', () => {
  423. // Regression guard: the nozzle-only branch must not swallow the forms
  424. // that carry a model — those still resolve to match/mismatch.
  425. expect(
  426. presetCompatibility({ name: 'Bambu PLA Basic @BBL P2S' }, 'filament', P2S_04, idx),
  427. ).toBe('match');
  428. expect(
  429. presetCompatibility({ name: 'My PLA @Bambu Lab X1 Carbon 0.4 nozzle' }, 'filament', P2S_04, idx),
  430. ).toBe('mismatch');
  431. });
  432. });
  433. describe("presetCompatibility — BambuStudio's \"# \" user-clone prefix", () => {
  434. const index = buildCompatibilityIndex(PRINTER_MODELS);
  435. // Editing a system preset saves a copy under this name; .bbscfg bundle
  436. // exports use the same convention. The backend already normalises it in
  437. // _canonical_printer_model.
  438. const CLONED_X1C = '# Bambu Lab X1 Carbon 0.4 nozzle';
  439. it('still matches a printer-tagged preset when the printer is a clone', () => {
  440. // Regression: the prefix failed the "Bambu Lab …" test, so every preset
  441. // came back 'unknown' and the dropdown filter silently did nothing.
  442. expect(
  443. presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', CLONED_X1C, index),
  444. ).toBe('match');
  445. });
  446. it('still rules out another printer when the selected printer is a clone', () => {
  447. expect(
  448. presetCompatibility({ name: '0.20mm Standard @BBL H2D' }, 'process', CLONED_X1C, index),
  449. ).toBe('mismatch');
  450. });
  451. it('matches a cloned preset against an unprefixed printer', () => {
  452. expect(
  453. presetCompatibility({ name: '# 0.20mm Standard @BBL X1C' }, 'process', X1C, index),
  454. ).toBe('match');
  455. });
  456. it('still compares the nozzle size through the prefix', () => {
  457. expect(
  458. presetCompatibility({ name: '0.20mm Standard @BBL X1C 0.6 nozzle' }, 'process', CLONED_X1C, index),
  459. ).toBe('mismatch');
  460. });
  461. it('matches compatible_printers with the prefix on either side', () => {
  462. // A preset cloned from a system printer lists the *unprefixed* name; a raw
  463. // comparison against the "# " form reads as a mismatch, which now hides
  464. // the preset rather than merely demoting it.
  465. expect(
  466. presetCompatibility({ name: 'My Process', compatible_printers: [X1C] }, 'process', CLONED_X1C, index),
  467. ).toBe('match');
  468. expect(
  469. presetCompatibility({ name: 'My Process', compatible_printers: [CLONED_X1C] }, 'process', X1C, index),
  470. ).toBe('match');
  471. });
  472. it('does not let the prefix turn a genuine mismatch into a match', () => {
  473. expect(
  474. presetCompatibility({ name: 'My Process', compatible_printers: [P2S] }, 'process', CLONED_X1C, index),
  475. ).toBe('mismatch');
  476. });
  477. it('leaves an untagged clone unknown rather than guessing', () => {
  478. expect(
  479. presetCompatibility({ name: '# My own profile' }, 'process', CLONED_X1C, index),
  480. ).toBe('unknown');
  481. });
  482. });