@shaykec/bridge 0.4.20 → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/README.md +2 -2
  2. package/canvas-dist/assets/{_basePickBy-CWoeT3J7.js → _basePickBy-BovdgFIW.js} +1 -1
  3. package/canvas-dist/assets/_basePickBy-BtkHe2u_.js +1 -0
  4. package/canvas-dist/assets/_basePickBy-C0936578.js +1 -0
  5. package/canvas-dist/assets/_basePickBy-CE2Qvuh7.js +1 -0
  6. package/canvas-dist/assets/_basePickBy-DV6sX4CG.js +1 -0
  7. package/canvas-dist/assets/_basePickBy-DZX6ZNMT.js +1 -0
  8. package/canvas-dist/assets/{_baseUniq-Dtuvtwtn.js → _baseUniq-B7dN28TM.js} +1 -1
  9. package/canvas-dist/assets/_baseUniq-Cl23fCdR.js +1 -0
  10. package/canvas-dist/assets/_baseUniq-CojWFw7B.js +1 -0
  11. package/canvas-dist/assets/_baseUniq-DA640BJl.js +1 -0
  12. package/canvas-dist/assets/_baseUniq-Ds-62CCj.js +1 -0
  13. package/canvas-dist/assets/_baseUniq-KG7SRw9H.js +1 -0
  14. package/canvas-dist/assets/{arc-YYWnrNJU.js → arc-7E9FFKlC.js} +1 -1
  15. package/canvas-dist/assets/arc-BSMfRZtt.js +1 -0
  16. package/canvas-dist/assets/arc-C6nT-koR.js +1 -0
  17. package/canvas-dist/assets/arc-D_fOnjmo.js +1 -0
  18. package/canvas-dist/assets/arc-Khfvgkr3.js +1 -0
  19. package/canvas-dist/assets/arc-ieS-i42x.js +1 -0
  20. package/canvas-dist/assets/{architectureDiagram-VXUJARFQ-CegbV-RR.js → architectureDiagram-VXUJARFQ-DF4t6GQD.js} +1 -1
  21. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DXgSlsio.js +36 -0
  22. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DiomxPB4.js +36 -0
  23. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DnFaxvXD.js +36 -0
  24. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-Dt38C0LJ.js +36 -0
  25. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-egbtMwua.js +36 -0
  26. package/canvas-dist/assets/{blockDiagram-VD42YOAC-C2e_j6ry.js → blockDiagram-VD42YOAC-CUNKQd-b.js} +1 -1
  27. package/canvas-dist/assets/blockDiagram-VD42YOAC-D-NiLXxd.js +122 -0
  28. package/canvas-dist/assets/blockDiagram-VD42YOAC-Dx6Dh9gg.js +122 -0
  29. package/canvas-dist/assets/blockDiagram-VD42YOAC-_r-PmlQy.js +122 -0
  30. package/canvas-dist/assets/blockDiagram-VD42YOAC-bvYKZLMc.js +122 -0
  31. package/canvas-dist/assets/blockDiagram-VD42YOAC-l85QT9Ig.js +122 -0
  32. package/canvas-dist/assets/{c4Diagram-YG6GDRKO-rIpnAud9.js → c4Diagram-YG6GDRKO-BWKCTyQi.js} +1 -1
  33. package/canvas-dist/assets/c4Diagram-YG6GDRKO-CbXs2xzC.js +10 -0
  34. package/canvas-dist/assets/c4Diagram-YG6GDRKO-CjiS-GNK.js +10 -0
  35. package/canvas-dist/assets/c4Diagram-YG6GDRKO-D7SnLlHp.js +10 -0
  36. package/canvas-dist/assets/c4Diagram-YG6GDRKO-RTTCSVf2.js +10 -0
  37. package/canvas-dist/assets/c4Diagram-YG6GDRKO-yvqJ_AqX.js +10 -0
  38. package/canvas-dist/assets/channel-CSXq7GP6.js +1 -0
  39. package/canvas-dist/assets/channel-CvujjGiJ.js +1 -0
  40. package/canvas-dist/assets/channel-D959Iony.js +1 -0
  41. package/canvas-dist/assets/channel-DOSwCnrB.js +1 -0
  42. package/canvas-dist/assets/channel-sw61LzxF.js +1 -0
  43. package/canvas-dist/assets/channel-vZVnNhOK.js +1 -0
  44. package/canvas-dist/assets/{chunk-4BX2VUAB-CpZGetnU.js → chunk-4BX2VUAB-BBjuAwXr.js} +1 -1
  45. package/canvas-dist/assets/chunk-4BX2VUAB-BXRNyucU.js +1 -0
  46. package/canvas-dist/assets/chunk-4BX2VUAB-Bgq5Z77T.js +1 -0
  47. package/canvas-dist/assets/chunk-4BX2VUAB-BuoMCMCr.js +1 -0
  48. package/canvas-dist/assets/chunk-4BX2VUAB-COD5n7vg.js +1 -0
  49. package/canvas-dist/assets/chunk-4BX2VUAB-K8DepKJO.js +1 -0
  50. package/canvas-dist/assets/{chunk-55IACEB6-L0OhcFdd.js → chunk-55IACEB6-Bic_bMrQ.js} +1 -1
  51. package/canvas-dist/assets/chunk-55IACEB6-DEy2QUDq.js +1 -0
  52. package/canvas-dist/assets/chunk-55IACEB6-Dcgbmfzg.js +1 -0
  53. package/canvas-dist/assets/chunk-55IACEB6-DfmuNm_E.js +1 -0
  54. package/canvas-dist/assets/chunk-55IACEB6-DlQRcczm.js +1 -0
  55. package/canvas-dist/assets/chunk-55IACEB6-p2qMY-fm.js +1 -0
  56. package/canvas-dist/assets/{chunk-B4BG7PRW-Cv9vsAzg.js → chunk-B4BG7PRW-BpbyxBP2.js} +1 -1
  57. package/canvas-dist/assets/chunk-B4BG7PRW-CCPqvPrP.js +165 -0
  58. package/canvas-dist/assets/chunk-B4BG7PRW-CEeDPAki.js +165 -0
  59. package/canvas-dist/assets/chunk-B4BG7PRW-D2UFN_2M.js +165 -0
  60. package/canvas-dist/assets/chunk-B4BG7PRW-DFI5h6HC.js +165 -0
  61. package/canvas-dist/assets/chunk-B4BG7PRW-DKOiFGMU.js +165 -0
  62. package/canvas-dist/assets/{chunk-DI55MBZ5-B3p1mU43.js → chunk-DI55MBZ5-BV6nHjNQ.js} +1 -1
  63. package/canvas-dist/assets/chunk-DI55MBZ5-CEZJmC0E.js +220 -0
  64. package/canvas-dist/assets/chunk-DI55MBZ5-DOZT99Ek.js +220 -0
  65. package/canvas-dist/assets/chunk-DI55MBZ5-DmC2LoG2.js +220 -0
  66. package/canvas-dist/assets/chunk-DI55MBZ5-DpkcJdZP.js +220 -0
  67. package/canvas-dist/assets/chunk-DI55MBZ5-fVTGx0zh.js +220 -0
  68. package/canvas-dist/assets/{chunk-FMBD7UC4-JCLAHw5x.js → chunk-FMBD7UC4-BOCyQpI7.js} +1 -1
  69. package/canvas-dist/assets/chunk-FMBD7UC4-C76FrRL8.js +15 -0
  70. package/canvas-dist/assets/chunk-FMBD7UC4-CAq-btWc.js +15 -0
  71. package/canvas-dist/assets/chunk-FMBD7UC4-CidVsej6.js +15 -0
  72. package/canvas-dist/assets/chunk-FMBD7UC4-DPpfskdX.js +15 -0
  73. package/canvas-dist/assets/chunk-FMBD7UC4-DnLtclge.js +15 -0
  74. package/canvas-dist/assets/{chunk-QN33PNHL-C9arKEVq.js → chunk-QN33PNHL-BclpCUi8.js} +1 -1
  75. package/canvas-dist/assets/chunk-QN33PNHL-DDUw8IU1.js +1 -0
  76. package/canvas-dist/assets/chunk-QN33PNHL-DdJFAUXw.js +1 -0
  77. package/canvas-dist/assets/chunk-QN33PNHL-DjV4jUn9.js +1 -0
  78. package/canvas-dist/assets/chunk-QN33PNHL-N-HTycqU.js +1 -0
  79. package/canvas-dist/assets/chunk-QN33PNHL-sd8p21DW.js +1 -0
  80. package/canvas-dist/assets/{chunk-QZHKN3VN-Bs1r3d9U.js → chunk-QZHKN3VN-B6mT-JkP.js} +1 -1
  81. package/canvas-dist/assets/chunk-QZHKN3VN-BCo8pc7x.js +1 -0
  82. package/canvas-dist/assets/chunk-QZHKN3VN-C8IIu6es.js +1 -0
  83. package/canvas-dist/assets/chunk-QZHKN3VN-D9FF492U.js +1 -0
  84. package/canvas-dist/assets/chunk-QZHKN3VN-DWMbUjXT.js +1 -0
  85. package/canvas-dist/assets/chunk-QZHKN3VN-l5FBJ77g.js +1 -0
  86. package/canvas-dist/assets/{chunk-TZMSLE5B-_Ye6r84Y.js → chunk-TZMSLE5B-BASt-UWt.js} +1 -1
  87. package/canvas-dist/assets/chunk-TZMSLE5B-BCfaZWLT.js +1 -0
  88. package/canvas-dist/assets/chunk-TZMSLE5B-BKIk_hBR.js +1 -0
  89. package/canvas-dist/assets/chunk-TZMSLE5B-C4pt-Ir8.js +1 -0
  90. package/canvas-dist/assets/chunk-TZMSLE5B-DwGlELvo.js +1 -0
  91. package/canvas-dist/assets/chunk-TZMSLE5B-jJKG-WvJ.js +1 -0
  92. package/canvas-dist/assets/classDiagram-2ON5EDUG-B7YQfPU4.js +1 -0
  93. package/canvas-dist/assets/classDiagram-2ON5EDUG-BZ61MaHY.js +1 -0
  94. package/canvas-dist/assets/classDiagram-2ON5EDUG-CGseYor2.js +1 -0
  95. package/canvas-dist/assets/classDiagram-2ON5EDUG-CKzOc99J.js +1 -0
  96. package/canvas-dist/assets/classDiagram-2ON5EDUG-Ce_LPjwW.js +1 -0
  97. package/canvas-dist/assets/classDiagram-2ON5EDUG-DorPdibv.js +1 -0
  98. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-B7YQfPU4.js +1 -0
  99. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BZ61MaHY.js +1 -0
  100. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CGseYor2.js +1 -0
  101. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CKzOc99J.js +1 -0
  102. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-Ce_LPjwW.js +1 -0
  103. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-DorPdibv.js +1 -0
  104. package/canvas-dist/assets/clone-74KSto7H.js +1 -0
  105. package/canvas-dist/assets/clone-CJQgAYVe.js +1 -0
  106. package/canvas-dist/assets/clone-DLeTuhHE.js +1 -0
  107. package/canvas-dist/assets/clone-D_IHK_lQ.js +1 -0
  108. package/canvas-dist/assets/clone-DxMUv1L9.js +1 -0
  109. package/canvas-dist/assets/clone-UNKf_nED.js +1 -0
  110. package/canvas-dist/assets/{cose-bilkent-S5V4N54A-2O2oovOj.js → cose-bilkent-S5V4N54A-BTyQiCkr.js} +1 -1
  111. package/canvas-dist/assets/cose-bilkent-S5V4N54A-BtPAe24N.js +1 -0
  112. package/canvas-dist/assets/cose-bilkent-S5V4N54A-DIjE7V3m.js +1 -0
  113. package/canvas-dist/assets/cose-bilkent-S5V4N54A-DKL_BGvE.js +1 -0
  114. package/canvas-dist/assets/cose-bilkent-S5V4N54A-LZ4OsCLU.js +1 -0
  115. package/canvas-dist/assets/cose-bilkent-S5V4N54A-XWeJtgga.js +1 -0
  116. package/canvas-dist/assets/{dagre-6UL2VRFP-gRmGLrEW.js → dagre-6UL2VRFP-BJ2vcFwR.js} +1 -1
  117. package/canvas-dist/assets/dagre-6UL2VRFP-C1FlE5s8.js +4 -0
  118. package/canvas-dist/assets/dagre-6UL2VRFP-C3BWFgl6.js +4 -0
  119. package/canvas-dist/assets/dagre-6UL2VRFP-CUnx73Rf.js +4 -0
  120. package/canvas-dist/assets/dagre-6UL2VRFP-Do10BY1y.js +4 -0
  121. package/canvas-dist/assets/dagre-6UL2VRFP-rOZEkrsg.js +4 -0
  122. package/canvas-dist/assets/{diagram-PSM6KHXK-B7Li-xxw.js → diagram-PSM6KHXK-BGi_qzbq.js} +1 -1
  123. package/canvas-dist/assets/diagram-PSM6KHXK-C3Nv7h_j.js +24 -0
  124. package/canvas-dist/assets/diagram-PSM6KHXK-CsMy-r0n.js +24 -0
  125. package/canvas-dist/assets/diagram-PSM6KHXK-Dj8g7kGt.js +24 -0
  126. package/canvas-dist/assets/diagram-PSM6KHXK-Dxb1w_7r.js +24 -0
  127. package/canvas-dist/assets/diagram-PSM6KHXK-kVMBkEyV.js +24 -0
  128. package/canvas-dist/assets/{diagram-QEK2KX5R-B_NNUAm3.js → diagram-QEK2KX5R-4bsrr1WZ.js} +1 -1
  129. package/canvas-dist/assets/diagram-QEK2KX5R-Bv7BmKfI.js +43 -0
  130. package/canvas-dist/assets/diagram-QEK2KX5R-C_FLN6hv.js +43 -0
  131. package/canvas-dist/assets/diagram-QEK2KX5R-Csuk5L3z.js +43 -0
  132. package/canvas-dist/assets/diagram-QEK2KX5R-D5Aszgz4.js +43 -0
  133. package/canvas-dist/assets/diagram-QEK2KX5R-DX58f87l.js +43 -0
  134. package/canvas-dist/assets/{diagram-S2PKOQOG-NcK-KHaA.js → diagram-S2PKOQOG-1Q7hwiSd.js} +1 -1
  135. package/canvas-dist/assets/diagram-S2PKOQOG-Bz9Vxi5V.js +24 -0
  136. package/canvas-dist/assets/diagram-S2PKOQOG-CdWgZIIc.js +24 -0
  137. package/canvas-dist/assets/diagram-S2PKOQOG-DBicbKFU.js +24 -0
  138. package/canvas-dist/assets/diagram-S2PKOQOG-DsXKwPtU.js +24 -0
  139. package/canvas-dist/assets/diagram-S2PKOQOG-L_SMHLXs.js +24 -0
  140. package/canvas-dist/assets/{erDiagram-Q2GNP2WA-CG7dqzk3.js → erDiagram-Q2GNP2WA-BYu7fh6H.js} +1 -1
  141. package/canvas-dist/assets/erDiagram-Q2GNP2WA-CvnQ69BF.js +60 -0
  142. package/canvas-dist/assets/erDiagram-Q2GNP2WA-D3xm-Tdm.js +60 -0
  143. package/canvas-dist/assets/erDiagram-Q2GNP2WA-DIPpD8sj.js +60 -0
  144. package/canvas-dist/assets/erDiagram-Q2GNP2WA-DNgu6dMd.js +60 -0
  145. package/canvas-dist/assets/erDiagram-Q2GNP2WA-Decm8aB4.js +60 -0
  146. package/canvas-dist/assets/{flowDiagram-NV44I4VS-CBzCj5D6.js → flowDiagram-NV44I4VS-2ymk2kw2.js} +1 -1
  147. package/canvas-dist/assets/flowDiagram-NV44I4VS-BEPFOt6U.js +162 -0
  148. package/canvas-dist/assets/flowDiagram-NV44I4VS-BwqXYGfK.js +162 -0
  149. package/canvas-dist/assets/flowDiagram-NV44I4VS-CS1jax_z.js +162 -0
  150. package/canvas-dist/assets/flowDiagram-NV44I4VS-DQz5bf7r.js +162 -0
  151. package/canvas-dist/assets/flowDiagram-NV44I4VS-KW4T1sqF.js +162 -0
  152. package/canvas-dist/assets/{ganttDiagram-JELNMOA3-CHw-4qJC.js → ganttDiagram-JELNMOA3-B811prZt.js} +1 -1
  153. package/canvas-dist/assets/ganttDiagram-JELNMOA3-C75pWm7X.js +267 -0
  154. package/canvas-dist/assets/ganttDiagram-JELNMOA3-CWsbo0fn.js +267 -0
  155. package/canvas-dist/assets/ganttDiagram-JELNMOA3-CbJozPBN.js +267 -0
  156. package/canvas-dist/assets/ganttDiagram-JELNMOA3-Co0cFt4c.js +267 -0
  157. package/canvas-dist/assets/ganttDiagram-JELNMOA3-I4PDqrRh.js +267 -0
  158. package/canvas-dist/assets/{gitGraphDiagram-V2S2FVAM-Dqrc4wUs.js → gitGraphDiagram-V2S2FVAM-B-z0cLPt.js} +1 -1
  159. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-Be40z-LF.js +65 -0
  160. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BejNaAVm.js +65 -0
  161. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BqWDYr0X.js +65 -0
  162. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-DSvWGY-e.js +65 -0
  163. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-HLYbyNJ5.js +65 -0
  164. package/canvas-dist/assets/{graph-X9Kzu-pf.js → graph-BE8KKsdf.js} +1 -1
  165. package/canvas-dist/assets/graph-D6DzzszU.js +1 -0
  166. package/canvas-dist/assets/graph-DFR8Y_8s.js +1 -0
  167. package/canvas-dist/assets/graph-Da385cDY.js +1 -0
  168. package/canvas-dist/assets/graph-MU7gZz2B.js +1 -0
  169. package/canvas-dist/assets/graph-wjSBJwnf.js +1 -0
  170. package/canvas-dist/assets/index--ztw-8Rw.js +647 -0
  171. package/canvas-dist/assets/{index-BQFKo-II.js → index-5TpIM6B1.js} +1 -1
  172. package/canvas-dist/assets/index-6GBZ9nXN.css +32 -0
  173. package/canvas-dist/assets/index-BSswTuBk.js +11 -0
  174. package/canvas-dist/assets/index-BVvhMmjs.js +11 -0
  175. package/canvas-dist/assets/index-BY92Mj5g.js +572 -0
  176. package/canvas-dist/assets/index-CV7palC3.js +572 -0
  177. package/canvas-dist/assets/index-D9bmQGsB.js +11 -0
  178. package/canvas-dist/assets/index-DDIKkGv8.js +592 -0
  179. package/canvas-dist/assets/index-Dyo0NkPb.js +574 -0
  180. package/canvas-dist/assets/index-iQWajCow.js +572 -0
  181. package/canvas-dist/assets/index-m68YlAMU.js +11 -0
  182. package/canvas-dist/assets/index-mEoP57az.js +11 -0
  183. package/canvas-dist/assets/{infoDiagram-HS3SLOUP-CflnZPsm.js → infoDiagram-HS3SLOUP--9BirqgJ.js} +1 -1
  184. package/canvas-dist/assets/infoDiagram-HS3SLOUP-CSJVED2y.js +2 -0
  185. package/canvas-dist/assets/infoDiagram-HS3SLOUP-D68HIb2t.js +2 -0
  186. package/canvas-dist/assets/infoDiagram-HS3SLOUP-DK2VLGGz.js +2 -0
  187. package/canvas-dist/assets/infoDiagram-HS3SLOUP-PaFhn4yD.js +2 -0
  188. package/canvas-dist/assets/infoDiagram-HS3SLOUP-zLNG47sU.js +2 -0
  189. package/canvas-dist/assets/{journeyDiagram-XKPGCS4Q-D2gkCipQ.js → journeyDiagram-XKPGCS4Q-Bue2dR2X.js} +1 -1
  190. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-CrgZfpdU.js +139 -0
  191. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-DUxWmkkC.js +139 -0
  192. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-OTFkv4pd.js +139 -0
  193. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-eK2_Zuu3.js +139 -0
  194. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-uds5Tz8D.js +139 -0
  195. package/canvas-dist/assets/{kanban-definition-3W4ZIXB7-CtLLz4o8.js → kanban-definition-3W4ZIXB7-BETdiI7I.js} +1 -1
  196. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-BdVh7KdN.js +89 -0
  197. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-Cxl8UM9S.js +89 -0
  198. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-DVPlx3I2.js +89 -0
  199. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-LtNWeoYB.js +89 -0
  200. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-uvhEMvyE.js +89 -0
  201. package/canvas-dist/assets/{layout-CjvV_Dms.js → layout-1OzszN14.js} +1 -1
  202. package/canvas-dist/assets/layout-CJSupFcF.js +1 -0
  203. package/canvas-dist/assets/layout-DFRmxN_c.js +1 -0
  204. package/canvas-dist/assets/layout-DSu-zk7y.js +1 -0
  205. package/canvas-dist/assets/layout-TGcrvApd.js +1 -0
  206. package/canvas-dist/assets/layout-eStc8SYK.js +1 -0
  207. package/canvas-dist/assets/{linear-D3cIYHoS.js → linear-9qlE6xa7.js} +1 -1
  208. package/canvas-dist/assets/linear-CBfFWnLD.js +1 -0
  209. package/canvas-dist/assets/linear-Cv4ai8Hq.js +1 -0
  210. package/canvas-dist/assets/linear-DDzz65E6.js +1 -0
  211. package/canvas-dist/assets/linear-wbIqhwDf.js +1 -0
  212. package/canvas-dist/assets/linear-wyNKl76F.js +1 -0
  213. package/canvas-dist/assets/{mindmap-definition-VGOIOE7T-DSgjVg-P.js → mindmap-definition-VGOIOE7T-3l4YzhEM.js} +1 -1
  214. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-B-KkpNlw.js +68 -0
  215. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-DHMHWgmT.js +68 -0
  216. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-Dqfyg4Z2.js +68 -0
  217. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-NeRYOzsq.js +68 -0
  218. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-xyu628P9.js +68 -0
  219. package/canvas-dist/assets/{pieDiagram-ADFJNKIX-B_lYaGFj.js → pieDiagram-ADFJNKIX-BWNzVAGj.js} +1 -1
  220. package/canvas-dist/assets/pieDiagram-ADFJNKIX-Bm3PXYs-.js +30 -0
  221. package/canvas-dist/assets/pieDiagram-ADFJNKIX-BvvN7VvQ.js +30 -0
  222. package/canvas-dist/assets/pieDiagram-ADFJNKIX-BwU7AN7W.js +30 -0
  223. package/canvas-dist/assets/pieDiagram-ADFJNKIX-CHgwWCaM.js +30 -0
  224. package/canvas-dist/assets/pieDiagram-ADFJNKIX-DlZc8YOh.js +30 -0
  225. package/canvas-dist/assets/{quadrantDiagram-AYHSOK5B-DLZLTJe3.js → quadrantDiagram-AYHSOK5B-B-Zd8OFp.js} +1 -1
  226. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-B1CnJyxI.js +7 -0
  227. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C0Qo00b9.js +7 -0
  228. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C9bx3nEJ.js +7 -0
  229. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-UHENkiRO.js +7 -0
  230. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-jKfurTPU.js +7 -0
  231. package/canvas-dist/assets/{requirementDiagram-UZGBJVZJ-CZE26rhL.js → requirementDiagram-UZGBJVZJ-BPpNNusD.js} +1 -1
  232. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-BwZF1NIK.js +64 -0
  233. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-CaT3Frtk.js +64 -0
  234. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-Dfoz7R_7.js +64 -0
  235. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-DsrX4TT-.js +64 -0
  236. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-dmouSXOl.js +64 -0
  237. package/canvas-dist/assets/{sankeyDiagram-TZEHDZUN-DQMRJAPV.js → sankeyDiagram-TZEHDZUN-BEy-A1Fu.js} +1 -1
  238. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BViMBiAQ.js +10 -0
  239. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BqrM-qWN.js +10 -0
  240. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DRkRC9qB.js +10 -0
  241. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DbuzKCtn.js +10 -0
  242. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-_aHMKbpw.js +10 -0
  243. package/canvas-dist/assets/{sequenceDiagram-WL72ISMW-BY723FEn.js → sequenceDiagram-WL72ISMW-B8FOaL2Q.js} +1 -1
  244. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-C02NQwOB.js +145 -0
  245. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CgyHivPj.js +145 -0
  246. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CzW1WaEm.js +145 -0
  247. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-DJhHI1pe.js +145 -0
  248. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-VFkpAeoG.js +145 -0
  249. package/canvas-dist/assets/{stateDiagram-FKZM4ZOC-C_UdOFhy.js → stateDiagram-FKZM4ZOC-BSqFX4PJ.js} +1 -1
  250. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-BnXhhxkN.js +1 -0
  251. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-ClARVrvt.js +1 -0
  252. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-CuC6xesY.js +1 -0
  253. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-DcAiGjph.js +1 -0
  254. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-aBg0hjTp.js +1 -0
  255. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-8fib9ftc.js +1 -0
  256. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-B-DO0ZqO.js +1 -0
  257. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-BksbsE4k.js +1 -0
  258. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-C2DJCNPK.js +1 -0
  259. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-CeA5jba6.js +1 -0
  260. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-zsAyq0tK.js +1 -0
  261. package/canvas-dist/assets/{timeline-definition-IT6M3QCI-DkrJqww0.js → timeline-definition-IT6M3QCI-BaHdYD2h.js} +1 -1
  262. package/canvas-dist/assets/timeline-definition-IT6M3QCI-Bl2hg8IM.js +61 -0
  263. package/canvas-dist/assets/timeline-definition-IT6M3QCI-CrVwLiGm.js +61 -0
  264. package/canvas-dist/assets/timeline-definition-IT6M3QCI-DrXGRjnB.js +61 -0
  265. package/canvas-dist/assets/timeline-definition-IT6M3QCI-cYAwshf6.js +61 -0
  266. package/canvas-dist/assets/timeline-definition-IT6M3QCI-flyL0y-3.js +61 -0
  267. package/canvas-dist/assets/{treemap-GDKQZRPO-B-6bMZqD.js → treemap-GDKQZRPO-C4Hg8kJ_.js} +1 -1
  268. package/canvas-dist/assets/treemap-GDKQZRPO-DVY2G9qY.js +162 -0
  269. package/canvas-dist/assets/treemap-GDKQZRPO-DpLWPA1z.js +162 -0
  270. package/canvas-dist/assets/treemap-GDKQZRPO-Ds86cUVw.js +162 -0
  271. package/canvas-dist/assets/treemap-GDKQZRPO-DwmoI6tH.js +162 -0
  272. package/canvas-dist/assets/treemap-GDKQZRPO-SsGFkgVd.js +162 -0
  273. package/canvas-dist/assets/{xychartDiagram-PRI3JC2R-DkBhUy_D.js → xychartDiagram-PRI3JC2R-B9c1iLBf.js} +1 -1
  274. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-BpX6MPWa.js +7 -0
  275. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CEgW_j0p.js +7 -0
  276. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CSEFGEQX.js +7 -0
  277. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CnG4XoMc.js +7 -0
  278. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-Dftj3Bt3.js +7 -0
  279. package/canvas-dist/index.html +3 -2
  280. package/package.json +3 -2
  281. package/src/protocol.js +1 -1
  282. package/src/protocol.test.js +1 -1
  283. package/src/pty-manager.js +281 -0
  284. package/src/pty-manager.test.js +212 -0
  285. package/src/router.js +31 -1
  286. package/src/router.test.js +1 -1
  287. package/src/sdk-e2e.test.js +101 -0
  288. package/src/server.e2e.test.js +804 -138
  289. package/src/server.js +1273 -137
  290. package/src/session-store.js +260 -0
  291. package/src/session-store.test.js +235 -0
  292. package/src/templates.js +1 -1
  293. package/src/templates.test.js +1 -1
  294. package/src/terminal.js +3 -3
  295. package/src/terminal.test.js +12 -12
  296. package/src/visual-interceptor.js +450 -0
  297. package/src/visual-interceptor.test.js +943 -0
  298. package/src/workshop-parser.js +251 -0
  299. package/src/workshop-parser.test.js +179 -0
  300. package/templates/celebrate.html +1 -1
  301. package/templates/code-playground.html +1 -1
  302. package/templates/dashboard.html +7 -8
  303. package/templates/diagram-architecture.html +1 -1
  304. package/templates/diagram-flow.html +1 -1
  305. package/templates/diagram-mermaid.html +1 -1
  306. package/templates/game-speed-round.html +1 -1
  307. package/templates/quiz-drag-order.html +1 -1
  308. package/templates/quiz-fill-blank.html +1 -1
  309. package/templates/quiz-matching.html +1 -1
  310. package/templates/quiz-timed-choice.html +1 -1
  311. package/templates/welcome.html +7 -7
  312. package/canvas-dist/assets/channel-BzJVlie3.js +0 -1
  313. package/canvas-dist/assets/classDiagram-2ON5EDUG-BTs-zEmB.js +0 -1
  314. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BTs-zEmB.js +0 -1
  315. package/canvas-dist/assets/clone-CXEfuXmc.js +0 -1
  316. package/canvas-dist/assets/index-DJ49c6u-.js +0 -426
  317. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-DXIiFh0L.js +0 -1
@@ -0,0 +1,943 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { extractAndRouteVisuals, extractAndRouteSuggestions, extractAndRouteMedia, detectMediaUrl, extractButtons, extractInlineBlocks, stripCanvasCommands } from './visual-interceptor.js';
3
+
4
+ describe('extractAndRouteVisuals', () => {
5
+ let tierManager, router;
6
+
7
+ beforeEach(() => {
8
+ tierManager = {
9
+ broadcastWs: vi.fn(),
10
+ broadcastSse: vi.fn(),
11
+ getTier: vi.fn(() => 2),
12
+ };
13
+ router = {
14
+ routeVisualCommand: vi.fn(),
15
+ tierManager,
16
+ };
17
+ });
18
+
19
+ it('does nothing when text is null or empty', () => {
20
+ extractAndRouteVisuals(null, tierManager, router);
21
+ extractAndRouteVisuals('', tierManager, router);
22
+ extractAndRouteVisuals(undefined, tierManager, router);
23
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
24
+ });
25
+
26
+ it('does nothing when text has no mermaid blocks', () => {
27
+ extractAndRouteVisuals('Here is some text without any code blocks.', tierManager, router);
28
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
29
+ });
30
+
31
+ it('does nothing for non-mermaid code blocks', () => {
32
+ const text = '```javascript\nconsole.log("hello");\n```';
33
+ extractAndRouteVisuals(text, tierManager, router);
34
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it('extracts a single mermaid block', () => {
38
+ const text = 'Here is a diagram:\n\n```mermaid\ngraph TD\n A-->B\n```\n\nNice!';
39
+ extractAndRouteVisuals(text, tierManager, router);
40
+
41
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
42
+ const envelope = router.routeVisualCommand.mock.calls[0][0];
43
+ expect(envelope.type).toBe('canvas:diagram');
44
+ expect(envelope.payload.format).toBe('mermaid');
45
+ expect(envelope.payload.content).toBe('graph TD\n A-->B');
46
+ expect(envelope.payload.autoRouted).toBe(true);
47
+ });
48
+
49
+ it('extracts multiple mermaid blocks', () => {
50
+ const text = [
51
+ '```mermaid',
52
+ 'graph TD',
53
+ ' A-->B',
54
+ '```',
55
+ 'Some text between',
56
+ '```mermaid',
57
+ 'sequenceDiagram',
58
+ ' Alice->>Bob: Hello',
59
+ '```',
60
+ ].join('\n');
61
+
62
+ extractAndRouteVisuals(text, tierManager, router);
63
+
64
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(2);
65
+ expect(router.routeVisualCommand.mock.calls[0][0].payload.content).toBe('graph TD\n A-->B');
66
+ expect(router.routeVisualCommand.mock.calls[1][0].payload.content).toBe('sequenceDiagram\n Alice->>Bob: Hello');
67
+ });
68
+
69
+ it('ignores empty mermaid blocks', () => {
70
+ const text = '```mermaid\n\n```';
71
+ extractAndRouteVisuals(text, tierManager, router);
72
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('handles mermaid block with extra whitespace in fence', () => {
76
+ const text = '```mermaid \ngraph LR\n X-->Y\n```';
77
+ extractAndRouteVisuals(text, tierManager, router);
78
+
79
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
80
+ expect(router.routeVisualCommand.mock.calls[0][0].payload.content).toBe('graph LR\n X-->Y');
81
+ });
82
+
83
+ it('does not match nested triple backticks inside mermaid', () => {
84
+ const text = '```mermaid\ngraph TD\n A["with ```backticks```"]-->B\n```';
85
+ extractAndRouteVisuals(text, tierManager, router);
86
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
87
+ });
88
+
89
+ it('handles non-string input gracefully', () => {
90
+ extractAndRouteVisuals(42, tierManager, router);
91
+ extractAndRouteVisuals({}, tierManager, router);
92
+ extractAndRouteVisuals([], tierManager, router);
93
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
94
+ });
95
+ });
96
+
97
+ describe('extractAndRouteSuggestions', () => {
98
+ let tierManager;
99
+
100
+ beforeEach(() => {
101
+ tierManager = {
102
+ broadcastWs: vi.fn(),
103
+ broadcastSse: vi.fn(),
104
+ };
105
+ });
106
+
107
+ it('does nothing when text is null, empty, or non-string', () => {
108
+ extractAndRouteSuggestions(null, tierManager);
109
+ extractAndRouteSuggestions('', tierManager);
110
+ extractAndRouteSuggestions(undefined, tierManager);
111
+ extractAndRouteSuggestions(42, tierManager);
112
+ expect(tierManager.broadcastWs).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it('does nothing when text has no suggestions comment', () => {
116
+ extractAndRouteSuggestions('Just a normal message.', tierManager);
117
+ expect(tierManager.broadcastWs).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it('extracts valid suggestions and broadcasts', () => {
121
+ const text = 'Great explanation!\n<!-- suggestions: [{"label":"Quiz me","text":"/teach --quiz-only"},{"label":"Continue","text":"/teach"}] -->';
122
+ extractAndRouteSuggestions(text, tierManager);
123
+
124
+ expect(tierManager.broadcastWs).toHaveBeenCalledTimes(1);
125
+ expect(tierManager.broadcastSse).toHaveBeenCalledTimes(1);
126
+
127
+ const envelope = JSON.parse(tierManager.broadcastWs.mock.calls[0][0]);
128
+ expect(envelope.type).toBe('chat:suggestions');
129
+ expect(envelope.payload.suggestions).toHaveLength(2);
130
+ expect(envelope.payload.suggestions[0].label).toBe('Quiz me');
131
+ expect(envelope.payload.suggestions[1].text).toBe('/teach');
132
+ });
133
+
134
+ it('handles suggestions with extra whitespace in comment', () => {
135
+ const text = '<!-- suggestions: [{"label":"Hint","text":"Give me a hint"}] -->';
136
+ extractAndRouteSuggestions(text, tierManager);
137
+
138
+ expect(tierManager.broadcastWs).toHaveBeenCalledTimes(1);
139
+ const envelope = JSON.parse(tierManager.broadcastWs.mock.calls[0][0]);
140
+ expect(envelope.payload.suggestions).toHaveLength(1);
141
+ expect(envelope.payload.suggestions[0].label).toBe('Hint');
142
+ });
143
+
144
+ it('ignores malformed JSON in suggestions comment', () => {
145
+ const text = '<!-- suggestions: [not valid json] -->';
146
+ extractAndRouteSuggestions(text, tierManager);
147
+ expect(tierManager.broadcastWs).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it('ignores empty suggestions array', () => {
151
+ const text = '<!-- suggestions: [] -->';
152
+ extractAndRouteSuggestions(text, tierManager);
153
+ expect(tierManager.broadcastWs).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it('filters out suggestions missing label or text', () => {
157
+ const text = '<!-- suggestions: [{"label":"Valid","text":"/teach"},{"label":"No text"},{"text":"no label"}] -->';
158
+ extractAndRouteSuggestions(text, tierManager);
159
+
160
+ expect(tierManager.broadcastWs).toHaveBeenCalledTimes(1);
161
+ const envelope = JSON.parse(tierManager.broadcastWs.mock.calls[0][0]);
162
+ expect(envelope.payload.suggestions).toHaveLength(1);
163
+ expect(envelope.payload.suggestions[0].label).toBe('Valid');
164
+ });
165
+
166
+ it('does not broadcast when all suggestions are invalid', () => {
167
+ const text = '<!-- suggestions: [{"label":"No text"},{"text":"no label"}] -->';
168
+ extractAndRouteSuggestions(text, tierManager);
169
+ expect(tierManager.broadcastWs).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('preserves optional icon field', () => {
173
+ const text = '<!-- suggestions: [{"label":"Quiz","text":"/teach --quiz-only","icon":"quiz"}] -->';
174
+ extractAndRouteSuggestions(text, tierManager);
175
+
176
+ const envelope = JSON.parse(tierManager.broadcastWs.mock.calls[0][0]);
177
+ expect(envelope.payload.suggestions[0].icon).toBe('quiz');
178
+ });
179
+ });
180
+
181
+ describe('detectMediaUrl', () => {
182
+ it('detects YouTube URLs', () => {
183
+ expect(detectMediaUrl('https://www.youtube.com/watch?v=HVsySz-h9r4')).toBe('youtube');
184
+ expect(detectMediaUrl('https://youtu.be/HVsySz-h9r4')).toBe('youtube');
185
+ });
186
+
187
+ it('detects Vimeo URLs', () => {
188
+ expect(detectMediaUrl('https://vimeo.com/123456')).toBe('vimeo');
189
+ });
190
+
191
+ it('detects image URLs', () => {
192
+ expect(detectMediaUrl('https://example.com/pic.png')).toBe('image');
193
+ expect(detectMediaUrl('https://example.com/pic.jpg?w=100')).toBe('image');
194
+ });
195
+
196
+ it('detects video URLs', () => {
197
+ expect(detectMediaUrl('https://example.com/clip.mp4')).toBe('video');
198
+ });
199
+
200
+ it('returns link for generic URLs', () => {
201
+ expect(detectMediaUrl('https://example.com/page')).toBe('link');
202
+ });
203
+ });
204
+
205
+ describe('extractAndRouteMedia', () => {
206
+ let router, seenUrls;
207
+
208
+ beforeEach(() => {
209
+ router = { routeVisualCommand: vi.fn() };
210
+ seenUrls = new Set();
211
+ });
212
+
213
+ it('does nothing for null/empty/non-string text', () => {
214
+ extractAndRouteMedia(null, router, seenUrls);
215
+ extractAndRouteMedia('', router, seenUrls);
216
+ extractAndRouteMedia(undefined, router, seenUrls);
217
+ extractAndRouteMedia(42, router, seenUrls);
218
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
219
+ });
220
+
221
+ it('does nothing when text has no URLs', () => {
222
+ extractAndRouteMedia('Here is a plain message with no links.', router, seenUrls);
223
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it('does nothing for generic link URLs (not embeddable)', () => {
227
+ extractAndRouteMedia('Check out https://example.com for details.', router, seenUrls);
228
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
229
+ });
230
+
231
+ it('routes YouTube URLs as canvas:html', () => {
232
+ extractAndRouteMedia('Watch this: https://www.youtube.com/watch?v=HVsySz-h9r4', router, seenUrls);
233
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
234
+ const env = router.routeVisualCommand.mock.calls[0][0];
235
+ expect(env.type).toBe('canvas:html');
236
+ expect(env.payload.subtype).toBe('youtube');
237
+ expect(env.payload.html).toContain('youtube.com/embed/HVsySz-h9r4');
238
+ expect(env.payload.autoRouted).toBe(true);
239
+ });
240
+
241
+ it('routes Vimeo URLs as canvas:html', () => {
242
+ extractAndRouteMedia('See https://vimeo.com/999999 for demo', router, seenUrls);
243
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
244
+ const env = router.routeVisualCommand.mock.calls[0][0];
245
+ expect(env.payload.subtype).toBe('vimeo');
246
+ expect(env.payload.html).toContain('player.vimeo.com/video/999999');
247
+ });
248
+
249
+ it('routes image URLs as canvas:html', () => {
250
+ extractAndRouteMedia('Here is a screenshot: https://example.com/screen.png', router, seenUrls);
251
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
252
+ const env = router.routeVisualCommand.mock.calls[0][0];
253
+ expect(env.payload.subtype).toBe('image');
254
+ expect(env.payload.html).toContain('<img');
255
+ });
256
+
257
+ it('routes video file URLs as canvas:html', () => {
258
+ extractAndRouteMedia('Download https://example.com/tutorial.mp4', router, seenUrls);
259
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
260
+ const env = router.routeVisualCommand.mock.calls[0][0];
261
+ expect(env.payload.subtype).toBe('video');
262
+ expect(env.payload.html).toContain('<video');
263
+ });
264
+
265
+ it('deduplicates: same URL is not routed twice', () => {
266
+ const text = 'Watch https://www.youtube.com/watch?v=HVsySz-h9r4 again';
267
+ extractAndRouteMedia(text, router, seenUrls);
268
+ extractAndRouteMedia(text, router, seenUrls);
269
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
270
+ });
271
+
272
+ it('respects MAX_AUTO_MEDIA limit (3)', () => {
273
+ const text = [
274
+ 'https://www.youtube.com/watch?v=aaaaaaaaaaa',
275
+ 'https://www.youtube.com/watch?v=bbbbbbbbbbb',
276
+ 'https://www.youtube.com/watch?v=ccccccccccc',
277
+ 'https://www.youtube.com/watch?v=ddddddddddd',
278
+ ].join(' ');
279
+ extractAndRouteMedia(text, router, seenUrls);
280
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(3);
281
+ });
282
+
283
+ it('ignores URLs inside code fences', () => {
284
+ const text = '```\nhttps://www.youtube.com/watch?v=HVsySz-h9r4\n```';
285
+ extractAndRouteMedia(text, router, seenUrls);
286
+ expect(router.routeVisualCommand).not.toHaveBeenCalled();
287
+ });
288
+
289
+ it('routes URLs outside code fences even if code fences exist', () => {
290
+ const text = '```\nsome code\n```\nWatch https://www.youtube.com/watch?v=HVsySz-h9r4';
291
+ extractAndRouteMedia(text, router, seenUrls);
292
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(1);
293
+ });
294
+
295
+ it('handles multiple different media types in one message', () => {
296
+ const text = 'Video: https://www.youtube.com/watch?v=aaaaaaaaaaa and image: https://example.com/pic.png';
297
+ extractAndRouteMedia(text, router, seenUrls);
298
+ expect(router.routeVisualCommand).toHaveBeenCalledTimes(2);
299
+ const types = router.routeVisualCommand.mock.calls.map(c => c[0].payload.subtype);
300
+ expect(types).toContain('youtube');
301
+ expect(types).toContain('image');
302
+ });
303
+ });
304
+
305
+ describe('extractButtons', () => {
306
+ it('returns empty buttons for null, empty, or non-string input', () => {
307
+ expect(extractButtons(null)).toEqual({ cleanText: '', buttons: [] });
308
+ expect(extractButtons('')).toEqual({ cleanText: '', buttons: [] });
309
+ expect(extractButtons(undefined)).toEqual({ cleanText: '', buttons: [] });
310
+ expect(extractButtons(42)).toEqual({ cleanText: '', buttons: [] });
311
+ });
312
+
313
+ it('returns original text when no buttons comment exists', () => {
314
+ const text = 'Just a normal message with no buttons.';
315
+ const result = extractButtons(text);
316
+ expect(result.cleanText).toBe(text);
317
+ expect(result.buttons).toEqual([]);
318
+ });
319
+
320
+ it('extracts a single button block and strips it from text', () => {
321
+ const text = 'Pick a topic:\n\n<!-- buttons: {"id":"topic-1","type":"single","prompt":"Choose:","options":[{"label":"A","value":"Option A"},{"label":"B","value":"Option B"}]} -->\n\nLet me know!';
322
+ const result = extractButtons(text);
323
+
324
+ expect(result.buttons).toHaveLength(1);
325
+ expect(result.buttons[0].id).toBe('topic-1');
326
+ expect(result.buttons[0].type).toBe('single');
327
+ expect(result.buttons[0].prompt).toBe('Choose:');
328
+ expect(result.buttons[0].options).toHaveLength(2);
329
+ expect(result.buttons[0].options[0]).toEqual({ label: 'A', value: 'Option A' });
330
+ expect(result.cleanText).not.toContain('<!-- buttons:');
331
+ expect(result.cleanText).toContain('Pick a topic:');
332
+ expect(result.cleanText).toContain('Let me know!');
333
+ });
334
+
335
+ it('extracts multiple button blocks', () => {
336
+ const text = [
337
+ 'First choice:',
338
+ '<!-- buttons: {"id":"q1","type":"single","options":[{"label":"Yes","value":"yes"},{"label":"No","value":"no"}]} -->',
339
+ 'Second choice:',
340
+ '<!-- buttons: {"id":"q2","type":"rating","prompt":"Rate it:","options":[{"label":"1","value":"1"},{"label":"2","value":"2"},{"label":"3","value":"3"}]} -->',
341
+ ].join('\n');
342
+
343
+ const result = extractButtons(text);
344
+ expect(result.buttons).toHaveLength(2);
345
+ expect(result.buttons[0].id).toBe('q1');
346
+ expect(result.buttons[0].type).toBe('single');
347
+ expect(result.buttons[1].id).toBe('q2');
348
+ expect(result.buttons[1].type).toBe('rating');
349
+ expect(result.cleanText).not.toContain('<!-- buttons:');
350
+ });
351
+
352
+ it('ignores malformed JSON in buttons comment', () => {
353
+ const text = 'Hello\n<!-- buttons: {not valid json} -->\nWorld';
354
+ const result = extractButtons(text);
355
+ expect(result.buttons).toEqual([]);
356
+ expect(result.cleanText).toContain('Hello');
357
+ });
358
+
359
+ it('ignores buttons with missing id', () => {
360
+ const text = '<!-- buttons: {"type":"single","options":[{"label":"A","value":"a"}]} -->';
361
+ const result = extractButtons(text);
362
+ expect(result.buttons).toEqual([]);
363
+ });
364
+
365
+ it('ignores buttons with invalid type', () => {
366
+ const text = '<!-- buttons: {"id":"x","type":"dropdown","options":[{"label":"A","value":"a"}]} -->';
367
+ const result = extractButtons(text);
368
+ expect(result.buttons).toEqual([]);
369
+ });
370
+
371
+ it('ignores buttons with empty options array', () => {
372
+ const text = '<!-- buttons: {"id":"x","type":"single","options":[]} -->';
373
+ const result = extractButtons(text);
374
+ expect(result.buttons).toEqual([]);
375
+ });
376
+
377
+ it('ignores buttons with no options field', () => {
378
+ const text = '<!-- buttons: {"id":"x","type":"single"} -->';
379
+ const result = extractButtons(text);
380
+ expect(result.buttons).toEqual([]);
381
+ });
382
+
383
+ it('filters out options missing label or value', () => {
384
+ const text = '<!-- buttons: {"id":"x","type":"single","options":[{"label":"Valid","value":"v"},{"label":"No value"},{"value":"no label"}]} -->';
385
+ const result = extractButtons(text);
386
+ expect(result.buttons).toHaveLength(1);
387
+ expect(result.buttons[0].options).toHaveLength(1);
388
+ expect(result.buttons[0].options[0].label).toBe('Valid');
389
+ });
390
+
391
+ it('drops button block entirely when all options are invalid', () => {
392
+ const text = '<!-- buttons: {"id":"x","type":"single","options":[{"label":"No value"},{"value":"no label"}]} -->';
393
+ const result = extractButtons(text);
394
+ expect(result.buttons).toEqual([]);
395
+ });
396
+
397
+ it('handles extra whitespace in comment delimiters', () => {
398
+ const text = '<!-- buttons: {"id":"x","type":"single","options":[{"label":"A","value":"a"}]} -->';
399
+ const result = extractButtons(text);
400
+ expect(result.buttons).toHaveLength(1);
401
+ expect(result.buttons[0].id).toBe('x');
402
+ });
403
+
404
+ it('accepts all three valid types: single, multi, rating', () => {
405
+ for (const type of ['single', 'multi', 'rating']) {
406
+ const text = `<!-- buttons: {"id":"t-${type}","type":"${type}","options":[{"label":"A","value":"a"}]} -->`;
407
+ const result = extractButtons(text);
408
+ expect(result.buttons).toHaveLength(1);
409
+ expect(result.buttons[0].type).toBe(type);
410
+ }
411
+ });
412
+
413
+ it('omits prompt field when not provided', () => {
414
+ const text = '<!-- buttons: {"id":"x","type":"single","options":[{"label":"A","value":"a"}]} -->';
415
+ const result = extractButtons(text);
416
+ expect(result.buttons[0].prompt).toBeUndefined();
417
+ });
418
+
419
+ it('preserves prompt field when provided', () => {
420
+ const text = '<!-- buttons: {"id":"x","type":"single","prompt":"Choose one:","options":[{"label":"A","value":"a"}]} -->';
421
+ const result = extractButtons(text);
422
+ expect(result.buttons[0].prompt).toBe('Choose one:');
423
+ });
424
+
425
+ it('handles buttons mixed with suggestions comment', () => {
426
+ const text = 'Hello\n<!-- buttons: {"id":"x","type":"single","options":[{"label":"A","value":"a"}]} -->\n<!-- suggestions: [{"label":"Continue","text":"/teach"}] -->';
427
+ const result = extractButtons(text);
428
+ expect(result.buttons).toHaveLength(1);
429
+ expect(result.cleanText).toContain('<!-- suggestions:');
430
+ });
431
+
432
+ it('does not strip non-button HTML comments', () => {
433
+ const text = 'Hello <!-- this is a regular comment --> world';
434
+ const result = extractButtons(text);
435
+ expect(result.buttons).toEqual([]);
436
+ expect(result.cleanText).toBe(text);
437
+ });
438
+
439
+ it('handles button block at the very start of text', () => {
440
+ const text = '<!-- buttons: {"id":"first","type":"single","options":[{"label":"Go","value":"go"}]} -->\nThen some text.';
441
+ const result = extractButtons(text);
442
+ expect(result.buttons).toHaveLength(1);
443
+ expect(result.buttons[0].id).toBe('first');
444
+ expect(result.cleanText).toContain('Then some text.');
445
+ expect(result.cleanText).not.toContain('<!-- buttons:');
446
+ });
447
+
448
+ it('handles button block at the very end of text', () => {
449
+ const text = 'Some text.\n<!-- buttons: {"id":"last","type":"rating","options":[{"label":"1","value":"1"},{"label":"2","value":"2"}]} -->';
450
+ const result = extractButtons(text);
451
+ expect(result.buttons).toHaveLength(1);
452
+ expect(result.buttons[0].id).toBe('last');
453
+ expect(result.cleanText).toBe('Some text.');
454
+ });
455
+
456
+ it('ignores non-object parsed JSON (e.g. array, string)', () => {
457
+ const text1 = '<!-- buttons: [1,2,3] -->';
458
+ expect(extractButtons(text1).buttons).toEqual([]);
459
+
460
+ const text2 = '<!-- buttons: "hello" -->';
461
+ expect(extractButtons(text2).buttons).toEqual([]);
462
+ });
463
+
464
+ it('ignores button with empty string id', () => {
465
+ const text = '<!-- buttons: {"id":"","type":"single","options":[{"label":"A","value":"a"}]} -->';
466
+ expect(extractButtons(text).buttons).toEqual([]);
467
+ });
468
+
469
+ it('preserves extra fields on options (extensible)', () => {
470
+ const text = '<!-- buttons: {"id":"x","type":"single","options":[{"label":"A","value":"a","icon":"star"}]} -->';
471
+ const result = extractButtons(text);
472
+ expect(result.buttons[0].options[0].label).toBe('A');
473
+ expect(result.buttons[0].options[0].value).toBe('a');
474
+ expect(result.buttons[0].options[0].icon).toBe('star');
475
+ });
476
+ });
477
+
478
+ describe('extractButtons — server integration simulation', () => {
479
+ it('enriches a chat:assistant envelope payload correctly', () => {
480
+ const envelope = {
481
+ type: 'chat:assistant',
482
+ payload: {
483
+ text: 'Choose a topic:\n\n<!-- buttons: {"id":"pick","type":"single","prompt":"Topic:","options":[{"label":"Git","value":"Teach git"},{"label":"React","value":"Teach react"}]} -->\n\n<!-- suggestions: [{"label":"Continue","text":"/teach"}] -->',
484
+ },
485
+ };
486
+
487
+ const { cleanText, buttons } = extractButtons(envelope.payload.text);
488
+ if (buttons.length > 0) {
489
+ envelope.payload.text = cleanText;
490
+ envelope.payload.buttons = buttons;
491
+ }
492
+
493
+ expect(envelope.payload.buttons).toHaveLength(1);
494
+ expect(envelope.payload.buttons[0].id).toBe('pick');
495
+ expect(envelope.payload.buttons[0].options).toHaveLength(2);
496
+ expect(envelope.payload.text).not.toContain('<!-- buttons:');
497
+ expect(envelope.payload.text).toContain('<!-- suggestions:');
498
+ expect(envelope.payload.text).toContain('Choose a topic:');
499
+ });
500
+
501
+ it('does not add buttons field when no buttons in text', () => {
502
+ const envelope = {
503
+ type: 'chat:assistant',
504
+ payload: { text: 'Just a normal response with no buttons.' },
505
+ };
506
+
507
+ const { cleanText, buttons } = extractButtons(envelope.payload.text);
508
+ if (buttons.length > 0) {
509
+ envelope.payload.text = cleanText;
510
+ envelope.payload.buttons = buttons;
511
+ }
512
+
513
+ expect(envelope.payload.buttons).toBeUndefined();
514
+ expect(envelope.payload.text).toBe('Just a normal response with no buttons.');
515
+ });
516
+
517
+ it('handles multiple button blocks in one assistant message', () => {
518
+ const envelope = {
519
+ type: 'chat:assistant',
520
+ payload: {
521
+ text: [
522
+ 'First, pick a topic:',
523
+ '<!-- buttons: {"id":"topic","type":"single","options":[{"label":"A","value":"a"},{"label":"B","value":"b"}]} -->',
524
+ 'Then rate the difficulty:',
525
+ '<!-- buttons: {"id":"rate","type":"rating","prompt":"Difficulty:","options":[{"label":"Easy","value":"1"},{"label":"Hard","value":"5"}]} -->',
526
+ ].join('\n'),
527
+ },
528
+ };
529
+
530
+ const { cleanText, buttons } = extractButtons(envelope.payload.text);
531
+ if (buttons.length > 0) {
532
+ envelope.payload.text = cleanText;
533
+ envelope.payload.buttons = buttons;
534
+ }
535
+
536
+ expect(envelope.payload.buttons).toHaveLength(2);
537
+ expect(envelope.payload.buttons[0].type).toBe('single');
538
+ expect(envelope.payload.buttons[1].type).toBe('rating');
539
+ expect(envelope.payload.text).toContain('First, pick a topic:');
540
+ expect(envelope.payload.text).toContain('Then rate the difficulty:');
541
+ expect(envelope.payload.text).not.toContain('<!-- buttons:');
542
+ });
543
+ });
544
+
545
+ describe('extractInlineBlocks', () => {
546
+ it('returns empty blocks for null, empty, or non-string input', () => {
547
+ expect(extractInlineBlocks(null)).toEqual({ cleanText: '', blocks: [] });
548
+ expect(extractInlineBlocks('')).toEqual({ cleanText: '', blocks: [] });
549
+ expect(extractInlineBlocks(undefined)).toEqual({ cleanText: '', blocks: [] });
550
+ expect(extractInlineBlocks(42)).toEqual({ cleanText: '', blocks: [] });
551
+ });
552
+
553
+ it('returns original text when no block comments exist', () => {
554
+ const text = 'Just a normal message.';
555
+ const result = extractInlineBlocks(text);
556
+ expect(result.cleanText).toBe(text);
557
+ expect(result.blocks).toEqual([]);
558
+ });
559
+
560
+ // --- List ---
561
+ it('extracts a list block with cards style', () => {
562
+ const text = 'Topics:\n\n<!-- list: {"id":"t1","style":"cards","items":[{"icon":"book","title":"Vars","description":"Data storage","action":"teach vars"}]} -->';
563
+ const result = extractInlineBlocks(text);
564
+ expect(result.blocks).toHaveLength(1);
565
+ expect(result.blocks[0].blockType).toBe('list');
566
+ expect(result.blocks[0].id).toBe('t1');
567
+ expect(result.blocks[0].style).toBe('cards');
568
+ expect(result.blocks[0].items).toHaveLength(1);
569
+ expect(result.blocks[0].items[0].title).toBe('Vars');
570
+ expect(result.blocks[0].items[0].action).toBe('teach vars');
571
+ expect(result.cleanText).not.toContain('<!-- list:');
572
+ });
573
+
574
+ it('defaults list style to cards for unknown style', () => {
575
+ const text = '<!-- list: {"id":"x","style":"fancy","items":[{"title":"A"}]} -->';
576
+ const result = extractInlineBlocks(text);
577
+ expect(result.blocks[0].style).toBe('cards');
578
+ });
579
+
580
+ it('rejects list with empty items array', () => {
581
+ const text = '<!-- list: {"id":"x","style":"cards","items":[]} -->';
582
+ const result = extractInlineBlocks(text);
583
+ expect(result.blocks).toEqual([]);
584
+ });
585
+
586
+ it('rejects list with items missing title', () => {
587
+ const text = '<!-- list: {"id":"x","items":[{"description":"no title"}]} -->';
588
+ const result = extractInlineBlocks(text);
589
+ expect(result.blocks).toEqual([]);
590
+ });
591
+
592
+ it('accepts all valid list styles', () => {
593
+ for (const style of ['cards', 'numbered', 'checklist', 'compact']) {
594
+ const text = `<!-- list: {"id":"s-${style}","style":"${style}","items":[{"title":"Item"}]} -->`;
595
+ const result = extractInlineBlocks(text);
596
+ expect(result.blocks).toHaveLength(1);
597
+ expect(result.blocks[0].style).toBe(style);
598
+ }
599
+ });
600
+
601
+ // --- Progress ---
602
+ it('extracts a progress block with bar style', () => {
603
+ const text = 'Progress:\n\n<!-- progress: {"id":"p1","label":"Steps","current":3,"total":7,"style":"bar"} -->';
604
+ const result = extractInlineBlocks(text);
605
+ expect(result.blocks).toHaveLength(1);
606
+ expect(result.blocks[0].blockType).toBe('progress');
607
+ expect(result.blocks[0].current).toBe(3);
608
+ expect(result.blocks[0].total).toBe(7);
609
+ expect(result.blocks[0].label).toBe('Steps');
610
+ expect(result.cleanText).not.toContain('<!-- progress:');
611
+ });
612
+
613
+ it('defaults progress style to bar for unknown style', () => {
614
+ const text = '<!-- progress: {"id":"x","current":1,"total":5,"style":"fancy"} -->';
615
+ const result = extractInlineBlocks(text);
616
+ expect(result.blocks[0].style).toBe('bar');
617
+ });
618
+
619
+ it('rejects progress with missing current or total', () => {
620
+ expect(extractInlineBlocks('<!-- progress: {"id":"x","current":1} -->').blocks).toEqual([]);
621
+ expect(extractInlineBlocks('<!-- progress: {"id":"x","total":5} -->').blocks).toEqual([]);
622
+ });
623
+
624
+ it('rejects progress with total <= 0', () => {
625
+ const text = '<!-- progress: {"id":"x","current":0,"total":0} -->';
626
+ const result = extractInlineBlocks(text);
627
+ expect(result.blocks).toEqual([]);
628
+ });
629
+
630
+ it('accepts all valid progress styles', () => {
631
+ for (const style of ['bar', 'steps', 'ring']) {
632
+ const text = `<!-- progress: {"id":"ps-${style}","current":1,"total":5,"style":"${style}"} -->`;
633
+ const result = extractInlineBlocks(text);
634
+ expect(result.blocks[0].style).toBe(style);
635
+ }
636
+ });
637
+
638
+ // --- Card ---
639
+ it('extracts a card block with tip type', () => {
640
+ const text = 'Tip:\n\n<!-- card: {"id":"c1","type":"tip","title":"Pro Tip","content":"Use `const` by default."} -->';
641
+ const result = extractInlineBlocks(text);
642
+ expect(result.blocks).toHaveLength(1);
643
+ expect(result.blocks[0].blockType).toBe('card');
644
+ expect(result.blocks[0].type).toBe('tip');
645
+ expect(result.blocks[0].title).toBe('Pro Tip');
646
+ expect(result.blocks[0].content).toBe('Use `const` by default.');
647
+ expect(result.cleanText).not.toContain('<!-- card:');
648
+ });
649
+
650
+ it('defaults card type to tip for unknown type', () => {
651
+ const text = '<!-- card: {"id":"x","type":"fancy","content":"text"} -->';
652
+ const result = extractInlineBlocks(text);
653
+ expect(result.blocks[0].type).toBe('tip');
654
+ });
655
+
656
+ it('rejects card with empty content', () => {
657
+ const text = '<!-- card: {"id":"x","type":"tip","content":""} -->';
658
+ const result = extractInlineBlocks(text);
659
+ expect(result.blocks).toEqual([]);
660
+ });
661
+
662
+ it('rejects card with missing content', () => {
663
+ const text = '<!-- card: {"id":"x","type":"tip","title":"No content"} -->';
664
+ const result = extractInlineBlocks(text);
665
+ expect(result.blocks).toEqual([]);
666
+ });
667
+
668
+ it('accepts all valid card types', () => {
669
+ for (const type of ['tip', 'warning', 'error', 'success', 'concept']) {
670
+ const text = `<!-- card: {"id":"ct-${type}","type":"${type}","content":"text"} -->`;
671
+ const result = extractInlineBlocks(text);
672
+ expect(result.blocks[0].type).toBe(type);
673
+ }
674
+ });
675
+
676
+ // --- Code ---
677
+ it('extracts a code block', () => {
678
+ const text = 'Example:\n\n<!-- code: {"id":"e1","language":"javascript","filename":"app.js","code":"const x = 1;","highlight":[1]} -->';
679
+ const result = extractInlineBlocks(text);
680
+ expect(result.blocks).toHaveLength(1);
681
+ expect(result.blocks[0].blockType).toBe('code');
682
+ expect(result.blocks[0].language).toBe('javascript');
683
+ expect(result.blocks[0].filename).toBe('app.js');
684
+ expect(result.blocks[0].code).toBe('const x = 1;');
685
+ expect(result.blocks[0].highlight).toEqual([1]);
686
+ expect(result.cleanText).not.toContain('<!-- code:');
687
+ });
688
+
689
+ it('defaults code language to text when missing', () => {
690
+ const text = '<!-- code: {"id":"x","code":"hello"} -->';
691
+ const result = extractInlineBlocks(text);
692
+ expect(result.blocks[0].language).toBe('text');
693
+ });
694
+
695
+ it('rejects code with empty code string', () => {
696
+ const text = '<!-- code: {"id":"x","code":""} -->';
697
+ const result = extractInlineBlocks(text);
698
+ expect(result.blocks).toEqual([]);
699
+ });
700
+
701
+ it('filters non-number highlight values', () => {
702
+ const text = '<!-- code: {"id":"x","code":"a","highlight":[1,"two",3]} -->';
703
+ const result = extractInlineBlocks(text);
704
+ expect(result.blocks[0].highlight).toEqual([1, 3]);
705
+ });
706
+
707
+ // --- Steps ---
708
+ it('extracts a steps block', () => {
709
+ const text = 'Steps:\n\n<!-- steps: {"id":"s1","current":2,"steps":[{"label":"Create","status":"done"},{"label":"Write","status":"active"},{"label":"Test","status":"pending"}]} -->';
710
+ const result = extractInlineBlocks(text);
711
+ expect(result.blocks).toHaveLength(1);
712
+ expect(result.blocks[0].blockType).toBe('steps');
713
+ expect(result.blocks[0].steps).toHaveLength(3);
714
+ expect(result.blocks[0].steps[0].status).toBe('done');
715
+ expect(result.blocks[0].steps[1].status).toBe('active');
716
+ expect(result.blocks[0].steps[2].status).toBe('pending');
717
+ expect(result.cleanText).not.toContain('<!-- steps:');
718
+ });
719
+
720
+ it('defaults step status to pending for unknown status', () => {
721
+ const text = '<!-- steps: {"id":"x","steps":[{"label":"A","status":"unknown"}]} -->';
722
+ const result = extractInlineBlocks(text);
723
+ expect(result.blocks[0].steps[0].status).toBe('pending');
724
+ });
725
+
726
+ it('rejects steps with empty steps array', () => {
727
+ const text = '<!-- steps: {"id":"x","steps":[]} -->';
728
+ const result = extractInlineBlocks(text);
729
+ expect(result.blocks).toEqual([]);
730
+ });
731
+
732
+ it('rejects steps with items missing label', () => {
733
+ const text = '<!-- steps: {"id":"x","steps":[{"status":"done"}]} -->';
734
+ const result = extractInlineBlocks(text);
735
+ expect(result.blocks).toEqual([]);
736
+ });
737
+
738
+ // --- Mixed ---
739
+ it('extracts multiple different block types from one message', () => {
740
+ const text = [
741
+ 'Here is your progress:',
742
+ '<!-- progress: {"id":"p","current":2,"total":5,"style":"bar"} -->',
743
+ 'And a tip:',
744
+ '<!-- card: {"id":"c","type":"tip","content":"Remember to save!"} -->',
745
+ 'Next steps:',
746
+ '<!-- steps: {"id":"s","steps":[{"label":"Save","status":"done"},{"label":"Push","status":"active"}]} -->',
747
+ ].join('\n');
748
+
749
+ const result = extractInlineBlocks(text);
750
+ expect(result.blocks).toHaveLength(3);
751
+ expect(result.blocks.map(b => b.blockType)).toEqual(['progress', 'card', 'steps']);
752
+ expect(result.cleanText).toContain('Here is your progress:');
753
+ expect(result.cleanText).not.toContain('<!-- progress:');
754
+ expect(result.cleanText).not.toContain('<!-- card:');
755
+ expect(result.cleanText).not.toContain('<!-- steps:');
756
+ });
757
+
758
+ it('ignores malformed JSON in block comments', () => {
759
+ const text = 'Hello\n<!-- list: {not valid json} -->\nWorld';
760
+ const result = extractInlineBlocks(text);
761
+ expect(result.blocks).toEqual([]);
762
+ expect(result.cleanText).toContain('Hello');
763
+ });
764
+
765
+ it('ignores blocks with missing id', () => {
766
+ const text = '<!-- card: {"type":"tip","content":"no id"} -->';
767
+ const result = extractInlineBlocks(text);
768
+ expect(result.blocks).toEqual([]);
769
+ });
770
+
771
+ it('ignores blocks with empty string id', () => {
772
+ const text = '<!-- card: {"id":"","type":"tip","content":"empty id"} -->';
773
+ const result = extractInlineBlocks(text);
774
+ expect(result.blocks).toEqual([]);
775
+ });
776
+
777
+ it('does not interfere with button or suggestion comments', () => {
778
+ const text = [
779
+ '<!-- buttons: {"id":"b","type":"single","options":[{"label":"A","value":"a"}]} -->',
780
+ '<!-- suggestions: [{"label":"Go","text":"/go"}] -->',
781
+ '<!-- card: {"id":"c","type":"tip","content":"A tip"} -->',
782
+ ].join('\n');
783
+ const result = extractInlineBlocks(text);
784
+ expect(result.blocks).toHaveLength(1);
785
+ expect(result.blocks[0].blockType).toBe('card');
786
+ expect(result.cleanText).toContain('<!-- buttons:');
787
+ expect(result.cleanText).toContain('<!-- suggestions:');
788
+ });
789
+
790
+ it('handles extra whitespace in comment delimiters', () => {
791
+ const text = '<!-- card: {"id":"x","type":"tip","content":"spaced"} -->';
792
+ const result = extractInlineBlocks(text);
793
+ expect(result.blocks).toHaveLength(1);
794
+ expect(result.blocks[0].content).toBe('spaced');
795
+ });
796
+ });
797
+
798
+ describe('extractInlineBlocks — server integration simulation', () => {
799
+ it('enriches a chat:assistant envelope with blocks', () => {
800
+ const envelope = {
801
+ type: 'chat:assistant',
802
+ payload: {
803
+ text: 'Here is a tip:\n\n<!-- card: {"id":"tip1","type":"tip","title":"Hint","content":"Try using map()."} -->\n\nAnd your progress:\n\n<!-- progress: {"id":"prog1","current":3,"total":5,"style":"bar"} -->',
804
+ },
805
+ };
806
+
807
+ const { cleanText: text1, buttons } = extractButtons(envelope.payload.text);
808
+ if (buttons.length > 0) {
809
+ envelope.payload.text = text1;
810
+ envelope.payload.buttons = buttons;
811
+ }
812
+ const { cleanText: text2, blocks } = extractInlineBlocks(envelope.payload.text);
813
+ if (blocks.length > 0) {
814
+ envelope.payload.text = text2;
815
+ envelope.payload.blocks = blocks;
816
+ }
817
+
818
+ expect(envelope.payload.blocks).toHaveLength(2);
819
+ const blockTypes = envelope.payload.blocks.map(b => b.blockType).sort();
820
+ expect(blockTypes).toEqual(['card', 'progress']);
821
+ expect(envelope.payload.text).not.toContain('<!-- card:');
822
+ expect(envelope.payload.text).not.toContain('<!-- progress:');
823
+ expect(envelope.payload.text).toContain('Here is a tip:');
824
+ expect(envelope.payload.buttons).toBeUndefined();
825
+ });
826
+
827
+ it('handles buttons and blocks coexisting', () => {
828
+ const envelope = {
829
+ type: 'chat:assistant',
830
+ payload: {
831
+ text: [
832
+ 'Choose:',
833
+ '<!-- buttons: {"id":"b1","type":"single","options":[{"label":"A","value":"a"}]} -->',
834
+ 'Tip:',
835
+ '<!-- card: {"id":"c1","type":"warning","content":"Watch out!"} -->',
836
+ ].join('\n'),
837
+ },
838
+ };
839
+
840
+ const { cleanText: text1, buttons } = extractButtons(envelope.payload.text);
841
+ if (buttons.length > 0) {
842
+ envelope.payload.text = text1;
843
+ envelope.payload.buttons = buttons;
844
+ }
845
+ const { cleanText: text2, blocks } = extractInlineBlocks(envelope.payload.text);
846
+ if (blocks.length > 0) {
847
+ envelope.payload.text = text2;
848
+ envelope.payload.blocks = blocks;
849
+ }
850
+
851
+ expect(envelope.payload.buttons).toHaveLength(1);
852
+ expect(envelope.payload.blocks).toHaveLength(1);
853
+ expect(envelope.payload.buttons[0].id).toBe('b1');
854
+ expect(envelope.payload.blocks[0].blockType).toBe('card');
855
+ expect(envelope.payload.text).not.toContain('<!-- buttons:');
856
+ expect(envelope.payload.text).not.toContain('<!-- card:');
857
+ });
858
+ });
859
+
860
+ describe('extractAndRouteVisuals — canvas command comments', () => {
861
+ it('routes <!-- canvas:quiz: {...} --> as a canvas:quiz visual command', () => {
862
+ const routed = [];
863
+ const router = { routeVisualCommand: (e) => routed.push(e) };
864
+ const tierManager = { broadcastWs: vi.fn(), broadcastSse: vi.fn() };
865
+ const text = 'Here is a quiz:\n<!-- canvas:quiz: {"quizType":"timed-choice","question":"What?","options":["A","B"],"correctIndex":0,"timeLimit":30} -->';
866
+ extractAndRouteVisuals(text, tierManager, router);
867
+ const quizEnvelope = routed.find(e => e.type === 'canvas:quiz');
868
+ expect(quizEnvelope).toBeDefined();
869
+ expect(quizEnvelope.payload.quizType).toBe('timed-choice');
870
+ expect(quizEnvelope.payload.question).toBe('What?');
871
+ expect(quizEnvelope.payload.autoRouted).toBe(true);
872
+ });
873
+
874
+ it('routes <!-- canvas:celebrate: {...} --> as a canvas:celebrate command', () => {
875
+ const routed = [];
876
+ const router = { routeVisualCommand: (e) => routed.push(e) };
877
+ const tierManager = { broadcastWs: vi.fn(), broadcastSse: vi.fn() };
878
+ const text = 'Great job!\n<!-- canvas:celebrate: {"type":"xp","xpAwarded":25} -->';
879
+ extractAndRouteVisuals(text, tierManager, router);
880
+ const cel = routed.find(e => e.type === 'canvas:celebrate');
881
+ expect(cel).toBeDefined();
882
+ expect(cel.payload.xpAwarded).toBe(25);
883
+ });
884
+
885
+ it('ignores invalid canvas types', () => {
886
+ const routed = [];
887
+ const router = { routeVisualCommand: (e) => routed.push(e) };
888
+ const tierManager = { broadcastWs: vi.fn(), broadcastSse: vi.fn() };
889
+ const text = '<!-- canvas:evil: {"payload":"hack"} -->';
890
+ extractAndRouteVisuals(text, tierManager, router);
891
+ expect(routed).toHaveLength(0);
892
+ });
893
+
894
+ it('skips malformed JSON in canvas commands', () => {
895
+ const routed = [];
896
+ const router = { routeVisualCommand: (e) => routed.push(e) };
897
+ const tierManager = { broadcastWs: vi.fn(), broadcastSse: vi.fn() };
898
+ const text = '<!-- canvas:quiz: {not valid json} -->';
899
+ extractAndRouteVisuals(text, tierManager, router);
900
+ expect(routed).toHaveLength(0);
901
+ });
902
+
903
+ it('routes multiple canvas commands from one message', () => {
904
+ const routed = [];
905
+ const router = { routeVisualCommand: (e) => routed.push(e) };
906
+ const tierManager = { broadcastWs: vi.fn(), broadcastSse: vi.fn() };
907
+ const text = [
908
+ '<!-- canvas:quiz: {"quizType":"timed-choice","question":"Q?","options":["A"],"correctIndex":0,"timeLimit":10} -->',
909
+ 'Some text',
910
+ '<!-- canvas:celebrate: {"type":"xp","xpAwarded":10} -->',
911
+ ].join('\n');
912
+ extractAndRouteVisuals(text, tierManager, router);
913
+ expect(routed.filter(e => e.type === 'canvas:quiz')).toHaveLength(1);
914
+ expect(routed.filter(e => e.type === 'canvas:celebrate')).toHaveLength(1);
915
+ });
916
+ });
917
+
918
+ describe('stripCanvasCommands', () => {
919
+ it('removes canvas command comments from text', () => {
920
+ const text = 'Hello\n<!-- canvas:quiz: {"quizType":"timed-choice","question":"Q?"} -->\nWorld';
921
+ const result = stripCanvasCommands(text);
922
+ expect(result).not.toContain('canvas:quiz');
923
+ expect(result).toContain('Hello');
924
+ expect(result).toContain('World');
925
+ });
926
+
927
+ it('collapses triple newlines after stripping', () => {
928
+ const text = 'Hello\n\n<!-- canvas:celebrate: {"type":"xp","xpAwarded":10} -->\n\nWorld';
929
+ const result = stripCanvasCommands(text);
930
+ expect(result).not.toMatch(/\n{3,}/);
931
+ });
932
+
933
+ it('returns original text when no canvas commands present', () => {
934
+ const text = 'Just plain text with <!-- buttons: {} --> inline';
935
+ expect(stripCanvasCommands(text)).toBe(text);
936
+ });
937
+
938
+ it('handles null/undefined gracefully', () => {
939
+ expect(stripCanvasCommands(null)).toBeNull();
940
+ expect(stripCanvasCommands(undefined)).toBeUndefined();
941
+ expect(stripCanvasCommands('')).toBe('');
942
+ });
943
+ });