@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.
- package/README.md +2 -2
- package/canvas-dist/assets/{_basePickBy-BOTBlJNd.js → _basePickBy-BovdgFIW.js} +1 -1
- package/canvas-dist/assets/_basePickBy-BtkHe2u_.js +1 -0
- package/canvas-dist/assets/_basePickBy-C0936578.js +1 -0
- package/canvas-dist/assets/_basePickBy-CE2Qvuh7.js +1 -0
- package/canvas-dist/assets/_basePickBy-DV6sX4CG.js +1 -0
- package/canvas-dist/assets/_basePickBy-DZX6ZNMT.js +1 -0
- package/canvas-dist/assets/{_baseUniq-EF6Y2_Wm.js → _baseUniq-B7dN28TM.js} +1 -1
- package/canvas-dist/assets/_baseUniq-Cl23fCdR.js +1 -0
- package/canvas-dist/assets/_baseUniq-CojWFw7B.js +1 -0
- package/canvas-dist/assets/_baseUniq-DA640BJl.js +1 -0
- package/canvas-dist/assets/_baseUniq-Ds-62CCj.js +1 -0
- package/canvas-dist/assets/_baseUniq-KG7SRw9H.js +1 -0
- package/canvas-dist/assets/{arc-C_vIirh2.js → arc-7E9FFKlC.js} +1 -1
- package/canvas-dist/assets/arc-BSMfRZtt.js +1 -0
- package/canvas-dist/assets/arc-C6nT-koR.js +1 -0
- package/canvas-dist/assets/arc-D_fOnjmo.js +1 -0
- package/canvas-dist/assets/arc-Khfvgkr3.js +1 -0
- package/canvas-dist/assets/arc-ieS-i42x.js +1 -0
- package/canvas-dist/assets/{architectureDiagram-VXUJARFQ-EvM6tQ7I.js → architectureDiagram-VXUJARFQ-DF4t6GQD.js} +1 -1
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DXgSlsio.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DiomxPB4.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DnFaxvXD.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-Dt38C0LJ.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-egbtMwua.js +36 -0
- package/canvas-dist/assets/{blockDiagram-VD42YOAC-B_rbZyqc.js → blockDiagram-VD42YOAC-CUNKQd-b.js} +1 -1
- package/canvas-dist/assets/blockDiagram-VD42YOAC-D-NiLXxd.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-Dx6Dh9gg.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-_r-PmlQy.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-bvYKZLMc.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-l85QT9Ig.js +122 -0
- package/canvas-dist/assets/{c4Diagram-YG6GDRKO-J9PHecY3.js → c4Diagram-YG6GDRKO-BWKCTyQi.js} +1 -1
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-CbXs2xzC.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-CjiS-GNK.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-D7SnLlHp.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-RTTCSVf2.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-yvqJ_AqX.js +10 -0
- package/canvas-dist/assets/channel-CSXq7GP6.js +1 -0
- package/canvas-dist/assets/channel-CvujjGiJ.js +1 -0
- package/canvas-dist/assets/channel-D959Iony.js +1 -0
- package/canvas-dist/assets/channel-DOSwCnrB.js +1 -0
- package/canvas-dist/assets/channel-sw61LzxF.js +1 -0
- package/canvas-dist/assets/channel-vZVnNhOK.js +1 -0
- package/canvas-dist/assets/{chunk-4BX2VUAB-DjcN96Mk.js → chunk-4BX2VUAB-BBjuAwXr.js} +1 -1
- package/canvas-dist/assets/chunk-4BX2VUAB-BXRNyucU.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-Bgq5Z77T.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-BuoMCMCr.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-COD5n7vg.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-K8DepKJO.js +1 -0
- package/canvas-dist/assets/{chunk-55IACEB6-CTdcUQSV.js → chunk-55IACEB6-Bic_bMrQ.js} +1 -1
- package/canvas-dist/assets/chunk-55IACEB6-DEy2QUDq.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-Dcgbmfzg.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-DfmuNm_E.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-DlQRcczm.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-p2qMY-fm.js +1 -0
- package/canvas-dist/assets/{chunk-B4BG7PRW-Dcov7eRi.js → chunk-B4BG7PRW-BpbyxBP2.js} +1 -1
- package/canvas-dist/assets/chunk-B4BG7PRW-CCPqvPrP.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-CEeDPAki.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-D2UFN_2M.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-DFI5h6HC.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-DKOiFGMU.js +165 -0
- package/canvas-dist/assets/{chunk-DI55MBZ5-DUJCBZzM.js → chunk-DI55MBZ5-BV6nHjNQ.js} +1 -1
- package/canvas-dist/assets/chunk-DI55MBZ5-CEZJmC0E.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-DOZT99Ek.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-DmC2LoG2.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-DpkcJdZP.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-fVTGx0zh.js +220 -0
- package/canvas-dist/assets/{chunk-FMBD7UC4-EfGA9ufe.js → chunk-FMBD7UC4-BOCyQpI7.js} +1 -1
- package/canvas-dist/assets/chunk-FMBD7UC4-C76FrRL8.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-CAq-btWc.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-CidVsej6.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-DPpfskdX.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-DnLtclge.js +15 -0
- package/canvas-dist/assets/{chunk-QN33PNHL-Cu6V1xBU.js → chunk-QN33PNHL-BclpCUi8.js} +1 -1
- package/canvas-dist/assets/chunk-QN33PNHL-DDUw8IU1.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-DdJFAUXw.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-DjV4jUn9.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-N-HTycqU.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-sd8p21DW.js +1 -0
- package/canvas-dist/assets/{chunk-QZHKN3VN-avF3sH_r.js → chunk-QZHKN3VN-B6mT-JkP.js} +1 -1
- package/canvas-dist/assets/chunk-QZHKN3VN-BCo8pc7x.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-C8IIu6es.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-D9FF492U.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-DWMbUjXT.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-l5FBJ77g.js +1 -0
- package/canvas-dist/assets/{chunk-TZMSLE5B-CkWW-qpk.js → chunk-TZMSLE5B-BASt-UWt.js} +1 -1
- package/canvas-dist/assets/chunk-TZMSLE5B-BCfaZWLT.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-BKIk_hBR.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-C4pt-Ir8.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-DwGlELvo.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-jJKG-WvJ.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-B7YQfPU4.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-BZ61MaHY.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CGseYor2.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CKzOc99J.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-Ce_LPjwW.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-DorPdibv.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-B7YQfPU4.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BZ61MaHY.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CGseYor2.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CKzOc99J.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-Ce_LPjwW.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-DorPdibv.js +1 -0
- package/canvas-dist/assets/clone-74KSto7H.js +1 -0
- package/canvas-dist/assets/clone-CJQgAYVe.js +1 -0
- package/canvas-dist/assets/clone-DLeTuhHE.js +1 -0
- package/canvas-dist/assets/clone-D_IHK_lQ.js +1 -0
- package/canvas-dist/assets/clone-DxMUv1L9.js +1 -0
- package/canvas-dist/assets/clone-UNKf_nED.js +1 -0
- package/canvas-dist/assets/{cose-bilkent-S5V4N54A-DDE4zf7X.js → cose-bilkent-S5V4N54A-BTyQiCkr.js} +1 -1
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-BtPAe24N.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-DIjE7V3m.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-DKL_BGvE.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-LZ4OsCLU.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-XWeJtgga.js +1 -0
- package/canvas-dist/assets/{dagre-6UL2VRFP-BD6MGb7B.js → dagre-6UL2VRFP-BJ2vcFwR.js} +1 -1
- package/canvas-dist/assets/dagre-6UL2VRFP-C1FlE5s8.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-C3BWFgl6.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-CUnx73Rf.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-Do10BY1y.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-rOZEkrsg.js +4 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-BGi_qzbq.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-C3Nv7h_j.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-CsMy-r0n.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-Dj8g7kGt.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-Dxb1w_7r.js +24 -0
- package/canvas-dist/assets/{diagram-PSM6KHXK-yyu-ytzf.js → diagram-PSM6KHXK-kVMBkEyV.js} +1 -1
- package/canvas-dist/assets/{diagram-QEK2KX5R-B_H957Uf.js → diagram-QEK2KX5R-4bsrr1WZ.js} +1 -1
- package/canvas-dist/assets/diagram-QEK2KX5R-Bv7BmKfI.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-C_FLN6hv.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-Csuk5L3z.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-D5Aszgz4.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-DX58f87l.js +43 -0
- package/canvas-dist/assets/{diagram-S2PKOQOG-DuebuBVv.js → diagram-S2PKOQOG-1Q7hwiSd.js} +1 -1
- package/canvas-dist/assets/diagram-S2PKOQOG-Bz9Vxi5V.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-CdWgZIIc.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-DBicbKFU.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-DsXKwPtU.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-L_SMHLXs.js +24 -0
- package/canvas-dist/assets/{erDiagram-Q2GNP2WA-AxqPt6IZ.js → erDiagram-Q2GNP2WA-BYu7fh6H.js} +1 -1
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-CvnQ69BF.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-D3xm-Tdm.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-DIPpD8sj.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-DNgu6dMd.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-Decm8aB4.js +60 -0
- package/canvas-dist/assets/{flowDiagram-NV44I4VS-mDhW3D3Q.js → flowDiagram-NV44I4VS-2ymk2kw2.js} +1 -1
- package/canvas-dist/assets/flowDiagram-NV44I4VS-BEPFOt6U.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-BwqXYGfK.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-CS1jax_z.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-DQz5bf7r.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-KW4T1sqF.js +162 -0
- package/canvas-dist/assets/{ganttDiagram-JELNMOA3-sA8pHJPp.js → ganttDiagram-JELNMOA3-B811prZt.js} +1 -1
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-C75pWm7X.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-CWsbo0fn.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-CbJozPBN.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-Co0cFt4c.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-I4PDqrRh.js +267 -0
- package/canvas-dist/assets/{gitGraphDiagram-V2S2FVAM-CvLzvhKr.js → gitGraphDiagram-V2S2FVAM-B-z0cLPt.js} +1 -1
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-Be40z-LF.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BejNaAVm.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BqWDYr0X.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-DSvWGY-e.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-HLYbyNJ5.js +65 -0
- package/canvas-dist/assets/{graph-BVZqMrwW.js → graph-BE8KKsdf.js} +1 -1
- package/canvas-dist/assets/graph-D6DzzszU.js +1 -0
- package/canvas-dist/assets/graph-DFR8Y_8s.js +1 -0
- package/canvas-dist/assets/graph-Da385cDY.js +1 -0
- package/canvas-dist/assets/graph-MU7gZz2B.js +1 -0
- package/canvas-dist/assets/graph-wjSBJwnf.js +1 -0
- package/canvas-dist/assets/index--ztw-8Rw.js +647 -0
- package/canvas-dist/assets/{index-CF3qc2Xb.js → index-5TpIM6B1.js} +1 -1
- package/canvas-dist/assets/index-6GBZ9nXN.css +32 -0
- package/canvas-dist/assets/index-BSswTuBk.js +11 -0
- package/canvas-dist/assets/index-BVvhMmjs.js +11 -0
- package/canvas-dist/assets/index-BY92Mj5g.js +572 -0
- package/canvas-dist/assets/index-CV7palC3.js +572 -0
- package/canvas-dist/assets/index-D9bmQGsB.js +11 -0
- package/canvas-dist/assets/index-DDIKkGv8.js +592 -0
- package/canvas-dist/assets/index-Dyo0NkPb.js +574 -0
- package/canvas-dist/assets/index-iQWajCow.js +572 -0
- package/canvas-dist/assets/index-m68YlAMU.js +11 -0
- package/canvas-dist/assets/index-mEoP57az.js +11 -0
- package/canvas-dist/assets/{infoDiagram-HS3SLOUP-D1Kg3Q9d.js → infoDiagram-HS3SLOUP--9BirqgJ.js} +1 -1
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-CSJVED2y.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-D68HIb2t.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-DK2VLGGz.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-PaFhn4yD.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-zLNG47sU.js +2 -0
- package/canvas-dist/assets/{journeyDiagram-XKPGCS4Q-D7ogbx9z.js → journeyDiagram-XKPGCS4Q-Bue2dR2X.js} +1 -1
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-CrgZfpdU.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-DUxWmkkC.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-OTFkv4pd.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-eK2_Zuu3.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-uds5Tz8D.js +139 -0
- package/canvas-dist/assets/{kanban-definition-3W4ZIXB7-CDcnICM9.js → kanban-definition-3W4ZIXB7-BETdiI7I.js} +1 -1
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-BdVh7KdN.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-Cxl8UM9S.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-DVPlx3I2.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-LtNWeoYB.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-uvhEMvyE.js +89 -0
- package/canvas-dist/assets/{layout-CuaK7i3M.js → layout-1OzszN14.js} +1 -1
- package/canvas-dist/assets/layout-CJSupFcF.js +1 -0
- package/canvas-dist/assets/layout-DFRmxN_c.js +1 -0
- package/canvas-dist/assets/layout-DSu-zk7y.js +1 -0
- package/canvas-dist/assets/layout-TGcrvApd.js +1 -0
- package/canvas-dist/assets/layout-eStc8SYK.js +1 -0
- package/canvas-dist/assets/{linear-CLSTOJ0g.js → linear-9qlE6xa7.js} +1 -1
- package/canvas-dist/assets/linear-CBfFWnLD.js +1 -0
- package/canvas-dist/assets/linear-Cv4ai8Hq.js +1 -0
- package/canvas-dist/assets/linear-DDzz65E6.js +1 -0
- package/canvas-dist/assets/linear-wbIqhwDf.js +1 -0
- package/canvas-dist/assets/linear-wyNKl76F.js +1 -0
- package/canvas-dist/assets/{mindmap-definition-VGOIOE7T-TrK7CIKt.js → mindmap-definition-VGOIOE7T-3l4YzhEM.js} +1 -1
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-B-KkpNlw.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-DHMHWgmT.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-Dqfyg4Z2.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-NeRYOzsq.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-xyu628P9.js +68 -0
- package/canvas-dist/assets/{pieDiagram-ADFJNKIX-BcIKTRbi.js → pieDiagram-ADFJNKIX-BWNzVAGj.js} +1 -1
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-Bm3PXYs-.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-BvvN7VvQ.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-BwU7AN7W.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-CHgwWCaM.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-DlZc8YOh.js +30 -0
- package/canvas-dist/assets/{quadrantDiagram-AYHSOK5B-EOHXFGoQ.js → quadrantDiagram-AYHSOK5B-B-Zd8OFp.js} +1 -1
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-B1CnJyxI.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C0Qo00b9.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C9bx3nEJ.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-UHENkiRO.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-jKfurTPU.js +7 -0
- package/canvas-dist/assets/{requirementDiagram-UZGBJVZJ-CJ8lImGs.js → requirementDiagram-UZGBJVZJ-BPpNNusD.js} +1 -1
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-BwZF1NIK.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-CaT3Frtk.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-Dfoz7R_7.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-DsrX4TT-.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-dmouSXOl.js +64 -0
- package/canvas-dist/assets/{sankeyDiagram-TZEHDZUN-4cANY87E.js → sankeyDiagram-TZEHDZUN-BEy-A1Fu.js} +1 -1
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BViMBiAQ.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BqrM-qWN.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DRkRC9qB.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DbuzKCtn.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-_aHMKbpw.js +10 -0
- package/canvas-dist/assets/{sequenceDiagram-WL72ISMW-D9HrEsci.js → sequenceDiagram-WL72ISMW-B8FOaL2Q.js} +1 -1
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-C02NQwOB.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CgyHivPj.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CzW1WaEm.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-DJhHI1pe.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-VFkpAeoG.js +145 -0
- package/canvas-dist/assets/{stateDiagram-FKZM4ZOC-qVbMjauZ.js → stateDiagram-FKZM4ZOC-BSqFX4PJ.js} +1 -1
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-BnXhhxkN.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-ClARVrvt.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-CuC6xesY.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-DcAiGjph.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-aBg0hjTp.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-8fib9ftc.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-B-DO0ZqO.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-BksbsE4k.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-C2DJCNPK.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-CeA5jba6.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-zsAyq0tK.js +1 -0
- package/canvas-dist/assets/{timeline-definition-IT6M3QCI-DDBlkydm.js → timeline-definition-IT6M3QCI-BaHdYD2h.js} +1 -1
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-Bl2hg8IM.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-CrVwLiGm.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-DrXGRjnB.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-cYAwshf6.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-flyL0y-3.js +61 -0
- package/canvas-dist/assets/{treemap-GDKQZRPO-D4a8udjO.js → treemap-GDKQZRPO-C4Hg8kJ_.js} +1 -1
- package/canvas-dist/assets/treemap-GDKQZRPO-DVY2G9qY.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-DpLWPA1z.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-Ds86cUVw.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-DwmoI6tH.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-SsGFkgVd.js +162 -0
- package/canvas-dist/assets/{xychartDiagram-PRI3JC2R-DteXAAAu.js → xychartDiagram-PRI3JC2R-B9c1iLBf.js} +1 -1
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-BpX6MPWa.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CEgW_j0p.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CSEFGEQX.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CnG4XoMc.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-Dftj3Bt3.js +7 -0
- package/canvas-dist/index.html +3 -2
- package/package.json +4 -2
- package/src/protocol.js +1 -1
- package/src/protocol.test.js +1 -1
- package/src/pty-manager.js +281 -0
- package/src/pty-manager.test.js +212 -0
- package/src/router.js +31 -1
- package/src/router.test.js +1 -1
- package/src/sdk-e2e.test.js +101 -0
- package/src/server.e2e.test.js +812 -177
- package/src/server.js +1516 -264
- package/src/session-store.js +260 -0
- package/src/session-store.test.js +235 -0
- package/src/templates.js +1 -1
- package/src/templates.test.js +1 -1
- package/src/terminal.js +3 -3
- package/src/terminal.test.js +12 -12
- package/src/visual-interceptor.js +450 -0
- package/src/visual-interceptor.test.js +943 -0
- package/src/workshop-parser.js +251 -0
- package/src/workshop-parser.test.js +179 -0
- package/templates/celebrate.html +1 -1
- package/templates/code-playground.html +1 -1
- package/templates/dashboard.html +7 -8
- package/templates/diagram-architecture.html +1 -1
- package/templates/diagram-flow.html +1 -1
- package/templates/diagram-mermaid.html +1 -1
- package/templates/game-speed-round.html +1 -1
- package/templates/quiz-drag-order.html +1 -1
- package/templates/quiz-fill-blank.html +1 -1
- package/templates/quiz-matching.html +1 -1
- package/templates/quiz-timed-choice.html +1 -1
- package/templates/welcome.html +7 -7
- package/canvas-dist/assets/channel-saCUO1KA.js +0 -1
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/clone-DXnda9BY.js +0 -1
- package/canvas-dist/assets/index-DYNtb52W.js +0 -426
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-MT16RLO4.js +0 -1
- package/src/claude-session.js +0 -414
- package/src/claude-session.test.js +0 -326
package/src/server.e2e.test.js
CHANGED
|
@@ -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
|
|
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/
|
|
270
|
+
// Group D — Unified POST /api/canvas
|
|
258
271
|
// =====================================================================
|
|
259
272
|
|
|
260
|
-
describe('Bridge E2E: POST /api/
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
nodes: [],
|
|
266
|
-
edges: [],
|
|
278
|
+
format: 'mermaid',
|
|
279
|
+
content: 'graph TD; A-->B',
|
|
267
280
|
});
|
|
268
|
-
const { status, data } = await postJson('/api/
|
|
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('
|
|
274
|
-
const envelope = makeEnvelope(
|
|
275
|
-
const { status, data } = await postJson('/api/
|
|
276
|
-
expect(status).toBe(
|
|
277
|
-
expect(data.
|
|
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('
|
|
281
|
-
const envelope = makeEnvelope(
|
|
282
|
-
const { status, data } = await postJson('/api/
|
|
283
|
-
expect(status).toBe(
|
|
284
|
-
expect(data.
|
|
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('
|
|
288
|
-
const
|
|
289
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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('
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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('
|
|
339
|
-
const envelope =
|
|
340
|
-
|
|
341
|
-
title: 'Test
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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/
|
|
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);
|
|
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', {
|
|
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
|
|
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).
|
|
552
|
+
expect(status).toBeGreaterThanOrEqual(400);
|
|
465
553
|
expect(data.error).toBeDefined();
|
|
466
554
|
});
|
|
467
555
|
|
|
468
|
-
it('POST /api/chat/stop returns
|
|
469
|
-
const { status
|
|
470
|
-
expect(status).toBe(
|
|
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);
|
|
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/
|
|
572
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
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/
|
|
700
|
-
|
|
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 →
|
|
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 =
|
|
729
|
-
|
|
730
|
-
options: ['A', 'B'],
|
|
731
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
+
});
|