@runfusion/fusion 0.43.1 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/automation-store-EKXXEE2S.js +10 -0
- package/dist/automation-store-PLNG3X72.js +10 -0
- package/dist/automation-store-RDDYZFUJ.js +10 -0
- package/dist/bin.js +14420 -3360
- package/dist/chunk-2JI3F6MG.js +948 -0
- package/dist/chunk-4L6CLWJC.js +8345 -0
- package/dist/chunk-4VNS5WPM.js +42 -0
- package/dist/chunk-5RGECGQL.js +1139 -0
- package/dist/chunk-6JDLN7IL.js +409 -0
- package/dist/chunk-AICJATVW.js +948 -0
- package/dist/chunk-AWKNRUOR.js +1148 -0
- package/dist/chunk-BHM3UYWS.js +4651 -0
- package/dist/chunk-BMUXDSBF.js +4647 -0
- package/dist/chunk-GETECT6V.js +353 -0
- package/dist/chunk-GUV4QQT3.js +1143 -0
- package/dist/chunk-JKQO7YYZ.js +8369 -0
- package/dist/chunk-LYZVU6UV.js +409 -0
- package/dist/chunk-MAN4WYHO.js +353 -0
- package/dist/chunk-MCUG2Z7M.js +4651 -0
- package/dist/chunk-PSDYB45U.js +948 -0
- package/dist/chunk-RR2HM46R.js +8345 -0
- package/dist/chunk-RXMQ5OMY.js +9226 -0
- package/dist/chunk-S2EVJUY3.js +353 -0
- package/dist/chunk-UXR5XZFZ.js +409 -0
- package/dist/client/.vite/manifest.json +1138 -340
- package/dist/client/assets/AgentDetailView-CgbB51Iw.css +1 -0
- package/dist/client/assets/AgentDetailView-DJTi6hKC.js +18 -0
- package/dist/client/assets/{AgentPermissionPolicyEditor-BiY1a4KR.js → AgentPermissionPolicyEditor-C5DR9dUS.js} +2 -2
- package/dist/client/assets/AgentsView-BNd90PSx.js +29 -0
- package/dist/client/assets/AgentsView-CzMa5Olf.css +1 -0
- package/dist/client/assets/ChatView-B7JaZCL_.js +19 -0
- package/dist/client/assets/ChatView-BtjkE0V7.css +1 -0
- package/dist/client/assets/CommandCenter-CHO21xEW.js +74 -0
- package/dist/client/assets/CommandCenter-Xs5Ftv8s.css +1 -0
- package/dist/client/assets/DevServerView-B_p91Mj_.css +1 -0
- package/dist/client/assets/DevServerView-H9OkT1ld.js +1 -0
- package/dist/client/assets/{DirectoryPicker-DTKtd8L2.css → DirectoryPicker-0rspvcMz.css} +1 -1
- package/dist/client/assets/DirectoryPicker-DegxT8ia.js +1 -0
- package/dist/client/assets/DocumentsView-kVVQYISc.js +1 -0
- package/dist/client/assets/EvalsView-C9-iTSat.js +1 -0
- package/dist/client/assets/EvalsView-CTbgPUne.css +1 -0
- package/dist/client/assets/ExperimentalAgentOnboardingModal-3ZEFv8sr.css +1 -0
- package/dist/client/assets/ExperimentalAgentOnboardingModal-DjP_xNkc.js +1 -0
- package/dist/client/assets/GoalsView-CUh8JBnJ.css +1 -0
- package/dist/client/assets/GoalsView-VoIHJJ5l.js +2 -0
- package/dist/client/assets/InsightsView-BIZaUcpd.js +11 -0
- package/dist/client/assets/InsightsView-C7yRTMPn.css +1 -0
- package/dist/client/assets/MemoryView-7v7rv6ns.js +2 -0
- package/dist/client/assets/MemoryView-CBDUr2Jy.css +1 -0
- package/dist/client/assets/PiExtensionsManager-CB89rV4U.js +11 -0
- package/dist/client/assets/PluginManager-BNSuW_R4.js +1 -0
- package/dist/client/assets/PluginManager-DSiHizRD.css +1 -0
- package/dist/client/assets/PullRequestView-CI5jdKBL.js +6 -0
- package/dist/client/assets/PullRequestView-CpdE3Yo_.css +1 -0
- package/dist/client/assets/ResearchView-BT5dR_m8.js +1 -0
- package/dist/client/assets/ResearchView-DIVNQF4_.css +1 -0
- package/dist/client/assets/SecretsView-CvFioX7R.css +1 -0
- package/dist/client/assets/SecretsView-WPYI58Uo.js +1 -0
- package/dist/client/assets/SessionTerminal-C6gyOWLt.js +2 -0
- package/dist/client/assets/SessionTerminal-D4R3ScvQ.css +1 -0
- package/dist/client/assets/SettingsModal-BESN59Ao.css +1 -0
- package/dist/client/assets/SettingsModal-BZokaNyx.css +1 -0
- package/dist/client/assets/SettingsModal-Bq_3AbaB.js +1 -0
- package/dist/client/assets/SettingsModal-JHACoQJe.js +21 -0
- package/dist/client/assets/SetupWizardModal-CrgbnR_d.js +2 -0
- package/dist/client/assets/ShadcnColorPicker-1icHuwhC.js +11 -0
- package/dist/client/assets/ShadcnColorPicker-pqVWCC5-.css +1 -0
- package/dist/client/assets/SkillsView-CJ8snrma.css +1 -0
- package/dist/client/assets/SkillsView-byuSe3e0.js +1 -0
- package/dist/client/assets/TodoView-CIsL_uvQ.css +1 -0
- package/dist/client/assets/TodoView-CQ9GjYBg.js +6 -0
- package/dist/client/assets/WorkflowNodeEditor-DW7sJZUX.js +8 -0
- package/dist/client/assets/WorkflowNodeEditor-f_XPV5Jd.css +1 -0
- package/dist/client/assets/{addon-fit-B4jkcKk4.js → addon-fit-C1WQXRvu.js} +1 -1
- package/dist/client/assets/{addon-unicode11-Cdne2Sj4.js → addon-unicode11-BVmNDUBM.js} +1 -1
- package/dist/client/assets/{addon-web-links-CRNLzj7m.js → addon-web-links-CkVew5xs.js} +1 -1
- package/dist/client/assets/{addon-webgl-CeCSULF1.js → addon-webgl-uLc0RKcC.js} +1 -1
- package/dist/client/assets/agentSkills-CeytXLHF.js +1 -0
- package/dist/client/assets/agentSkills-bQJ4X1gH.css +1 -0
- package/dist/client/assets/agentsOrgChartLayout-BMvyuaLA.js +1 -0
- package/dist/client/assets/app-3wbZ08YC.js +13 -0
- package/dist/client/assets/app-7-5DWLtg.js +13 -0
- package/dist/client/assets/app-CSvE5Noc.js +13 -0
- package/dist/client/assets/app-CZ2LTFce.js +13 -0
- package/dist/client/assets/app-Cv9xEnll.js +13 -0
- package/dist/client/assets/app-x_yPcO5Y.js +13 -0
- package/dist/client/assets/arc-cxkkNsnK.js +1 -0
- package/dist/client/assets/architectureDiagram-3BPJPVTR-BRI0QwDf.js +36 -0
- package/dist/client/assets/band-CquvqAHh.js +1 -0
- package/dist/client/assets/blockDiagram-GPEHLZMM-BXkN8Hv_.js +132 -0
- package/dist/client/assets/c4Diagram-AAUBKEIU-C_Kcokep.js +10 -0
- package/dist/client/assets/channel-Clzpzu32.js +1 -0
- package/dist/client/assets/chunk-2J33WTMH-BGeuCz9C.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-DcVdctr0.js +1 -0
- package/dist/client/assets/chunk-55IACEB6-Dg6gDigQ.js +1 -0
- package/dist/client/assets/chunk-727SXJPM-B9B-vlEb.js +206 -0
- package/dist/client/assets/chunk-AQP2D5EJ-LrWQ-2yi.js +231 -0
- package/dist/client/assets/chunk-FMBD7UC4-Bnd6hAED.js +15 -0
- package/dist/client/assets/chunk-ND2GUHAM-nAw93UB5.js +1 -0
- package/dist/client/assets/chunk-QZHKN3VN-CgprKcZo.js +1 -0
- package/dist/client/assets/classDiagram-4FO5ZUOK-CzYHP-af.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CzYHP-af.js +1 -0
- package/dist/client/assets/common-BDp2y6CF.js +1 -0
- package/dist/client/assets/common-BnEo5fLN.js +1 -0
- package/dist/client/assets/common-Bzr_Y8h4.js +1 -0
- package/dist/client/assets/common-CvwyFzfv.js +1 -0
- package/dist/client/assets/common-CztCXRkz.js +1 -0
- package/dist/client/assets/common-DPe2mbb7.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-Bhn0g_iK.js +1 -0
- package/dist/client/assets/cytoscape.esm-OyMbaexL.js +331 -0
- package/dist/client/assets/dagre-BM42HDAG-CP4kfwqd.js +4 -0
- package/dist/client/assets/{dashboard-view-C3SsjXo1.js → dashboard-view-CM6Xin_H.js} +1680 -1620
- package/dist/client/assets/dashboard-view-DV1nEhh9.js +1 -0
- package/dist/client/assets/dashboard-view-D_zsA-kz.css +1 -0
- package/dist/client/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/client/assets/diagram-2AECGRRQ-DPhzVEUf.js +43 -0
- package/dist/client/assets/diagram-5GNKFQAL-8QOC3K_L.js +10 -0
- package/dist/client/assets/diagram-KO2AKTUF-C1twgEch.js +3 -0
- package/dist/client/assets/diagram-LMA3HP47-3bEegSuf.js +24 -0
- package/dist/client/assets/diagram-OG6HWLK6-BJ_x0rm6.js +24 -0
- package/dist/client/assets/erDiagram-TEJ5UH35-DNiiS5q6.js +85 -0
- package/dist/client/assets/flowDiagram-I6XJVG4X-DoouPklt.js +162 -0
- package/dist/client/assets/{folder-open-Bw1mxVfa.js → folder-open-CuUs2W7f.js} +1 -1
- package/dist/client/assets/ganttDiagram-6RSMTGT7-BrR6VnP5.js +292 -0
- package/dist/client/assets/gitGraphDiagram-PVQCEYII-BV8_J6Ee.js +106 -0
- package/dist/client/assets/graph--OzhPTMs.js +1 -0
- package/dist/client/assets/index-BZBQ_KsL.css +1 -0
- package/dist/client/assets/index-CZcWjLAb.js +1459 -0
- package/dist/client/assets/infoDiagram-5YYISTIA-DGwIIFkR.js +2 -0
- package/dist/client/assets/init-Dmth1JHB.js +1 -0
- package/dist/client/assets/ishikawaDiagram-YF4QCWOH-D5dEH4RE.js +70 -0
- package/dist/client/assets/journeyDiagram-JHISSGLW-BuuSWPw5.js +139 -0
- package/dist/client/assets/kanban-definition-UN3LZRKU-DA34uUla.js +89 -0
- package/dist/client/assets/katex-HP8lGamR.js +257 -0
- package/dist/client/assets/layout-SsrduOYp.js +1 -0
- package/dist/client/assets/linear-dXivciGL.js +1 -0
- package/dist/client/assets/mermaid.core-Ch4HatqA.js +309 -0
- package/dist/client/assets/mindmap-definition-RKZ34NQL-DUCG-qD3.js +96 -0
- package/dist/client/assets/ordinal-DILIJJjt.js +1 -0
- package/dist/client/assets/pieDiagram-4H26LBE5-DWvFGiLa.js +30 -0
- package/dist/client/assets/quadrantDiagram-W4KKPZXB-OPTVW3WT.js +7 -0
- package/dist/client/assets/requirementDiagram-4Y6WPE33-D7feZvlU.js +84 -0
- package/dist/client/assets/sankeyDiagram-5OEKKPKP-AtnSsEX1.js +40 -0
- package/dist/client/assets/sequenceDiagram-3UESZ5HK-BZjHUeI5.js +162 -0
- package/dist/client/assets/shield-alert-CzaF9ROV.js +6 -0
- package/dist/client/assets/stateDiagram-AJRCARHV-C8j23Pvn.js +1 -0
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CiZR_S5-.js +1 -0
- package/dist/client/assets/step-yuy0qJer.js +1 -0
- package/dist/client/assets/time-DoDTxsna.js +1 -0
- package/dist/client/assets/timeline-definition-PNZ67QCA-DvfNz25n.js +120 -0
- package/dist/client/assets/{upload-BCmd-1I0.js → upload-DTsYywhN.js} +1 -1
- package/dist/client/assets/{users-DO_t3V1p.js → users-yAd09tKP.js} +1 -1
- package/dist/client/assets/{vendor-i18n-CV0mwX_-.js → vendor-i18n-GF3RlKbq.js} +2 -2
- package/dist/client/assets/vendor-react-SZ_qB27O.js +49 -0
- package/dist/client/assets/vendor-reactflow-Bg-Qjzh8.js +15 -0
- package/dist/client/assets/{vendor-xterm-tcedd2Jt.js → vendor-xterm-BNq00GSj.js} +1 -1
- package/dist/client/assets/vennDiagram-CIIHVFJN-CG2aMt3v.js +34 -0
- package/dist/client/assets/wardley-L42UT6IY-BnhBteen.js +173 -0
- package/dist/client/assets/wardleyDiagram-YWT4CUSO-DwqTzM3T.js +78 -0
- package/dist/client/assets/xychartDiagram-2RQKCTM6-5Cv2ZKMV.js +7 -0
- package/dist/client/assets/zoom-out-NWbVkreR.js +41 -0
- package/dist/client/icons/icon-192.png +0 -0
- package/dist/client/icons/icon-512.png +0 -0
- package/dist/client/index.html +30 -10
- package/dist/client/sw.js +1 -1
- package/dist/client/theme-data.css +3348 -469
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/index.ts +56 -40
- package/dist/droid-cli/package.json +1 -1
- package/dist/droid-cli/src/__tests__/execsync-guard.test.ts +17 -0
- package/dist/droid-cli/src/__tests__/index.test.ts +47 -21
- package/dist/droid-cli/src/__tests__/process-manager.test.ts +51 -78
- package/dist/extension.js +9651 -1556
- package/dist/extract-zip-WL55Z3AU.js +2497 -0
- package/dist/index.min-CHQ3ABYJ.js +2926 -0
- package/dist/memory-dreams-7USISKVE.js +32 -0
- package/dist/memory-dreams-PBPISUDD.js +32 -0
- package/dist/memory-dreams-XNO2B2QF.js +32 -0
- package/dist/migration-EH2YOCYQ.js +15 -0
- package/dist/migration-PLGTTKL5.js +15 -0
- package/dist/migration-WZEZK22R.js +15 -0
- package/dist/pi-claude-cli/index.ts +47 -8
- package/dist/pi-claude-cli/package.json +4 -1
- package/dist/pi-claude-cli/src/__tests__/acp-dispatch.test.ts +80 -0
- package/dist/pi-claude-cli/src/__tests__/acp-driver.test.ts +423 -0
- package/dist/pi-claude-cli/src/__tests__/execsync-guard.test.ts +14 -0
- package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +26 -45
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +0 -1
- package/dist/pi-claude-cli/src/acp-driver.ts +585 -0
- package/dist/pi-claude-cli/src/event-bridge.ts +7 -0
- package/dist/pi-claude-cli/src/mcp-config.ts +29 -0
- package/dist/pi-claude-cli/src/process-manager.ts +17 -39
- package/dist/plugin-sdk/index.d.ts +5305 -5148
- package/dist/plugins/fusion-plugin-acp-runtime/bundled.js +263 -19
- package/dist/plugins/fusion-plugin-acp-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
- package/dist/plugins/fusion-plugin-compound-engineering/bundled.js +37 -7
- package/dist/plugins/fusion-plugin-compound-engineering/manifest.json +4 -4
- package/dist/plugins/fusion-plugin-compound-engineering/package.json +1 -1
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
- package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +69 -45
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/package.json +1 -1
- package/dist/plugins/fusion-plugin-roadmap/bundled.js +5 -11
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -11
- package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +5 -1
- package/dist/routine-store-GIJ6MXWY.js +10 -0
- package/dist/routine-store-HTVC2PUQ.js +10 -0
- package/dist/routine-store-KGHZBPDF.js +10 -0
- package/package.json +3 -3
- package/skill/fusion/SKILL.md +1 -1
- package/skill/fusion/references/cli-commands.md +2 -1
- package/skill/fusion/references/engine-tools.md +6 -4
- package/skill/fusion/references/extension-tools.md +10 -0
- package/skill/fusion/references/fusion-capabilities.md +3 -1
- package/dist/client/assets/AgentDetailView-BGN03dyz.css +0 -1
- package/dist/client/assets/AgentDetailView-DKMz3r04.js +0 -18
- package/dist/client/assets/AgentsView-Co-k4vjh.js +0 -29
- package/dist/client/assets/AgentsView-DfznJDek.css +0 -1
- package/dist/client/assets/ChatView-BkAG99bj.js +0 -6
- package/dist/client/assets/ChatView-DwJAd5G1.css +0 -1
- package/dist/client/assets/DevServerView-C6DC0URX.js +0 -1
- package/dist/client/assets/DevServerView-VTQY28P7.css +0 -1
- package/dist/client/assets/DirectoryPicker-CkcGjyv0.js +0 -1
- package/dist/client/assets/DocumentsView-OCK1Vsw_.js +0 -1
- package/dist/client/assets/DocumentsView-WfVoTZt8.css +0 -1
- package/dist/client/assets/EvalsView-CUNJ1TLc.css +0 -1
- package/dist/client/assets/EvalsView-CUfyj5GW.js +0 -1
- package/dist/client/assets/ExperimentalAgentOnboardingModal-BNpveCEk.css +0 -1
- package/dist/client/assets/ExperimentalAgentOnboardingModal-Dau9RTU_.js +0 -499
- package/dist/client/assets/GoalsView-CNLTn0rX.js +0 -2
- package/dist/client/assets/GoalsView-DI66UsjT.css +0 -1
- package/dist/client/assets/InsightsView-BQPxHxKI.js +0 -11
- package/dist/client/assets/InsightsView-C9-mqVvf.css +0 -1
- package/dist/client/assets/MemoryView-D3xQIFjj.js +0 -2
- package/dist/client/assets/MemoryView-zaXewZzi.css +0 -1
- package/dist/client/assets/NodesView-7VInxyf_.css +0 -1
- package/dist/client/assets/NodesView-CSMvGwDU.js +0 -14
- package/dist/client/assets/PiExtensionsManager-BtqXF3fl.js +0 -11
- package/dist/client/assets/PluginManager-CSSDb3VO.js +0 -1
- package/dist/client/assets/PluginManager-UHVLvWWl.css +0 -1
- package/dist/client/assets/PullRequestView-Bjyfffdu.js +0 -6
- package/dist/client/assets/PullRequestView-WQIS0zUC.css +0 -1
- package/dist/client/assets/ReliabilityView-QKMUDebT.js +0 -1
- package/dist/client/assets/ReliabilityView-eoKBhBc4.css +0 -1
- package/dist/client/assets/ResearchView-CRU_NPho.js +0 -1
- package/dist/client/assets/ResearchView-DTh8FLUm.css +0 -1
- package/dist/client/assets/SecretsView-Bs8ySyC5.js +0 -1
- package/dist/client/assets/SecretsView-Djo1rahi.css +0 -1
- package/dist/client/assets/SessionTerminal-BiigPP2l.css +0 -1
- package/dist/client/assets/SessionTerminal-Dk1NgmN2.js +0 -2
- package/dist/client/assets/SettingsModal-DpAxDq6M.css +0 -1
- package/dist/client/assets/SettingsModal-JCaNvdAb.js +0 -31
- package/dist/client/assets/SettingsModal-Nw5cTKAK.js +0 -1
- package/dist/client/assets/SettingsModal-nF_GaeeK.css +0 -1
- package/dist/client/assets/SetupWizardModal-CE79myEy.js +0 -1
- package/dist/client/assets/SetupWizardModal-CIqFbkrD.css +0 -1
- package/dist/client/assets/SkillsView-3bhGyWs1.css +0 -1
- package/dist/client/assets/SkillsView-Dc8YskAX.js +0 -1
- package/dist/client/assets/StashRecoveryView-B_8WIQEo.css +0 -1
- package/dist/client/assets/StashRecoveryView-CqKOFiXT.js +0 -1
- package/dist/client/assets/TodoView-7xIh-Fmi.css +0 -1
- package/dist/client/assets/TodoView-BqIRwcZw.js +0 -6
- package/dist/client/assets/WorkflowNodeEditor-2jDuk2FR.js +0 -8
- package/dist/client/assets/WorkflowNodeEditor-BokhW3de.css +0 -1
- package/dist/client/assets/app-B2ZxMfJT.js +0 -13
- package/dist/client/assets/app-BffGU_HQ.js +0 -13
- package/dist/client/assets/app-C8AHhs5N.js +0 -13
- package/dist/client/assets/app-CUu1sm3x.js +0 -13
- package/dist/client/assets/app-Ri9Te2Dv.js +0 -13
- package/dist/client/assets/app-sEkTyT-w.js +0 -13
- package/dist/client/assets/common-BRyX4VxB.js +0 -1
- package/dist/client/assets/common-CN9QPI2d.js +0 -1
- package/dist/client/assets/common-CNbjPELi.js +0 -1
- package/dist/client/assets/common-DFDqu_n0.js +0 -1
- package/dist/client/assets/common-DgWWKG8Z.js +0 -1
- package/dist/client/assets/common-X6PzBnVA.js +0 -1
- package/dist/client/assets/createLucideIcon-BYvOGRiO.js +0 -21
- package/dist/client/assets/dashboard-view-BhbN8LPh.js +0 -1
- package/dist/client/assets/dashboard-view-DdGlfuu-.css +0 -1
- package/dist/client/assets/dashboard-view-DlKsVySD.js +0 -3
- package/dist/client/assets/dashboard-view-Du53JbmQ.css +0 -1
- package/dist/client/assets/index-CpTBp1RG.js +0 -793
- package/dist/client/assets/index-DqhCiU35.css +0 -1
- package/dist/client/assets/vendor-react-D37HfzIf.js +0 -49
- package/dist/client/assets/vendor-reactflow-CRZcPCXF.js +0 -15
- package/dist/client/assets/x-CqNX_9E2.js +0 -61
- package/dist/client/assets/zoom-out-CSTHbJgD.js +0 -21
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
4
|
+
|
|
5
|
+
// Synthetic ACP session/update sequence the mocked prompt() will replay.
|
|
6
|
+
let scriptedUpdates: Array<Record<string, unknown>> = [];
|
|
7
|
+
let scriptedUsage: Record<string, number> | undefined;
|
|
8
|
+
// When set, prompt() never resolves — simulates a turn waiting on the bridge so
|
|
9
|
+
// only an out-of-band event (child death / abort) can end it.
|
|
10
|
+
let scriptedHang = false;
|
|
11
|
+
|
|
12
|
+
// Driver validates the bridge path with existsSync — make the fake path "exist".
|
|
13
|
+
// writeFileSync/unlinkSync back the R17 auth-failure signal (spied).
|
|
14
|
+
const fsSpies = vi.hoisted(() => ({ writeFileSync: vi.fn(), unlinkSync: vi.fn() }));
|
|
15
|
+
vi.mock("node:fs", () => ({ existsSync: () => true, writeFileSync: fsSpies.writeFileSync, unlinkSync: fsSpies.unlinkSync }));
|
|
16
|
+
|
|
17
|
+
vi.mock("node:child_process", () => ({
|
|
18
|
+
spawn: vi.fn(() => {
|
|
19
|
+
const proc = new EventEmitter() as EventEmitter & Record<string, unknown>;
|
|
20
|
+
proc.stdin = new PassThrough();
|
|
21
|
+
proc.stdout = new PassThrough();
|
|
22
|
+
proc.stderr = new PassThrough();
|
|
23
|
+
proc.kill = vi.fn();
|
|
24
|
+
proc.pid = 4242;
|
|
25
|
+
return proc;
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Mock the ACP SDK: ClientSideConnection.prompt() replays scriptedUpdates onto
|
|
30
|
+
// the client handler, then resolves — so we exercise the real translation logic.
|
|
31
|
+
vi.mock("@agentclientprotocol/sdk", () => ({
|
|
32
|
+
PROTOCOL_VERSION: 1,
|
|
33
|
+
ndJsonStream: vi.fn(() => ({})),
|
|
34
|
+
ClientSideConnection: vi.fn(function (this: Record<string, unknown>, factory: () => { sessionUpdate: (p: unknown) => Promise<void> }) {
|
|
35
|
+
const handler = factory();
|
|
36
|
+
this.initialize = vi.fn(async () => ({ protocolVersion: 1 }));
|
|
37
|
+
this.newSession = vi.fn(async () => ({ sessionId: "s1" }));
|
|
38
|
+
this.prompt = vi.fn(async () => {
|
|
39
|
+
if (scriptedHang) return new Promise(() => {}); // never resolves
|
|
40
|
+
for (const u of scriptedUpdates) await handler.sessionUpdate({ update: u });
|
|
41
|
+
return { stopReason: "end_turn", usage: scriptedUsage };
|
|
42
|
+
});
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const { MockStream } = vi.hoisted(() => {
|
|
47
|
+
const MockStream: unknown = vi.fn(function (this: Record<string, unknown>) {
|
|
48
|
+
const events: Array<Record<string, unknown>> = [];
|
|
49
|
+
this.push = vi.fn((e: Record<string, unknown>) => events.push(e));
|
|
50
|
+
this.end = vi.fn();
|
|
51
|
+
this._events = events;
|
|
52
|
+
});
|
|
53
|
+
return { MockStream };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
vi.mock("@earendil-works/pi-ai", () => ({
|
|
57
|
+
AssistantMessageEventStream: MockStream,
|
|
58
|
+
calculateCost: vi.fn(),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
import { spawn } from "node:child_process";
|
|
62
|
+
import { ClientSideConnection } from "@agentclientprotocol/sdk";
|
|
63
|
+
import { streamViaAcp, buildBridgeEnv } from "../acp-driver.js";
|
|
64
|
+
|
|
65
|
+
const MODEL = { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" } as never;
|
|
66
|
+
const CTX = { messages: [{ role: "user", content: "hi" }] } as never;
|
|
67
|
+
const OPTS = { bridgePath: "/fake/claude-code-cli-acp", cwd: "/tmp", mcpServers: [], bridgeEnv: { HOME: "/h", PATH: "/b" } };
|
|
68
|
+
|
|
69
|
+
function eventsOf(stream: { _events: Array<Record<string, unknown>> }) {
|
|
70
|
+
return stream._events;
|
|
71
|
+
}
|
|
72
|
+
const flush = () => new Promise((r) => setTimeout(r, 30));
|
|
73
|
+
|
|
74
|
+
describe("streamViaAcp — ACP→pi translation (U11)", () => {
|
|
75
|
+
beforeEach(() => { scriptedUpdates = []; scriptedUsage = undefined; scriptedHang = false; });
|
|
76
|
+
|
|
77
|
+
it("feeds ACP token usage (incl. cache tokens) into the done message (item 2)", async () => {
|
|
78
|
+
scriptedUsage = { inputTokens: 11, outputTokens: 22, cachedReadTokens: 5, cachedWriteTokens: 3 };
|
|
79
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hi" } }];
|
|
80
|
+
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
|
|
81
|
+
await flush();
|
|
82
|
+
const done = stream._events.find((e) => e.type === "done") as { message?: { usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; totalTokens?: number } } };
|
|
83
|
+
expect(done?.message?.usage?.input).toBe(11);
|
|
84
|
+
expect(done?.message?.usage?.output).toBe(22);
|
|
85
|
+
expect(done?.message?.usage?.cacheRead).toBe(5);
|
|
86
|
+
expect(done?.message?.usage?.cacheWrite).toBe(3);
|
|
87
|
+
expect(done?.message?.usage?.totalTokens).toBe(41);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("ignores a malformed/untrusted usage payload (string/NaN/negative)", async () => {
|
|
91
|
+
scriptedUsage = { inputTokens: "99" as unknown as number, outputTokens: NaN, cachedReadTokens: -5 };
|
|
92
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "hi" } }];
|
|
93
|
+
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
|
|
94
|
+
await flush();
|
|
95
|
+
const done = stream._events.find((e) => e.type === "done") as { message?: { usage?: { input?: number; output?: number } } };
|
|
96
|
+
// Coerced to undefined → bridge leaves usage at 0; never a string/NaN.
|
|
97
|
+
expect(done?.message?.usage?.input).toBe(0);
|
|
98
|
+
expect(Number.isNaN(done?.message?.usage?.output)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does not emit usage on a tool-use (break-early) turn", async () => {
|
|
102
|
+
scriptedUsage = { inputTokens: 11, outputTokens: 22 };
|
|
103
|
+
scriptedUpdates = [
|
|
104
|
+
{ sessionUpdate: "tool_call", toolCallId: "t1", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
|
|
105
|
+
];
|
|
106
|
+
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
|
|
107
|
+
await flush();
|
|
108
|
+
const done = stream._events.find((e) => e.type === "done") as { message?: { usage?: { input?: number } } };
|
|
109
|
+
expect(done?.message?.usage?.input ?? 0).toBe(0); // tool-use turn reports zero usage
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("translates agent_message_chunk text into pi text events + done(stop)", async () => {
|
|
113
|
+
scriptedUpdates = [
|
|
114
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Hello " } },
|
|
115
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "world" } },
|
|
116
|
+
];
|
|
117
|
+
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
|
|
118
|
+
await flush();
|
|
119
|
+
const types = eventsOf(stream).map((e) => e.type);
|
|
120
|
+
expect(types).toContain("start");
|
|
121
|
+
expect(types).toContain("text_start");
|
|
122
|
+
expect(types.filter((t) => t === "text_delta").length).toBe(2);
|
|
123
|
+
const done = eventsOf(stream).find((e) => e.type === "done");
|
|
124
|
+
expect(done).toBeDefined();
|
|
125
|
+
expect(done!.reason).toBe("stop");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("breaks early on a tool_call: emits toolcall_start + done(toolUse), no execution", async () => {
|
|
129
|
+
scriptedUpdates = [
|
|
130
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "let me check" } },
|
|
131
|
+
{ sessionUpdate: "tool_call", toolCallId: "t1", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
|
|
132
|
+
// anything after the tool call must be ignored (break-early)
|
|
133
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "SHOULD NOT APPEAR" } },
|
|
134
|
+
];
|
|
135
|
+
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
|
|
136
|
+
await flush();
|
|
137
|
+
const types = eventsOf(stream).map((e) => e.type);
|
|
138
|
+
expect(types).toContain("toolcall_start");
|
|
139
|
+
const done = eventsOf(stream).find((e) => e.type === "done");
|
|
140
|
+
expect(done!.reason).toBe("toolUse");
|
|
141
|
+
// break-early: the post-tool text delta must not have been translated
|
|
142
|
+
const deltas = eventsOf(stream).filter((e) => e.type === "text_delta").map((e) => e.delta);
|
|
143
|
+
expect(deltas.join("")).not.toContain("SHOULD NOT APPEAR");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("does NOT break early on an internal ToolSearch; breaks on the real fn_* tool (U9 sequence)", async () => {
|
|
147
|
+
// Claude emits ToolSearch (not pi-known) to load the deferred MCP tool FIRST,
|
|
148
|
+
// then the real mcp__custom-tools__fn_task_list. The old code aborted on
|
|
149
|
+
// ToolSearch; the gated code must wait for the real tool (P0 fix).
|
|
150
|
+
scriptedUpdates = [
|
|
151
|
+
{ sessionUpdate: "tool_call", toolCallId: "ts1", _meta: { claudeCode: { toolName: "ToolSearch" } }, rawInput: { query: "x" } },
|
|
152
|
+
{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "found it, calling" } },
|
|
153
|
+
{ sessionUpdate: "tool_call", toolCallId: "real", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
|
|
154
|
+
];
|
|
155
|
+
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
|
|
156
|
+
await flush();
|
|
157
|
+
// The text AFTER ToolSearch must have been processed (we didn't abort on ToolSearch)
|
|
158
|
+
const deltas = eventsOf(stream).filter((e) => e.type === "text_delta").map((e) => e.delta);
|
|
159
|
+
expect(deltas.join("")).toContain("found it");
|
|
160
|
+
// And we broke on the real tool
|
|
161
|
+
expect(eventsOf(stream).some((e) => e.type === "toolcall_start")).toBe(true);
|
|
162
|
+
const done = eventsOf(stream).find((e) => e.type === "done");
|
|
163
|
+
expect(done!.reason).toBe("toolUse");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("R17 auth-signal: sets on a 'Not logged in' turn, clears on a real response, ignores long answers", async () => {
|
|
167
|
+
const run = async (text: string) => {
|
|
168
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text } }];
|
|
169
|
+
streamViaAcp(MODEL, CTX, OPTS);
|
|
170
|
+
await flush();
|
|
171
|
+
};
|
|
172
|
+
const wroteAuthFailed = () =>
|
|
173
|
+
fsSpies.writeFileSync.mock.calls.some((c) => String(c[1]).includes('"authFailed":true'));
|
|
174
|
+
|
|
175
|
+
// Baseline: a real response leaves the signal cleared (lastAuthFailed=false).
|
|
176
|
+
await run("Here is a normal answer.");
|
|
177
|
+
fsSpies.writeFileSync.mockClear();
|
|
178
|
+
fsSpies.unlinkSync.mockClear();
|
|
179
|
+
|
|
180
|
+
// 1. A turn that is ONLY the bridge's "Not logged in" message → signal written.
|
|
181
|
+
await run("Not logged in · Please run /login");
|
|
182
|
+
expect(wroteAuthFailed()).toBe(true);
|
|
183
|
+
|
|
184
|
+
// 2. A real response → signal cleared (unlink).
|
|
185
|
+
fsSpies.unlinkSync.mockClear();
|
|
186
|
+
await run("Sure — here's the result you asked for.");
|
|
187
|
+
expect(fsSpies.unlinkSync).toHaveBeenCalled();
|
|
188
|
+
|
|
189
|
+
// 3. A LONG legit answer that merely mentions the phrase → NOT flagged.
|
|
190
|
+
fsSpies.writeFileSync.mockClear();
|
|
191
|
+
await run(`If you are not logged in, the CLI prompts you to authenticate. ${"detail ".repeat(20)}`);
|
|
192
|
+
expect(wroteAuthFailed()).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("ends with done even when the turn produces no content", async () => {
|
|
196
|
+
scriptedUpdates = [];
|
|
197
|
+
const stream = streamViaAcp(MODEL, CTX, OPTS) as unknown as { _events: Array<Record<string, unknown>> };
|
|
198
|
+
await flush();
|
|
199
|
+
expect(eventsOf(stream).some((e) => e.type === "done")).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("connection reuse (item 1) — gated by FUSION_CLAUDE_ACP_REUSE", () => {
|
|
204
|
+
const savedReuse = process.env.FUSION_CLAUDE_ACP_REUSE;
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
scriptedUpdates = [];
|
|
207
|
+
scriptedUsage = undefined;
|
|
208
|
+
scriptedHang = false;
|
|
209
|
+
vi.mocked(spawn).mockClear();
|
|
210
|
+
vi.mocked(ClientSideConnection).mockClear();
|
|
211
|
+
});
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
if (savedReuse === undefined) delete process.env.FUSION_CLAUDE_ACP_REUSE;
|
|
214
|
+
else process.env.FUSION_CLAUDE_ACP_REUSE = savedReuse;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("reuses one warm bridge connection across turns; turn 2 skips spawn + session/new", async () => {
|
|
218
|
+
process.env.FUSION_CLAUDE_ACP_REUSE = "1";
|
|
219
|
+
const reuseOpts = { ...OPTS, sessionId: "conv-reuse-1" };
|
|
220
|
+
|
|
221
|
+
// Turn 1 (cold): needs >1 message so reuseKey activates and the connection caches.
|
|
222
|
+
const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
|
|
223
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "turn one" } }];
|
|
224
|
+
streamViaAcp(MODEL, ctx1, reuseOpts);
|
|
225
|
+
await flush();
|
|
226
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(vi.mocked(ClientSideConnection)).toHaveBeenCalledTimes(1);
|
|
228
|
+
|
|
229
|
+
// Turn 2 (warm): same sessionId → no new spawn, no new connection.
|
|
230
|
+
const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "again" }] } as never;
|
|
231
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "turn two" } }];
|
|
232
|
+
const s2 = streamViaAcp(MODEL, ctx2, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
|
|
233
|
+
await flush();
|
|
234
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1); // no second spawn
|
|
235
|
+
expect(vi.mocked(ClientSideConnection)).toHaveBeenCalledTimes(1); // no second connection
|
|
236
|
+
|
|
237
|
+
// The single warm connection's prompt() ran once per turn.
|
|
238
|
+
const conn = vi.mocked(ClientSideConnection).mock.instances[0] as unknown as { prompt: ReturnType<typeof vi.fn>; newSession: ReturnType<typeof vi.fn> };
|
|
239
|
+
expect(conn.prompt).toHaveBeenCalledTimes(2);
|
|
240
|
+
expect(conn.newSession).toHaveBeenCalledTimes(1); // session/new only on the cold turn
|
|
241
|
+
const done = s2._events.find((e) => e.type === "done");
|
|
242
|
+
expect(done!.reason).toBe("stop");
|
|
243
|
+
|
|
244
|
+
// Cleanup: evict the warm connection + clear its (unref'd) idle timer.
|
|
245
|
+
(vi.mocked(spawn).mock.results[0].value as EventEmitter).emit("close", 0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("fails a reuse turn FAST when the warm child dies mid-prompt (P0: no 30min hang)", async () => {
|
|
249
|
+
process.env.FUSION_CLAUDE_ACP_REUSE = "1";
|
|
250
|
+
const reuseOpts = { ...OPTS, sessionId: "conv-death" };
|
|
251
|
+
|
|
252
|
+
// Turn 1 (cold) caches the warm connection.
|
|
253
|
+
const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
|
|
254
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "one" } }];
|
|
255
|
+
streamViaAcp(MODEL, ctx1, reuseOpts);
|
|
256
|
+
await flush();
|
|
257
|
+
const child = vi.mocked(spawn).mock.results[0].value as EventEmitter;
|
|
258
|
+
|
|
259
|
+
// Turn 2 (warm) hangs on prompt() — only the child-death path can end it.
|
|
260
|
+
scriptedHang = true;
|
|
261
|
+
const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "again" }] } as never;
|
|
262
|
+
const s2 = streamViaAcp(MODEL, ctx2, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
|
|
263
|
+
await flush();
|
|
264
|
+
expect(s2._events.some((e) => e.type === "done")).toBe(false); // still waiting
|
|
265
|
+
|
|
266
|
+
// The warm child dies. The cold turn's close handler routes failure to the
|
|
267
|
+
// CURRENT (reuse) turn via router.fail, so it ends immediately.
|
|
268
|
+
child.emit("close", 1);
|
|
269
|
+
await flush();
|
|
270
|
+
const done = s2._events.find((e) => e.type === "done") as { reason?: string; message?: { content?: Array<{ text?: string }> } };
|
|
271
|
+
expect(done).toBeDefined();
|
|
272
|
+
expect(done!.reason).toBe("stop");
|
|
273
|
+
expect(JSON.stringify(done!.message?.content)).toContain("Error");
|
|
274
|
+
|
|
275
|
+
// Cache was evicted: a subsequent turn cold-spawns a fresh bridge.
|
|
276
|
+
scriptedHang = false;
|
|
277
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "fresh" } }];
|
|
278
|
+
const ctx3 = { messages: [...(ctx2 as unknown as { messages: unknown[] }).messages, { role: "assistant", content: "" }, { role: "user", content: "q3" }] } as never;
|
|
279
|
+
streamViaAcp(MODEL, ctx3, reuseOpts);
|
|
280
|
+
await flush();
|
|
281
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2); // turn 1 + the post-death cold restart
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("does NOT keep the connection warm after a tool-use (break-early) turn — prompt() still pending", async () => {
|
|
285
|
+
process.env.FUSION_CLAUDE_ACP_REUSE = "1";
|
|
286
|
+
const reuseOpts = { ...OPTS, sessionId: "conv-tooluse" };
|
|
287
|
+
|
|
288
|
+
// Turn 1 (cold) breaks early on a pi-known tool — prompt() never resolves
|
|
289
|
+
// (the break happens mid-stream), so the connection must be torn down, not
|
|
290
|
+
// released warm, or turn 2 would launch a concurrent prompt on it.
|
|
291
|
+
const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
|
|
292
|
+
scriptedUpdates = [
|
|
293
|
+
{ sessionUpdate: "tool_call", toolCallId: "t1", _meta: { claudeCode: { toolName: "mcp__custom-tools__fn_task_list" } }, rawInput: {} },
|
|
294
|
+
];
|
|
295
|
+
const s1 = streamViaAcp(MODEL, ctx1, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
|
|
296
|
+
await flush();
|
|
297
|
+
expect(s1._events.find((e) => e.type === "done")!.reason).toBe("toolUse");
|
|
298
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1);
|
|
299
|
+
|
|
300
|
+
// Turn 2 must cold-spawn a fresh bridge (no warm reuse after a tool turn).
|
|
301
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "after tool" } }];
|
|
302
|
+
const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "again" }] } as never;
|
|
303
|
+
streamViaAcp(MODEL, ctx2, reuseOpts);
|
|
304
|
+
await flush();
|
|
305
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2); // fresh spawn → no concurrent prompt on a warm conn
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("cold-starts (no warm reuse) when the resume delta is empty (P1: no empty-prompt hang)", async () => {
|
|
309
|
+
process.env.FUSION_CLAUDE_ACP_REUSE = "1";
|
|
310
|
+
const reuseOpts = { ...OPTS, sessionId: "conv-empty" };
|
|
311
|
+
|
|
312
|
+
// Turn 1 (cold) caches.
|
|
313
|
+
const ctx1 = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "hello" }] } as never;
|
|
314
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "one" } }];
|
|
315
|
+
streamViaAcp(MODEL, ctx1, reuseOpts);
|
|
316
|
+
await flush();
|
|
317
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(1);
|
|
318
|
+
|
|
319
|
+
// Turn 2 whose context ends in an assistant message → buildResumePrompt is
|
|
320
|
+
// empty → must NOT take the warm path (would hang); cold-starts instead.
|
|
321
|
+
const ctx2 = { messages: [...(ctx1 as unknown as { messages: unknown[] }).messages, { role: "user", content: "x" }, { role: "assistant", content: "y" }] } as never;
|
|
322
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "two" } }];
|
|
323
|
+
const s2 = streamViaAcp(MODEL, ctx2, reuseOpts) as unknown as { _events: Array<Record<string, unknown>> };
|
|
324
|
+
await flush();
|
|
325
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2); // empty resume → fresh spawn
|
|
326
|
+
const done = s2._events.find((e) => e.type === "done");
|
|
327
|
+
expect(done!.reason).toBe("stop"); // produced a normal turn, did not hang
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("does NOT reuse when the flag is off (default): each turn spawns a fresh bridge", async () => {
|
|
331
|
+
delete process.env.FUSION_CLAUDE_ACP_REUSE;
|
|
332
|
+
const reuseOpts = { ...OPTS, sessionId: "conv-off" };
|
|
333
|
+
const ctx = { messages: [{ role: "user", content: "hi" }, { role: "assistant", content: "x" }] } as never;
|
|
334
|
+
scriptedUpdates = [{ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "a" } }];
|
|
335
|
+
streamViaAcp(MODEL, ctx, reuseOpts);
|
|
336
|
+
await flush();
|
|
337
|
+
streamViaAcp(MODEL, ctx, reuseOpts);
|
|
338
|
+
await flush();
|
|
339
|
+
expect(vi.mocked(spawn)).toHaveBeenCalledTimes(2);
|
|
340
|
+
expect(vi.mocked(ClientSideConnection)).toHaveBeenCalledTimes(2);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("buildBridgeEnv — R17 auth opt-in (item 3)", () => {
|
|
345
|
+
const saved = {
|
|
346
|
+
flag: process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH,
|
|
347
|
+
oauth: process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
348
|
+
authTok: process.env.ANTHROPIC_AUTH_TOKEN,
|
|
349
|
+
key: process.env.ANTHROPIC_API_KEY,
|
|
350
|
+
};
|
|
351
|
+
// Start each case from a clean slate so an ambient auth var in the runner's
|
|
352
|
+
// env can't shadow the token a test means to exercise (precedence is global).
|
|
353
|
+
beforeEach(() => {
|
|
354
|
+
delete process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH;
|
|
355
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
356
|
+
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
|
357
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
358
|
+
});
|
|
359
|
+
afterEach(() => {
|
|
360
|
+
for (const [k, v] of [
|
|
361
|
+
["FUSION_CLAUDE_ACP_FORWARD_AUTH", saved.flag],
|
|
362
|
+
["CLAUDE_CODE_OAUTH_TOKEN", saved.oauth],
|
|
363
|
+
["ANTHROPIC_AUTH_TOKEN", saved.authTok],
|
|
364
|
+
["ANTHROPIC_API_KEY", saved.key],
|
|
365
|
+
] as const) {
|
|
366
|
+
if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("does NOT forward auth vars by default (secure default)", () => {
|
|
371
|
+
delete process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH;
|
|
372
|
+
process.env.ANTHROPIC_API_KEY = "sk-secret";
|
|
373
|
+
const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
|
|
374
|
+
expect(env.ANTHROPIC_API_KEY).toBeUndefined();
|
|
375
|
+
expect(env.HOME).toBe("/h");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("forwards a single auth token when opted in", () => {
|
|
379
|
+
process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
|
|
380
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
381
|
+
process.env.ANTHROPIC_API_KEY = "sk-secret";
|
|
382
|
+
const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
|
|
383
|
+
expect(env.ANTHROPIC_API_KEY).toBe("sk-secret");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("prefers CLAUDE_CODE_OAUTH_TOKEN and forwards only one token", () => {
|
|
387
|
+
process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
|
|
388
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-tok";
|
|
389
|
+
process.env.ANTHROPIC_API_KEY = "sk-secret";
|
|
390
|
+
const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
|
|
391
|
+
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBe("oauth-tok");
|
|
392
|
+
expect(env.ANTHROPIC_API_KEY).toBeUndefined();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("forwards ANTHROPIC_AUTH_TOKEN (middle precedence) when no OAuth token", () => {
|
|
396
|
+
process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
|
|
397
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
398
|
+
process.env.ANTHROPIC_AUTH_TOKEN = "auth-tok";
|
|
399
|
+
process.env.ANTHROPIC_API_KEY = "sk-secret";
|
|
400
|
+
const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
|
|
401
|
+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe("auth-tok");
|
|
402
|
+
expect(env.ANTHROPIC_API_KEY).toBeUndefined();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("treats a whitespace-only higher-preference token as absent (no shadowing, no blank forward)", () => {
|
|
406
|
+
process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
|
|
407
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = " "; // blank → must be skipped
|
|
408
|
+
process.env.ANTHROPIC_API_KEY = "sk-real";
|
|
409
|
+
const env = buildBridgeEnv({ HOME: "/h", PATH: "/b" });
|
|
410
|
+
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
411
|
+
expect(env.ANTHROPIC_API_KEY).toBe("sk-real"); // real lower-preference token wins
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("reads the auth token from process.env, never a caller-supplied value (no token substitution)", () => {
|
|
415
|
+
process.env.FUSION_CLAUDE_ACP_FORWARD_AUTH = "1";
|
|
416
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
417
|
+
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
|
418
|
+
process.env.ANTHROPIC_API_KEY = "real-from-env";
|
|
419
|
+
// A caller trying to inject a different token via the supplied env must be ignored.
|
|
420
|
+
const env = buildBridgeEnv({ HOME: "/h", PATH: "/b", ANTHROPIC_API_KEY: "attacker" } as NodeJS.ProcessEnv);
|
|
421
|
+
expect(env.ANTHROPIC_API_KEY).toBe("real-from-env");
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
describe("process-manager execSync guard", () => {
|
|
6
|
+
it("keeps Claude CLI probing non-blocking", () => {
|
|
7
|
+
const sourcePath = path.resolve(import.meta.dirname, "../process-manager.ts");
|
|
8
|
+
|
|
9
|
+
expect(existsSync(sourcePath)).toBe(true);
|
|
10
|
+
const src = readFileSync(sourcePath, "utf-8");
|
|
11
|
+
|
|
12
|
+
expect(/\bexecSync\b/.test(src)).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -16,7 +16,6 @@ vi.mock("node:child_process", () => ({
|
|
|
16
16
|
proc.pid = 12345;
|
|
17
17
|
return proc;
|
|
18
18
|
}),
|
|
19
|
-
execSync: vi.fn(),
|
|
20
19
|
}));
|
|
21
20
|
|
|
22
21
|
const mocks = vi.hoisted(() => ({
|
|
@@ -38,15 +37,13 @@ vi.mock("node:os", () => ({
|
|
|
38
37
|
tmpdir: mocks.tmpdir,
|
|
39
38
|
}));
|
|
40
39
|
|
|
41
|
-
import { spawn
|
|
40
|
+
import { spawn } from "node:child_process";
|
|
42
41
|
import {
|
|
43
42
|
spawnClaude,
|
|
44
43
|
buildClaudeSpawnArgs,
|
|
45
44
|
writeUserMessage,
|
|
46
45
|
cleanupProcess,
|
|
47
46
|
captureStderr,
|
|
48
|
-
validateCliPresence,
|
|
49
|
-
validateCliAuth,
|
|
50
47
|
validateCliPresenceAsync,
|
|
51
48
|
validateCliAuthAsync,
|
|
52
49
|
forceKillProcess,
|
|
@@ -368,47 +365,6 @@ describe("captureStderr", () => {
|
|
|
368
365
|
});
|
|
369
366
|
});
|
|
370
367
|
|
|
371
|
-
describe("validateCliPresence", () => {
|
|
372
|
-
it("does not throw when claude --version succeeds", () => {
|
|
373
|
-
(execSync as any).mockReturnValue(Buffer.from("1.0.0"));
|
|
374
|
-
expect(() => validateCliPresence()).not.toThrow();
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("throws with install instructions when claude --version fails", () => {
|
|
378
|
-
(execSync as any).mockImplementation(() => {
|
|
379
|
-
throw new Error("command not found");
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
expect(() => validateCliPresence()).toThrow();
|
|
383
|
-
try {
|
|
384
|
-
validateCliPresence();
|
|
385
|
-
} catch (e: any) {
|
|
386
|
-
expect(e.message).toContain("Claude Code CLI not found");
|
|
387
|
-
expect(e.message).toContain("npm install");
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
describe("validateCliAuth", () => {
|
|
393
|
-
it("returns true when claude auth status succeeds", () => {
|
|
394
|
-
(execSync as any).mockReturnValue(Buffer.from("Logged in"));
|
|
395
|
-
expect(validateCliAuth()).toBe(true);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("returns false and warns when claude auth status fails", () => {
|
|
399
|
-
(execSync as any).mockImplementation(() => {
|
|
400
|
-
throw new Error("not authenticated");
|
|
401
|
-
});
|
|
402
|
-
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
403
|
-
|
|
404
|
-
expect(validateCliAuth()).toBe(false);
|
|
405
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
406
|
-
expect.stringContaining("not authenticated"),
|
|
407
|
-
);
|
|
408
|
-
warnSpy.mockRestore();
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
368
|
describe("validateCliPresenceAsync", () => {
|
|
413
369
|
beforeEach(() => {
|
|
414
370
|
vi.clearAllMocks();
|
|
@@ -458,6 +414,16 @@ describe("validateCliPresenceAsync", () => {
|
|
|
458
414
|
const result = await validateCliPresenceAsync();
|
|
459
415
|
expect(result.ok).toBe(false);
|
|
460
416
|
});
|
|
417
|
+
|
|
418
|
+
it("resolves ok=false instead of rejecting when claude spawn throws synchronously", async () => {
|
|
419
|
+
(spawn as any).mockImplementationOnce(() => {
|
|
420
|
+
throw new Error("Real AI CLI launch blocked during tests: claude --version");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await expect(validateCliPresenceAsync()).resolves.toMatchObject({
|
|
424
|
+
ok: false,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
461
427
|
});
|
|
462
428
|
|
|
463
429
|
describe("validateCliAuthAsync", () => {
|
|
@@ -495,6 +461,21 @@ describe("validateCliAuthAsync", () => {
|
|
|
495
461
|
);
|
|
496
462
|
warnSpy.mockRestore();
|
|
497
463
|
});
|
|
464
|
+
|
|
465
|
+
it("resolves false instead of rejecting when claude auth spawn throws synchronously", async () => {
|
|
466
|
+
(spawn as any).mockImplementationOnce(() => {
|
|
467
|
+
throw new Error(
|
|
468
|
+
"Real AI CLI launch blocked during tests: claude auth status",
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
472
|
+
|
|
473
|
+
await expect(validateCliAuthAsync()).resolves.toBe(false);
|
|
474
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
475
|
+
expect.stringContaining("not authenticated"),
|
|
476
|
+
);
|
|
477
|
+
warnSpy.mockRestore();
|
|
478
|
+
});
|
|
498
479
|
});
|
|
499
480
|
|
|
500
481
|
describe("CLI flags", () => {
|