@shaykec/bridge 0.4.19 → 0.4.21

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 (319) hide show
  1. package/README.md +2 -2
  2. package/canvas-dist/assets/{_basePickBy-BOTBlJNd.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-EF6Y2_Wm.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-C_vIirh2.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-EvM6tQ7I.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-B_rbZyqc.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-J9PHecY3.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-DjcN96Mk.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-CTdcUQSV.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-Dcov7eRi.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-DUJCBZzM.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-EfGA9ufe.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-Cu6V1xBU.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-avF3sH_r.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-CkWW-qpk.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-DDE4zf7X.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-BD6MGb7B.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-BGi_qzbq.js +24 -0
  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-yyu-ytzf.js → diagram-PSM6KHXK-kVMBkEyV.js} +1 -1
  128. package/canvas-dist/assets/{diagram-QEK2KX5R-B_H957Uf.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-DuebuBVv.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-AxqPt6IZ.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-mDhW3D3Q.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-sA8pHJPp.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-CvLzvhKr.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-BVZqMrwW.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-CF3qc2Xb.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-D1Kg3Q9d.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-D7ogbx9z.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-CDcnICM9.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-CuaK7i3M.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-CLSTOJ0g.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-TrK7CIKt.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-BcIKTRbi.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-EOHXFGoQ.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-CJ8lImGs.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-4cANY87E.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-D9HrEsci.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-qVbMjauZ.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-DDBlkydm.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-D4a8udjO.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-DteXAAAu.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 +4 -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 +812 -177
  289. package/src/server.js +1516 -264
  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-saCUO1KA.js +0 -1
  313. package/canvas-dist/assets/classDiagram-2ON5EDUG-CBLbQwHx.js +0 -1
  314. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CBLbQwHx.js +0 -1
  315. package/canvas-dist/assets/clone-DXnda9BY.js +0 -1
  316. package/canvas-dist/assets/index-DYNtb52W.js +0 -426
  317. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-MT16RLO4.js +0 -1
  318. package/src/claude-session.js +0 -414
  319. package/src/claude-session.test.js +0 -326
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Bridge server E2E tests.
3
3
  * Starts the server programmatically and exercises all major endpoints,
4
- * WebSocket handshake, SSE, and the visual pipeline round-trip.
4
+ * WebSocket handshake, SSE, and the unified visual pipeline.
5
5
  */
6
6
 
7
7
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
8
- import { startServer } from './server.js';
8
+ import { startServer, enrichModulesWithMetadata, parseProgressYaml, serializeProgressYaml, createFileProgressProvider } from './server.js';
9
9
  import WebSocket from 'ws';
10
+ import { writeFileSync, unlinkSync, existsSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { tmpdir } from 'os';
10
13
  import {
11
14
  createEnvelope,
12
15
  PROTOCOL_VERSION,
@@ -16,22 +19,32 @@ import {
16
19
  MSG_CANVAS_GAME,
17
20
  MSG_CANVAS_HTML,
18
21
  MSG_CANVAS_DASHBOARD,
22
+ MSG_CANVAS_SLIDES,
19
23
  MSG_EVENT_CLICK,
20
24
  MSG_EVENT_QUIZ_ANSWER,
21
25
  MSG_EVENT_GAME_RESULT,
26
+ MSG_EVENT_SLIDE_CHANGE,
22
27
  MSG_CHAT_SEND,
23
- MSG_CHAT_STATUS,
24
28
  } from '@shaykec/shared';
25
29
 
26
30
  // --- Shared state ---
27
31
 
28
32
  let bridge, port, BASE_URL;
29
33
 
34
+ function createInMemoryProgressProvider() {
35
+ let data = { user: { xp: 0 }, modules: {}, journey: { active: null, progress: {} } };
36
+ return {
37
+ getProgress: () => data,
38
+ saveProgress: (updated) => { data = updated; },
39
+ };
40
+ }
41
+
30
42
  beforeAll(async () => {
31
43
  port = 20000 + Math.floor(Math.random() * 10000);
32
44
  await new Promise((resolve) => {
33
45
  bridge = startServer({
34
46
  port,
47
+ progressProvider: createInMemoryProgressProvider(),
35
48
  onReady: () => resolve(),
36
49
  });
37
50
  });
@@ -77,6 +90,10 @@ function makeEnvelope(type, payload = {}, source = 'plugin') {
77
90
  return createEnvelope(type, payload, source);
78
91
  }
79
92
 
93
+ function makeEnvelopeWithAwait(type, payload, source, awaitSpec) {
94
+ return createEnvelope(type, payload, source, { await: awaitSpec });
95
+ }
96
+
80
97
  async function connectWs(clientType = 'canvas') {
81
98
  return new Promise((resolve, reject) => {
82
99
  const ws = new WebSocket(`ws://localhost:${port}/ws`);
@@ -205,9 +222,6 @@ describe('Bridge E2E: Static file serving', () => {
205
222
  });
206
223
 
207
224
  it('path traversal does not leak files outside canvas-dist', async () => {
208
- // Node's HTTP parser normalises /../.. but even if a traversal path gets through,
209
- // the server's startsWith guard in serveStatic prevents leaking files.
210
- // We verify that requesting a path like /etc/passwd never returns passwd content.
211
225
  const { get } = await import('http');
212
226
  const body = await new Promise((resolve, reject) => {
213
227
  const req = get({ hostname: 'localhost', port, path: '/../../etc/passwd' }, (res) => {
@@ -217,7 +231,6 @@ describe('Bridge E2E: Static file serving', () => {
217
231
  });
218
232
  req.on('error', reject);
219
233
  });
220
- // Must not contain actual /etc/passwd content
221
234
  expect(body).not.toContain('root:');
222
235
  });
223
236
  });
@@ -254,73 +267,122 @@ describe('Bridge E2E: POST /api/event', () => {
254
267
  });
255
268
 
256
269
  // =====================================================================
257
- // Group D — POST /api/visual
270
+ // Group D — Unified POST /api/canvas
258
271
  // =====================================================================
259
272
 
260
- describe('Bridge E2E: POST /api/visual', () => {
261
- it('accepts a valid canvas:diagram envelope', async () => {
273
+ describe('Bridge E2E: Unified POST /api/canvas', () => {
274
+ beforeEach(() => drainEvents());
275
+
276
+ it('accepts a fire-and-forget canvas:diagram (no await)', async () => {
262
277
  const envelope = makeEnvelope(MSG_CANVAS_DIAGRAM, {
263
- type: 'flow',
264
- title: 'Test',
265
- nodes: [],
266
- edges: [],
278
+ format: 'mermaid',
279
+ content: 'graph TD; A-->B',
267
280
  });
268
- const { status, data } = await postJson('/api/visual', envelope);
281
+ const { status, data } = await postJson('/api/canvas', envelope);
269
282
  expect(status).toBe(200);
270
283
  expect(data.ok).toBe(true);
284
+ expect(data).toHaveProperty('tier');
271
285
  });
272
286
 
273
- it('rejects canvas:quiz with redirect message', async () => {
274
- const envelope = makeEnvelope(MSG_CANVAS_QUIZ, { question: 'test?' });
275
- const { status, data } = await postJson('/api/visual', envelope);
276
- expect(status).toBe(400);
277
- expect(data.error).toContain('/api/quiz');
287
+ it('accepts canvas:dashboard (no await)', async () => {
288
+ const envelope = makeEnvelope(MSG_CANVAS_DASHBOARD, { belt: 'white', xp: 0 });
289
+ const { status, data } = await postJson('/api/canvas', envelope);
290
+ expect(status).toBe(200);
291
+ expect(data.ok).toBe(true);
278
292
  });
279
293
 
280
- it('rejects canvas:game with redirect message', async () => {
281
- const envelope = makeEnvelope(MSG_CANVAS_GAME, { gameType: 'speed-round' });
282
- const { status, data } = await postJson('/api/visual', envelope);
283
- expect(status).toBe(400);
284
- expect(data.error).toContain('/api/game');
294
+ it('accepts canvas:html (no await)', async () => {
295
+ const envelope = makeEnvelope(MSG_CANVAS_HTML, { html: '<h1>Test</h1>' });
296
+ const { status, data } = await postJson('/api/canvas', envelope);
297
+ expect(status).toBe(200);
298
+ expect(data.ok).toBe(true);
285
299
  });
286
300
 
287
- it('rejects invalid envelope', async () => {
288
- const { status } = await postJson('/api/visual', { bad: true });
289
- expect(status).toBe(400);
301
+ it('accepts canvas:slides (no await)', async () => {
302
+ const envelope = makeEnvelope(MSG_CANVAS_SLIDES, {
303
+ title: 'Git Branching',
304
+ module: 'git',
305
+ slides: [
306
+ { title: 'Slide 1', layout: 'center', blocks: [{ type: 'markdown', content: '## Hello' }] },
307
+ { title: 'Slide 2', layout: 'split', blocks: [{ type: 'code', content: 'git branch', language: 'bash' }] },
308
+ ],
309
+ });
310
+ const { status, data } = await postJson('/api/canvas', envelope);
311
+ expect(status).toBe(200);
312
+ expect(data.ok).toBe(true);
313
+ expect(data).toHaveProperty('tier');
290
314
  });
291
- });
292
-
293
- // =====================================================================
294
- // Group E — Quiz & Game Atomic Endpoints
295
- // =====================================================================
296
315
 
297
- describe('Bridge E2E: Atomic quiz & game endpoints', () => {
298
- beforeEach(() => drainEvents());
316
+ it('broadcasts canvas:slides to WebSocket clients', async () => {
317
+ const { ws } = await connectWs('canvas');
318
+ const messages = [];
319
+ ws.on('message', (raw) => {
320
+ try { messages.push(JSON.parse(raw.toString())); } catch {}
321
+ });
322
+ await new Promise(r => setTimeout(r, 200));
299
323
 
300
- it('POST /api/quiz?timeout=1 times out with empty events', async () => {
301
- const envelope = makeEnvelope(MSG_CANVAS_QUIZ, {
302
- question: 'What is 2+2?',
303
- options: ['3', '4'],
304
- answer: 1,
324
+ const envelope = makeEnvelope(MSG_CANVAS_SLIDES, {
325
+ title: 'Test Deck',
326
+ slides: [{ title: 'Only Slide', blocks: [{ type: 'markdown', content: 'Content' }] }],
305
327
  });
306
- const { status, data } = await postJson('/api/quiz?timeout=1', envelope);
328
+ await postJson('/api/canvas', envelope);
329
+ await new Promise(r => setTimeout(r, 300));
330
+
331
+ const slideMsg = messages.find(m => m.type === 'canvas:slides');
332
+ expect(slideMsg).toBeDefined();
333
+ expect(slideMsg.payload.title).toBe('Test Deck');
334
+ expect(slideMsg.payload.slides).toHaveLength(1);
335
+ ws.close();
336
+ });
337
+
338
+ it('routes event:slide-change through the event endpoint', async () => {
339
+ const envelope = makeEnvelope(MSG_EVENT_SLIDE_CHANGE, {
340
+ slide: 2,
341
+ total: 5,
342
+ title: 'Slide Three',
343
+ }, 'canvas');
344
+ const { status, data } = await postJson('/api/event', envelope);
345
+ expect(status).toBe(200);
346
+ expect(data.ok).toBe(true);
347
+ });
348
+
349
+ it('accepts canvas:quiz with await and times out with empty events', async () => {
350
+ const envelope = makeEnvelopeWithAwait(
351
+ MSG_CANVAS_QUIZ,
352
+ { question: 'What is 2+2?', options: ['3', '4'] },
353
+ 'plugin',
354
+ { event: 'event:quiz-answer', timeout: 1 },
355
+ );
356
+ const { status, data } = await postJson('/api/canvas', envelope);
307
357
  expect(status).toBe(200);
308
358
  expect(data.ok).toBe(true);
309
359
  expect(data.events).toEqual([]);
310
360
  expect(data.count).toBe(0);
311
361
  });
312
362
 
313
- it('POST /api/quiz resolves with answer when sent during wait', async () => {
314
- const quizEnvelope = makeEnvelope(MSG_CANVAS_QUIZ, {
315
- question: 'Capital of France?',
316
- options: ['Berlin', 'Paris'],
317
- answer: 1,
318
- });
363
+ it('accepts canvas:game with await and times out with empty events', async () => {
364
+ const envelope = makeEnvelopeWithAwait(
365
+ MSG_CANVAS_GAME,
366
+ { gameType: 'speed-round', title: 'Test Game', rounds: [] },
367
+ 'plugin',
368
+ { event: 'event:game-result', timeout: 1 },
369
+ );
370
+ const { status, data } = await postJson('/api/canvas', envelope);
371
+ expect(status).toBe(200);
372
+ expect(data.ok).toBe(true);
373
+ expect(data.events).toEqual([]);
374
+ });
375
+
376
+ it('canvas:quiz with await resolves with answer when sent during wait', async () => {
377
+ const envelope = makeEnvelopeWithAwait(
378
+ MSG_CANVAS_QUIZ,
379
+ { question: 'Capital of France?', options: ['Berlin', 'Paris'] },
380
+ 'plugin',
381
+ { event: 'event:quiz-answer', timeout: 5 },
382
+ );
319
383
 
320
- // Start quiz (long timeout) and concurrently send answer
321
- const quizPromise = postJson('/api/quiz?timeout=5', quizEnvelope);
384
+ const quizPromise = postJson('/api/canvas', envelope);
322
385
 
323
- // Send answer after a short delay
324
386
  await new Promise(r => setTimeout(r, 300));
325
387
  const answerEnvelope = makeEnvelope(MSG_EVENT_QUIZ_ANSWER, {
326
388
  answer: 1,
@@ -335,32 +397,19 @@ describe('Bridge E2E: Atomic quiz & game endpoints', () => {
335
397
  expect(answer).toBeDefined();
336
398
  });
337
399
 
338
- it('POST /api/game?timeout=1 times out with empty events', async () => {
339
- const envelope = makeEnvelope(MSG_CANVAS_GAME, {
340
- gameType: 'speed-round',
341
- title: 'Test Game',
342
- rounds: [{ question: '2+2?', options: ['3', '4'], answer: 1, timeLimit: 10 }],
343
- });
344
- const { status, data } = await postJson('/api/game?timeout=1', envelope);
345
- expect(status).toBe(200);
346
- expect(data.ok).toBe(true);
347
- expect(data.events).toEqual([]);
348
- });
349
-
350
- it('POST /api/game resolves with result when sent during wait', async () => {
351
- const gameEnvelope = makeEnvelope(MSG_CANVAS_GAME, {
352
- gameType: 'speed-round',
353
- title: 'Test',
354
- rounds: [{ question: '1+1?', options: ['2', '3'], answer: 0, timeLimit: 10 }],
355
- });
400
+ it('canvas:game with await resolves with result when sent during wait', async () => {
401
+ const envelope = makeEnvelopeWithAwait(
402
+ MSG_CANVAS_GAME,
403
+ { gameType: 'speed-round', title: 'Test', rounds: [] },
404
+ 'plugin',
405
+ { event: 'event:game-result', timeout: 5 },
406
+ );
356
407
 
357
- const gamePromise = postJson('/api/game?timeout=5', gameEnvelope);
408
+ const gamePromise = postJson('/api/canvas', envelope);
358
409
 
359
410
  await new Promise(r => setTimeout(r, 300));
360
411
  const resultEnvelope = makeEnvelope(MSG_EVENT_GAME_RESULT, {
361
- score: 300,
362
- accuracy: 1.0,
363
- stars: 3,
412
+ score: 300, accuracy: 1.0, stars: 3,
364
413
  }, 'canvas');
365
414
  await postJson('/api/event', resultEnvelope);
366
415
 
@@ -370,6 +419,46 @@ describe('Bridge E2E: Atomic quiz & game endpoints', () => {
370
419
  const result = data.events.find(e => e.type === MSG_EVENT_GAME_RESULT);
371
420
  expect(result).toBeDefined();
372
421
  });
422
+
423
+ it('rejects invalid envelope', async () => {
424
+ const { status } = await postJson('/api/canvas', { bad: true });
425
+ expect(status).toBe(400);
426
+ });
427
+
428
+ it('canvas:quiz without await is fire-and-forget (no waiting)', async () => {
429
+ const envelope = makeEnvelope(MSG_CANVAS_QUIZ, { question: 'Quick?' });
430
+ const start = Date.now();
431
+ const { status, data } = await postJson('/api/canvas', envelope);
432
+ const elapsed = Date.now() - start;
433
+ expect(status).toBe(200);
434
+ expect(data.ok).toBe(true);
435
+ expect(data.events).toBeUndefined();
436
+ expect(elapsed).toBeLessThan(2000);
437
+ });
438
+ });
439
+
440
+ // =====================================================================
441
+ // Group E — Old Endpoints Removed
442
+ // =====================================================================
443
+
444
+ describe('Bridge E2E: Old endpoints removed', () => {
445
+ it('POST /api/visual returns 404', async () => {
446
+ const envelope = makeEnvelope(MSG_CANVAS_DIAGRAM, { content: 'graph TD' });
447
+ const { status } = await postJson('/api/visual', envelope);
448
+ expect(status).toBe(404);
449
+ });
450
+
451
+ it('POST /api/quiz returns 404', async () => {
452
+ const envelope = makeEnvelope(MSG_CANVAS_QUIZ, { question: 'test?' });
453
+ const { status } = await postJson('/api/quiz?timeout=1', envelope);
454
+ expect(status).toBe(404);
455
+ });
456
+
457
+ it('POST /api/game returns 404', async () => {
458
+ const envelope = makeEnvelope(MSG_CANVAS_GAME, { gameType: 'speed-round' });
459
+ const { status } = await postJson('/api/game?timeout=1', envelope);
460
+ expect(status).toBe(404);
461
+ });
373
462
  });
374
463
 
375
464
  // =====================================================================
@@ -387,7 +476,6 @@ describe('Bridge E2E: Long-poll /api/events/wait', () => {
387
476
  });
388
477
 
389
478
  it('resolves immediately when events are already queued', async () => {
390
- // Queue an event first
391
479
  await postJson('/api/event', makeEnvelope(MSG_EVENT_CLICK, { target: 'x' }, 'canvas'));
392
480
 
393
481
  const start = Date.now();
@@ -395,7 +483,7 @@ describe('Bridge E2E: Long-poll /api/events/wait', () => {
395
483
  const elapsed = Date.now() - start;
396
484
 
397
485
  expect(data.count).toBeGreaterThanOrEqual(1);
398
- expect(elapsed).toBeLessThan(2000); // Should resolve near-instantly
486
+ expect(elapsed).toBeLessThan(2000);
399
487
  });
400
488
 
401
489
  it('resolves when event arrives during the wait', async () => {
@@ -439,12 +527,12 @@ describe('Bridge E2E: POST /api/terminal/exec', () => {
439
527
  });
440
528
 
441
529
  // =====================================================================
442
- // Group H — Chat Endpoints
530
+ // Group H — Chat Endpoints (via @shaykec/agent-web middleware)
443
531
  // =====================================================================
444
532
 
445
- describe('Bridge E2E: Chat endpoints', () => {
533
+ describe('Bridge E2E: Chat endpoints (agent-web)', () => {
446
534
  it('POST /api/chat/start returns 200 (SDK available) or 500 (SDK missing)', async () => {
447
- const { status, data } = await postJson('/api/chat/start', { clientId: 'e2e-test' });
535
+ const { status, data } = await postJson('/api/chat/start', {});
448
536
  expect([200, 500]).toContain(status);
449
537
  if (status === 200) {
450
538
  expect(data.sessionId).toBeDefined();
@@ -459,16 +547,15 @@ describe('Bridge E2E: Chat endpoints', () => {
459
547
  expect(Array.isArray(data.sessions)).toBe(true);
460
548
  });
461
549
 
462
- it('POST /api/chat/message returns 400 without valid sessionId', async () => {
550
+ it('POST /api/chat/message returns error without valid sessionId', async () => {
463
551
  const { status, data } = await postJson('/api/chat/message', { text: 'hello' });
464
- expect(status).toBe(400);
552
+ expect(status).toBeGreaterThanOrEqual(400);
465
553
  expect(data.error).toBeDefined();
466
554
  });
467
555
 
468
- it('POST /api/chat/stop returns 400 without sessionId', async () => {
469
- const { status, data } = await postJson('/api/chat/stop', {});
470
- expect(status).toBe(400);
471
- expect(data.error).toContain('sessionId');
556
+ it('POST /api/chat/stop returns 200 (best-effort)', async () => {
557
+ const { status } = await postJson('/api/chat/stop', {});
558
+ expect(status).toBe(200);
472
559
  });
473
560
  });
474
561
 
@@ -529,13 +616,11 @@ describe('Bridge E2E: WebSocket handshake', () => {
529
616
  it('canvas client upgrades tier to TIER_CANVAS', async () => {
530
617
  const { ws } = await connectWs('canvas');
531
618
  try {
532
- // Give server a moment to update tier
533
619
  await new Promise(r => setTimeout(r, 100));
534
620
  const { data } = await getJson('/api/tier');
535
- expect(data.tier).toBe(2); // TIER_CANVAS
621
+ expect(data.tier).toBe(2);
536
622
  } finally {
537
623
  ws.close();
538
- // Wait for disconnect processing
539
624
  await new Promise(r => setTimeout(r, 200));
540
625
  }
541
626
  });
@@ -563,16 +648,15 @@ describe('Bridge E2E: WebSocket message routing', () => {
563
648
  }
564
649
  });
565
650
 
566
- it('WS client receives visual command broadcast', async () => {
651
+ it('WS client receives visual command broadcast from /api/canvas', async () => {
567
652
  const { ws } = await connectWs('canvas');
568
653
  try {
569
654
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DIAGRAM);
570
655
 
571
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_DIAGRAM, {
572
- type: 'flow',
656
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_DIAGRAM, {
657
+ format: 'mermaid',
658
+ content: 'graph TD; A-->B',
573
659
  title: 'WS Test',
574
- nodes: [],
575
- edges: [],
576
660
  }));
577
661
 
578
662
  const msg = await msgPromise;
@@ -590,7 +674,7 @@ describe('Bridge E2E: WebSocket message routing', () => {
590
674
  const msg1Promise = waitForWsMessage(client1.ws, m => m.type === MSG_CANVAS_HTML);
591
675
  const msg2Promise = waitForWsMessage(client2.ws, m => m.type === MSG_CANVAS_HTML);
592
676
 
593
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_HTML, {
677
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_HTML, {
594
678
  html: '<h1>Multi</h1>',
595
679
  title: 'Multi Test',
596
680
  }));
@@ -617,12 +701,10 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
617
701
  expect(resp.status).toBe(200);
618
702
  expect(resp.headers.get('content-type')).toContain('text/event-stream');
619
703
 
620
- // Read the first chunk from the SSE stream
621
704
  const reader = resp.body.getReader();
622
705
  const decoder = new TextDecoder();
623
706
  let buffer = '';
624
707
 
625
- // Read until we get a complete SSE message
626
708
  while (true) {
627
709
  const { value, done } = await reader.read();
628
710
  if (done) break;
@@ -630,7 +712,6 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
630
712
  if (buffer.includes('\n\n')) break;
631
713
  }
632
714
 
633
- // Parse the SSE data line
634
715
  const dataLine = buffer.split('\n').find(l => l.startsWith('data:'));
635
716
  expect(dataLine).toBeDefined();
636
717
  const msg = JSON.parse(dataLine.replace('data:', '').trim());
@@ -641,7 +722,7 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
641
722
  }
642
723
  });
643
724
 
644
- it('SSE client receives visual command broadcasts', async () => {
725
+ it('SSE client receives visual command broadcasts from /api/canvas', async () => {
645
726
  const controller = new AbortController();
646
727
  try {
647
728
  const resp = await fetch(`${BASE_URL}/sse`, { signal: controller.signal });
@@ -649,7 +730,6 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
649
730
  const decoder = new TextDecoder();
650
731
  let buffer = '';
651
732
 
652
- // Read the initial sys:connect message
653
733
  while (true) {
654
734
  const { value, done } = await reader.read();
655
735
  if (done) break;
@@ -657,16 +737,13 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
657
737
  if (buffer.includes('\n\n')) break;
658
738
  }
659
739
 
660
- // Clear buffer after connect
661
740
  buffer = '';
662
741
 
663
- // Send a visual command
664
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_DASHBOARD, {
742
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_DASHBOARD, {
665
743
  belt: 'white',
666
744
  xp: 0,
667
745
  }));
668
746
 
669
- // Read the broadcast
670
747
  while (true) {
671
748
  const { value, done } = await reader.read();
672
749
  if (done) break;
@@ -691,27 +768,22 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
691
768
  describe('Bridge E2E: Visual pipeline integration', () => {
692
769
  beforeEach(() => drainEvents());
693
770
 
694
- it('full round-trip: POST visual → WS receives → send event → poll returns it', async () => {
771
+ it('full round-trip: POST canvas → WS receives → send event → poll returns it', async () => {
695
772
  const { ws } = await connectWs('canvas');
696
773
  try {
697
- // Step 1: POST visual command
698
774
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DIAGRAM);
699
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_DIAGRAM, {
700
- type: 'flow',
775
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_DIAGRAM, {
776
+ format: 'mermaid',
777
+ content: 'graph TD; A-->B',
701
778
  title: 'Pipeline Test',
702
- nodes: [],
703
- edges: [],
704
779
  }));
705
780
 
706
- // Step 2: WS client receives it
707
781
  const received = await msgPromise;
708
782
  expect(received.type).toBe(MSG_CANVAS_DIAGRAM);
709
783
 
710
- // Step 3: Send event via WS (user interaction response)
711
784
  ws.send(JSON.stringify(makeEnvelope(MSG_EVENT_CLICK, { target: 'pipeline' }, 'canvas')));
712
785
  await new Promise(r => setTimeout(r, 100));
713
786
 
714
- // Step 4: Poll returns the event
715
787
  const { data } = await getJson('/api/events');
716
788
  expect(data.count).toBeGreaterThanOrEqual(1);
717
789
  const click = data.events.find(e => e.payload?.target === 'pipeline');
@@ -721,30 +793,26 @@ describe('Bridge E2E: Visual pipeline integration', () => {
721
793
  }
722
794
  });
723
795
 
724
- it('atomic quiz: send + WS answer → quiz response includes answer', async () => {
725
- // Connect a WS client to receive the quiz
796
+ it('atomic quiz: send canvas:quiz with await + WS answer → response includes answer', async () => {
726
797
  const { ws } = await connectWs('canvas');
727
798
  try {
728
- const quizEnvelope = makeEnvelope(MSG_CANVAS_QUIZ, {
729
- question: 'Pipeline quiz?',
730
- options: ['A', 'B'],
731
- answer: 0,
732
- });
799
+ const quizEnvelope = makeEnvelopeWithAwait(
800
+ MSG_CANVAS_QUIZ,
801
+ { question: 'Pipeline quiz?', options: ['A', 'B'], answer: 0 },
802
+ 'plugin',
803
+ { event: 'event:quiz-answer', timeout: 5 },
804
+ );
733
805
 
734
- // Start quiz with timeout
735
- const quizPromise = postJson('/api/quiz?timeout=5', quizEnvelope);
806
+ const quizPromise = postJson('/api/canvas', quizEnvelope);
736
807
 
737
- // WS client receives the quiz broadcast
738
808
  const quizMsg = await waitForWsMessage(ws, m => m.type === MSG_CANVAS_QUIZ);
739
809
  expect(quizMsg.payload.question).toBe('Pipeline quiz?');
740
810
 
741
- // Send answer via HTTP (as canvas would)
742
811
  await postJson('/api/event', makeEnvelope(MSG_EVENT_QUIZ_ANSWER, {
743
812
  answer: 0,
744
813
  correct: true,
745
814
  }, 'canvas'));
746
815
 
747
- // Quiz endpoint resolves with the answer
748
816
  const { data } = await quizPromise;
749
817
  expect(data.count).toBeGreaterThanOrEqual(1);
750
818
  const answerEvent = data.events.find(e => e.type === MSG_EVENT_QUIZ_ANSWER);
@@ -760,17 +828,14 @@ describe('Bridge E2E: Visual pipeline integration', () => {
760
828
  // =====================================================================
761
829
 
762
830
  describe('Bridge E2E: Chat message routing', () => {
763
- it('chat:send via WS is routed to the chat handler (no active session)', async () => {
831
+ it('chat:send via bridge WS is silently accepted (no crash)', async () => {
764
832
  const { ws } = await connectWs('canvas');
765
833
  try {
766
- // Send a chat message via WebSocket — no session active, so it should be
767
- // silently ignored (sessionId check fails) but NOT error the connection
768
834
  ws.send(JSON.stringify(createEnvelope(MSG_CHAT_SEND, {
769
835
  text: '/teach git',
770
836
  sessionId: 'nonexistent-session',
771
837
  }, 'canvas')));
772
838
 
773
- // Connection should remain open
774
839
  await new Promise(r => setTimeout(r, 200));
775
840
  expect(ws.readyState).toBe(WebSocket.OPEN);
776
841
  } finally {
@@ -787,50 +852,24 @@ describe('Bridge E2E: Chat message routing', () => {
787
852
  expect(data.ok).toBe(true);
788
853
  });
789
854
 
790
- it('POST /api/chat/start returns 200 with sessionId (SDK available) or 500 with error', async () => {
791
- const { status, data } = await postJson('/api/chat/start', { clientId: 'test-client-123' });
792
- expect([200, 500]).toContain(status);
793
- if (status === 200) {
794
- expect(data.sessionId).toBeDefined();
795
- expect(data.ok).toBe(true);
796
- } else {
797
- expect(data.error).toBeDefined();
798
- }
799
- });
800
-
801
- it('POST /api/chat/message requires sessionId', async () => {
802
- const resp = await postJson('/api/chat/message', { text: 'hello' });
803
- expect(resp.status).toBe(400);
804
- });
805
-
806
855
  it('POST /api/chat/message with invalid sessionId returns error', async () => {
807
856
  const resp = await postJson('/api/chat/message', {
808
857
  text: '/teach git',
809
858
  sessionId: 'nonexistent',
810
859
  });
811
- // Should be 400 or 500 since session doesn't exist
812
860
  expect(resp.status).toBeGreaterThanOrEqual(400);
813
861
  });
814
862
 
815
863
  it('POST /api/chat/resume with invalid sessionId does not crash server', async () => {
816
864
  const { status } = await postJson('/api/chat/resume', {
817
865
  sessionId: 'nonexistent',
818
- clientId: 'test-client',
819
866
  });
820
- // 200 if SDK accepts the ID, 500 if it rejects — either is valid
821
867
  expect([200, 500]).toContain(status);
822
868
  });
823
-
824
- it('GET /api/chat/sessions returns array', async () => {
825
- const resp = await fetch(`${BASE_URL}/api/chat/sessions`);
826
- expect(resp.status).toBe(200);
827
- const data = await resp.json();
828
- expect(Array.isArray(data.sessions)).toBe(true);
829
- });
830
869
  });
831
870
 
832
871
  // =====================================================================
833
- // Group N — Teaching Module Flow (via HTTP API)
872
+ // Group N — Teaching Module Flow (via unified /api/canvas)
834
873
  // =====================================================================
835
874
 
836
875
  describe('Bridge E2E: Teaching module flow', () => {
@@ -839,33 +878,34 @@ describe('Bridge E2E: Teaching module flow', () => {
839
878
  it('teaching quiz is broadcast to WS clients and answer is collected', async () => {
840
879
  const { ws } = await connectWs('canvas');
841
880
  try {
842
- // Simulate a teaching quiz being sent (as the plugin would via /api/quiz)
843
- const quizPayload = createEnvelope(MSG_CANVAS_QUIZ, {
844
- question: 'What does `git add .` do?',
845
- options: [
846
- 'Stages all files in the current directory',
847
- 'Creates a new branch',
848
- 'Pushes to remote',
849
- 'Deletes untracked files',
850
- ],
851
- answer: 0,
852
- hint: 'Think about the staging area',
853
- }, 'plugin');
854
-
855
- const quizPromise = postJson('/api/quiz?timeout=3', quizPayload);
881
+ const quizPayload = makeEnvelopeWithAwait(
882
+ MSG_CANVAS_QUIZ,
883
+ {
884
+ question: 'What does `git add .` do?',
885
+ options: [
886
+ 'Stages all files in the current directory',
887
+ 'Creates a new branch',
888
+ 'Pushes to remote',
889
+ 'Deletes untracked files',
890
+ ],
891
+ answer: 0,
892
+ hint: 'Think about the staging area',
893
+ },
894
+ 'plugin',
895
+ { event: 'event:quiz-answer', timeout: 3 },
896
+ );
897
+
898
+ const quizPromise = postJson('/api/canvas', quizPayload);
856
899
 
857
- // WS client receives the quiz
858
900
  const quizMsg = await waitForWsMessage(ws, m => m.type === MSG_CANVAS_QUIZ);
859
901
  expect(quizMsg.payload.question).toContain('git add');
860
902
  expect(quizMsg.payload.options).toHaveLength(4);
861
903
 
862
- // User answers the quiz via event POST (as the canvas UI would)
863
904
  await postJson('/api/event', createEnvelope(MSG_EVENT_QUIZ_ANSWER, {
864
905
  answer: 0,
865
906
  correct: true,
866
907
  }, 'canvas'));
867
908
 
868
- // Quiz resolves with the answer
869
909
  const { data } = await quizPromise;
870
910
  const answerEvent = data.events.find(e => e.type === MSG_EVENT_QUIZ_ANSWER);
871
911
  expect(answerEvent).toBeDefined();
@@ -880,8 +920,7 @@ describe('Bridge E2E: Teaching module flow', () => {
880
920
  try {
881
921
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DIAGRAM);
882
922
 
883
- // Simulate plugin sending a teaching diagram
884
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DIAGRAM, {
923
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DIAGRAM, {
885
924
  format: 'mermaid',
886
925
  code: 'graph TD; A[Working Dir]-->B[Staging]; B-->C[Repository];',
887
926
  title: 'Git Workflow',
@@ -901,8 +940,7 @@ describe('Bridge E2E: Teaching module flow', () => {
901
940
  try {
902
941
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DASHBOARD);
903
942
 
904
- // Simulate progress update after completing a module step
905
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DASHBOARD, {
943
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DASHBOARD, {
906
944
  belt: 'white',
907
945
  xp: 50,
908
946
  streak: 1,
@@ -928,14 +966,13 @@ describe('Bridge E2E: Teaching module flow', () => {
928
966
  if (msg.type.startsWith('canvas:')) received.push(msg);
929
967
  });
930
968
 
931
- // Send diagram, then dashboard update (like a teaching step)
932
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DIAGRAM, {
969
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DIAGRAM, {
933
970
  format: 'mermaid',
934
971
  code: 'graph LR; A-->B;',
935
972
  title: 'Step 1',
936
973
  }, 'plugin'));
937
974
 
938
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DASHBOARD, {
975
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DASHBOARD, {
939
976
  belt: 'white',
940
977
  xp: 25,
941
978
  }, 'plugin'));
@@ -949,3 +986,601 @@ describe('Bridge E2E: Teaching module flow', () => {
949
986
  }
950
987
  });
951
988
  });
989
+
990
+ // =====================================================================
991
+ // Group O — Module Content Endpoint
992
+ // =====================================================================
993
+
994
+ describe('Bridge E2E: Module content endpoint', () => {
995
+ it('GET /api/module/git returns module content (if git module exists)', async () => {
996
+ const { status, data } = await getJson('/api/module/git');
997
+ if (status === 200) {
998
+ expect(data.slug).toBe('git');
999
+ expect(data).toHaveProperty('meta');
1000
+ expect(data).toHaveProperty('files');
1001
+ expect(data.files).toHaveProperty('module.yaml');
1002
+ } else {
1003
+ expect(status).toBe(404);
1004
+ }
1005
+ });
1006
+
1007
+ it('GET /api/module/nonexistent returns 404', async () => {
1008
+ const { status, data } = await getJson('/api/module/nonexistent-slug-xyz');
1009
+ expect(status).toBe(404);
1010
+ expect(data.error).toContain('not found');
1011
+ });
1012
+ });
1013
+
1014
+ // =====================================================================
1015
+ // Group P — SDK Config Verification
1016
+ // =====================================================================
1017
+
1018
+ describe('Bridge E2E: SDK config verification', () => {
1019
+ it('agentServer was created with proper config', () => {
1020
+ expect(bridge.agentServer).toBeDefined();
1021
+ });
1022
+ });
1023
+
1024
+ // =====================================================================
1025
+ // Group Q — Journey Endpoints
1026
+ // =====================================================================
1027
+
1028
+ describe('Bridge E2E: Journey endpoints', () => {
1029
+ it('GET /api/journeys returns journeys array', async () => {
1030
+ const { status, data } = await getJson('/api/journeys');
1031
+ expect(status).toBe(200);
1032
+ expect(data).toHaveProperty('journeys');
1033
+ expect(data.journeys).toBeInstanceOf(Array);
1034
+ expect(data.journeys.length).toBeGreaterThanOrEqual(1);
1035
+ expect(data).toHaveProperty('activeJourney');
1036
+ });
1037
+
1038
+ it('each journey has slug, title, stages, and stats', async () => {
1039
+ const { data } = await getJson('/api/journeys');
1040
+ for (const j of data.journeys) {
1041
+ expect(j).toHaveProperty('slug');
1042
+ expect(j).toHaveProperty('title');
1043
+ expect(j).toHaveProperty('stages');
1044
+ expect(j.stages).toBeInstanceOf(Array);
1045
+ expect(j.stages.length).toBeGreaterThan(0);
1046
+ expect(j).toHaveProperty('stats');
1047
+ expect(j.stats).toHaveProperty('totalModules');
1048
+ expect(j.stats).toHaveProperty('comingSoonModules');
1049
+ expect(j.stats).toHaveProperty('progressPercent');
1050
+ expect(j.stats).toHaveProperty('totalStages');
1051
+ }
1052
+ });
1053
+
1054
+ it('journey stages contain modules with status fields', async () => {
1055
+ const { data } = await getJson('/api/journeys');
1056
+ const journey = data.journeys[0];
1057
+ const firstStage = journey.stages[0];
1058
+ expect(firstStage).toHaveProperty('name');
1059
+ expect(firstStage).toHaveProperty('modules');
1060
+ expect(firstStage.modules.length).toBeGreaterThan(0);
1061
+ for (const mod of firstStage.modules) {
1062
+ expect(mod).toHaveProperty('slug');
1063
+ expect(mod).toHaveProperty('title');
1064
+ expect(mod).toHaveProperty('status');
1065
+ expect(['available', 'completed', 'in_progress', 'locked', 'coming_soon']).toContain(mod.status);
1066
+ }
1067
+ });
1068
+
1069
+ it('journeys include coming_soon modules for TBD content', async () => {
1070
+ const { data } = await getJson('/api/journeys');
1071
+ const hasComingSoon = data.journeys.some(j => j.stats.comingSoonModules > 0);
1072
+ expect(hasComingSoon).toBe(true);
1073
+
1074
+ const journeyWithTBD = data.journeys.find(j => j.stats.comingSoonModules > 0);
1075
+ const allModules = journeyWithTBD.stages.flatMap(s => s.modules);
1076
+ const comingSoon = allModules.filter(m => m.status === 'coming_soon');
1077
+ expect(comingSoon.length).toBe(journeyWithTBD.stats.comingSoonModules);
1078
+ });
1079
+
1080
+ it('POST /api/journeys/activate sets active journey', async () => {
1081
+ const { status, data } = await postJson('/api/journeys/activate', { slug: 'frontend-developer' });
1082
+ expect(status).toBe(200);
1083
+ expect(data.ok).toBe(true);
1084
+ expect(data.active).toBe('frontend-developer');
1085
+ });
1086
+
1087
+ it('GET /api/journeys reflects activated journey', async () => {
1088
+ await postJson('/api/journeys/activate', { slug: 'frontend-developer' });
1089
+ const { data } = await getJson('/api/journeys');
1090
+ expect(data.activeJourney).toBe('frontend-developer');
1091
+ });
1092
+
1093
+ it('POST /api/journeys/activate with null deactivates', async () => {
1094
+ await postJson('/api/journeys/activate', { slug: 'frontend-developer' });
1095
+ const { status, data } = await postJson('/api/journeys/activate', { slug: null });
1096
+ expect(status).toBe(200);
1097
+ expect(data.ok).toBe(true);
1098
+ expect(data.active).toBeNull();
1099
+ });
1100
+
1101
+ it('GET /api/journeys shows null activeJourney after deactivation', async () => {
1102
+ await postJson('/api/journeys/activate', { slug: 'frontend-developer' });
1103
+ await postJson('/api/journeys/activate', { slug: null });
1104
+ const { data } = await getJson('/api/journeys');
1105
+ expect(data.activeJourney).toBeNull();
1106
+ });
1107
+
1108
+ it('first stage is available, later stages locked for fresh progress', async () => {
1109
+ const { data } = await getJson('/api/journeys');
1110
+ const journey = data.journeys.find(j => j.stages.length >= 3);
1111
+ if (journey) {
1112
+ expect(journey.stages[0].status).toBe('available');
1113
+ const lockedStages = journey.stages.slice(2).filter(s => s.status === 'locked');
1114
+ expect(lockedStages.length).toBeGreaterThan(0);
1115
+ }
1116
+ });
1117
+ });
1118
+
1119
+ // =====================================================================
1120
+ // Group R — Progress Sync (POST /api/progress + provider + broadcast)
1121
+ // =====================================================================
1122
+
1123
+ describe('Bridge E2E: Progress sync', () => {
1124
+ it('POST /api/progress updates XP via progressProvider', async () => {
1125
+ const { status, data } = await postJson('/api/progress', { xp: 150 });
1126
+ expect(status).toBe(200);
1127
+ expect(data.ok).toBe(true);
1128
+
1129
+ const { data: progress } = await getJson('/api/progress');
1130
+ expect(progress.user.xp).toBe(150);
1131
+ });
1132
+
1133
+ it('POST /api/progress updates module status', async () => {
1134
+ const { status, data } = await postJson('/api/progress', {
1135
+ module: 'git',
1136
+ status: 'in-progress',
1137
+ });
1138
+ expect(status).toBe(200);
1139
+ expect(data.ok).toBe(true);
1140
+
1141
+ const { data: progress } = await getJson('/api/progress');
1142
+ expect(progress.modules.git).toBeDefined();
1143
+ expect(progress.modules.git.status).toBe('in-progress');
1144
+ expect(progress.modules.git.last_session).toBeDefined();
1145
+ });
1146
+
1147
+ it('POST /api/progress updates module with walkthroughStep', async () => {
1148
+ await postJson('/api/progress', {
1149
+ module: 'hooks',
1150
+ status: 'in-progress',
1151
+ walkthroughStep: 3,
1152
+ });
1153
+
1154
+ const { data: progress } = await getJson('/api/progress');
1155
+ expect(progress.modules.hooks).toBeDefined();
1156
+ expect(progress.modules.hooks.walkthrough_step).toBe(3);
1157
+ });
1158
+
1159
+ it('POST /api/progress marks module completed and updates count', async () => {
1160
+ await postJson('/api/progress', { module: 'test-mod', status: 'completed' });
1161
+
1162
+ const { data: progress } = await getJson('/api/progress');
1163
+ expect(progress.modules['test-mod'].status).toBe('completed');
1164
+ expect(progress.user.modules_completed).toBeGreaterThanOrEqual(1);
1165
+ });
1166
+
1167
+ it('POST /api/progress rejects invalid body', async () => {
1168
+ const { status } = await postRaw('/api/progress', 'not json');
1169
+ expect(status).toBe(400);
1170
+ });
1171
+
1172
+ it('POST /api/progress rejects non-object body', async () => {
1173
+ const { status } = await postJson('/api/progress', 'just a string');
1174
+ expect(status).toBe(400);
1175
+ });
1176
+
1177
+ it('POST /api/progress broadcasts canvas:dashboard to WebSocket clients', async () => {
1178
+ const { ws } = await connectWs('canvas');
1179
+ const messages = [];
1180
+
1181
+ ws.on('message', (raw) => {
1182
+ try { messages.push(JSON.parse(raw.toString())); } catch {}
1183
+ });
1184
+
1185
+ await postJson('/api/progress', { xp: 999 });
1186
+
1187
+ await new Promise(r => setTimeout(r, 300));
1188
+
1189
+ ws.close();
1190
+
1191
+ const dashboardMsg = messages.find(m => m.type === 'canvas:dashboard');
1192
+ expect(dashboardMsg).toBeDefined();
1193
+ expect(dashboardMsg.payload.progress.xp).toBe(999);
1194
+ });
1195
+
1196
+ it('journey activation persists via saveProgress', async () => {
1197
+ await postJson('/api/journeys/activate', { slug: 'frontend-developer' });
1198
+ const { data } = await getJson('/api/journeys');
1199
+ expect(data.activeJourney).toBe('frontend-developer');
1200
+
1201
+ await postJson('/api/journeys/activate', { slug: null });
1202
+ const { data: data2 } = await getJson('/api/journeys');
1203
+ expect(data2.activeJourney).toBeNull();
1204
+ });
1205
+ });
1206
+
1207
+ // =====================================================================
1208
+ // Group S — Constants Endpoint
1209
+ // =====================================================================
1210
+
1211
+ describe('Bridge E2E: Constants endpoint', () => {
1212
+ it('GET /api/constants returns belt data', async () => {
1213
+ const { status, data } = await getJson('/api/constants');
1214
+ expect(status).toBe(200);
1215
+ expect(data).toHaveProperty('belts');
1216
+ expect(data.belts).toBeInstanceOf(Array);
1217
+ expect(data.belts.length).toBeGreaterThanOrEqual(7);
1218
+ });
1219
+
1220
+ it('belt data includes required fields', async () => {
1221
+ const { data } = await getJson('/api/constants');
1222
+ for (const belt of data.belts) {
1223
+ expect(belt).toHaveProperty('name');
1224
+ expect(belt).toHaveProperty('minXP');
1225
+ expect(belt).toHaveProperty('badge');
1226
+ expect(typeof belt.name).toBe('string');
1227
+ expect(typeof belt.minXP).toBe('number');
1228
+ }
1229
+ });
1230
+
1231
+ it('belt thresholds match shared constants', async () => {
1232
+ const { BELTS: sharedBelts } = await import('@shaykec/shared');
1233
+ const { data } = await getJson('/api/constants');
1234
+ expect(data.belts).toEqual(sharedBelts);
1235
+ });
1236
+ });
1237
+
1238
+ // =====================================================================
1239
+ // Group S2 — Modules Catalog Endpoint
1240
+ // =====================================================================
1241
+
1242
+ describe('Bridge E2E: Modules catalog endpoint', () => {
1243
+ it('GET /api/modules returns a modules array', async () => {
1244
+ const { status, data } = await getJson('/api/modules');
1245
+ expect(status).toBe(200);
1246
+ expect(data).toHaveProperty('modules');
1247
+ expect(data.modules).toBeInstanceOf(Array);
1248
+ });
1249
+
1250
+ it('each module has slug, title, category, difficulty', async () => {
1251
+ const { data } = await getJson('/api/modules');
1252
+ expect(data.modules.length).toBeGreaterThan(0);
1253
+ for (const mod of data.modules) {
1254
+ expect(mod).toHaveProperty('slug');
1255
+ expect(mod).toHaveProperty('title');
1256
+ expect(mod).toHaveProperty('category');
1257
+ expect(mod).toHaveProperty('difficulty');
1258
+ expect(typeof mod.slug).toBe('string');
1259
+ expect(typeof mod.title).toBe('string');
1260
+ }
1261
+ });
1262
+
1263
+ it('includes known built-in modules', async () => {
1264
+ const { data } = await getJson('/api/modules');
1265
+ const slugs = data.modules.map(m => m.slug);
1266
+ expect(slugs).toContain('git');
1267
+ });
1268
+
1269
+ it('returns CORS headers', async () => {
1270
+ const { headers } = await getJson('/api/modules');
1271
+ expect(headers.get('access-control-allow-origin')).toBeTruthy();
1272
+ });
1273
+
1274
+ it('returns JSON content type', async () => {
1275
+ const resp = await fetch(`${BASE_URL}/api/modules`);
1276
+ expect(resp.headers.get('content-type')).toMatch(/application\/json/);
1277
+ });
1278
+
1279
+ it('slugs are unique across all modules', async () => {
1280
+ const { data } = await getJson('/api/modules');
1281
+ const slugs = data.modules.map(m => m.slug);
1282
+ expect(new Set(slugs).size).toBe(slugs.length);
1283
+ });
1284
+
1285
+ it('slugs contain only valid characters (lowercase, hyphens, numbers)', async () => {
1286
+ const { data } = await getJson('/api/modules');
1287
+ for (const mod of data.modules) {
1288
+ expect(mod.slug).toMatch(/^[a-z0-9-]+$/);
1289
+ }
1290
+ });
1291
+
1292
+ it('difficulty values are from expected set', async () => {
1293
+ const validDifficulties = ['beginner', 'intermediate', 'advanced'];
1294
+ const { data } = await getJson('/api/modules');
1295
+ for (const mod of data.modules) {
1296
+ expect(validDifficulties).toContain(mod.difficulty);
1297
+ }
1298
+ });
1299
+
1300
+ it('includes multiple categories', async () => {
1301
+ const { data } = await getJson('/api/modules');
1302
+ const categories = new Set(data.modules.map(m => m.category));
1303
+ expect(categories.size).toBeGreaterThan(1);
1304
+ });
1305
+ });
1306
+
1307
+ // =====================================================================
1308
+ // Group T — Module Enrichment
1309
+ // =====================================================================
1310
+
1311
+ describe('Bridge E2E: Module enrichment', () => {
1312
+ it('GET /api/progress returns modules with title field', async () => {
1313
+ await postJson('/api/progress', { module: 'git', status: 'in-progress' });
1314
+ const { data } = await getJson('/api/progress');
1315
+ expect(data.modules.git).toBeDefined();
1316
+ expect(data.modules.git.title).toBeDefined();
1317
+ expect(typeof data.modules.git.title).toBe('string');
1318
+ expect(data.modules.git.title.length).toBeGreaterThan(0);
1319
+ });
1320
+
1321
+ it('GET /api/progress includes available modules from catalog', async () => {
1322
+ const { data } = await getJson('/api/progress');
1323
+ const slugs = Object.keys(data.modules);
1324
+ expect(slugs.length).toBeGreaterThan(1);
1325
+ const available = Object.values(data.modules).filter(m => m.status === 'available');
1326
+ expect(available.length).toBeGreaterThan(0);
1327
+ for (const mod of available) {
1328
+ expect(mod.title).toBeDefined();
1329
+ expect(mod.title.length).toBeGreaterThan(0);
1330
+ }
1331
+ });
1332
+
1333
+ it('GET /api/progress enriches modules with description and difficulty', async () => {
1334
+ const { data } = await getJson('/api/progress');
1335
+ const anyModule = Object.values(data.modules).find(m => m.title && m.title !== m.slug);
1336
+ expect(anyModule).toBeDefined();
1337
+ expect(anyModule).toHaveProperty('description');
1338
+ expect(anyModule).toHaveProperty('difficulty');
1339
+ });
1340
+
1341
+ it('POST /api/progress broadcast includes enriched module titles', async () => {
1342
+ const { ws } = await connectWs('canvas');
1343
+ const messages = [];
1344
+
1345
+ ws.on('message', (raw) => {
1346
+ try { messages.push(JSON.parse(raw.toString())); } catch {}
1347
+ });
1348
+
1349
+ await postJson('/api/progress', { module: 'git', status: 'in-progress' });
1350
+ await new Promise(r => setTimeout(r, 300));
1351
+ ws.close();
1352
+
1353
+ const dashboardMsg = messages.find(m => m.type === 'canvas:dashboard');
1354
+ expect(dashboardMsg).toBeDefined();
1355
+ const gitModule = dashboardMsg.payload.progress.modules.find(m => m.slug === 'git');
1356
+ expect(gitModule).toBeDefined();
1357
+ expect(gitModule.title).toBeDefined();
1358
+ expect(gitModule.title.length).toBeGreaterThan(0);
1359
+ });
1360
+ });
1361
+
1362
+ // =====================================================================
1363
+ // Group U — Unit: enrichModulesWithMetadata
1364
+ // =====================================================================
1365
+
1366
+ describe('enrichModulesWithMetadata', () => {
1367
+ it('merges catalog metadata into progress modules', () => {
1368
+ const progressModules = {
1369
+ git: { status: 'in-progress', xp_earned: 30 },
1370
+ };
1371
+ const moduleMap = new Map([
1372
+ ['git', { slug: 'git', title: 'Git Fundamentals', description: 'Learn git', difficulty: 'beginner' }],
1373
+ ]);
1374
+
1375
+ const result = enrichModulesWithMetadata(progressModules, moduleMap);
1376
+ expect(result.git.title).toBe('Git Fundamentals');
1377
+ expect(result.git.description).toBe('Learn git');
1378
+ expect(result.git.difficulty).toBe('beginner');
1379
+ expect(result.git.status).toBe('in-progress');
1380
+ expect(result.git.xp_earned).toBe(30);
1381
+ });
1382
+
1383
+ it('falls back to slug as title when module not in catalog', () => {
1384
+ const progressModules = {
1385
+ 'custom-mod': { status: 'completed', xp_earned: 100 },
1386
+ };
1387
+ const moduleMap = new Map();
1388
+
1389
+ const result = enrichModulesWithMetadata(progressModules, moduleMap);
1390
+ expect(result['custom-mod'].title).toBe('custom-mod');
1391
+ expect(result['custom-mod'].status).toBe('completed');
1392
+ });
1393
+
1394
+ it('includes catalog modules not in progress as available', () => {
1395
+ const progressModules = {};
1396
+ const moduleMap = new Map([
1397
+ ['hooks', { slug: 'hooks', title: 'React Hooks', description: 'Hooks guide', difficulty: 'intermediate' }],
1398
+ ]);
1399
+
1400
+ const result = enrichModulesWithMetadata(progressModules, moduleMap);
1401
+ expect(result.hooks).toBeDefined();
1402
+ expect(result.hooks.status).toBe('available');
1403
+ expect(result.hooks.title).toBe('React Hooks');
1404
+ });
1405
+
1406
+ it('handles null/undefined progressModules', () => {
1407
+ const moduleMap = new Map([
1408
+ ['git', { slug: 'git', title: 'Git', description: '', difficulty: 'beginner' }],
1409
+ ]);
1410
+
1411
+ const result = enrichModulesWithMetadata(null, moduleMap);
1412
+ expect(result.git.status).toBe('available');
1413
+ });
1414
+
1415
+ it('handles empty moduleMap', () => {
1416
+ const progressModules = { git: { status: 'in-progress' } };
1417
+ const result = enrichModulesWithMetadata(progressModules, new Map());
1418
+ expect(result.git.title).toBe('git');
1419
+ expect(result.git.status).toBe('in-progress');
1420
+ });
1421
+ });
1422
+
1423
+ // =====================================================================
1424
+ // Group V — Unit: parseProgressYaml / serializeProgressYaml
1425
+ // =====================================================================
1426
+
1427
+ describe('parseProgressYaml', () => {
1428
+ it('parses a typical progress file', () => {
1429
+ const yaml = `user:
1430
+ xp: 250
1431
+ belt: green
1432
+ modules_completed: 2
1433
+ modules:
1434
+ git:
1435
+ status: completed
1436
+ xp_earned: 100
1437
+ started: 2026-03-01
1438
+ hooks:
1439
+ status: in-progress
1440
+ xp_earned: 50
1441
+ walkthrough_step: 3
1442
+ journey:
1443
+ active: frontend-developer
1444
+ progress: {}
1445
+ `;
1446
+ const result = parseProgressYaml(yaml);
1447
+ expect(result.user.xp).toBe(250);
1448
+ expect(result.user.belt).toBe('green');
1449
+ expect(result.user.modules_completed).toBe(2);
1450
+ expect(result.modules.git.status).toBe('completed');
1451
+ expect(result.modules.git.xp_earned).toBe(100);
1452
+ expect(result.modules.hooks.walkthrough_step).toBe(3);
1453
+ expect(result.journey.active).toBe('frontend-developer');
1454
+ });
1455
+
1456
+ it('returns defaults for empty input', () => {
1457
+ const result = parseProgressYaml('');
1458
+ expect(result.user.xp).toBe(0);
1459
+ expect(result.modules).toEqual({});
1460
+ expect(result.journey.active).toBeNull();
1461
+ });
1462
+
1463
+ it('returns defaults for null input', () => {
1464
+ const result = parseProgressYaml(null);
1465
+ expect(result.user.xp).toBe(0);
1466
+ });
1467
+
1468
+ it('parses journey active: null correctly', () => {
1469
+ const yaml = `journey:
1470
+ active: null
1471
+ progress: {}
1472
+ `;
1473
+ const result = parseProgressYaml(yaml);
1474
+ expect(result.journey.active).toBeNull();
1475
+ });
1476
+
1477
+ it('parses quoted string values', () => {
1478
+ const yaml = `user:
1479
+ xp: 0
1480
+ belt: 'white'
1481
+ modules:
1482
+ git:
1483
+ quiz_score: "4/5"
1484
+ `;
1485
+ const result = parseProgressYaml(yaml);
1486
+ expect(result.user.belt).toBe('white');
1487
+ expect(result.modules.git.quiz_score).toBe('4/5');
1488
+ });
1489
+ });
1490
+
1491
+ describe('serializeProgressYaml', () => {
1492
+ it('round-trips a progress object', () => {
1493
+ const original = {
1494
+ user: { xp: 100, belt: 'yellow', modules_completed: 1 },
1495
+ modules: {
1496
+ git: { status: 'completed', xp_earned: 100, started: '2026-03-01' },
1497
+ },
1498
+ journey: { active: 'frontend-developer', progress: {} },
1499
+ };
1500
+
1501
+ const yaml = serializeProgressYaml(original);
1502
+ const parsed = parseProgressYaml(yaml);
1503
+ expect(parsed.user.xp).toBe(100);
1504
+ expect(parsed.user.belt).toBe('yellow');
1505
+ expect(parsed.modules.git.status).toBe('completed');
1506
+ expect(parsed.modules.git.xp_earned).toBe(100);
1507
+ expect(parsed.journey.active).toBe('frontend-developer');
1508
+ });
1509
+
1510
+ it('serializes empty modules as modules: {}', () => {
1511
+ const yaml = serializeProgressYaml({
1512
+ user: { xp: 0 },
1513
+ modules: {},
1514
+ journey: { active: null, progress: {} },
1515
+ });
1516
+ expect(yaml).toContain('modules: {}');
1517
+ });
1518
+
1519
+ it('serializes null journey active', () => {
1520
+ const yaml = serializeProgressYaml({
1521
+ user: { xp: 0 },
1522
+ modules: {},
1523
+ journey: { active: null, progress: {} },
1524
+ });
1525
+ expect(yaml).toContain('active: null');
1526
+ });
1527
+ });
1528
+
1529
+ // =====================================================================
1530
+ // Group W — Unit: createFileProgressProvider
1531
+ // =====================================================================
1532
+
1533
+ describe('createFileProgressProvider', () => {
1534
+ const testFile = join(tmpdir(), `.socrates-progress-test-${Date.now()}.yaml`);
1535
+
1536
+ afterAll(() => {
1537
+ try { if (existsSync(testFile)) unlinkSync(testFile); } catch {}
1538
+ });
1539
+
1540
+ it('returns defaults when file does not exist', () => {
1541
+ const provider = createFileProgressProvider(join(tmpdir(), 'nonexistent-progress.yaml'));
1542
+ const progress = provider.getProgress();
1543
+ expect(progress.user.xp).toBe(0);
1544
+ expect(progress.modules).toEqual({});
1545
+ });
1546
+
1547
+ it('reads and writes progress to file', () => {
1548
+ const provider = createFileProgressProvider(testFile);
1549
+
1550
+ const progress = provider.getProgress();
1551
+ progress.user.xp = 500;
1552
+ progress.modules.git = { status: 'completed', xp_earned: 200 };
1553
+ provider.saveProgress(progress);
1554
+
1555
+ const reloaded = provider.getProgress();
1556
+ expect(reloaded.user.xp).toBe(500);
1557
+ expect(reloaded.modules.git.status).toBe('completed');
1558
+ expect(reloaded.modules.git.xp_earned).toBe(200);
1559
+ });
1560
+
1561
+ it('reads an externally written progress file', () => {
1562
+ const externalFile = join(tmpdir(), `.socrates-progress-ext-${Date.now()}.yaml`);
1563
+ writeFileSync(externalFile, `user:
1564
+ xp: 300
1565
+ belt: green
1566
+ modules:
1567
+ hooks:
1568
+ status: in-progress
1569
+ xp_earned: 75
1570
+ journey:
1571
+ active: null
1572
+ progress: {}
1573
+ `, 'utf-8');
1574
+
1575
+ try {
1576
+ const provider = createFileProgressProvider(externalFile);
1577
+ const progress = provider.getProgress();
1578
+ expect(progress.user.xp).toBe(300);
1579
+ expect(progress.user.belt).toBe('green');
1580
+ expect(progress.modules.hooks.status).toBe('in-progress');
1581
+ expect(progress.modules.hooks.xp_earned).toBe(75);
1582
+ } finally {
1583
+ try { unlinkSync(externalFile); } catch {}
1584
+ }
1585
+ });
1586
+ });