@shaykec/bridge 0.4.20 → 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 (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 +2 -1
  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
@@ -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,9 +19,11 @@ 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
28
  } from '@shaykec/shared';
24
29
 
@@ -26,11 +31,20 @@ import {
26
31
 
27
32
  let bridge, port, BASE_URL;
28
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
+
29
42
  beforeAll(async () => {
30
43
  port = 20000 + Math.floor(Math.random() * 10000);
31
44
  await new Promise((resolve) => {
32
45
  bridge = startServer({
33
46
  port,
47
+ progressProvider: createInMemoryProgressProvider(),
34
48
  onReady: () => resolve(),
35
49
  });
36
50
  });
@@ -76,6 +90,10 @@ function makeEnvelope(type, payload = {}, source = 'plugin') {
76
90
  return createEnvelope(type, payload, source);
77
91
  }
78
92
 
93
+ function makeEnvelopeWithAwait(type, payload, source, awaitSpec) {
94
+ return createEnvelope(type, payload, source, { await: awaitSpec });
95
+ }
96
+
79
97
  async function connectWs(clientType = 'canvas') {
80
98
  return new Promise((resolve, reject) => {
81
99
  const ws = new WebSocket(`ws://localhost:${port}/ws`);
@@ -204,9 +222,6 @@ describe('Bridge E2E: Static file serving', () => {
204
222
  });
205
223
 
206
224
  it('path traversal does not leak files outside canvas-dist', async () => {
207
- // Node's HTTP parser normalises /../.. but even if a traversal path gets through,
208
- // the server's startsWith guard in serveStatic prevents leaking files.
209
- // We verify that requesting a path like /etc/passwd never returns passwd content.
210
225
  const { get } = await import('http');
211
226
  const body = await new Promise((resolve, reject) => {
212
227
  const req = get({ hostname: 'localhost', port, path: '/../../etc/passwd' }, (res) => {
@@ -216,7 +231,6 @@ describe('Bridge E2E: Static file serving', () => {
216
231
  });
217
232
  req.on('error', reject);
218
233
  });
219
- // Must not contain actual /etc/passwd content
220
234
  expect(body).not.toContain('root:');
221
235
  });
222
236
  });
@@ -253,73 +267,122 @@ describe('Bridge E2E: POST /api/event', () => {
253
267
  });
254
268
 
255
269
  // =====================================================================
256
- // Group D — POST /api/visual
270
+ // Group D — Unified POST /api/canvas
257
271
  // =====================================================================
258
272
 
259
- describe('Bridge E2E: POST /api/visual', () => {
260
- 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 () => {
261
277
  const envelope = makeEnvelope(MSG_CANVAS_DIAGRAM, {
262
- type: 'flow',
263
- title: 'Test',
264
- nodes: [],
265
- edges: [],
278
+ format: 'mermaid',
279
+ content: 'graph TD; A-->B',
266
280
  });
267
- const { status, data } = await postJson('/api/visual', envelope);
281
+ const { status, data } = await postJson('/api/canvas', envelope);
268
282
  expect(status).toBe(200);
269
283
  expect(data.ok).toBe(true);
284
+ expect(data).toHaveProperty('tier');
270
285
  });
271
286
 
272
- it('rejects canvas:quiz with redirect message', async () => {
273
- const envelope = makeEnvelope(MSG_CANVAS_QUIZ, { question: 'test?' });
274
- const { status, data } = await postJson('/api/visual', envelope);
275
- expect(status).toBe(400);
276
- 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);
277
292
  });
278
293
 
279
- it('rejects canvas:game with redirect message', async () => {
280
- const envelope = makeEnvelope(MSG_CANVAS_GAME, { gameType: 'speed-round' });
281
- const { status, data } = await postJson('/api/visual', envelope);
282
- expect(status).toBe(400);
283
- 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);
284
299
  });
285
300
 
286
- it('rejects invalid envelope', async () => {
287
- const { status } = await postJson('/api/visual', { bad: true });
288
- 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');
289
314
  });
290
- });
291
315
 
292
- // =====================================================================
293
- // Group E Quiz & Game Atomic Endpoints
294
- // =====================================================================
295
-
296
- describe('Bridge E2E: Atomic quiz & game endpoints', () => {
297
- 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));
298
323
 
299
- it('POST /api/quiz?timeout=1 times out with empty events', async () => {
300
- const envelope = makeEnvelope(MSG_CANVAS_QUIZ, {
301
- question: 'What is 2+2?',
302
- options: ['3', '4'],
303
- answer: 1,
324
+ const envelope = makeEnvelope(MSG_CANVAS_SLIDES, {
325
+ title: 'Test Deck',
326
+ slides: [{ title: 'Only Slide', blocks: [{ type: 'markdown', content: 'Content' }] }],
304
327
  });
305
- 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);
306
357
  expect(status).toBe(200);
307
358
  expect(data.ok).toBe(true);
308
359
  expect(data.events).toEqual([]);
309
360
  expect(data.count).toBe(0);
310
361
  });
311
362
 
312
- it('POST /api/quiz resolves with answer when sent during wait', async () => {
313
- const quizEnvelope = makeEnvelope(MSG_CANVAS_QUIZ, {
314
- question: 'Capital of France?',
315
- options: ['Berlin', 'Paris'],
316
- answer: 1,
317
- });
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
+ );
318
383
 
319
- // Start quiz (long timeout) and concurrently send answer
320
- const quizPromise = postJson('/api/quiz?timeout=5', quizEnvelope);
384
+ const quizPromise = postJson('/api/canvas', envelope);
321
385
 
322
- // Send answer after a short delay
323
386
  await new Promise(r => setTimeout(r, 300));
324
387
  const answerEnvelope = makeEnvelope(MSG_EVENT_QUIZ_ANSWER, {
325
388
  answer: 1,
@@ -334,32 +397,19 @@ describe('Bridge E2E: Atomic quiz & game endpoints', () => {
334
397
  expect(answer).toBeDefined();
335
398
  });
336
399
 
337
- it('POST /api/game?timeout=1 times out with empty events', async () => {
338
- const envelope = makeEnvelope(MSG_CANVAS_GAME, {
339
- gameType: 'speed-round',
340
- title: 'Test Game',
341
- rounds: [{ question: '2+2?', options: ['3', '4'], answer: 1, timeLimit: 10 }],
342
- });
343
- const { status, data } = await postJson('/api/game?timeout=1', envelope);
344
- expect(status).toBe(200);
345
- expect(data.ok).toBe(true);
346
- expect(data.events).toEqual([]);
347
- });
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
+ );
348
407
 
349
- it('POST /api/game resolves with result when sent during wait', async () => {
350
- const gameEnvelope = makeEnvelope(MSG_CANVAS_GAME, {
351
- gameType: 'speed-round',
352
- title: 'Test',
353
- rounds: [{ question: '1+1?', options: ['2', '3'], answer: 0, timeLimit: 10 }],
354
- });
355
-
356
- const gamePromise = postJson('/api/game?timeout=5', gameEnvelope);
408
+ const gamePromise = postJson('/api/canvas', envelope);
357
409
 
358
410
  await new Promise(r => setTimeout(r, 300));
359
411
  const resultEnvelope = makeEnvelope(MSG_EVENT_GAME_RESULT, {
360
- score: 300,
361
- accuracy: 1.0,
362
- stars: 3,
412
+ score: 300, accuracy: 1.0, stars: 3,
363
413
  }, 'canvas');
364
414
  await postJson('/api/event', resultEnvelope);
365
415
 
@@ -369,6 +419,46 @@ describe('Bridge E2E: Atomic quiz & game endpoints', () => {
369
419
  const result = data.events.find(e => e.type === MSG_EVENT_GAME_RESULT);
370
420
  expect(result).toBeDefined();
371
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
+ });
372
462
  });
373
463
 
374
464
  // =====================================================================
@@ -386,7 +476,6 @@ describe('Bridge E2E: Long-poll /api/events/wait', () => {
386
476
  });
387
477
 
388
478
  it('resolves immediately when events are already queued', async () => {
389
- // Queue an event first
390
479
  await postJson('/api/event', makeEnvelope(MSG_EVENT_CLICK, { target: 'x' }, 'canvas'));
391
480
 
392
481
  const start = Date.now();
@@ -394,7 +483,7 @@ describe('Bridge E2E: Long-poll /api/events/wait', () => {
394
483
  const elapsed = Date.now() - start;
395
484
 
396
485
  expect(data.count).toBeGreaterThanOrEqual(1);
397
- expect(elapsed).toBeLessThan(2000); // Should resolve near-instantly
486
+ expect(elapsed).toBeLessThan(2000);
398
487
  });
399
488
 
400
489
  it('resolves when event arrives during the wait', async () => {
@@ -527,13 +616,11 @@ describe('Bridge E2E: WebSocket handshake', () => {
527
616
  it('canvas client upgrades tier to TIER_CANVAS', async () => {
528
617
  const { ws } = await connectWs('canvas');
529
618
  try {
530
- // Give server a moment to update tier
531
619
  await new Promise(r => setTimeout(r, 100));
532
620
  const { data } = await getJson('/api/tier');
533
- expect(data.tier).toBe(2); // TIER_CANVAS
621
+ expect(data.tier).toBe(2);
534
622
  } finally {
535
623
  ws.close();
536
- // Wait for disconnect processing
537
624
  await new Promise(r => setTimeout(r, 200));
538
625
  }
539
626
  });
@@ -561,16 +648,15 @@ describe('Bridge E2E: WebSocket message routing', () => {
561
648
  }
562
649
  });
563
650
 
564
- it('WS client receives visual command broadcast', async () => {
651
+ it('WS client receives visual command broadcast from /api/canvas', async () => {
565
652
  const { ws } = await connectWs('canvas');
566
653
  try {
567
654
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DIAGRAM);
568
655
 
569
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_DIAGRAM, {
570
- type: 'flow',
656
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_DIAGRAM, {
657
+ format: 'mermaid',
658
+ content: 'graph TD; A-->B',
571
659
  title: 'WS Test',
572
- nodes: [],
573
- edges: [],
574
660
  }));
575
661
 
576
662
  const msg = await msgPromise;
@@ -588,7 +674,7 @@ describe('Bridge E2E: WebSocket message routing', () => {
588
674
  const msg1Promise = waitForWsMessage(client1.ws, m => m.type === MSG_CANVAS_HTML);
589
675
  const msg2Promise = waitForWsMessage(client2.ws, m => m.type === MSG_CANVAS_HTML);
590
676
 
591
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_HTML, {
677
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_HTML, {
592
678
  html: '<h1>Multi</h1>',
593
679
  title: 'Multi Test',
594
680
  }));
@@ -615,12 +701,10 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
615
701
  expect(resp.status).toBe(200);
616
702
  expect(resp.headers.get('content-type')).toContain('text/event-stream');
617
703
 
618
- // Read the first chunk from the SSE stream
619
704
  const reader = resp.body.getReader();
620
705
  const decoder = new TextDecoder();
621
706
  let buffer = '';
622
707
 
623
- // Read until we get a complete SSE message
624
708
  while (true) {
625
709
  const { value, done } = await reader.read();
626
710
  if (done) break;
@@ -628,7 +712,6 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
628
712
  if (buffer.includes('\n\n')) break;
629
713
  }
630
714
 
631
- // Parse the SSE data line
632
715
  const dataLine = buffer.split('\n').find(l => l.startsWith('data:'));
633
716
  expect(dataLine).toBeDefined();
634
717
  const msg = JSON.parse(dataLine.replace('data:', '').trim());
@@ -639,7 +722,7 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
639
722
  }
640
723
  });
641
724
 
642
- it('SSE client receives visual command broadcasts', async () => {
725
+ it('SSE client receives visual command broadcasts from /api/canvas', async () => {
643
726
  const controller = new AbortController();
644
727
  try {
645
728
  const resp = await fetch(`${BASE_URL}/sse`, { signal: controller.signal });
@@ -647,7 +730,6 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
647
730
  const decoder = new TextDecoder();
648
731
  let buffer = '';
649
732
 
650
- // Read the initial sys:connect message
651
733
  while (true) {
652
734
  const { value, done } = await reader.read();
653
735
  if (done) break;
@@ -655,16 +737,13 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
655
737
  if (buffer.includes('\n\n')) break;
656
738
  }
657
739
 
658
- // Clear buffer after connect
659
740
  buffer = '';
660
741
 
661
- // Send a visual command
662
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_DASHBOARD, {
742
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_DASHBOARD, {
663
743
  belt: 'white',
664
744
  xp: 0,
665
745
  }));
666
746
 
667
- // Read the broadcast
668
747
  while (true) {
669
748
  const { value, done } = await reader.read();
670
749
  if (done) break;
@@ -689,27 +768,22 @@ describe('Bridge E2E: SSE /sse endpoint', () => {
689
768
  describe('Bridge E2E: Visual pipeline integration', () => {
690
769
  beforeEach(() => drainEvents());
691
770
 
692
- 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 () => {
693
772
  const { ws } = await connectWs('canvas');
694
773
  try {
695
- // Step 1: POST visual command
696
774
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DIAGRAM);
697
- await postJson('/api/visual', makeEnvelope(MSG_CANVAS_DIAGRAM, {
698
- type: 'flow',
775
+ await postJson('/api/canvas', makeEnvelope(MSG_CANVAS_DIAGRAM, {
776
+ format: 'mermaid',
777
+ content: 'graph TD; A-->B',
699
778
  title: 'Pipeline Test',
700
- nodes: [],
701
- edges: [],
702
779
  }));
703
780
 
704
- // Step 2: WS client receives it
705
781
  const received = await msgPromise;
706
782
  expect(received.type).toBe(MSG_CANVAS_DIAGRAM);
707
783
 
708
- // Step 3: Send event via WS (user interaction response)
709
784
  ws.send(JSON.stringify(makeEnvelope(MSG_EVENT_CLICK, { target: 'pipeline' }, 'canvas')));
710
785
  await new Promise(r => setTimeout(r, 100));
711
786
 
712
- // Step 4: Poll returns the event
713
787
  const { data } = await getJson('/api/events');
714
788
  expect(data.count).toBeGreaterThanOrEqual(1);
715
789
  const click = data.events.find(e => e.payload?.target === 'pipeline');
@@ -719,30 +793,26 @@ describe('Bridge E2E: Visual pipeline integration', () => {
719
793
  }
720
794
  });
721
795
 
722
- it('atomic quiz: send + WS answer → quiz response includes answer', async () => {
723
- // Connect a WS client to receive the quiz
796
+ it('atomic quiz: send canvas:quiz with await + WS answer → response includes answer', async () => {
724
797
  const { ws } = await connectWs('canvas');
725
798
  try {
726
- const quizEnvelope = makeEnvelope(MSG_CANVAS_QUIZ, {
727
- question: 'Pipeline quiz?',
728
- options: ['A', 'B'],
729
- answer: 0,
730
- });
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
+ );
731
805
 
732
- // Start quiz with timeout
733
- const quizPromise = postJson('/api/quiz?timeout=5', quizEnvelope);
806
+ const quizPromise = postJson('/api/canvas', quizEnvelope);
734
807
 
735
- // WS client receives the quiz broadcast
736
808
  const quizMsg = await waitForWsMessage(ws, m => m.type === MSG_CANVAS_QUIZ);
737
809
  expect(quizMsg.payload.question).toBe('Pipeline quiz?');
738
810
 
739
- // Send answer via HTTP (as canvas would)
740
811
  await postJson('/api/event', makeEnvelope(MSG_EVENT_QUIZ_ANSWER, {
741
812
  answer: 0,
742
813
  correct: true,
743
814
  }, 'canvas'));
744
815
 
745
- // Quiz endpoint resolves with the answer
746
816
  const { data } = await quizPromise;
747
817
  expect(data.count).toBeGreaterThanOrEqual(1);
748
818
  const answerEvent = data.events.find(e => e.type === MSG_EVENT_QUIZ_ANSWER);
@@ -754,7 +824,7 @@ describe('Bridge E2E: Visual pipeline integration', () => {
754
824
  });
755
825
 
756
826
  // =====================================================================
757
- // Group M — Chat Message Routing (bridge WS + agent-web REST)
827
+ // Group M — Chat Message Routing
758
828
  // =====================================================================
759
829
 
760
830
  describe('Bridge E2E: Chat message routing', () => {
@@ -799,7 +869,7 @@ describe('Bridge E2E: Chat message routing', () => {
799
869
  });
800
870
 
801
871
  // =====================================================================
802
- // Group N — Teaching Module Flow (via HTTP API)
872
+ // Group N — Teaching Module Flow (via unified /api/canvas)
803
873
  // =====================================================================
804
874
 
805
875
  describe('Bridge E2E: Teaching module flow', () => {
@@ -808,33 +878,34 @@ describe('Bridge E2E: Teaching module flow', () => {
808
878
  it('teaching quiz is broadcast to WS clients and answer is collected', async () => {
809
879
  const { ws } = await connectWs('canvas');
810
880
  try {
811
- // Simulate a teaching quiz being sent (as the plugin would via /api/quiz)
812
- const quizPayload = createEnvelope(MSG_CANVAS_QUIZ, {
813
- question: 'What does `git add .` do?',
814
- options: [
815
- 'Stages all files in the current directory',
816
- 'Creates a new branch',
817
- 'Pushes to remote',
818
- 'Deletes untracked files',
819
- ],
820
- answer: 0,
821
- hint: 'Think about the staging area',
822
- }, 'plugin');
823
-
824
- 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);
825
899
 
826
- // WS client receives the quiz
827
900
  const quizMsg = await waitForWsMessage(ws, m => m.type === MSG_CANVAS_QUIZ);
828
901
  expect(quizMsg.payload.question).toContain('git add');
829
902
  expect(quizMsg.payload.options).toHaveLength(4);
830
903
 
831
- // User answers the quiz via event POST (as the canvas UI would)
832
904
  await postJson('/api/event', createEnvelope(MSG_EVENT_QUIZ_ANSWER, {
833
905
  answer: 0,
834
906
  correct: true,
835
907
  }, 'canvas'));
836
908
 
837
- // Quiz resolves with the answer
838
909
  const { data } = await quizPromise;
839
910
  const answerEvent = data.events.find(e => e.type === MSG_EVENT_QUIZ_ANSWER);
840
911
  expect(answerEvent).toBeDefined();
@@ -849,8 +920,7 @@ describe('Bridge E2E: Teaching module flow', () => {
849
920
  try {
850
921
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DIAGRAM);
851
922
 
852
- // Simulate plugin sending a teaching diagram
853
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DIAGRAM, {
923
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DIAGRAM, {
854
924
  format: 'mermaid',
855
925
  code: 'graph TD; A[Working Dir]-->B[Staging]; B-->C[Repository];',
856
926
  title: 'Git Workflow',
@@ -870,8 +940,7 @@ describe('Bridge E2E: Teaching module flow', () => {
870
940
  try {
871
941
  const msgPromise = waitForWsMessage(ws, m => m.type === MSG_CANVAS_DASHBOARD);
872
942
 
873
- // Simulate progress update after completing a module step
874
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DASHBOARD, {
943
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DASHBOARD, {
875
944
  belt: 'white',
876
945
  xp: 50,
877
946
  streak: 1,
@@ -897,14 +966,13 @@ describe('Bridge E2E: Teaching module flow', () => {
897
966
  if (msg.type.startsWith('canvas:')) received.push(msg);
898
967
  });
899
968
 
900
- // Send diagram, then dashboard update (like a teaching step)
901
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DIAGRAM, {
969
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DIAGRAM, {
902
970
  format: 'mermaid',
903
971
  code: 'graph LR; A-->B;',
904
972
  title: 'Step 1',
905
973
  }, 'plugin'));
906
974
 
907
- await postJson('/api/visual', createEnvelope(MSG_CANVAS_DASHBOARD, {
975
+ await postJson('/api/canvas', createEnvelope(MSG_CANVAS_DASHBOARD, {
908
976
  belt: 'white',
909
977
  xp: 25,
910
978
  }, 'plugin'));
@@ -918,3 +986,601 @@ describe('Bridge E2E: Teaching module flow', () => {
918
986
  }
919
987
  });
920
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
+ });