@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.
- package/README.md +2 -2
- package/canvas-dist/assets/{_basePickBy-CWoeT3J7.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-Dtuvtwtn.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-YYWnrNJU.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-CegbV-RR.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-C2e_j6ry.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-rIpnAud9.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-CpZGetnU.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-L0OhcFdd.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-Cv9vsAzg.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-B3p1mU43.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-JCLAHw5x.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-C9arKEVq.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-Bs1r3d9U.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-_Ye6r84Y.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-2O2oovOj.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-gRmGLrEW.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-B7Li-xxw.js → diagram-PSM6KHXK-BGi_qzbq.js} +1 -1
- 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-kVMBkEyV.js +24 -0
- package/canvas-dist/assets/{diagram-QEK2KX5R-B_NNUAm3.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-NcK-KHaA.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-CG7dqzk3.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-CBzCj5D6.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-CHw-4qJC.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-Dqrc4wUs.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-X9Kzu-pf.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-BQFKo-II.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-CflnZPsm.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-D2gkCipQ.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-CtLLz4o8.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-CjvV_Dms.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-D3cIYHoS.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-DSgjVg-P.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-B_lYaGFj.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-DLZLTJe3.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-CZE26rhL.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-DQMRJAPV.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-BY723FEn.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-C_UdOFhy.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-DkrJqww0.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-B-6bMZqD.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-DkBhUy_D.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 +2 -1
- 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 +804 -138
- package/src/server.js +1273 -137
- 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-BzJVlie3.js +0 -1
- package/canvas-dist/assets/classDiagram-2ON5EDUG-BTs-zEmB.js +0 -1
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BTs-zEmB.js +0 -1
- package/canvas-dist/assets/clone-CXEfuXmc.js +0 -1
- package/canvas-dist/assets/index-DJ49c6u-.js +0 -426
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-DXIiFh0L.js +0 -1
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,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/
|
|
270
|
+
// Group D — Unified POST /api/canvas
|
|
257
271
|
// =====================================================================
|
|
258
272
|
|
|
259
|
-
describe('Bridge E2E: POST /api/
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
nodes: [],
|
|
265
|
-
edges: [],
|
|
278
|
+
format: 'mermaid',
|
|
279
|
+
content: 'graph TD; A-->B',
|
|
266
280
|
});
|
|
267
|
-
const { status, data } = await postJson('/api/
|
|
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('
|
|
273
|
-
const envelope = makeEnvelope(
|
|
274
|
-
const { status, data } = await postJson('/api/
|
|
275
|
-
expect(status).toBe(
|
|
276
|
-
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);
|
|
277
292
|
});
|
|
278
293
|
|
|
279
|
-
it('
|
|
280
|
-
const envelope = makeEnvelope(
|
|
281
|
-
const { status, data } = await postJson('/api/
|
|
282
|
-
expect(status).toBe(
|
|
283
|
-
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);
|
|
284
299
|
});
|
|
285
300
|
|
|
286
|
-
it('
|
|
287
|
-
const
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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('
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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('
|
|
338
|
-
const envelope =
|
|
339
|
-
|
|
340
|
-
title: 'Test
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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);
|
|
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);
|
|
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/
|
|
570
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
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/
|
|
698
|
-
|
|
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 →
|
|
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 =
|
|
727
|
-
|
|
728
|
-
options: ['A', 'B'],
|
|
729
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
+
});
|