@recombine-ai/bosun 0.16.2 → 0.17.1
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 +24 -0
- package/bin/bosun.js +153 -20
- package/bin/git-mode.js +1139 -0
- package/build/client/_app/immutable/assets/0.D9HpHezk.css +1 -0
- package/build/client/_app/immutable/assets/0.D9HpHezk.css.br +0 -0
- package/build/client/_app/immutable/assets/0.D9HpHezk.css.gz +0 -0
- package/build/client/_app/immutable/assets/{2.DtxQDOXx.css → 2.Bc0Sf3YD.css} +1 -1
- package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.br +0 -0
- package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{DneQQ7D5.js → -3ZOIHDA.js} +1 -1
- package/build/client/_app/immutable/chunks/-3ZOIHDA.js.br +0 -0
- package/build/client/_app/immutable/chunks/-3ZOIHDA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/7hV1SZxK.js +1 -0
- package/build/client/_app/immutable/chunks/7hV1SZxK.js.br +2 -0
- package/build/client/_app/immutable/chunks/7hV1SZxK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B-dTD_wm.js +1 -0
- package/build/client/_app/immutable/chunks/B-dTD_wm.js.br +0 -0
- package/build/client/_app/immutable/chunks/B-dTD_wm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{UqjCSZmA.js → B-iasAJv.js} +1 -1
- package/build/client/_app/immutable/chunks/B-iasAJv.js.br +0 -0
- package/build/client/_app/immutable/chunks/B-iasAJv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BDijIo6D.js +33 -0
- package/build/client/_app/immutable/chunks/BDijIo6D.js.br +0 -0
- package/build/client/_app/immutable/chunks/BDijIo6D.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BHB0Pj1h.js +2 -0
- package/build/client/_app/immutable/chunks/BHB0Pj1h.js.br +0 -0
- package/build/client/_app/immutable/chunks/BHB0Pj1h.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Bog028VR.js → BKfmN1Rj.js} +1 -1
- package/build/client/_app/immutable/chunks/BKfmN1Rj.js.br +0 -0
- package/build/client/_app/immutable/chunks/BKfmN1Rj.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{pPpIn6tB.js → BlFOg-Sd.js} +1 -1
- package/build/client/_app/immutable/chunks/BlFOg-Sd.js.br +0 -0
- package/build/client/_app/immutable/chunks/BlFOg-Sd.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CJcZ2GVC.js → Byg3jr1u.js} +1 -1
- package/build/client/_app/immutable/chunks/Byg3jr1u.js.br +0 -0
- package/build/client/_app/immutable/chunks/Byg3jr1u.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Bmd5zhWC.js → BzxxLgHi.js} +1 -1
- package/build/client/_app/immutable/chunks/BzxxLgHi.js.br +0 -0
- package/build/client/_app/immutable/chunks/BzxxLgHi.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CEHsomMo.js → C4UC6AgE.js} +1 -1
- package/build/client/_app/immutable/chunks/C4UC6AgE.js.br +0 -0
- package/build/client/_app/immutable/chunks/{CEHsomMo.js.gz → C4UC6AgE.js.gz} +0 -0
- package/build/client/_app/immutable/chunks/C60AG6QQ.js +1 -0
- package/build/client/_app/immutable/chunks/C60AG6QQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/C60AG6QQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CAIfVN23.js +1 -0
- package/build/client/_app/immutable/chunks/CAIfVN23.js.br +0 -0
- package/build/client/_app/immutable/chunks/CAIfVN23.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{JcG4w2gF.js → CB9iDxwI.js} +1 -1
- package/build/client/_app/immutable/chunks/CB9iDxwI.js.br +0 -0
- package/build/client/_app/immutable/chunks/CB9iDxwI.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DoD3q5w5.js → CD_KdWs9.js} +1 -1
- package/build/client/_app/immutable/chunks/CD_KdWs9.js.br +0 -0
- package/build/client/_app/immutable/chunks/CD_KdWs9.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CDy1LBR5.js +1 -0
- package/build/client/_app/immutable/chunks/CDy1LBR5.js.br +0 -0
- package/build/client/_app/immutable/chunks/CDy1LBR5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DCXjBDmY.js → CMFNmSvo.js} +1 -1
- package/build/client/_app/immutable/chunks/CMFNmSvo.js.br +0 -0
- package/build/client/_app/immutable/chunks/CMFNmSvo.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Bv1mNXs0.js → CNIn4L55.js} +1 -1
- package/build/client/_app/immutable/chunks/CNIn4L55.js.br +0 -0
- package/build/client/_app/immutable/chunks/CNIn4L55.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CUKoVL36.js +1 -0
- package/build/client/_app/immutable/chunks/CUKoVL36.js.br +0 -0
- package/build/client/_app/immutable/chunks/CUKoVL36.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CYGbCr1i.js +3 -0
- package/build/client/_app/immutable/chunks/CYGbCr1i.js.br +0 -0
- package/build/client/_app/immutable/chunks/CYGbCr1i.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CYr0FGgn.js +1 -0
- package/build/client/_app/immutable/chunks/CYr0FGgn.js.br +0 -0
- package/build/client/_app/immutable/chunks/CYr0FGgn.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DtXXy94i.js → Ce6a22qU.js} +1 -1
- package/build/client/_app/immutable/chunks/Ce6a22qU.js.br +0 -0
- package/build/client/_app/immutable/chunks/Ce6a22qU.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DfcDjAv-.js → CeoQ-OhK.js} +1 -1
- package/build/client/_app/immutable/chunks/CeoQ-OhK.js.br +0 -0
- package/build/client/_app/immutable/chunks/CeoQ-OhK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{D8mA-uZ4.js → CupXtwVS.js} +1 -1
- package/build/client/_app/immutable/chunks/CupXtwVS.js.br +0 -0
- package/build/client/_app/immutable/chunks/CupXtwVS.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{B5zf3x62.js → Cy2Uze-c.js} +1 -1
- package/build/client/_app/immutable/chunks/Cy2Uze-c.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cy2Uze-c.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Dg0A8wPA.js → D8Lmn3S5.js} +1 -1
- package/build/client/_app/immutable/chunks/D8Lmn3S5.js.br +0 -0
- package/build/client/_app/immutable/chunks/D8Lmn3S5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D8doM1Y6.js +1 -0
- package/build/client/_app/immutable/chunks/D8doM1Y6.js.br +2 -0
- package/build/client/_app/immutable/chunks/D8doM1Y6.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BHHfrjbV.js → D9BWOD7V.js} +1 -1
- package/build/client/_app/immutable/chunks/D9BWOD7V.js.br +3 -0
- package/build/client/_app/immutable/chunks/D9BWOD7V.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Cp_st-64.js → DBB88ZB7.js} +1 -1
- package/build/client/_app/immutable/chunks/DBB88ZB7.js.br +2 -0
- package/build/client/_app/immutable/chunks/DBB88ZB7.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Dm49pcMm.js → DC7lPrn4.js} +1 -1
- package/build/client/_app/immutable/chunks/DC7lPrn4.js.br +0 -0
- package/build/client/_app/immutable/chunks/DC7lPrn4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DCwWNTpf.js +9 -0
- package/build/client/_app/immutable/chunks/DCwWNTpf.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCwWNTpf.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{wcTQUnZO.js → DEnUeQNt.js} +1 -1
- package/build/client/_app/immutable/chunks/DEnUeQNt.js.br +0 -0
- package/build/client/_app/immutable/chunks/DEnUeQNt.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D_laxq3h.js +1 -0
- package/build/client/_app/immutable/chunks/D_laxq3h.js.br +0 -0
- package/build/client/_app/immutable/chunks/D_laxq3h.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Ddv5LgEc.js → DfkYlKBk.js} +1 -1
- package/build/client/_app/immutable/chunks/DfkYlKBk.js.br +0 -0
- package/build/client/_app/immutable/chunks/DfkYlKBk.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DnMS2X6M.js +2 -0
- package/build/client/_app/immutable/chunks/DnMS2X6M.js.br +0 -0
- package/build/client/_app/immutable/chunks/DnMS2X6M.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{B1HdnQRQ.js → DrE1ro9S.js} +1 -1
- package/build/client/_app/immutable/chunks/DrE1ro9S.js.br +0 -0
- package/build/client/_app/immutable/chunks/DrE1ro9S.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DzY_RPb9.js +1 -0
- package/build/client/_app/immutable/chunks/DzY_RPb9.js.br +0 -0
- package/build/client/_app/immutable/chunks/DzY_RPb9.js.gz +0 -0
- package/build/client/_app/immutable/chunks/PrCSpCPg.js +1 -0
- package/build/client/_app/immutable/chunks/PrCSpCPg.js.br +0 -0
- package/build/client/_app/immutable/chunks/PrCSpCPg.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CN555stc.js → eMofpOrR.js} +1 -1
- package/build/client/_app/immutable/chunks/eMofpOrR.js.br +0 -0
- package/build/client/_app/immutable/chunks/eMofpOrR.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.BQUuHkSR.js +2 -0
- package/build/client/_app/immutable/entry/app.BQUuHkSR.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BQUuHkSR.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DoeTycdu.js +1 -0
- package/build/client/_app/immutable/entry/start.DoeTycdu.js.br +2 -0
- package/build/client/_app/immutable/entry/start.DoeTycdu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.DBnFpwRM.js +4 -0
- package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.D-kI-fLd.js +1 -0
- package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.DWb5H7vX.js +28 -0
- package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{15.D2I0y3ys.js → 15.CGc3_07c.js} +1 -1
- package/build/client/_app/immutable/nodes/15.CGc3_07c.js.br +3 -0
- package/build/client/_app/immutable/nodes/15.CGc3_07c.js.gz +0 -0
- package/build/client/_app/immutable/nodes/17.BIba62hW.js +3 -0
- package/build/client/_app/immutable/nodes/17.BIba62hW.js.br +0 -0
- package/build/client/_app/immutable/nodes/17.BIba62hW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/18.Clxr6FwI.js +11 -0
- package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.br +0 -0
- package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.cbpnR8be.js +35 -0
- package/build/client/_app/immutable/nodes/2.cbpnR8be.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.cbpnR8be.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DsP759xk.js +1 -0
- package/build/client/_app/immutable/nodes/3.DsP759xk.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DsP759xk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.C8VxVbVn.js → 4.BIGLAIDe.js} +1 -1
- package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.BjrnKYaA.js → 5.zMHOyhDv.js} +1 -1
- package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js +1 -0
- package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.CTgEdr7L.js +2 -0
- package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.BidF5Uvq.js +2 -0
- package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/0-BZOFUIfF.js +9 -0
- package/build/server/chunks/0-BZOFUIfF.js.map +1 -0
- package/build/server/chunks/1-XwGU5VVN.js +9 -0
- package/build/server/chunks/{1-C0HdTvXW.js.map → 1-XwGU5VVN.js.map} +1 -1
- package/build/server/chunks/10-BgFSx4ib.js +9 -0
- package/build/server/chunks/10-BgFSx4ib.js.map +1 -0
- package/build/server/chunks/15-BjTTC1jf.js +9 -0
- package/build/server/chunks/{15-BQcb9C_y.js.map → 15-BjTTC1jf.js.map} +1 -1
- package/build/server/chunks/17-BEkWCfzc.js +9 -0
- package/build/server/chunks/17-BEkWCfzc.js.map +1 -0
- package/build/server/chunks/18-C-S0wKdp.js +9 -0
- package/build/server/chunks/18-C-S0wKdp.js.map +1 -0
- package/build/server/chunks/2-o-cIkIjs.js +9 -0
- package/build/server/chunks/2-o-cIkIjs.js.map +1 -0
- package/build/server/chunks/3-B-ABlyFO.js +9 -0
- package/build/server/chunks/3-B-ABlyFO.js.map +1 -0
- package/build/server/chunks/4-joAFA_Y8.js +9 -0
- package/build/server/chunks/4-joAFA_Y8.js.map +1 -0
- package/build/server/chunks/5-DIhC9k4l.js +9 -0
- package/build/server/chunks/5-DIhC9k4l.js.map +1 -0
- package/build/server/chunks/6-CMuAQIcW.js +9 -0
- package/build/server/chunks/6-CMuAQIcW.js.map +1 -0
- package/build/server/chunks/8-CbETTVRG.js +9 -0
- package/build/server/chunks/8-CbETTVRG.js.map +1 -0
- package/build/server/chunks/9-nMV90uVU.js +9 -0
- package/build/server/chunks/9-nMV90uVU.js.map +1 -0
- package/build/server/chunks/{AgentSidebar-BWBnqHnV.js → AgentSidebar-BEbSdZdN.js} +4 -4
- package/build/server/chunks/{AgentSidebar-BWBnqHnV.js.map → AgentSidebar-BEbSdZdN.js.map} +1 -1
- package/build/server/chunks/{Button-DH7gdU0s.js → Button-Dc9CR4Wh.js} +2 -2
- package/build/server/chunks/{Button-DH7gdU0s.js.map → Button-Dc9CR4Wh.js.map} +1 -1
- package/build/server/chunks/{ChatStepTrace-BivoNYOY.js → ChatStepTrace-OM96X4g8.js} +18 -7
- package/build/server/chunks/ChatStepTrace-OM96X4g8.js.map +1 -0
- package/build/server/chunks/{Dropdown-DnARl86Q.js → Dropdown-Cgd7pG-k.js} +2 -2
- package/build/server/chunks/{Dropdown-DnARl86Q.js.map → Dropdown-Cgd7pG-k.js.map} +1 -1
- package/build/server/chunks/GitControlsButton-BLcNzrDt.js +81 -0
- package/build/server/chunks/GitControlsButton-BLcNzrDt.js.map +1 -0
- package/build/server/chunks/{Icon-DP1YKCRV.js → Icon-Bogvl2n9.js} +2 -2
- package/build/server/chunks/{Icon-DP1YKCRV.js.map → Icon-Bogvl2n9.js.map} +1 -1
- package/build/server/chunks/{Input-CUJ9Cg7b.js → Input-7hpw1WLW.js} +2 -2
- package/build/server/chunks/{Input-CUJ9Cg7b.js.map → Input-7hpw1WLW.js.map} +1 -1
- package/build/server/chunks/{Modal-CGJuNpZ0.js → Modal-7IIIfFEG.js} +5 -5
- package/build/server/chunks/{Modal-CGJuNpZ0.js.map → Modal-7IIIfFEG.js.map} +1 -1
- package/build/server/chunks/{Panel-zvjxe036.js → Panel-Cj1mXx9-.js} +2 -2
- package/build/server/chunks/{Panel-zvjxe036.js.map → Panel-Cj1mXx9-.js.map} +1 -1
- package/build/server/chunks/{Tab-CUkyLqbN.js → Tab-bf4ZDs5y.js} +4 -4
- package/build/server/chunks/{Tab-CUkyLqbN.js.map → Tab-bf4ZDs5y.js.map} +1 -1
- package/build/server/chunks/{_layout.svelte-BnrWYuLv.js → _layout.svelte-C71luIwv.js} +10 -10
- package/build/server/chunks/{_layout.svelte-BnrWYuLv.js.map → _layout.svelte-C71luIwv.js.map} +1 -1
- package/build/server/chunks/{_layout.svelte-wi6o5Ecu.js → _layout.svelte-CFhHgXI_.js} +24 -18
- package/build/server/chunks/_layout.svelte-CFhHgXI_.js.map +1 -0
- package/build/server/chunks/_layout.svelte-CJd_pmkx.js +431 -0
- package/build/server/chunks/_layout.svelte-CJd_pmkx.js.map +1 -0
- package/build/server/chunks/{_layout.svelte-B88WMyk0.js → _layout.svelte-D6rhwm98.js} +445 -70
- package/build/server/chunks/_layout.svelte-D6rhwm98.js.map +1 -0
- package/build/server/chunks/{_page.svelte-CeS_21Ho.js → _page.svelte-BBvDMiTV.js} +25 -21
- package/build/server/chunks/_page.svelte-BBvDMiTV.js.map +1 -0
- package/build/server/chunks/{_page.svelte-h_J4lNT9.js → _page.svelte-BQs9dEuq.js} +8 -7
- package/build/server/chunks/{_page.svelte-h_J4lNT9.js.map → _page.svelte-BQs9dEuq.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-DhFNFMnl.js → _page.svelte-BoO2LWVX.js} +34 -14
- package/build/server/chunks/_page.svelte-BoO2LWVX.js.map +1 -0
- package/build/server/chunks/{_page.svelte-CHgQjBQj.js → _page.svelte-DUCTSdv-.js} +2 -2
- package/build/server/chunks/{_page.svelte-CHgQjBQj.js.map → _page.svelte-DUCTSdv-.js.map} +1 -1
- package/build/server/chunks/_page.svelte-FXYLKyMw.js +110 -0
- package/build/server/chunks/_page.svelte-FXYLKyMw.js.map +1 -0
- package/build/server/chunks/{_page.svelte-57pyhszV.js → _page.svelte-KA1hqKj2.js} +20 -107
- package/build/server/chunks/_page.svelte-KA1hqKj2.js.map +1 -0
- package/build/server/chunks/{_page.svelte-BSVJ2d2G.js → _page.svelte-y8CZFL8a.js} +210 -106
- package/build/server/chunks/_page.svelte-y8CZFL8a.js.map +1 -0
- package/build/server/chunks/{_page.svelte-DHSYVLoj.js → _page.svelte-yIHSIEW6.js} +21 -15
- package/build/server/chunks/_page.svelte-yIHSIEW6.js.map +1 -0
- package/build/server/chunks/_server.ts-Cp9DgLcE.js +336 -0
- package/build/server/chunks/_server.ts-Cp9DgLcE.js.map +1 -0
- package/build/server/chunks/{agents-context-CbFsZmi5.js → agents-context-owgusUPR.js} +2 -2
- package/build/server/chunks/{agents-context-CbFsZmi5.js.map → agents-context-owgusUPR.js.map} +1 -1
- package/build/server/chunks/{client-DO-hqMbT.js → client-P-7bDULt.js} +4 -4
- package/build/server/chunks/{client-DO-hqMbT.js.map → client-P-7bDULt.js.map} +1 -1
- package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js → conversation-view-memory-SdpdqTR7.js} +2 -2
- package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js.map → conversation-view-memory-SdpdqTR7.js.map} +1 -1
- package/build/server/chunks/{edit-workspace-context-tyHBPujC.js → edit-workspace-context-hanFnXR3.js} +2 -2
- package/build/server/chunks/{edit-workspace-context-tyHBPujC.js.map → edit-workspace-context-hanFnXR3.js.map} +1 -1
- package/build/server/chunks/error.svelte-yI9RS6H4.js +15 -0
- package/build/server/chunks/{error.svelte-DmKKN1RU.js.map → error.svelte-yI9RS6H4.js.map} +1 -1
- package/build/server/chunks/git-mode-context-BpNsAUGF.js +18 -0
- package/build/server/chunks/git-mode-context-BpNsAUGF.js.map +1 -0
- package/build/server/chunks/{index-LMsd1ih2.js → index-DGtbUu8Y.js} +2 -2
- package/build/server/chunks/{index-LMsd1ih2.js.map → index-DGtbUu8Y.js.map} +1 -1
- package/build/server/chunks/{index-server-BRr5mZEq.js → index-server-l8-WsVn4.js} +2 -2
- package/build/server/chunks/{index-server-BRr5mZEq.js.map → index-server-l8-WsVn4.js.map} +1 -1
- package/build/server/chunks/{index2-ClgfzZ89.js → index2-CtgLiJVO.js} +3 -3
- package/build/server/chunks/{index2-ClgfzZ89.js.map → index2-CtgLiJVO.js.map} +1 -1
- package/build/server/chunks/{index3-BdRUAffj.js → index3-4hbbCqW0.js} +2 -2
- package/build/server/chunks/{index3-BdRUAffj.js.map → index3-4hbbCqW0.js.map} +1 -1
- package/build/server/chunks/{root-4q2Pr7ck.js → root-sWPnlpi3.js} +2 -2
- package/build/server/chunks/{root-4q2Pr7ck.js.map → root-sWPnlpi3.js.map} +1 -1
- package/build/server/chunks/{source-selection-CcN1cgXc.js → source-selection-DSkaPGfZ.js} +7 -2
- package/build/server/chunks/source-selection-DSkaPGfZ.js.map +1 -0
- package/build/server/chunks/{state.svelte-Bd1i-u49.js → state.svelte-BkvaElc6.js} +3 -3
- package/build/server/chunks/{state.svelte-Bd1i-u49.js.map → state.svelte-BkvaElc6.js.map} +1 -1
- package/build/server/chunks/trpc-client-Cr8RJ4UO.js +260 -0
- package/build/server/chunks/trpc-client-Cr8RJ4UO.js.map +1 -0
- package/build/server/index.js +4 -4
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +15 -15
- package/build/server/manifest.js.map +1 -1
- package/package.json +1 -1
- package/src/backend/git-mode/command.spec.ts +172 -0
- package/src/backend/git-mode/command.ts +405 -0
- package/src/backend/git-mode/config.spec.ts +45 -0
- package/src/backend/git-mode/config.ts +90 -0
- package/src/backend/git-mode/index.ts +20 -0
- package/src/backend/git-mode/repository-write-gate.spec.ts +116 -0
- package/src/backend/git-mode/repository-write-gate.ts +260 -0
- package/src/backend/git-mode/router.ts +59 -0
- package/src/backend/git-mode/service.spec.ts +1653 -0
- package/src/backend/git-mode/service.ts +2183 -0
- package/src/backend/git-mode/types.ts +82 -0
- package/src/backend/git-mode/write-tracker.spec.ts +46 -0
- package/src/backend/git-mode/write-tracker.ts +39 -0
- package/src/backend/lib/backend-restart.spec.ts +27 -0
- package/src/backend/lib/backend-restart.ts +16 -2
- package/src/backend/lib/middleware/cors.spec.ts +67 -0
- package/src/backend/lib/middleware/cors.ts +40 -12
- package/src/backend/roleplay/agent-loader.ts +49 -0
- package/src/backend/roleplay/old-server.ts +106 -56
- package/src/backend/roleplay/prompt-edit-proposals.ts +38 -4
- package/src/backend/roleplay/prompt-file.ts +59 -0
- package/src/backend/roleplay/router.spec.ts +75 -0
- package/src/backend/roleplay/router.ts +123 -69
- package/src/backend/roleplay/suggest-file-edit.ts +160 -0
- package/src/backend/roleplay/suggest-prompt-edit.ts +9 -138
- package/src/backend/roleplay/suggest-test-fix.spec.ts +50 -0
- package/src/backend/roleplay/suggest-test-fix.ts +65 -0
- package/src/backend/roleplay/test-fix-proposal-acceptance.spec.ts +253 -0
- package/src/backend/server.ts +116 -13
- package/src/backend/tests/academy.ts +17 -3
- package/src/backend/tests/data.spec.ts +81 -2
- package/src/backend/tests/data.ts +74 -4
- package/src/backend/tests/router.ts +101 -36
- package/src/backend/tests/suggest-test-history-edit.spec.ts +20 -0
- package/src/backend/tests/suggest-test-history-edit.ts +52 -0
- package/src/backend/tests/test-edit-source.spec.ts +61 -0
- package/src/backend/tests/test-edit-source.ts +34 -0
- package/src/backend/tests/test-history-edit-proposals.ts +69 -0
- package/src/backend/tests/test-history-overrides.spec.ts +24 -0
- package/src/backend/tests/test-selection.ts +27 -0
- package/src/lib/api-telescope/source-selection.spec.ts +44 -0
- package/src/lib/api-telescope/source-selection.ts +28 -0
- package/src/lib/components/Modal.svelte +1 -1
- package/src/lib/edit/edit-workspace-context.ts +33 -3
- package/src/lib/edit/test-ai-edit.spec.ts +31 -0
- package/src/lib/edit/test-ai-edit.ts +52 -0
- package/src/lib/git-mode/BlockingGitDialog.svelte +118 -0
- package/src/lib/git-mode/GitControlsButton.svelte +96 -0
- package/src/lib/git-mode/client-admission.spec.ts +304 -0
- package/src/lib/git-mode/client-admission.ts +343 -0
- package/src/lib/git-mode/errors.ts +2 -0
- package/src/lib/git-mode/git-mode-context.ts +70 -0
- package/src/lib/git-mode/refresh-policy.spec.ts +95 -0
- package/src/lib/git-mode/refresh-policy.ts +39 -0
- package/src/lib/git-mode/status-recovery.spec.ts +86 -0
- package/src/lib/git-mode/status-recovery.ts +33 -0
- package/src/lib/monaco/bosun-theme.ts +6 -83
- package/src/lib/monaco/editor-appearance.spec.ts +84 -0
- package/src/lib/monaco/editor-appearance.ts +154 -0
- package/src/lib/monaco/editor-decorations.spec.ts +91 -0
- package/src/lib/monaco/editor-decorations.ts +133 -0
- package/src/lib/tests/assertion-types.ts +74 -0
- package/src/lib/trpc-client.ts +15 -1
- package/src/lib/ws-client.spec.ts +208 -0
- package/src/lib/ws-client.ts +196 -14
- package/src/lib/ws-message.ts +24 -0
- package/build/client/_app/immutable/assets/0.Rrc0O--C.css +0 -1
- package/build/client/_app/immutable/assets/0.Rrc0O--C.css.br +0 -0
- package/build/client/_app/immutable/assets/0.Rrc0O--C.css.gz +0 -0
- package/build/client/_app/immutable/assets/2.DtxQDOXx.css.br +0 -0
- package/build/client/_app/immutable/assets/2.DtxQDOXx.css.gz +0 -0
- package/build/client/_app/immutable/chunks/B1HdnQRQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/B1HdnQRQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B5zf3x62.js.br +0 -0
- package/build/client/_app/immutable/chunks/B5zf3x62.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BHHfrjbV.js.br +0 -2
- package/build/client/_app/immutable/chunks/BHHfrjbV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BU7aeYn4.js +0 -1
- package/build/client/_app/immutable/chunks/BU7aeYn4.js.br +0 -0
- package/build/client/_app/immutable/chunks/BU7aeYn4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BbGYcfPY.js +0 -3
- package/build/client/_app/immutable/chunks/BbGYcfPY.js.br +0 -0
- package/build/client/_app/immutable/chunks/BbGYcfPY.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bmd5zhWC.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bmd5zhWC.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BnqH7p3U.js +0 -33
- package/build/client/_app/immutable/chunks/BnqH7p3U.js.br +0 -0
- package/build/client/_app/immutable/chunks/BnqH7p3U.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bog028VR.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bog028VR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bv1mNXs0.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bv1mNXs0.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BvjXTrOz.js +0 -1
- package/build/client/_app/immutable/chunks/BvjXTrOz.js.br +0 -0
- package/build/client/_app/immutable/chunks/BvjXTrOz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C1R-s4aD.js +0 -1
- package/build/client/_app/immutable/chunks/C1R-s4aD.js.br +0 -0
- package/build/client/_app/immutable/chunks/C1R-s4aD.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CC1OcqhR.js +0 -1
- package/build/client/_app/immutable/chunks/CC1OcqhR.js.br +0 -0
- package/build/client/_app/immutable/chunks/CC1OcqhR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CEHsomMo.js.br +0 -0
- package/build/client/_app/immutable/chunks/CJcZ2GVC.js.br +0 -0
- package/build/client/_app/immutable/chunks/CJcZ2GVC.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CN555stc.js.br +0 -0
- package/build/client/_app/immutable/chunks/CN555stc.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CQg2Mme0.js +0 -9
- package/build/client/_app/immutable/chunks/CQg2Mme0.js.br +0 -0
- package/build/client/_app/immutable/chunks/CQg2Mme0.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Cp_st-64.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cp_st-64.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D8mA-uZ4.js.br +0 -0
- package/build/client/_app/immutable/chunks/D8mA-uZ4.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DCXjBDmY.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCXjBDmY.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DSLyVE1P.js +0 -1
- package/build/client/_app/immutable/chunks/DSLyVE1P.js.br +0 -0
- package/build/client/_app/immutable/chunks/DSLyVE1P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Ddv5LgEc.js.br +0 -0
- package/build/client/_app/immutable/chunks/Ddv5LgEc.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DfcDjAv-.js.br +0 -0
- package/build/client/_app/immutable/chunks/DfcDjAv-.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dg0A8wPA.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dg0A8wPA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dm49pcMm.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dm49pcMm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DneQQ7D5.js.br +0 -0
- package/build/client/_app/immutable/chunks/DneQQ7D5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DoD3q5w5.js.br +0 -0
- package/build/client/_app/immutable/chunks/DoD3q5w5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DtXXy94i.js.br +0 -0
- package/build/client/_app/immutable/chunks/DtXXy94i.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dy2CFyD6.js +0 -2
- package/build/client/_app/immutable/chunks/Dy2CFyD6.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dy2CFyD6.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DyBwTDJp.js +0 -2
- package/build/client/_app/immutable/chunks/DyBwTDJp.js.br +0 -0
- package/build/client/_app/immutable/chunks/DyBwTDJp.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ILTCveYH.js +0 -1
- package/build/client/_app/immutable/chunks/ILTCveYH.js.br +0 -0
- package/build/client/_app/immutable/chunks/ILTCveYH.js.gz +0 -0
- package/build/client/_app/immutable/chunks/JcG4w2gF.js.br +0 -0
- package/build/client/_app/immutable/chunks/JcG4w2gF.js.gz +0 -0
- package/build/client/_app/immutable/chunks/UqjCSZmA.js.br +0 -0
- package/build/client/_app/immutable/chunks/UqjCSZmA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/_YcrHZ6P.js +0 -1
- package/build/client/_app/immutable/chunks/_YcrHZ6P.js.br +0 -2
- package/build/client/_app/immutable/chunks/_YcrHZ6P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/i6ygh6h8.js +0 -1
- package/build/client/_app/immutable/chunks/i6ygh6h8.js.br +0 -2
- package/build/client/_app/immutable/chunks/i6ygh6h8.js.gz +0 -0
- package/build/client/_app/immutable/chunks/oRi68ch3.js +0 -1
- package/build/client/_app/immutable/chunks/oRi68ch3.js.br +0 -0
- package/build/client/_app/immutable/chunks/oRi68ch3.js.gz +0 -0
- package/build/client/_app/immutable/chunks/pPpIn6tB.js.br +0 -0
- package/build/client/_app/immutable/chunks/pPpIn6tB.js.gz +0 -0
- package/build/client/_app/immutable/chunks/r0sHOWrM.js +0 -1
- package/build/client/_app/immutable/chunks/r0sHOWrM.js.br +0 -0
- package/build/client/_app/immutable/chunks/r0sHOWrM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/wcTQUnZO.js.br +0 -0
- package/build/client/_app/immutable/chunks/wcTQUnZO.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.DdO0gkPg.js +0 -2
- package/build/client/_app/immutable/entry/app.DdO0gkPg.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DdO0gkPg.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.eAjDq0c9.js +0 -1
- package/build/client/_app/immutable/entry/start.eAjDq0c9.js.br +0 -2
- package/build/client/_app/immutable/entry/start.eAjDq0c9.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.nG3DdvIX.js +0 -3
- package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js +0 -1
- package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.dm2dgy31.js +0 -13
- package/build/client/_app/immutable/nodes/10.dm2dgy31.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.dm2dgy31.js.gz +0 -0
- package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.br +0 -0
- package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.gz +0 -0
- package/build/client/_app/immutable/nodes/17.CKxGp0E7.js +0 -3
- package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.br +0 -0
- package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.gz +0 -0
- package/build/client/_app/immutable/nodes/18.DHJxyQpw.js +0 -11
- package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.br +0 -0
- package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DesR2keg.js +0 -35
- package/build/client/_app/immutable/nodes/2.DesR2keg.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DesR2keg.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BUKrXGR_.js +0 -1
- package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.C_hEzPrU.js +0 -1
- package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.BTYaOiDC.js +0 -2
- package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.DVspuDYY.js +0 -2
- package/build/client/_app/immutable/nodes/9.DVspuDYY.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.DVspuDYY.js.gz +0 -0
- package/build/server/chunks/0-D_duQDWy.js +0 -9
- package/build/server/chunks/0-D_duQDWy.js.map +0 -1
- package/build/server/chunks/1-C0HdTvXW.js +0 -9
- package/build/server/chunks/10-DcfhCR4R.js +0 -9
- package/build/server/chunks/10-DcfhCR4R.js.map +0 -1
- package/build/server/chunks/15-BQcb9C_y.js +0 -9
- package/build/server/chunks/17-DuVCTioj.js +0 -9
- package/build/server/chunks/17-DuVCTioj.js.map +0 -1
- package/build/server/chunks/18-CT0VFW_p.js +0 -9
- package/build/server/chunks/18-CT0VFW_p.js.map +0 -1
- package/build/server/chunks/2-DgTzKQAP.js +0 -9
- package/build/server/chunks/2-DgTzKQAP.js.map +0 -1
- package/build/server/chunks/3-eQMSMzXP.js +0 -9
- package/build/server/chunks/3-eQMSMzXP.js.map +0 -1
- package/build/server/chunks/4-OaAJtDqP.js +0 -9
- package/build/server/chunks/4-OaAJtDqP.js.map +0 -1
- package/build/server/chunks/5-CBdMutbv.js +0 -9
- package/build/server/chunks/5-CBdMutbv.js.map +0 -1
- package/build/server/chunks/6-FoEi7ggm.js +0 -9
- package/build/server/chunks/6-FoEi7ggm.js.map +0 -1
- package/build/server/chunks/8-B44yRkfX.js +0 -9
- package/build/server/chunks/8-B44yRkfX.js.map +0 -1
- package/build/server/chunks/9-eUjFpHzI.js +0 -9
- package/build/server/chunks/9-eUjFpHzI.js.map +0 -1
- package/build/server/chunks/ChatStepTrace-BivoNYOY.js.map +0 -1
- package/build/server/chunks/_layout.svelte-B-0DHjLB.js +0 -45
- package/build/server/chunks/_layout.svelte-B-0DHjLB.js.map +0 -1
- package/build/server/chunks/_layout.svelte-B88WMyk0.js.map +0 -1
- package/build/server/chunks/_layout.svelte-wi6o5Ecu.js.map +0 -1
- package/build/server/chunks/_page.svelte-57pyhszV.js.map +0 -1
- package/build/server/chunks/_page.svelte-BSVJ2d2G.js.map +0 -1
- package/build/server/chunks/_page.svelte-BoHt2cpr.js +0 -61
- package/build/server/chunks/_page.svelte-BoHt2cpr.js.map +0 -1
- package/build/server/chunks/_page.svelte-CeS_21Ho.js.map +0 -1
- package/build/server/chunks/_page.svelte-DHSYVLoj.js.map +0 -1
- package/build/server/chunks/_page.svelte-DhFNFMnl.js.map +0 -1
- package/build/server/chunks/_server.ts-D_eo_OAK.js +0 -139
- package/build/server/chunks/_server.ts-D_eo_OAK.js.map +0 -1
- package/build/server/chunks/error.svelte-DmKKN1RU.js +0 -15
- package/build/server/chunks/source-selection-CcN1cgXc.js.map +0 -1
- package/build/server/chunks/trpc-client-D8L6vWf4.js +0 -14
- package/build/server/chunks/trpc-client-D8L6vWf4.js.map +0 -1
|
@@ -0,0 +1,1653 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { CommandError, runCommand, type CommandRunner } from './command'
|
|
11
|
+
import { RepositoryWriteGate } from './repository-write-gate'
|
|
12
|
+
import { createGitModeService } from './service'
|
|
13
|
+
import type { GitModeConfig, GitModeService, GitModeStatus } from './types'
|
|
14
|
+
|
|
15
|
+
const execFileAsync = promisify(execFile)
|
|
16
|
+
const temporaryDirectories: string[] = []
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await Promise.all(
|
|
20
|
+
temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe.sequential('Git mode service', () => {
|
|
25
|
+
it('validates the repository before asking browsers to flush', async () => {
|
|
26
|
+
const fixture = await createFixture()
|
|
27
|
+
await git(fixture.project, ['switch', '-c', 'unexpected-branch'])
|
|
28
|
+
const settleWrites = vi.fn(async () => {})
|
|
29
|
+
const service = createTestService(fixture, { settleWrites })
|
|
30
|
+
|
|
31
|
+
const status = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
32
|
+
|
|
33
|
+
expect(status.phase).toBe('fatal')
|
|
34
|
+
expect(status.operationId).toBeNull()
|
|
35
|
+
expect(status.tabNeedsFlush).toBe(false)
|
|
36
|
+
expect(status.fatal?.kind).toBe('branch-mismatch')
|
|
37
|
+
expect(settleWrites).not.toHaveBeenCalled()
|
|
38
|
+
await service.dispose()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('serializes ancillary Git reads ahead of activation validation', async () => {
|
|
42
|
+
const fixture = await createFixture()
|
|
43
|
+
let releaseStatus!: () => void
|
|
44
|
+
let markStatusStarted!: () => void
|
|
45
|
+
const statusStarted = new Promise<void>((resolve) => (markStatusStarted = resolve))
|
|
46
|
+
const statusRelease = new Promise<void>((resolve) => (releaseStatus = resolve))
|
|
47
|
+
let activeCommands = 0
|
|
48
|
+
let maximumActiveCommands = 0
|
|
49
|
+
const observedRunner: CommandRunner = async (executable, args, options) => {
|
|
50
|
+
activeCommands += 1
|
|
51
|
+
maximumActiveCommands = Math.max(maximumActiveCommands, activeCommands)
|
|
52
|
+
try {
|
|
53
|
+
if (executable === 'git' && args.includes('status')) {
|
|
54
|
+
markStatusStarted()
|
|
55
|
+
await statusRelease
|
|
56
|
+
}
|
|
57
|
+
return await runCommand(executable, args, options)
|
|
58
|
+
} finally {
|
|
59
|
+
activeCommands -= 1
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const service = createTestService(fixture, { runCommand: observedRunner })
|
|
63
|
+
|
|
64
|
+
const status = service.status({ tabId: 'tab-a' })
|
|
65
|
+
await statusStarted
|
|
66
|
+
const activation = service.activate({ tabId: 'tab-b', reason: 'page-open' })
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
68
|
+
expect(maximumActiveCommands).toBe(1)
|
|
69
|
+
releaseStatus()
|
|
70
|
+
await status
|
|
71
|
+
expect((await activation).phase).toBe('preparing')
|
|
72
|
+
expect(maximumActiveCommands).toBe(1)
|
|
73
|
+
await service.dispose()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('finishes shutdown cleanly once an in-flight repository operation has settled', async () => {
|
|
77
|
+
const fixture = await createFixture()
|
|
78
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
79
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
80
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
81
|
+
await git(fixture.collaborator, ['push'])
|
|
82
|
+
let markInstallStarted!: () => void
|
|
83
|
+
let releaseInstall!: () => void
|
|
84
|
+
const installStarted = new Promise<void>((resolve) => (markInstallStarted = resolve))
|
|
85
|
+
const installRelease = new Promise<void>((resolve) => (releaseInstall = resolve))
|
|
86
|
+
const service = createTestService(fixture, {
|
|
87
|
+
installProject: async () => {
|
|
88
|
+
markInstallStarted()
|
|
89
|
+
await installRelease
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
93
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
94
|
+
await installStarted
|
|
95
|
+
|
|
96
|
+
const disposal = service.dispose()
|
|
97
|
+
releaseInstall()
|
|
98
|
+
await expect(disposal).resolves.toBeUndefined()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('locks writers, drains admitted backend work, and revalidates before Git mutation', async () => {
|
|
102
|
+
const fixture = await createFixture()
|
|
103
|
+
let releaseWrites!: () => void
|
|
104
|
+
const settleWrites = vi.fn(() => new Promise<void>((resolve) => (releaseWrites = resolve)))
|
|
105
|
+
const installProject = vi.fn(async () => {})
|
|
106
|
+
const service = createTestService(fixture, { settleWrites, installProject })
|
|
107
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
108
|
+
await service.flushAck({
|
|
109
|
+
tabId: 'tab-a',
|
|
110
|
+
operationId: started.operationId!,
|
|
111
|
+
stage: 'preflight'
|
|
112
|
+
})
|
|
113
|
+
await git(fixture.project, ['switch', '-c', 'changed-during-preparation'])
|
|
114
|
+
const locked = await service.flushAck({
|
|
115
|
+
tabId: 'tab-a',
|
|
116
|
+
operationId: started.operationId!,
|
|
117
|
+
stage: 'browser-writes'
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(locked.phase).toBe('flushing')
|
|
121
|
+
expect(service.isUsageBlocked()).toBe(true)
|
|
122
|
+
expect(settleWrites).toHaveBeenCalledOnce()
|
|
123
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
124
|
+
|
|
125
|
+
releaseWrites()
|
|
126
|
+
const failed = await waitForPhase(service, 'tab-a', ['fatal'])
|
|
127
|
+
expect(failed.fatal?.kind).toBe('branch-mismatch')
|
|
128
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
129
|
+
await service.dispose()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('completes an unchanged activation without install, restart, reload, or a closed write gate', async () => {
|
|
133
|
+
const fixture = await createFixture()
|
|
134
|
+
const gateConfig = {
|
|
135
|
+
enabled: true,
|
|
136
|
+
projectDir: fixture.project,
|
|
137
|
+
leaseId: 'test-lease',
|
|
138
|
+
launchId: fixture.config.launchId
|
|
139
|
+
}
|
|
140
|
+
const backendGate = new RepositoryWriteGate(gateConfig)
|
|
141
|
+
const frontendGate = new RepositoryWriteGate(gateConfig)
|
|
142
|
+
await backendGate.initializeBackend()
|
|
143
|
+
|
|
144
|
+
const installProject = vi.fn(async () => {})
|
|
145
|
+
const scheduleRestart = vi.fn(() => true)
|
|
146
|
+
const resumeWrites = vi.fn(() => backendGate.unlock())
|
|
147
|
+
const service = createTestService(fixture, {
|
|
148
|
+
installProject,
|
|
149
|
+
scheduleRestart,
|
|
150
|
+
settleWrites: () => backendGate.lockAndDrain(),
|
|
151
|
+
resumeWrites,
|
|
152
|
+
now: () => 1_000
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
156
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
157
|
+
const finished = await waitForOperationEnd(service, 'tab-a')
|
|
158
|
+
|
|
159
|
+
expect(finished.phase).toBe('idle')
|
|
160
|
+
expect(finished.operationId).toBeNull()
|
|
161
|
+
expect(finished.fatal).toBeNull()
|
|
162
|
+
expect(finished.reloadToken).toBeNull()
|
|
163
|
+
expect(finished.hasChanges).toBe(false)
|
|
164
|
+
expect(service.isUsageBlocked()).toBe(false)
|
|
165
|
+
expect(resumeWrites).toHaveBeenCalledOnce()
|
|
166
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
167
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
168
|
+
await expect(frontendGate.withWriteAdmission(async () => 'written')).resolves.toBe('written')
|
|
169
|
+
|
|
170
|
+
const persisted = JSON.parse(await readFile(fixture.config.runtimeStatePath, 'utf8'))
|
|
171
|
+
expect(persisted.firstActivationCompleted).toBe(true)
|
|
172
|
+
expect(persisted.lastCompletedActivationAt).toBe(1_000)
|
|
173
|
+
expect((await service.activate({ tabId: 'tab-a', reason: 'focus' })).operationId).toBeNull()
|
|
174
|
+
await service.dispose()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('preserves dirty status when an unchanged activation can pull without saving', async () => {
|
|
178
|
+
const fixture = await createFixture()
|
|
179
|
+
await writeFile(path.join(fixture.project, 'local-draft.txt'), 'draft\n')
|
|
180
|
+
const installProject = vi.fn(async () => {})
|
|
181
|
+
const scheduleRestart = vi.fn(() => true)
|
|
182
|
+
const resumeWrites = vi.fn(async () => {})
|
|
183
|
+
const service = createTestService(fixture, {
|
|
184
|
+
installProject,
|
|
185
|
+
scheduleRestart,
|
|
186
|
+
resumeWrites
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
expect((await service.status({ tabId: 'tab-a' })).hasChanges).toBe(true)
|
|
190
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
191
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
192
|
+
const finished = await waitForOperationEnd(service, 'tab-a')
|
|
193
|
+
|
|
194
|
+
expect(finished.phase).toBe('idle')
|
|
195
|
+
expect(finished.hasChanges).toBe(true)
|
|
196
|
+
expect(finished.reloadToken).toBeNull()
|
|
197
|
+
expect(resumeWrites).toHaveBeenCalledOnce()
|
|
198
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
199
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
200
|
+
await service.dispose()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('keeps writes blocked if an unchanged activation cannot release its gate', async () => {
|
|
204
|
+
const fixture = await createFixture()
|
|
205
|
+
const installProject = vi.fn(async () => {})
|
|
206
|
+
const scheduleRestart = vi.fn(() => true)
|
|
207
|
+
const service = createTestService(fixture, {
|
|
208
|
+
installProject,
|
|
209
|
+
scheduleRestart,
|
|
210
|
+
resumeWrites: async () => {
|
|
211
|
+
throw new Error('unlock failed')
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
216
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
217
|
+
const finished = await waitForPhase(service, 'tab-a', ['fatal'])
|
|
218
|
+
|
|
219
|
+
expect(finished.fatal?.kind).toBe('general')
|
|
220
|
+
expect(service.isUsageBlocked()).toBe(true)
|
|
221
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
222
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
223
|
+
await service.dispose()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('relocks writes when shutdown begins during no-op gate release', async () => {
|
|
227
|
+
const fixture = await createFixture()
|
|
228
|
+
let markResumeStarted!: () => void
|
|
229
|
+
let releaseResume!: () => void
|
|
230
|
+
const resumeStarted = new Promise<void>((resolve) => (markResumeStarted = resolve))
|
|
231
|
+
const resumeRelease = new Promise<void>((resolve) => (releaseResume = resolve))
|
|
232
|
+
const settleWrites = vi.fn(async () => {})
|
|
233
|
+
const installProject = vi.fn(async () => {})
|
|
234
|
+
const scheduleRestart = vi.fn(() => true)
|
|
235
|
+
const service = createTestService(fixture, {
|
|
236
|
+
installProject,
|
|
237
|
+
scheduleRestart,
|
|
238
|
+
settleWrites,
|
|
239
|
+
resumeWrites: async () => {
|
|
240
|
+
markResumeStarted()
|
|
241
|
+
await resumeRelease
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
246
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
247
|
+
await resumeStarted
|
|
248
|
+
const disposal = service.dispose()
|
|
249
|
+
releaseResume()
|
|
250
|
+
await disposal
|
|
251
|
+
|
|
252
|
+
expect(settleWrites).toHaveBeenCalledTimes(2)
|
|
253
|
+
expect(service.isUsageBlocked()).toBe(true)
|
|
254
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
255
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('installs, restarts, and reloads after a fast-forward pull changes project files', async () => {
|
|
259
|
+
const fixture = await createFixture()
|
|
260
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
261
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
262
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
263
|
+
await git(fixture.collaborator, ['push'])
|
|
264
|
+
const installProject = vi.fn(async () => {})
|
|
265
|
+
const scheduleRestart = vi.fn(() => true)
|
|
266
|
+
const resumeWrites = vi.fn(async () => {})
|
|
267
|
+
const service = createTestService(fixture, {
|
|
268
|
+
installProject,
|
|
269
|
+
scheduleRestart,
|
|
270
|
+
resumeWrites
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
274
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
275
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
276
|
+
|
|
277
|
+
expect(finished.phase).toBe('restarting')
|
|
278
|
+
expect(finished.reloadToken).not.toBeNull()
|
|
279
|
+
expect(installProject).toHaveBeenCalledOnce()
|
|
280
|
+
expect(scheduleRestart).toHaveBeenCalledWith(76)
|
|
281
|
+
expect(resumeWrites).not.toHaveBeenCalled()
|
|
282
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(
|
|
283
|
+
await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])
|
|
284
|
+
)
|
|
285
|
+
await service.dispose()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('merges a release update into personal and pushes it without moving the local release ref', async () => {
|
|
289
|
+
const fixture = await createFixture()
|
|
290
|
+
const localCurrentHead = await gitText(fixture.project, ['rev-parse', 'next-release'])
|
|
291
|
+
await switchCollaboratorToCurrent(fixture)
|
|
292
|
+
await writeFile(path.join(fixture.collaborator, 'release.txt'), 'release\n')
|
|
293
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
294
|
+
await git(fixture.collaborator, ['commit', '-m', 'release work'])
|
|
295
|
+
await git(fixture.collaborator, ['push'])
|
|
296
|
+
const currentHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
297
|
+
const installProject = vi.fn(async () => {})
|
|
298
|
+
const scheduleRestart = vi.fn(() => true)
|
|
299
|
+
const service = createTestService(fixture, { installProject, scheduleRestart })
|
|
300
|
+
|
|
301
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
302
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
303
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
304
|
+
|
|
305
|
+
expect(finished.fatal).toBeNull()
|
|
306
|
+
expect(finished.phase).toBe('restarting')
|
|
307
|
+
expect(await readFile(path.join(fixture.project, 'release.txt'), 'utf8')).toBe('release\n')
|
|
308
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
309
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(currentHead)
|
|
310
|
+
expect(await gitText(fixture.project, ['rev-parse', 'origin/next-release'])).toBe(currentHead)
|
|
311
|
+
expect(await gitText(fixture.project, ['rev-parse', 'next-release'])).toBe(localCurrentHead)
|
|
312
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(currentHead)
|
|
313
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'next-release'])).toBe(currentHead)
|
|
314
|
+
expect(installProject).toHaveBeenCalledOnce()
|
|
315
|
+
expect(scheduleRestart).toHaveBeenCalledWith(76)
|
|
316
|
+
await service.dispose()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('does not restart when a release update changes history but not project files', async () => {
|
|
320
|
+
const fixture = await createFixture()
|
|
321
|
+
await switchCollaboratorToCurrent(fixture)
|
|
322
|
+
await git(fixture.collaborator, ['commit', '--allow-empty', '-m', 'metadata only'])
|
|
323
|
+
await git(fixture.collaborator, ['push'])
|
|
324
|
+
const currentHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
325
|
+
const installProject = vi.fn(async () => {})
|
|
326
|
+
const scheduleRestart = vi.fn(() => true)
|
|
327
|
+
const resumeWrites = vi.fn(async () => {})
|
|
328
|
+
const service = createTestService(fixture, {
|
|
329
|
+
installProject,
|
|
330
|
+
scheduleRestart,
|
|
331
|
+
resumeWrites
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
335
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
336
|
+
const finished = await waitForOperationEnd(service, 'tab-a')
|
|
337
|
+
|
|
338
|
+
expect(finished.phase).toBe('idle')
|
|
339
|
+
expect(finished.reloadToken).toBeNull()
|
|
340
|
+
expect(finished.canPrepareRelease).toBe(false)
|
|
341
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(currentHead)
|
|
342
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(currentHead)
|
|
343
|
+
expect(resumeWrites).toHaveBeenCalledOnce()
|
|
344
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
345
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
346
|
+
await service.dispose()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('recovers an interrupted content-changing pull before reopening Bosun', async () => {
|
|
350
|
+
const fixture = await createFixture()
|
|
351
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
352
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
353
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
354
|
+
await git(fixture.collaborator, ['push'])
|
|
355
|
+
let interruptPull = true
|
|
356
|
+
const runner: CommandRunner = async (executable, args, options) => {
|
|
357
|
+
const result = await runCommand(executable, args, options)
|
|
358
|
+
if (interruptPull && executable === 'git' && args.includes('pull')) {
|
|
359
|
+
interruptPull = false
|
|
360
|
+
throw new Error('simulated shutdown after pull')
|
|
361
|
+
}
|
|
362
|
+
return result
|
|
363
|
+
}
|
|
364
|
+
const interrupted = createTestService(fixture, { runCommand: runner })
|
|
365
|
+
const started = await interrupted.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
366
|
+
await acknowledgePreparationAndFlush(interrupted, 'tab-a', started.operationId!)
|
|
367
|
+
expect((await waitForPhase(interrupted, 'tab-a', ['fatal'])).fatal?.kind).toBe('general')
|
|
368
|
+
expect(await readFile(path.join(fixture.project, 'remote.txt'), 'utf8')).toBe('remote\n')
|
|
369
|
+
await interrupted.dispose()
|
|
370
|
+
|
|
371
|
+
fixture.config.launchId = 'recovery-launch'
|
|
372
|
+
const installProject = vi.fn(async () => {})
|
|
373
|
+
const scheduleRestart = vi.fn(() => true)
|
|
374
|
+
const resumeWrites = vi.fn(async () => {})
|
|
375
|
+
const recovered = createTestService(fixture, {
|
|
376
|
+
installProject,
|
|
377
|
+
scheduleRestart,
|
|
378
|
+
resumeWrites
|
|
379
|
+
})
|
|
380
|
+
const recovering = await recovered.status({ tabId: 'tab-b' })
|
|
381
|
+
|
|
382
|
+
expect(recovering.phase).toBe('restarting')
|
|
383
|
+
expect(recovering.reloadToken).not.toBeNull()
|
|
384
|
+
expect(installProject).toHaveBeenCalledOnce()
|
|
385
|
+
expect(scheduleRestart).toHaveBeenCalledWith(76)
|
|
386
|
+
expect(resumeWrites).not.toHaveBeenCalled()
|
|
387
|
+
await recovered.dispose()
|
|
388
|
+
|
|
389
|
+
const managedRestartInstall = vi.fn(async () => {})
|
|
390
|
+
const managedRestartResume = vi.fn(async () => {})
|
|
391
|
+
const managedRestart = createTestService(fixture, {
|
|
392
|
+
installProject: managedRestartInstall,
|
|
393
|
+
resumeWrites: managedRestartResume
|
|
394
|
+
})
|
|
395
|
+
const ready = await managedRestart.status({ tabId: 'tab-c' })
|
|
396
|
+
|
|
397
|
+
expect(ready.phase).toBe('idle')
|
|
398
|
+
expect(ready.reloadToken).toBe(recovering.reloadToken)
|
|
399
|
+
expect(managedRestartInstall).not.toHaveBeenCalled()
|
|
400
|
+
expect(managedRestartResume).toHaveBeenCalledOnce()
|
|
401
|
+
await expect(
|
|
402
|
+
readFile(path.join(fixture.project, '.git', 'bosun-content-refresh.json'))
|
|
403
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
404
|
+
await managedRestart.dispose()
|
|
405
|
+
}, 15_000)
|
|
406
|
+
|
|
407
|
+
it('clears an interrupted pull record when project files stayed identical', async () => {
|
|
408
|
+
const fixture = await createFixture()
|
|
409
|
+
await git(fixture.collaborator, ['commit', '--allow-empty', '-m', 'metadata only'])
|
|
410
|
+
await git(fixture.collaborator, ['push'])
|
|
411
|
+
let interruptPull = true
|
|
412
|
+
const runner: CommandRunner = async (executable, args, options) => {
|
|
413
|
+
const result = await runCommand(executable, args, options)
|
|
414
|
+
if (interruptPull && executable === 'git' && args.includes('pull')) {
|
|
415
|
+
interruptPull = false
|
|
416
|
+
throw new Error('simulated shutdown after pull')
|
|
417
|
+
}
|
|
418
|
+
return result
|
|
419
|
+
}
|
|
420
|
+
const interrupted = createTestService(fixture, { runCommand: runner })
|
|
421
|
+
const started = await interrupted.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
422
|
+
await acknowledgePreparationAndFlush(interrupted, 'tab-a', started.operationId!)
|
|
423
|
+
expect((await waitForPhase(interrupted, 'tab-a', ['fatal'])).fatal?.kind).toBe('general')
|
|
424
|
+
await interrupted.dispose()
|
|
425
|
+
|
|
426
|
+
fixture.config.launchId = 'unchanged-recovery-launch'
|
|
427
|
+
const installProject = vi.fn(async () => {})
|
|
428
|
+
const scheduleRestart = vi.fn(() => true)
|
|
429
|
+
const resumeWrites = vi.fn(async () => {})
|
|
430
|
+
const recovered = createTestService(fixture, {
|
|
431
|
+
installProject,
|
|
432
|
+
scheduleRestart,
|
|
433
|
+
resumeWrites
|
|
434
|
+
})
|
|
435
|
+
const ready = await recovered.status({ tabId: 'tab-b' })
|
|
436
|
+
|
|
437
|
+
expect(ready.phase).toBe('idle')
|
|
438
|
+
expect(ready.reloadToken).toBeNull()
|
|
439
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
440
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
441
|
+
expect(resumeWrites).toHaveBeenCalledOnce()
|
|
442
|
+
await expect(
|
|
443
|
+
readFile(path.join(fixture.project, '.git', 'bosun-content-refresh.json'))
|
|
444
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
445
|
+
await recovered.dispose()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('installs dependencies without running package scripts', async () => {
|
|
449
|
+
const fixture = await createFixture()
|
|
450
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
451
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
452
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
453
|
+
await git(fixture.collaborator, ['push'])
|
|
454
|
+
const installCommands: string[][] = []
|
|
455
|
+
const observedRunner: CommandRunner = async (executable, args, options) => {
|
|
456
|
+
if (executable === 'pnpm') {
|
|
457
|
+
installCommands.push([...args])
|
|
458
|
+
return { stdout: '', stderr: '', exitCode: 0 }
|
|
459
|
+
}
|
|
460
|
+
return runCommand(executable, args, options)
|
|
461
|
+
}
|
|
462
|
+
const service = createGitModeService(fixture.config, {
|
|
463
|
+
runCommand: observedRunner,
|
|
464
|
+
scheduleRestart: () => true,
|
|
465
|
+
watchFileChanges: false,
|
|
466
|
+
logError: () => {}
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
470
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
471
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
472
|
+
|
|
473
|
+
expect(finished.phase).toBe('restarting')
|
|
474
|
+
expect(installCommands).toEqual([
|
|
475
|
+
['install', '--frozen-lockfile', '--ignore-scripts', '--ignore-pnpmfile']
|
|
476
|
+
])
|
|
477
|
+
await service.dispose()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('rejects repositories containing tracked submodules before browser preparation', async () => {
|
|
481
|
+
const fixture = await createFixture()
|
|
482
|
+
const head = await gitText(fixture.project, ['rev-parse', 'HEAD'])
|
|
483
|
+
await git(fixture.project, [
|
|
484
|
+
'update-index',
|
|
485
|
+
'--add',
|
|
486
|
+
'--cacheinfo',
|
|
487
|
+
`160000,${head},vendor/example`
|
|
488
|
+
])
|
|
489
|
+
const service = createTestService(fixture)
|
|
490
|
+
|
|
491
|
+
const status = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
492
|
+
|
|
493
|
+
expect(status.phase).toBe('fatal')
|
|
494
|
+
expect(status.fatal?.message).toContain('tracked submodules')
|
|
495
|
+
expect(status.tabNeedsFlush).toBe(false)
|
|
496
|
+
await service.dispose()
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('uses explicit merge semantics, labels the merge, bypasses hooks, and pushes once', async () => {
|
|
500
|
+
const fixture = await createFixture()
|
|
501
|
+
await writeFile(path.join(fixture.project, 'local.txt'), 'local\n')
|
|
502
|
+
await git(fixture.project, ['add', '-A'])
|
|
503
|
+
await git(fixture.project, ['commit', '-m', 'local work'])
|
|
504
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
505
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
506
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
507
|
+
await git(fixture.collaborator, ['push'])
|
|
508
|
+
await setHostilePullConfiguration(fixture.project)
|
|
509
|
+
await installFailingHooks(fixture.project)
|
|
510
|
+
|
|
511
|
+
const installProject = vi.fn(async () => {})
|
|
512
|
+
const scheduleRestart = vi.fn(() => true)
|
|
513
|
+
const service = createTestService(fixture, { installProject, scheduleRestart })
|
|
514
|
+
|
|
515
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
516
|
+
expect(started.phase).toBe('preparing')
|
|
517
|
+
expect(started.tabNeedsFlush).toBe(true)
|
|
518
|
+
expect(service.isUsageBlocked()).toBe(false)
|
|
519
|
+
const flushing = await service.flushAck({
|
|
520
|
+
tabId: 'tab-a',
|
|
521
|
+
operationId: started.operationId!,
|
|
522
|
+
stage: 'preflight'
|
|
523
|
+
})
|
|
524
|
+
expect(flushing.phase).toBe('preparing')
|
|
525
|
+
expect(flushing.preparationStage).toBe('browser-writes')
|
|
526
|
+
expect(flushing.tabNeedsFlush).toBe(true)
|
|
527
|
+
expect(service.isUsageBlocked()).toBe(false)
|
|
528
|
+
await service.flushAck({
|
|
529
|
+
tabId: 'tab-a',
|
|
530
|
+
operationId: started.operationId!,
|
|
531
|
+
stage: 'browser-writes'
|
|
532
|
+
})
|
|
533
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
534
|
+
|
|
535
|
+
expect(finished.fatal).toBeNull()
|
|
536
|
+
expect(finished.phase).toBe('restarting')
|
|
537
|
+
expect(service.isUsageBlocked()).toBe(true)
|
|
538
|
+
expect(installProject).toHaveBeenCalledOnce()
|
|
539
|
+
expect(scheduleRestart).toHaveBeenCalledWith(76)
|
|
540
|
+
expect(await gitText(fixture.project, ['log', '-1', '--pretty=%s'])).toBe(
|
|
541
|
+
'[BOSUN] [Merge] Merge origin/sb/bosun into sb/bosun'
|
|
542
|
+
)
|
|
543
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(
|
|
544
|
+
await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])
|
|
545
|
+
)
|
|
546
|
+
await service.dispose()
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('merges divergent release work into personal with one labelled push', async () => {
|
|
550
|
+
const fixture = await createFixture()
|
|
551
|
+
const previousCurrentHead = await gitText(fixture.project, ['rev-parse', 'origin/next-release'])
|
|
552
|
+
await writeFile(path.join(fixture.project, 'personal.txt'), 'personal\n')
|
|
553
|
+
await git(fixture.project, ['add', '-A'])
|
|
554
|
+
await git(fixture.project, ['commit', '-m', 'personal work'])
|
|
555
|
+
const personalHead = await gitText(fixture.project, ['rev-parse', 'HEAD'])
|
|
556
|
+
|
|
557
|
+
await switchCollaboratorToCurrent(fixture)
|
|
558
|
+
await writeFile(path.join(fixture.collaborator, 'release.txt'), 'release\n')
|
|
559
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
560
|
+
await git(fixture.collaborator, ['commit', '-m', 'release work'])
|
|
561
|
+
await git(fixture.collaborator, ['push'])
|
|
562
|
+
const currentHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
563
|
+
await setHostilePullConfiguration(fixture.project)
|
|
564
|
+
await installFailingHooks(fixture.project)
|
|
565
|
+
|
|
566
|
+
const pushes: string[][] = []
|
|
567
|
+
let movedCurrentTrackingRef = false
|
|
568
|
+
const observedRunner: CommandRunner = async (executable, args, options) => {
|
|
569
|
+
if (executable === 'git' && args.includes('push')) pushes.push([...args])
|
|
570
|
+
if (executable === 'git' && args.includes('merge') && !movedCurrentTrackingRef) {
|
|
571
|
+
movedCurrentTrackingRef = true
|
|
572
|
+
await git(fixture.project, [
|
|
573
|
+
'update-ref',
|
|
574
|
+
'refs/remotes/origin/next-release',
|
|
575
|
+
previousCurrentHead
|
|
576
|
+
])
|
|
577
|
+
}
|
|
578
|
+
return runCommand(executable, args, options)
|
|
579
|
+
}
|
|
580
|
+
const service = createTestService(fixture, { runCommand: observedRunner })
|
|
581
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
582
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
583
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
584
|
+
|
|
585
|
+
expect(finished.fatal).toBeNull()
|
|
586
|
+
expect(finished.phase).toBe('restarting')
|
|
587
|
+
expect(await gitText(fixture.project, ['log', '-1', '--pretty=%s'])).toBe(
|
|
588
|
+
'[BOSUN] [Merge] Merge origin/next-release into sb/bosun'
|
|
589
|
+
)
|
|
590
|
+
expect((await gitText(fixture.project, ['log', '-1', '--pretty=%P'])).split(' ')).toEqual([
|
|
591
|
+
personalHead,
|
|
592
|
+
currentHead
|
|
593
|
+
])
|
|
594
|
+
expect(movedCurrentTrackingRef).toBe(true)
|
|
595
|
+
expect(pushes).toHaveLength(1)
|
|
596
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
597
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(
|
|
598
|
+
await gitText(fixture.project, ['rev-parse', 'HEAD'])
|
|
599
|
+
)
|
|
600
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'next-release'])).toBe(currentHead)
|
|
601
|
+
await service.dispose()
|
|
602
|
+
}, 15_000)
|
|
603
|
+
|
|
604
|
+
it('does not mistake a clean pending merge for a conflict when its labelled commit fails', async () => {
|
|
605
|
+
const fixture = await createFixture()
|
|
606
|
+
await writeFile(path.join(fixture.project, 'local.txt'), 'local\n')
|
|
607
|
+
await git(fixture.project, ['add', '-A'])
|
|
608
|
+
await git(fixture.project, ['commit', '-m', 'local work'])
|
|
609
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
610
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
611
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
612
|
+
await git(fixture.collaborator, ['push'])
|
|
613
|
+
|
|
614
|
+
const rejectMergeCommit: CommandRunner = (executable, args, options) => {
|
|
615
|
+
if (executable === 'git' && args.includes('commit')) {
|
|
616
|
+
return Promise.reject(
|
|
617
|
+
new CommandError(executable, args, {
|
|
618
|
+
stdout: '',
|
|
619
|
+
stderr: 'simulated commit failure',
|
|
620
|
+
exitCode: 1
|
|
621
|
+
})
|
|
622
|
+
)
|
|
623
|
+
}
|
|
624
|
+
return runCommand(executable, args, options)
|
|
625
|
+
}
|
|
626
|
+
const service = createTestService(fixture, { runCommand: rejectMergeCommit })
|
|
627
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
628
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
629
|
+
const failed = await waitForPhase(service, 'tab-a', ['fatal'])
|
|
630
|
+
|
|
631
|
+
expect(failed.fatal?.kind).toBe('general')
|
|
632
|
+
expect(await gitText(fixture.project, ['ls-files', '-u'])).toBe('')
|
|
633
|
+
expect(await gitText(fixture.project, ['rev-parse', '--verify', 'MERGE_HEAD'])).not.toBe('')
|
|
634
|
+
await service.dispose()
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('waits for every live tab and defers without blocking while a browser draft exists', async () => {
|
|
638
|
+
const fixture = await createFixture()
|
|
639
|
+
const installProject = vi.fn(async () => {})
|
|
640
|
+
const service = createTestService(fixture, { installProject })
|
|
641
|
+
await service.status({ tabId: 'tab-b' })
|
|
642
|
+
|
|
643
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
644
|
+
await service.flushAck({
|
|
645
|
+
tabId: 'tab-a',
|
|
646
|
+
operationId: started.operationId!,
|
|
647
|
+
stage: 'preflight'
|
|
648
|
+
})
|
|
649
|
+
expect((await service.status({ tabId: 'tab-a' })).phase).toBe('preparing')
|
|
650
|
+
await service.flushAck({
|
|
651
|
+
tabId: 'tab-b',
|
|
652
|
+
operationId: started.operationId!,
|
|
653
|
+
stage: 'preflight',
|
|
654
|
+
blockedReason: 'Save or close the open schema draft.'
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
const deferred = await service.status({ tabId: 'tab-a' })
|
|
658
|
+
expect(deferred.phase).toBe('idle')
|
|
659
|
+
expect(deferred.deferredReason).toBe('Save or close the open schema draft.')
|
|
660
|
+
expect(service.isUsageBlocked()).toBe(false)
|
|
661
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
662
|
+
await service.dispose()
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
it('takes a fresh all-tab snapshot for the locked flush barrier', async () => {
|
|
666
|
+
const fixture = await createFixture()
|
|
667
|
+
const service = createTestService(fixture)
|
|
668
|
+
await service.status({ tabId: 'tab-b' })
|
|
669
|
+
|
|
670
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
671
|
+
await service.flushAck({
|
|
672
|
+
tabId: 'tab-a',
|
|
673
|
+
operationId: started.operationId!,
|
|
674
|
+
stage: 'preflight'
|
|
675
|
+
})
|
|
676
|
+
const flushing = await service.flushAck({
|
|
677
|
+
tabId: 'tab-b',
|
|
678
|
+
operationId: started.operationId!,
|
|
679
|
+
stage: 'preflight'
|
|
680
|
+
})
|
|
681
|
+
expect(flushing.phase).toBe('preparing')
|
|
682
|
+
expect(flushing.preparationStage).toBe('browser-writes')
|
|
683
|
+
expect((await service.status({ tabId: 'tab-a' })).tabNeedsFlush).toBe(true)
|
|
684
|
+
expect((await service.status({ tabId: 'tab-b' })).tabNeedsFlush).toBe(true)
|
|
685
|
+
|
|
686
|
+
await service.flushAck({
|
|
687
|
+
tabId: 'tab-a',
|
|
688
|
+
operationId: started.operationId!,
|
|
689
|
+
stage: 'browser-writes'
|
|
690
|
+
})
|
|
691
|
+
expect((await service.status({ tabId: 'tab-a' })).phase).toBe('preparing')
|
|
692
|
+
await service.flushAck({
|
|
693
|
+
tabId: 'tab-b',
|
|
694
|
+
operationId: started.operationId!,
|
|
695
|
+
stage: 'browser-writes',
|
|
696
|
+
blockedReason: 'A pending edit could not be flushed.'
|
|
697
|
+
})
|
|
698
|
+
const deferred = await service.status({ tabId: 'tab-a' })
|
|
699
|
+
expect(deferred.phase).toBe('idle')
|
|
700
|
+
expect(deferred.deferredReason).toBe('A pending edit could not be flushed.')
|
|
701
|
+
await service.dispose()
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
it('adds a tab that appears during browser-write preparation and rejects a stale-stage ACK', async () => {
|
|
705
|
+
const fixture = await createFixture()
|
|
706
|
+
const service = createTestService(fixture)
|
|
707
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
708
|
+
await service.flushAck({
|
|
709
|
+
tabId: 'tab-a',
|
|
710
|
+
operationId: started.operationId!,
|
|
711
|
+
stage: 'preflight'
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
const joined = await service.status({ tabId: 'tab-b' })
|
|
715
|
+
expect(joined.phase).toBe('preparing')
|
|
716
|
+
expect(joined.preparationStage).toBe('browser-writes')
|
|
717
|
+
expect(joined.tabNeedsFlush).toBe(true)
|
|
718
|
+
const staleAck = await service.flushAck({
|
|
719
|
+
tabId: 'tab-b',
|
|
720
|
+
operationId: started.operationId!,
|
|
721
|
+
stage: 'preflight'
|
|
722
|
+
})
|
|
723
|
+
expect(staleAck.tabNeedsFlush).toBe(true)
|
|
724
|
+
|
|
725
|
+
await service.flushAck({
|
|
726
|
+
tabId: 'tab-a',
|
|
727
|
+
operationId: started.operationId!,
|
|
728
|
+
stage: 'browser-writes'
|
|
729
|
+
})
|
|
730
|
+
expect((await service.status({ tabId: 'tab-a' })).phase).toBe('preparing')
|
|
731
|
+
await service.flushAck({
|
|
732
|
+
tabId: 'tab-b',
|
|
733
|
+
operationId: started.operationId!,
|
|
734
|
+
stage: 'browser-writes'
|
|
735
|
+
})
|
|
736
|
+
expect((await waitForOperationEnd(service, 'tab-a')).phase).toBe('idle')
|
|
737
|
+
await service.dispose()
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it('expires a disappeared tab before the browser barrier deadline', async () => {
|
|
741
|
+
const fixture = await createFixture()
|
|
742
|
+
let now = 1_000
|
|
743
|
+
const service = createTestService(fixture, {
|
|
744
|
+
now: () => now,
|
|
745
|
+
liveTabMs: 5,
|
|
746
|
+
flushTimeoutMs: 2_000
|
|
747
|
+
})
|
|
748
|
+
await service.status({ tabId: 'closed-tab' })
|
|
749
|
+
|
|
750
|
+
const started = await service.activate({ tabId: 'active-tab', reason: 'page-open' })
|
|
751
|
+
await service.flushAck({
|
|
752
|
+
tabId: 'active-tab',
|
|
753
|
+
operationId: started.operationId!,
|
|
754
|
+
stage: 'preflight'
|
|
755
|
+
})
|
|
756
|
+
now += 6
|
|
757
|
+
await service.status({ tabId: 'active-tab' })
|
|
758
|
+
await new Promise((resolve) => setTimeout(resolve, 550))
|
|
759
|
+
|
|
760
|
+
const flushing = await service.status({ tabId: 'active-tab' })
|
|
761
|
+
expect(flushing.phase).toBe('preparing')
|
|
762
|
+
expect(flushing.preparationStage).toBe('browser-writes')
|
|
763
|
+
expect(flushing.tabNeedsFlush).toBe(true)
|
|
764
|
+
await service.flushAck({
|
|
765
|
+
tabId: 'active-tab',
|
|
766
|
+
operationId: started.operationId!,
|
|
767
|
+
stage: 'browser-writes'
|
|
768
|
+
})
|
|
769
|
+
expect((await waitForOperationEnd(service, 'active-tab')).phase).toBe('idle')
|
|
770
|
+
await service.dispose()
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
it('saves and publishes files without restarting when their content stays the same', async () => {
|
|
774
|
+
const fixture = await createFixture()
|
|
775
|
+
await writeFile(path.join(fixture.project, 'manual.txt'), 'ready\n')
|
|
776
|
+
const installProject = vi.fn(async () => {})
|
|
777
|
+
const scheduleRestart = vi.fn(() => true)
|
|
778
|
+
const resumeWrites = vi.fn(async () => {})
|
|
779
|
+
const service = createTestService(fixture, {
|
|
780
|
+
installProject,
|
|
781
|
+
scheduleRestart,
|
|
782
|
+
resumeWrites
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
expect((await service.status({ tabId: 'tab-a' })).hasChanges).toBe(true)
|
|
786
|
+
const started = await service.startManual({
|
|
787
|
+
tabId: 'tab-a',
|
|
788
|
+
action: 'save',
|
|
789
|
+
description: 'manual release'
|
|
790
|
+
})
|
|
791
|
+
expect(started.phase).toBe('preparing')
|
|
792
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
793
|
+
const finished = await waitForOperationEnd(service, 'tab-a')
|
|
794
|
+
|
|
795
|
+
expect(finished.fatal).toBeNull()
|
|
796
|
+
expect(finished.phase).toBe('idle')
|
|
797
|
+
expect(finished.reloadToken).toBeNull()
|
|
798
|
+
expect(resumeWrites).toHaveBeenCalledOnce()
|
|
799
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
800
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
801
|
+
expect(await gitText(fixture.project, ['log', '-1', '--pretty=%s'])).toBe(
|
|
802
|
+
'[BOSUN] manual release'
|
|
803
|
+
)
|
|
804
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(
|
|
805
|
+
await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])
|
|
806
|
+
)
|
|
807
|
+
await service.dispose()
|
|
808
|
+
}, 15_000)
|
|
809
|
+
|
|
810
|
+
it('restarts after manual Discard changes the checked-out project files', async () => {
|
|
811
|
+
const fixture = await createFixture()
|
|
812
|
+
await writeFile(path.join(fixture.project, 'app.txt'), 'discard me\n')
|
|
813
|
+
await writeFile(path.join(fixture.project, 'untracked.txt'), 'discard me too\n')
|
|
814
|
+
const installProject = vi.fn(async () => {})
|
|
815
|
+
const scheduleRestart = vi.fn(() => true)
|
|
816
|
+
const service = createTestService(fixture, { installProject, scheduleRestart })
|
|
817
|
+
|
|
818
|
+
const started = await service.startManual({ tabId: 'tab-a', action: 'discard' })
|
|
819
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
820
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
821
|
+
|
|
822
|
+
expect(finished.fatal).toBeNull()
|
|
823
|
+
expect(finished.phase).toBe('restarting')
|
|
824
|
+
expect(finished.reloadToken).not.toBeNull()
|
|
825
|
+
expect(installProject).toHaveBeenCalledOnce()
|
|
826
|
+
expect(scheduleRestart).toHaveBeenCalledWith(76)
|
|
827
|
+
expect(await readFile(path.join(fixture.project, 'app.txt'), 'utf8')).toBe('base\n')
|
|
828
|
+
await expect(
|
|
829
|
+
readFile(path.join(fixture.project, 'untracked.txt'), 'utf8')
|
|
830
|
+
).rejects.toMatchObject({
|
|
831
|
+
code: 'ENOENT'
|
|
832
|
+
})
|
|
833
|
+
await service.dispose()
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('saves refused local changes with a Bosun prefix, merges, and publishes both commits', async () => {
|
|
837
|
+
const fixture = await createFixture('one\ntwo\nthree\nfour\n')
|
|
838
|
+
await writeFile(path.join(fixture.project, 'app.txt'), 'one\ntwo\nthree\nlocal four\n')
|
|
839
|
+
await writeFile(path.join(fixture.collaborator, 'app.txt'), 'remote one\ntwo\nthree\nfour\n')
|
|
840
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
841
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote line'])
|
|
842
|
+
await git(fixture.collaborator, ['push'])
|
|
843
|
+
await installFailingHooks(fixture.project)
|
|
844
|
+
|
|
845
|
+
const service = createTestService(fixture)
|
|
846
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
847
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
848
|
+
const awaiting = await waitForPhase(service, 'tab-a', ['awaiting-changes', 'fatal'])
|
|
849
|
+
expect(awaiting.phase).toBe('awaiting-changes')
|
|
850
|
+
|
|
851
|
+
await service.resolveChanges({
|
|
852
|
+
tabId: 'tab-a',
|
|
853
|
+
operationId: awaiting.operationId!,
|
|
854
|
+
action: 'save',
|
|
855
|
+
description: ' keep both lines '
|
|
856
|
+
})
|
|
857
|
+
await expect(
|
|
858
|
+
service.resolveChanges({
|
|
859
|
+
tabId: 'tab-b',
|
|
860
|
+
operationId: awaiting.operationId!,
|
|
861
|
+
action: 'discard'
|
|
862
|
+
})
|
|
863
|
+
).rejects.toThrow('This refresh choice was already handled in another Bosun tab.')
|
|
864
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
865
|
+
expect(finished.fatal).toBeNull()
|
|
866
|
+
const subjects = (await gitText(fixture.project, ['log', '-2', '--pretty=%s'])).split('\n')
|
|
867
|
+
expect(subjects).toEqual([
|
|
868
|
+
'[BOSUN] [Merge] Merge origin/sb/bosun into sb/bosun',
|
|
869
|
+
'[BOSUN] keep both lines'
|
|
870
|
+
])
|
|
871
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(
|
|
872
|
+
await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])
|
|
873
|
+
)
|
|
874
|
+
await service.dispose()
|
|
875
|
+
}, 15_000)
|
|
876
|
+
|
|
877
|
+
it('stashes refused tracked and untracked changes before pulling', async () => {
|
|
878
|
+
const fixture = await createFixture('base\n')
|
|
879
|
+
await writeFile(path.join(fixture.project, 'app.txt'), 'local dirty\n')
|
|
880
|
+
await writeFile(path.join(fixture.project, 'untracked.txt'), 'recover me\n')
|
|
881
|
+
await switchCollaboratorToCurrent(fixture)
|
|
882
|
+
await writeFile(path.join(fixture.collaborator, 'app.txt'), 'remote update\n')
|
|
883
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
884
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote update'])
|
|
885
|
+
await git(fixture.collaborator, ['push'])
|
|
886
|
+
|
|
887
|
+
const service = createTestService(fixture)
|
|
888
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
889
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
890
|
+
const awaiting = await waitForPhase(service, 'tab-a', ['awaiting-changes', 'fatal'])
|
|
891
|
+
expect(awaiting.phase).toBe('awaiting-changes')
|
|
892
|
+
await service.resolveChanges({
|
|
893
|
+
tabId: 'tab-a',
|
|
894
|
+
operationId: awaiting.operationId!,
|
|
895
|
+
action: 'discard'
|
|
896
|
+
})
|
|
897
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
898
|
+
|
|
899
|
+
expect(finished.fatal).toBeNull()
|
|
900
|
+
expect(await readFile(path.join(fixture.project, 'app.txt'), 'utf8')).toBe('remote update\n')
|
|
901
|
+
expect(await gitText(fixture.project, ['stash', 'list'])).toContain(
|
|
902
|
+
'[BOSUN] Discard before refresh'
|
|
903
|
+
)
|
|
904
|
+
expect(
|
|
905
|
+
await gitText(fixture.project, ['ls-tree', '-r', '--name-only', 'stash@{0}^3'])
|
|
906
|
+
).toContain('untracked.txt')
|
|
907
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(
|
|
908
|
+
await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
909
|
+
)
|
|
910
|
+
await service.dispose()
|
|
911
|
+
}, 15_000)
|
|
912
|
+
|
|
913
|
+
it('aborts a conflict, publishes a unique conflict branch, and keeps the current branch intact', async () => {
|
|
914
|
+
const fixture = await createFixture('base\n')
|
|
915
|
+
await writeFile(path.join(fixture.project, 'app.txt'), 'local commit\n')
|
|
916
|
+
await git(fixture.project, ['add', '-A'])
|
|
917
|
+
await git(fixture.project, ['commit', '-m', 'local conflicting work'])
|
|
918
|
+
const localHead = await gitText(fixture.project, ['rev-parse', 'HEAD'])
|
|
919
|
+
await writeFile(path.join(fixture.collaborator, 'app.txt'), 'remote commit\n')
|
|
920
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
921
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote conflicting work'])
|
|
922
|
+
await git(fixture.collaborator, ['push'])
|
|
923
|
+
|
|
924
|
+
const githubUrl = 'https://github.com/recombine-ai/example.git'
|
|
925
|
+
await git(fixture.project, ['remote', 'set-url', 'origin', githubUrl])
|
|
926
|
+
await git(fixture.project, [
|
|
927
|
+
'config',
|
|
928
|
+
`url.${pathToFileURL(fixture.remote).href}.insteadOf`,
|
|
929
|
+
githubUrl
|
|
930
|
+
])
|
|
931
|
+
|
|
932
|
+
const installProject = vi.fn(async () => {})
|
|
933
|
+
const service = createTestService(fixture, { installProject })
|
|
934
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
935
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
936
|
+
const failed = await waitForPhase(service, 'tab-a', ['fatal'])
|
|
937
|
+
|
|
938
|
+
expect(failed.fatal?.kind).toBe('merge-conflict')
|
|
939
|
+
expect(failed.prUrl).toMatch(
|
|
940
|
+
/^https:\/\/github\.com\/recombine-ai\/example\/compare\/sb%2Fbosun\.\.\.bosun-conflict%2Fsb%2Fbosun%2F/
|
|
941
|
+
)
|
|
942
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
943
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
944
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(localHead)
|
|
945
|
+
expect(await gitText(fixture.project, ['ls-files', '-u'])).toBe('')
|
|
946
|
+
const conflictRefs = (
|
|
947
|
+
await gitText(fixture.remote, [
|
|
948
|
+
'for-each-ref',
|
|
949
|
+
'--format=%(refname:short) %(objectname)',
|
|
950
|
+
'refs/heads/bosun-conflict/'
|
|
951
|
+
])
|
|
952
|
+
)
|
|
953
|
+
.split('\n')
|
|
954
|
+
.filter(Boolean)
|
|
955
|
+
expect(conflictRefs).toHaveLength(1)
|
|
956
|
+
expect(conflictRefs[0]).toContain(localHead)
|
|
957
|
+
expect(service.isUsageBlocked()).toBe(true)
|
|
958
|
+
await service.dispose()
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
it('publishes a release snapshot into personal when the release merge conflicts', async () => {
|
|
962
|
+
const fixture = await createFixture('base\n')
|
|
963
|
+
await writeFile(path.join(fixture.project, 'app.txt'), 'personal commit\n')
|
|
964
|
+
await git(fixture.project, ['add', '-A'])
|
|
965
|
+
await git(fixture.project, ['commit', '-m', 'personal conflicting work'])
|
|
966
|
+
const personalHead = await gitText(fixture.project, ['rev-parse', 'HEAD'])
|
|
967
|
+
|
|
968
|
+
await switchCollaboratorToCurrent(fixture)
|
|
969
|
+
await writeFile(path.join(fixture.collaborator, 'app.txt'), 'release commit\n')
|
|
970
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
971
|
+
await git(fixture.collaborator, ['commit', '-m', 'release conflicting work'])
|
|
972
|
+
await git(fixture.collaborator, ['push'])
|
|
973
|
+
const currentHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
974
|
+
|
|
975
|
+
const githubUrl = 'https://github.com/recombine-ai/example.git'
|
|
976
|
+
await git(fixture.project, ['remote', 'set-url', 'origin', githubUrl])
|
|
977
|
+
await git(fixture.project, [
|
|
978
|
+
'config',
|
|
979
|
+
`url.${pathToFileURL(fixture.remote).href}.insteadOf`,
|
|
980
|
+
githubUrl
|
|
981
|
+
])
|
|
982
|
+
|
|
983
|
+
const installProject = vi.fn(async () => {})
|
|
984
|
+
const service = createTestService(fixture, { installProject })
|
|
985
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
986
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
987
|
+
const failed = await waitForPhase(service, 'tab-a', ['fatal'])
|
|
988
|
+
|
|
989
|
+
expect(failed.fatal?.kind).toBe('merge-conflict')
|
|
990
|
+
expect(failed.prUrl).toMatch(
|
|
991
|
+
/^https:\/\/github\.com\/recombine-ai\/example\/compare\/sb%2Fbosun\.\.\.bosun-conflict%2Fsb%2Fbosun%2F/
|
|
992
|
+
)
|
|
993
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
994
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
995
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(personalHead)
|
|
996
|
+
expect(await gitText(fixture.project, ['ls-files', '-u'])).toBe('')
|
|
997
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(personalHead)
|
|
998
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'next-release'])).toBe(currentHead)
|
|
999
|
+
const conflictRefs = (
|
|
1000
|
+
await gitText(fixture.remote, [
|
|
1001
|
+
'for-each-ref',
|
|
1002
|
+
'--format=%(refname:short) %(objectname)',
|
|
1003
|
+
'refs/heads/bosun-conflict/'
|
|
1004
|
+
])
|
|
1005
|
+
)
|
|
1006
|
+
.split('\n')
|
|
1007
|
+
.filter(Boolean)
|
|
1008
|
+
expect(conflictRefs).toHaveLength(1)
|
|
1009
|
+
expect(conflictRefs[0]?.split(' ')[1]).toBe(currentHead)
|
|
1010
|
+
expect(service.isUsageBlocked()).toBe(true)
|
|
1011
|
+
await service.dispose()
|
|
1012
|
+
}, 15_000)
|
|
1013
|
+
|
|
1014
|
+
it('publishes a clean personal branch to the current branch without leaving personal', async () => {
|
|
1015
|
+
const fixture = await createFixture()
|
|
1016
|
+
await writeFile(path.join(fixture.project, 'release.txt'), 'ready\n')
|
|
1017
|
+
await git(fixture.project, ['add', '-A'])
|
|
1018
|
+
await git(fixture.project, ['commit', '-m', 'personal release'])
|
|
1019
|
+
await git(fixture.project, ['push'])
|
|
1020
|
+
const installProject = vi.fn(async () => {})
|
|
1021
|
+
const scheduleRestart = vi.fn(() => true)
|
|
1022
|
+
const service = createTestService(fixture, { installProject, scheduleRestart })
|
|
1023
|
+
|
|
1024
|
+
const available = await service.status({ tabId: 'tab-a' })
|
|
1025
|
+
expect(available.hasChanges).toBe(false)
|
|
1026
|
+
expect(available.canPrepareRelease).toBe(true)
|
|
1027
|
+
const started = await service.prepareRelease({ tabId: 'tab-a' })
|
|
1028
|
+
expect(started.phase).toBe('preparing')
|
|
1029
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
1030
|
+
const finished = await waitForOperationEnd(service, 'tab-a')
|
|
1031
|
+
|
|
1032
|
+
expect(finished.phase).toBe('idle')
|
|
1033
|
+
expect(finished.canPrepareRelease).toBe(false)
|
|
1034
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
1035
|
+
const personalRemoteHead = await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])
|
|
1036
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'next-release'])).toBe(personalRemoteHead)
|
|
1037
|
+
expect(await gitText(fixture.project, ['rev-parse', 'next-release'])).toBe(personalRemoteHead)
|
|
1038
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
1039
|
+
expect(scheduleRestart).not.toHaveBeenCalled()
|
|
1040
|
+
await service.dispose()
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
it('builds a divergent release outside the primary checkout and atomically publishes both branches', async () => {
|
|
1044
|
+
const fixture = await createFixture()
|
|
1045
|
+
await git(fixture.collaborator, ['branch', '--track', 'next-release', 'origin/next-release'])
|
|
1046
|
+
await git(fixture.collaborator, ['switch', 'next-release'])
|
|
1047
|
+
await writeFile(path.join(fixture.collaborator, 'current.txt'), 'current work\n')
|
|
1048
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
1049
|
+
await git(fixture.collaborator, ['commit', '-m', 'current work'])
|
|
1050
|
+
await git(fixture.collaborator, ['push'])
|
|
1051
|
+
|
|
1052
|
+
await writeFile(path.join(fixture.project, 'personal.txt'), 'personal work\n')
|
|
1053
|
+
await git(fixture.project, ['add', '-A'])
|
|
1054
|
+
await git(fixture.project, ['commit', '-m', 'personal work'])
|
|
1055
|
+
await git(fixture.project, ['push'])
|
|
1056
|
+
await installFailingHooks(fixture.project)
|
|
1057
|
+
const installProject = vi.fn(async () => {})
|
|
1058
|
+
const scheduleRestart = vi.fn(() => true)
|
|
1059
|
+
const service = createTestService(fixture, { installProject, scheduleRestart })
|
|
1060
|
+
|
|
1061
|
+
expect((await service.status({ tabId: 'tab-a' })).canPrepareRelease).toBe(true)
|
|
1062
|
+
const started = await service.prepareRelease({ tabId: 'tab-a' })
|
|
1063
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
1064
|
+
const finished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
1065
|
+
|
|
1066
|
+
expect(finished.fatal).toBeNull()
|
|
1067
|
+
expect(finished.phase).toBe('restarting')
|
|
1068
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
1069
|
+
const resultHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
1070
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(resultHead)
|
|
1071
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(resultHead)
|
|
1072
|
+
expect(await gitText(fixture.project, ['rev-parse', 'next-release'])).toBe(resultHead)
|
|
1073
|
+
expect(await gitText(fixture.project, ['log', '-1', '--pretty=%s'])).toBe(
|
|
1074
|
+
'[BOSUN] [Merge] Merge sb/bosun into next-release'
|
|
1075
|
+
)
|
|
1076
|
+
expect(await readFile(path.join(fixture.project, 'current.txt'), 'utf8')).toBe('current work\n')
|
|
1077
|
+
expect(installProject).toHaveBeenCalledOnce()
|
|
1078
|
+
expect(scheduleRestart).toHaveBeenCalledOnce()
|
|
1079
|
+
await expect(
|
|
1080
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release.json'))
|
|
1081
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
1082
|
+
await service.dispose()
|
|
1083
|
+
}, 15_000)
|
|
1084
|
+
|
|
1085
|
+
it('publishes a conflict branch for a Prepare conflict without changing either release branch', async () => {
|
|
1086
|
+
const fixture = await createFixture('base\n')
|
|
1087
|
+
await git(fixture.collaborator, ['branch', '--track', 'next-release', 'origin/next-release'])
|
|
1088
|
+
await git(fixture.collaborator, ['switch', 'next-release'])
|
|
1089
|
+
await writeFile(path.join(fixture.collaborator, 'app.txt'), 'current change\n')
|
|
1090
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
1091
|
+
await git(fixture.collaborator, ['commit', '-m', 'current conflict'])
|
|
1092
|
+
await git(fixture.collaborator, ['push'])
|
|
1093
|
+
const currentHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
1094
|
+
|
|
1095
|
+
await writeFile(path.join(fixture.project, 'app.txt'), 'personal change\n')
|
|
1096
|
+
await git(fixture.project, ['add', '-A'])
|
|
1097
|
+
await git(fixture.project, ['commit', '-m', 'personal conflict'])
|
|
1098
|
+
await git(fixture.project, ['push'])
|
|
1099
|
+
const personalHead = await gitText(fixture.project, ['rev-parse', 'HEAD'])
|
|
1100
|
+
const githubUrl = 'https://github.com/recombine-ai/example.git'
|
|
1101
|
+
await git(fixture.project, ['remote', 'set-url', 'origin', githubUrl])
|
|
1102
|
+
await git(fixture.project, [
|
|
1103
|
+
'config',
|
|
1104
|
+
`url.${pathToFileURL(fixture.remote).href}.insteadOf`,
|
|
1105
|
+
githubUrl
|
|
1106
|
+
])
|
|
1107
|
+
const installProject = vi.fn(async () => {})
|
|
1108
|
+
const service = createTestService(fixture, { installProject })
|
|
1109
|
+
|
|
1110
|
+
const started = await service.prepareRelease({ tabId: 'tab-a' })
|
|
1111
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
1112
|
+
const failed = await waitForPhase(service, 'tab-a', ['fatal'])
|
|
1113
|
+
|
|
1114
|
+
expect(failed.fatal?.kind).toBe('merge-conflict')
|
|
1115
|
+
expect(failed.prUrl).toMatch(
|
|
1116
|
+
/^https:\/\/github\.com\/recombine-ai\/example\/compare\/next-release\.\.\.bosun-conflict%2Fnext-release%2F/
|
|
1117
|
+
)
|
|
1118
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'next-release'])).toBe(currentHead)
|
|
1119
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(personalHead)
|
|
1120
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
1121
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(personalHead)
|
|
1122
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
1123
|
+
await expect(
|
|
1124
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release.json'))
|
|
1125
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
1126
|
+
await expect(
|
|
1127
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release-workspace', 'HEAD'))
|
|
1128
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
1129
|
+
await service.dispose()
|
|
1130
|
+
}, 15_000)
|
|
1131
|
+
|
|
1132
|
+
it('refuses to overwrite unpublished commits on the local current branch', async () => {
|
|
1133
|
+
const fixture = await createFixture()
|
|
1134
|
+
await git(fixture.project, ['switch', 'next-release'])
|
|
1135
|
+
await writeFile(path.join(fixture.project, 'local-current.txt'), 'local only\n')
|
|
1136
|
+
await git(fixture.project, ['add', '-A'])
|
|
1137
|
+
await git(fixture.project, ['commit', '-m', 'unpublished current work'])
|
|
1138
|
+
const localCurrentHead = await gitText(fixture.project, ['rev-parse', 'HEAD'])
|
|
1139
|
+
await git(fixture.project, ['switch', 'sb/bosun'])
|
|
1140
|
+
await writeFile(path.join(fixture.project, 'personal.txt'), 'release me\n')
|
|
1141
|
+
await git(fixture.project, ['add', '-A'])
|
|
1142
|
+
await git(fixture.project, ['commit', '-m', 'personal release'])
|
|
1143
|
+
await git(fixture.project, ['push'])
|
|
1144
|
+
const oldRemoteCurrent = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
1145
|
+
const service = createTestService(fixture)
|
|
1146
|
+
|
|
1147
|
+
const started = await service.prepareRelease({ tabId: 'tab-a' })
|
|
1148
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
1149
|
+
const failed = await waitForPhase(service, 'tab-a', ['fatal'])
|
|
1150
|
+
|
|
1151
|
+
expect(failed.fatal?.kind).toBe('general')
|
|
1152
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'next-release'])).toBe(oldRemoteCurrent)
|
|
1153
|
+
expect(await gitText(fixture.project, ['rev-parse', 'next-release'])).toBe(localCurrentHead)
|
|
1154
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
1155
|
+
await service.dispose()
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
it('blocks Prepare if the release branch is checked out in a worktree added after launch', async () => {
|
|
1159
|
+
const fixture = await createFixture()
|
|
1160
|
+
await writeFile(path.join(fixture.project, 'personal.txt'), 'release me\n')
|
|
1161
|
+
await git(fixture.project, ['add', '-A'])
|
|
1162
|
+
await git(fixture.project, ['commit', '-m', 'personal release'])
|
|
1163
|
+
await git(fixture.project, ['push'])
|
|
1164
|
+
const service = createTestService(fixture)
|
|
1165
|
+
expect((await service.status({ tabId: 'tab-a' })).canPrepareRelease).toBe(true)
|
|
1166
|
+
|
|
1167
|
+
const releaseWorktree = path.join(fixture.root, 'release-worktree')
|
|
1168
|
+
await git(fixture.project, ['worktree', 'add', releaseWorktree, 'next-release'])
|
|
1169
|
+
const status = await service.prepareRelease({ tabId: 'tab-a' })
|
|
1170
|
+
|
|
1171
|
+
expect(status.phase).toBe('fatal')
|
|
1172
|
+
expect(status.operationId).toBeNull()
|
|
1173
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
1174
|
+
expect(await gitText(releaseWorktree, ['branch', '--show-current'])).toBe('next-release')
|
|
1175
|
+
await service.dispose()
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
it('blocks activation if the shared remote gains a different push URL after launch', async () => {
|
|
1179
|
+
const fixture = await createFixture()
|
|
1180
|
+
const service = createTestService(fixture)
|
|
1181
|
+
await service.status({ tabId: 'tab-a' })
|
|
1182
|
+
await git(fixture.project, ['remote', 'set-url', '--push', 'origin', `${fixture.remote}-other`])
|
|
1183
|
+
|
|
1184
|
+
const status = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
1185
|
+
|
|
1186
|
+
expect(status.phase).toBe('fatal')
|
|
1187
|
+
expect(status.operationId).toBeNull()
|
|
1188
|
+
expect(status.fatal?.message).toContain('matching push URL')
|
|
1189
|
+
await service.dispose()
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
it('rejects explicit Save and Prepare starts while another operation owns Git mode', async () => {
|
|
1193
|
+
const fixture = await createFixture()
|
|
1194
|
+
await writeFile(path.join(fixture.project, 'local.txt'), 'local draft\n')
|
|
1195
|
+
const service = createTestService(fixture)
|
|
1196
|
+
const activation = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
1197
|
+
expect(activation.phase).toBe('preparing')
|
|
1198
|
+
|
|
1199
|
+
await expect(
|
|
1200
|
+
service.startManual({
|
|
1201
|
+
tabId: 'tab-b',
|
|
1202
|
+
action: 'save',
|
|
1203
|
+
description: 'must not attach to activation'
|
|
1204
|
+
})
|
|
1205
|
+
).rejects.toThrow('already in progress')
|
|
1206
|
+
await expect(service.prepareRelease({ tabId: 'tab-b' })).rejects.toThrow('already in progress')
|
|
1207
|
+
await service.dispose()
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
it('recovers a fully published release after shutdown before local alignment', async () => {
|
|
1211
|
+
const fixture = await createFixture()
|
|
1212
|
+
await writeFile(path.join(fixture.project, 'release.txt'), 'published\n')
|
|
1213
|
+
await git(fixture.project, ['add', '-A'])
|
|
1214
|
+
await git(fixture.project, ['commit', '-m', 'personal release'])
|
|
1215
|
+
await git(fixture.project, ['push'])
|
|
1216
|
+
let lostAtomicPushResponse = false
|
|
1217
|
+
const runner: CommandRunner = async (executable, args, options) => {
|
|
1218
|
+
const result = await runCommand(executable, args, options)
|
|
1219
|
+
if (!lostAtomicPushResponse && executable === 'git' && args.includes('--atomic')) {
|
|
1220
|
+
lostAtomicPushResponse = true
|
|
1221
|
+
throw new Error('simulated shutdown after atomic publication')
|
|
1222
|
+
}
|
|
1223
|
+
return result
|
|
1224
|
+
}
|
|
1225
|
+
const interrupted = createTestService(fixture, { runCommand: runner })
|
|
1226
|
+
const started = await interrupted.prepareRelease({ tabId: 'tab-a' })
|
|
1227
|
+
await acknowledgePreparationAndFlush(interrupted, 'tab-a', started.operationId!)
|
|
1228
|
+
expect((await waitForPhase(interrupted, 'tab-a', ['fatal'])).fatal?.kind).toBe('general')
|
|
1229
|
+
await interrupted.dispose()
|
|
1230
|
+
|
|
1231
|
+
const publishedHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
1232
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(publishedHead)
|
|
1233
|
+
expect(await gitText(fixture.project, ['rev-parse', 'next-release'])).not.toBe(publishedHead)
|
|
1234
|
+
fixture.config.launchId = 'recovery-launch'
|
|
1235
|
+
const resumeWrites = vi.fn(async () => {})
|
|
1236
|
+
const recovered = createTestService(fixture, { resumeWrites })
|
|
1237
|
+
const status = await recovered.status({ tabId: 'tab-b' })
|
|
1238
|
+
|
|
1239
|
+
expect(status.phase).toBe('idle')
|
|
1240
|
+
expect(status.fatal).toBeNull()
|
|
1241
|
+
expect(status.canPrepareRelease).toBe(false)
|
|
1242
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
1243
|
+
expect(await gitText(fixture.project, ['rev-parse', 'next-release'])).toBe(publishedHead)
|
|
1244
|
+
expect(resumeWrites).toHaveBeenCalledOnce()
|
|
1245
|
+
await expect(
|
|
1246
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release.json'))
|
|
1247
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
1248
|
+
await recovered.dispose()
|
|
1249
|
+
}, 15_000)
|
|
1250
|
+
|
|
1251
|
+
it('installs and restarts when divergent release recovery changes the personal tree', async () => {
|
|
1252
|
+
const fixture = await createFixture()
|
|
1253
|
+
await git(fixture.collaborator, ['branch', '--track', 'next-release', 'origin/next-release'])
|
|
1254
|
+
await git(fixture.collaborator, ['switch', 'next-release'])
|
|
1255
|
+
await writeFile(path.join(fixture.collaborator, 'current.txt'), 'current work\n')
|
|
1256
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
1257
|
+
await git(fixture.collaborator, ['commit', '-m', 'current work'])
|
|
1258
|
+
await git(fixture.collaborator, ['push'])
|
|
1259
|
+
await writeFile(path.join(fixture.project, 'personal.txt'), 'personal work\n')
|
|
1260
|
+
await git(fixture.project, ['add', '-A'])
|
|
1261
|
+
await git(fixture.project, ['commit', '-m', 'personal work'])
|
|
1262
|
+
await git(fixture.project, ['push'])
|
|
1263
|
+
|
|
1264
|
+
let lostAtomicPushResponse = false
|
|
1265
|
+
const runner: CommandRunner = async (executable, args, options) => {
|
|
1266
|
+
const result = await runCommand(executable, args, options)
|
|
1267
|
+
if (!lostAtomicPushResponse && executable === 'git' && args.includes('--atomic')) {
|
|
1268
|
+
lostAtomicPushResponse = true
|
|
1269
|
+
throw new Error('simulated shutdown after divergent atomic publication')
|
|
1270
|
+
}
|
|
1271
|
+
return result
|
|
1272
|
+
}
|
|
1273
|
+
const interrupted = createTestService(fixture, { runCommand: runner })
|
|
1274
|
+
const started = await interrupted.prepareRelease({ tabId: 'tab-a' })
|
|
1275
|
+
await acknowledgePreparationAndFlush(interrupted, 'tab-a', started.operationId!)
|
|
1276
|
+
await waitForPhase(interrupted, 'tab-a', ['fatal'])
|
|
1277
|
+
await interrupted.dispose()
|
|
1278
|
+
|
|
1279
|
+
const resultHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
1280
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'sb/bosun'])).toBe(resultHead)
|
|
1281
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).not.toBe(resultHead)
|
|
1282
|
+
fixture.config.launchId = 'divergent-recovery-launch'
|
|
1283
|
+
const installProject = vi.fn(async () => {})
|
|
1284
|
+
const scheduleRestart = vi.fn(() => true)
|
|
1285
|
+
const resumeWrites = vi.fn(async () => {})
|
|
1286
|
+
const recovered = createTestService(fixture, {
|
|
1287
|
+
installProject,
|
|
1288
|
+
scheduleRestart,
|
|
1289
|
+
resumeWrites
|
|
1290
|
+
})
|
|
1291
|
+
const status = await recovered.status({ tabId: 'tab-b' })
|
|
1292
|
+
|
|
1293
|
+
expect(status.phase).toBe('restarting')
|
|
1294
|
+
expect(status.reloadToken).not.toBeNull()
|
|
1295
|
+
expect(await gitText(fixture.project, ['branch', '--show-current'])).toBe('sb/bosun')
|
|
1296
|
+
expect(await gitText(fixture.project, ['rev-parse', 'HEAD'])).toBe(resultHead)
|
|
1297
|
+
expect(await gitText(fixture.project, ['rev-parse', 'next-release'])).toBe(resultHead)
|
|
1298
|
+
expect(await readFile(path.join(fixture.project, 'current.txt'), 'utf8')).toBe('current work\n')
|
|
1299
|
+
expect(installProject).toHaveBeenCalledOnce()
|
|
1300
|
+
expect(scheduleRestart).toHaveBeenCalledWith(76)
|
|
1301
|
+
expect(resumeWrites).not.toHaveBeenCalled()
|
|
1302
|
+
await expect(
|
|
1303
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release.json'))
|
|
1304
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
1305
|
+
await recovered.dispose()
|
|
1306
|
+
}, 20_000)
|
|
1307
|
+
|
|
1308
|
+
it('cleans an interrupted Prepare attempt when neither remote branch was published', async () => {
|
|
1309
|
+
const fixture = await createFixture()
|
|
1310
|
+
await writeFile(path.join(fixture.project, 'release.txt'), 'not published\n')
|
|
1311
|
+
await git(fixture.project, ['add', '-A'])
|
|
1312
|
+
await git(fixture.project, ['commit', '-m', 'personal release'])
|
|
1313
|
+
await git(fixture.project, ['push'])
|
|
1314
|
+
const rejectAtomicPush: CommandRunner = (executable, args, options) => {
|
|
1315
|
+
if (executable === 'git' && args.includes('--atomic')) {
|
|
1316
|
+
return Promise.reject(new Error('simulated shutdown before atomic publication'))
|
|
1317
|
+
}
|
|
1318
|
+
return runCommand(executable, args, options)
|
|
1319
|
+
}
|
|
1320
|
+
const interrupted = createTestService(fixture, { runCommand: rejectAtomicPush })
|
|
1321
|
+
const started = await interrupted.prepareRelease({ tabId: 'tab-a' })
|
|
1322
|
+
await acknowledgePreparationAndFlush(interrupted, 'tab-a', started.operationId!)
|
|
1323
|
+
await waitForPhase(interrupted, 'tab-a', ['fatal'])
|
|
1324
|
+
await interrupted.dispose()
|
|
1325
|
+
|
|
1326
|
+
const oldCurrentHead = await gitText(fixture.remote, ['rev-parse', 'next-release'])
|
|
1327
|
+
fixture.config.launchId = 'unpublished-recovery-launch'
|
|
1328
|
+
const resumeWrites = vi.fn(async () => {})
|
|
1329
|
+
const recovered = createTestService(fixture, { resumeWrites })
|
|
1330
|
+
const status = await recovered.status({ tabId: 'tab-b' })
|
|
1331
|
+
|
|
1332
|
+
expect(status.phase).toBe('idle')
|
|
1333
|
+
expect(status.canPrepareRelease).toBe(true)
|
|
1334
|
+
expect(await gitText(fixture.remote, ['rev-parse', 'next-release'])).toBe(oldCurrentHead)
|
|
1335
|
+
expect(resumeWrites).toHaveBeenCalledOnce()
|
|
1336
|
+
await expect(
|
|
1337
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release.json'))
|
|
1338
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
1339
|
+
await recovered.dispose()
|
|
1340
|
+
}, 15_000)
|
|
1341
|
+
|
|
1342
|
+
it('blocks recovery when the interrupted release refs have a foreign split state', async () => {
|
|
1343
|
+
const fixture = await createFixture()
|
|
1344
|
+
await writeFile(path.join(fixture.project, 'release.txt'), 'not published\n')
|
|
1345
|
+
await git(fixture.project, ['add', '-A'])
|
|
1346
|
+
await git(fixture.project, ['commit', '-m', 'personal release'])
|
|
1347
|
+
await git(fixture.project, ['push'])
|
|
1348
|
+
const rejectAtomicPush: CommandRunner = (executable, args, options) => {
|
|
1349
|
+
if (executable === 'git' && args.includes('--atomic')) {
|
|
1350
|
+
return Promise.reject(new Error('simulated shutdown before atomic publication'))
|
|
1351
|
+
}
|
|
1352
|
+
return runCommand(executable, args, options)
|
|
1353
|
+
}
|
|
1354
|
+
const interrupted = createTestService(fixture, { runCommand: rejectAtomicPush })
|
|
1355
|
+
const started = await interrupted.prepareRelease({ tabId: 'tab-a' })
|
|
1356
|
+
await acknowledgePreparationAndFlush(interrupted, 'tab-a', started.operationId!)
|
|
1357
|
+
await waitForPhase(interrupted, 'tab-a', ['fatal'])
|
|
1358
|
+
await interrupted.dispose()
|
|
1359
|
+
|
|
1360
|
+
await git(fixture.collaborator, ['branch', '--track', 'next-release', 'origin/next-release'])
|
|
1361
|
+
await git(fixture.collaborator, ['switch', 'next-release'])
|
|
1362
|
+
await writeFile(path.join(fixture.collaborator, 'foreign.txt'), 'foreign update\n')
|
|
1363
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
1364
|
+
await git(fixture.collaborator, ['commit', '-m', 'foreign current update'])
|
|
1365
|
+
await git(fixture.collaborator, ['push'])
|
|
1366
|
+
|
|
1367
|
+
fixture.config.launchId = 'foreign-recovery-launch'
|
|
1368
|
+
const resumeWrites = vi.fn(async () => {})
|
|
1369
|
+
const recovered = createTestService(fixture, { resumeWrites })
|
|
1370
|
+
const status = await recovered.status({ tabId: 'tab-b' })
|
|
1371
|
+
|
|
1372
|
+
expect(status.phase).toBe('fatal')
|
|
1373
|
+
expect(status.fatal?.kind).toBe('general')
|
|
1374
|
+
expect(resumeWrites).not.toHaveBeenCalled()
|
|
1375
|
+
await expect(
|
|
1376
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release.json'), 'utf8')
|
|
1377
|
+
).resolves.toContain('oldCurrentRemoteHead')
|
|
1378
|
+
await recovered.dispose()
|
|
1379
|
+
}, 15_000)
|
|
1380
|
+
|
|
1381
|
+
it('blocks interrupted Prepare recovery if the effective remote URL changed', async () => {
|
|
1382
|
+
const fixture = await createFixture()
|
|
1383
|
+
await writeFile(path.join(fixture.project, 'release.txt'), 'not published\n')
|
|
1384
|
+
await git(fixture.project, ['add', '-A'])
|
|
1385
|
+
await git(fixture.project, ['commit', '-m', 'personal release'])
|
|
1386
|
+
await git(fixture.project, ['push'])
|
|
1387
|
+
const rejectAtomicPush: CommandRunner = (executable, args, options) => {
|
|
1388
|
+
if (executable === 'git' && args.includes('--atomic')) {
|
|
1389
|
+
return Promise.reject(new Error('simulated shutdown before atomic publication'))
|
|
1390
|
+
}
|
|
1391
|
+
return runCommand(executable, args, options)
|
|
1392
|
+
}
|
|
1393
|
+
const interrupted = createTestService(fixture, { runCommand: rejectAtomicPush })
|
|
1394
|
+
const started = await interrupted.prepareRelease({ tabId: 'tab-a' })
|
|
1395
|
+
await acknowledgePreparationAndFlush(interrupted, 'tab-a', started.operationId!)
|
|
1396
|
+
await waitForPhase(interrupted, 'tab-a', ['fatal'])
|
|
1397
|
+
await interrupted.dispose()
|
|
1398
|
+
|
|
1399
|
+
await git(fixture.project, [
|
|
1400
|
+
'remote',
|
|
1401
|
+
'set-url',
|
|
1402
|
+
'origin',
|
|
1403
|
+
path.join(fixture.root, 'different-remote.git')
|
|
1404
|
+
])
|
|
1405
|
+
fixture.config.launchId = 'changed-url-recovery-launch'
|
|
1406
|
+
const resumeWrites = vi.fn(async () => {})
|
|
1407
|
+
const recovered = createTestService(fixture, { resumeWrites })
|
|
1408
|
+
const status = await recovered.status({ tabId: 'tab-b' })
|
|
1409
|
+
|
|
1410
|
+
expect(status.phase).toBe('fatal')
|
|
1411
|
+
expect(resumeWrites).not.toHaveBeenCalled()
|
|
1412
|
+
await expect(
|
|
1413
|
+
readFile(path.join(fixture.project, '.git', 'bosun-prepare-release.json'), 'utf8')
|
|
1414
|
+
).resolves.toContain('remoteUrlDigest')
|
|
1415
|
+
await recovered.dispose()
|
|
1416
|
+
}, 15_000)
|
|
1417
|
+
|
|
1418
|
+
it('persists completion so a managed backend restart cannot reactivate immediately', async () => {
|
|
1419
|
+
const fixture = await createFixture()
|
|
1420
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
1421
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
1422
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
1423
|
+
await git(fixture.collaborator, ['push'])
|
|
1424
|
+
let now = 1_000
|
|
1425
|
+
const service = createTestService(fixture, { now: () => now })
|
|
1426
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
1427
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
1428
|
+
const firstFinished = await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])
|
|
1429
|
+
expect(firstFinished.phase).toBe('restarting')
|
|
1430
|
+
const reloadToken = firstFinished.reloadToken
|
|
1431
|
+
await service.dispose()
|
|
1432
|
+
const persisted = JSON.parse(await readFile(fixture.config.runtimeStatePath, 'utf8'))
|
|
1433
|
+
persisted.lastRelevantChangeAt = 1_500
|
|
1434
|
+
await writeFile(fixture.config.runtimeStatePath, `${JSON.stringify(persisted)}\n`)
|
|
1435
|
+
|
|
1436
|
+
const restarted = createTestService(fixture, { now: () => now })
|
|
1437
|
+
const initial = await restarted.status({ tabId: 'tab-b' })
|
|
1438
|
+
expect(initial.phase).toBe('idle')
|
|
1439
|
+
expect(initial.reloadToken).toBe(reloadToken)
|
|
1440
|
+
await expect(
|
|
1441
|
+
readFile(path.join(fixture.project, '.git', 'bosun-content-refresh.json'))
|
|
1442
|
+
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
1443
|
+
expect(
|
|
1444
|
+
(await restarted.activate({ tabId: 'tab-b', reason: 'page-open' })).operationId
|
|
1445
|
+
).toBeNull()
|
|
1446
|
+
|
|
1447
|
+
now += fixture.config.idleMs
|
|
1448
|
+
expect((await restarted.activate({ tabId: 'tab-b', reason: 'focus' })).operationId).toBeNull()
|
|
1449
|
+
now = 1_500 + fixture.config.idleMs
|
|
1450
|
+
const eligible = await restarted.activate({ tabId: 'tab-b', reason: 'focus' })
|
|
1451
|
+
expect(eligible.phase).toBe('preparing')
|
|
1452
|
+
await restarted.dispose()
|
|
1453
|
+
}, 15_000)
|
|
1454
|
+
|
|
1455
|
+
it('keeps recovery blocked if the checkout leaves the personal branch before restart', async () => {
|
|
1456
|
+
const fixture = await createFixture()
|
|
1457
|
+
await writeFile(path.join(fixture.collaborator, 'remote.txt'), 'remote\n')
|
|
1458
|
+
await git(fixture.collaborator, ['add', '-A'])
|
|
1459
|
+
await git(fixture.collaborator, ['commit', '-m', 'remote work'])
|
|
1460
|
+
await git(fixture.collaborator, ['push'])
|
|
1461
|
+
const service = createTestService(fixture)
|
|
1462
|
+
const started = await service.activate({ tabId: 'tab-a', reason: 'page-open' })
|
|
1463
|
+
await acknowledgePreparationAndFlush(service, 'tab-a', started.operationId!)
|
|
1464
|
+
expect((await waitForPhase(service, 'tab-a', ['restarting', 'fatal'])).phase).toBe('restarting')
|
|
1465
|
+
await service.dispose()
|
|
1466
|
+
|
|
1467
|
+
await git(fixture.project, ['switch', 'next-release'])
|
|
1468
|
+
const installProject = vi.fn(async () => {})
|
|
1469
|
+
const resumeWrites = vi.fn(async () => {})
|
|
1470
|
+
const restarted = createTestService(fixture, { installProject, resumeWrites })
|
|
1471
|
+
const blocked = await restarted.status({ tabId: 'tab-b' })
|
|
1472
|
+
|
|
1473
|
+
expect(blocked.phase).toBe('fatal')
|
|
1474
|
+
expect(blocked.fatal?.kind).toBe('branch-mismatch')
|
|
1475
|
+
expect(installProject).not.toHaveBeenCalled()
|
|
1476
|
+
expect(resumeWrites).not.toHaveBeenCalled()
|
|
1477
|
+
await expect(
|
|
1478
|
+
readFile(path.join(fixture.project, '.git', 'bosun-content-refresh.json'), 'utf8')
|
|
1479
|
+
).resolves.toContain('installedContent')
|
|
1480
|
+
await restarted.dispose()
|
|
1481
|
+
})
|
|
1482
|
+
})
|
|
1483
|
+
|
|
1484
|
+
interface Fixture {
|
|
1485
|
+
root: string
|
|
1486
|
+
remote: string
|
|
1487
|
+
project: string
|
|
1488
|
+
collaborator: string
|
|
1489
|
+
config: GitModeConfig
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
async function createFixture(appContents = 'base\n'): Promise<Fixture> {
|
|
1493
|
+
const root = await mkdtemp(path.join(tmpdir(), 'bosun-git-mode-'))
|
|
1494
|
+
temporaryDirectories.push(root)
|
|
1495
|
+
const remote = path.join(root, 'remote.git')
|
|
1496
|
+
const seed = path.join(root, 'seed')
|
|
1497
|
+
const project = path.join(root, 'project')
|
|
1498
|
+
const collaborator = path.join(root, 'collaborator')
|
|
1499
|
+
await git(root, ['init', '--bare', remote])
|
|
1500
|
+
await mkdir(seed)
|
|
1501
|
+
await git(seed, ['init'])
|
|
1502
|
+
await configureUser(seed)
|
|
1503
|
+
await git(seed, ['checkout', '-b', 'next-release'])
|
|
1504
|
+
await writeFile(path.join(seed, 'app.txt'), appContents)
|
|
1505
|
+
await writeFile(path.join(seed, 'package.json'), '{"name":"fixture","private":true}\n')
|
|
1506
|
+
await git(seed, ['add', '-A'])
|
|
1507
|
+
await git(seed, ['commit', '-m', 'initial'])
|
|
1508
|
+
await git(seed, ['remote', 'add', 'origin', remote])
|
|
1509
|
+
await git(seed, ['push', '-u', 'origin', 'next-release'])
|
|
1510
|
+
await git(seed, ['switch', '-c', 'sb/bosun'])
|
|
1511
|
+
await git(seed, ['push', '-u', 'origin', 'sb/bosun'])
|
|
1512
|
+
await git(root, ['clone', '--branch', 'sb/bosun', remote, project])
|
|
1513
|
+
await git(root, ['clone', '--branch', 'sb/bosun', remote, collaborator])
|
|
1514
|
+
await git(project, ['branch', '--track', 'next-release', 'origin/next-release'])
|
|
1515
|
+
await configureUser(project)
|
|
1516
|
+
await configureUser(collaborator)
|
|
1517
|
+
|
|
1518
|
+
return {
|
|
1519
|
+
root,
|
|
1520
|
+
remote,
|
|
1521
|
+
project,
|
|
1522
|
+
collaborator,
|
|
1523
|
+
config: {
|
|
1524
|
+
enabled: true,
|
|
1525
|
+
projectDir: project,
|
|
1526
|
+
currentBranch: 'next-release',
|
|
1527
|
+
personalBranch: 'sb/bosun',
|
|
1528
|
+
idleMs: 60_000,
|
|
1529
|
+
currentUpstream: 'origin/next-release',
|
|
1530
|
+
currentUpstreamRemote: 'origin',
|
|
1531
|
+
currentUpstreamBranch: 'next-release',
|
|
1532
|
+
personalUpstream: 'origin/sb/bosun',
|
|
1533
|
+
personalUpstreamRemote: 'origin',
|
|
1534
|
+
personalUpstreamBranch: 'sb/bosun',
|
|
1535
|
+
launchId: 'test-launch',
|
|
1536
|
+
runtimeStatePath: path.join(root, 'runtime', 'git-mode.json'),
|
|
1537
|
+
restartExitCode: 76
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function createTestService(
|
|
1543
|
+
fixture: Fixture,
|
|
1544
|
+
overrides: {
|
|
1545
|
+
installProject?: () => Promise<void>
|
|
1546
|
+
settleWrites?: () => Promise<void>
|
|
1547
|
+
resumeWrites?: () => Promise<void>
|
|
1548
|
+
scheduleRestart?: (exitCode: number) => boolean
|
|
1549
|
+
runCommand?: CommandRunner
|
|
1550
|
+
now?: () => number
|
|
1551
|
+
liveTabMs?: number
|
|
1552
|
+
flushTimeoutMs?: number
|
|
1553
|
+
} = {}
|
|
1554
|
+
): GitModeService {
|
|
1555
|
+
return createGitModeService(fixture.config, {
|
|
1556
|
+
runCommand: overrides.runCommand,
|
|
1557
|
+
installProject: overrides.installProject ?? (async () => {}),
|
|
1558
|
+
settleWrites: overrides.settleWrites,
|
|
1559
|
+
resumeWrites: overrides.resumeWrites ?? (async () => {}),
|
|
1560
|
+
scheduleRestart: overrides.scheduleRestart ?? (() => true),
|
|
1561
|
+
now: overrides.now,
|
|
1562
|
+
liveTabMs: overrides.liveTabMs,
|
|
1563
|
+
flushTimeoutMs: overrides.flushTimeoutMs,
|
|
1564
|
+
watchFileChanges: false,
|
|
1565
|
+
logError: () => {}
|
|
1566
|
+
})
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
async function acknowledgePreparationAndFlush(
|
|
1570
|
+
service: GitModeService,
|
|
1571
|
+
tabId: string,
|
|
1572
|
+
operationId: string
|
|
1573
|
+
): Promise<void> {
|
|
1574
|
+
const flushing = await service.flushAck({ tabId, operationId, stage: 'preflight' })
|
|
1575
|
+
expect(flushing.phase).toBe('preparing')
|
|
1576
|
+
expect(flushing.preparationStage).toBe('browser-writes')
|
|
1577
|
+
expect(flushing.tabNeedsFlush).toBe(true)
|
|
1578
|
+
await service.flushAck({ tabId, operationId, stage: 'browser-writes' })
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
async function waitForPhase(
|
|
1582
|
+
service: GitModeService,
|
|
1583
|
+
tabId: string,
|
|
1584
|
+
phases: GitModeStatus['phase'][]
|
|
1585
|
+
): Promise<GitModeStatus> {
|
|
1586
|
+
const deadline = Date.now() + 10_000
|
|
1587
|
+
while (Date.now() < deadline) {
|
|
1588
|
+
const status = await service.status({ tabId })
|
|
1589
|
+
if (phases.includes(status.phase)) return status
|
|
1590
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
1591
|
+
}
|
|
1592
|
+
throw new Error(`Timed out waiting for Git mode phase: ${phases.join(', ')}`)
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function waitForOperationEnd(service: GitModeService, tabId: string): Promise<GitModeStatus> {
|
|
1596
|
+
const deadline = Date.now() + 10_000
|
|
1597
|
+
while (Date.now() < deadline) {
|
|
1598
|
+
const status = await service.status({ tabId })
|
|
1599
|
+
if (
|
|
1600
|
+
status.phase === 'fatal' ||
|
|
1601
|
+
status.phase === 'restarting' ||
|
|
1602
|
+
(status.phase === 'idle' && status.operationId === null)
|
|
1603
|
+
) {
|
|
1604
|
+
return status
|
|
1605
|
+
}
|
|
1606
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
1607
|
+
}
|
|
1608
|
+
throw new Error('Timed out waiting for the Git mode operation to finish')
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
async function configureUser(repository: string): Promise<void> {
|
|
1612
|
+
await git(repository, ['config', 'user.name', 'Bosun Test'])
|
|
1613
|
+
await git(repository, ['config', 'user.email', 'bosun@example.test'])
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function switchCollaboratorToCurrent(fixture: Fixture): Promise<void> {
|
|
1617
|
+
await git(fixture.collaborator, ['branch', '--track', 'next-release', 'origin/next-release'])
|
|
1618
|
+
await git(fixture.collaborator, ['switch', 'next-release'])
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
async function setHostilePullConfiguration(repository: string): Promise<void> {
|
|
1622
|
+
await git(repository, ['config', 'pull.rebase', 'true'])
|
|
1623
|
+
await git(repository, ['config', 'pull.ff', 'only'])
|
|
1624
|
+
await git(repository, ['config', 'pull.squash', 'true'])
|
|
1625
|
+
await git(repository, ['config', 'merge.autostash', 'true'])
|
|
1626
|
+
await git(repository, ['config', 'commit.gpgSign', 'true'])
|
|
1627
|
+
await git(repository, ['config', 'merge.gpgSign', 'true'])
|
|
1628
|
+
await git(repository, ['config', 'push.gpgSign', 'true'])
|
|
1629
|
+
await git(repository, ['config', 'merge.verifySignatures', 'true'])
|
|
1630
|
+
await git(repository, ['config', 'branch.sb/bosun.mergeOptions', '--no-commit'])
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async function installFailingHooks(repository: string): Promise<void> {
|
|
1634
|
+
const hooks = path.join(repository, '.git', 'hooks')
|
|
1635
|
+
for (const name of ['pre-commit', 'pre-push']) {
|
|
1636
|
+
const hook = path.join(hooks, name)
|
|
1637
|
+
await writeFile(hook, '#!/bin/sh\nexit 91\n')
|
|
1638
|
+
await chmod(hook, 0o755)
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
async function git(cwd: string, args: string[]): Promise<void> {
|
|
1643
|
+
await execFileAsync('git', args, { cwd, env: { ...process.env, LC_ALL: 'C' } })
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
async function gitText(cwd: string, args: string[]): Promise<string> {
|
|
1647
|
+
const result = await execFileAsync('git', args, {
|
|
1648
|
+
cwd,
|
|
1649
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
1650
|
+
encoding: 'utf8'
|
|
1651
|
+
})
|
|
1652
|
+
return result.stdout.trim()
|
|
1653
|
+
}
|