chainlesschain 0.162.148 → 0.162.149
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 +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DwpCCp64.js → AIOps-BJif14yR.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C9pBYocA.js → ActionButton-CXek6gJY.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BWAkeXxK.js → Analytics-BgQhdAJi.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CivFGH6B.js → AppLayout-DTYl5NtR.js} +3 -3
- package/src/assets/web-panel/assets/{Audit-Cm8hRpZm.js → Audit-BzATQAeR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DXdFMsE8.js → Backup-BzdPEQhL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uAwSTeql.js → BaseInput-BM0KVh8E.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-KU8eeJJE.js → Chat-D7fHXHkz.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Wm34RR-b.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C6AMDkjd.js → Checkbox-CAiSQ9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-t2moDxcO.js → Codegen-Df40CrEe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DCcsRRhN.js → Col-DIT2fNFU.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DtB-8SAC.js → Community-CSTIbW2k.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CZm8THYA.js → Compact-DtqXZZUi.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-LiQgC7g1.js → Compliance-CUtzw6o7.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CA8SAxht.js → Cowork-CezmlnmU.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CtRaF1wD.js → Cron-P4BITarg.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-6-br6Hub.js → Crosschain-BRRiYwvq.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Do2EccrL.js → DID-L5ijB9i4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-wrXcbzGe.js → Dashboard-CFxro4ob.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-p3FsT245.js → Dropdown-BmAFCQ71.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D95A2L8q.js → EmailListRenderer-DJBCEuon.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-cLuJW2zo.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Brgq_cbN.js → Federation-DsVCpHMF.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DWLCkNgh.js → FormItemContext-DP3bP5th.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BBqUfQd6.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8F090w4I.js → Git-I3g_bAw9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CCogEbxb.js → Governance-jHS5-ioS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfLD7v7C.js → Inference-CPL7sfx9.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DDuHJewd.js → KnowledgeGraph-Jf236h4C.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CE8KSEVC.js → Logs-Cm2tcSKg.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DmwpZmhT.js → Marketplace-CZK82rhi.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Bf7AazU8.js → McpTools-CZCeji7a.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-D0QU_00S.js → Memory-3l2KVS9k.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-yXS9xdhx.js → MobileBridge-VYdpoLh6.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BuDUoGUP.js → MobileProjects-CiB9cmm1.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BYyHy28p.js → Mtc-wCFZbBuL.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CK0aqpiZ.js → MtcAudit-1a3THIyG.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-mNimKYeb.js → Multisig-DMzLB2hG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-AioAJ0YA.js → NLProgramming-D9wZbf6l.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CVVNCLHE.js → Notes-hrFe2ZM-.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C4ABj-TK.js → NotificationSettings-CHGX5Jwm.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
- package/src/assets/web-panel/assets/{Organization-uozl_gWK.js → Organization-By7FkhtY.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BCZOM2hK.js → Overflow--khGSzJn.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CArcaiaj.js → P2P-CAG9dH35.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Sgq2Srr8.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BYrQrXnH.js → Permissions-D35ec6JA.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BzHStAbz.js → PersonalDataHub-CxBF7hW_.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-k7KqxX1M.js → Pipeline-s6EtOO1s.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-IumTUWqx.js → Privacy-BbJYmWmO.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BRyImYTf.js → ProjectInit-VGQq1jMT.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BFIqTBFk.js → ProjectSettings-B1ew3Teh.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CyyFnUPs.js → Providers-mrO47FIK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C82cRx-X.js → QrScannerModal-IJF2mHyw.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DGxDF521.js → QuickAsk-CU0wN_Z0.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cns-Am1y.js → Recommend-DhN37R6G.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-R7OqAIlg.js → RemoteSession-D_xOX_hu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D7mgPIYV.js → Reputation-D_Ssv7uH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B3lEURyd.js → Row-Kx5dKxX7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-9_UVrpBt.js → RssFeed-BlGwqZkY.js} +2 -2
- package/src/assets/web-panel/assets/{Search-ClZeO80q.js → Search-hLFQzxpb.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BNNJczEp.js → Security-DMlvsVt4.js} +3 -3
- package/src/assets/web-panel/assets/{Services-C5SjrWUj.js → Services-Bd0Qsj2m.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Ci_obQ-g.js → Skeleton-DFclq9X3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DdebN15P.js → Skills-BNtln2qY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-OinZP2vi.js → Sla-Dch5H19G.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CxzbNF51.js → SpeechSettings-2UfQcU1g.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D06N_66b.js → SyncSettings-CD9YQr4i.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BdEdW0AZ.js → Tasks-BNu-7MzI.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-F1ctKmPa.js → Templates-D1n9CaIR.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CVz1Urot.js → Tenant-DenL3iBW.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Doitzjmf.js → TimelineRenderer-D87IfukO.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BpyVRpVA.js → Tokens-BkiZc8E3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-nWhWYTbU.js → Trigger-BNRSytGN.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DK_G-wLx.js → Trust-Bwbd4X2m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B5yBUojO.js → UkeySign-B8O2bs0o.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C5D51qAe.js → VideoEditing-C2fL8zmH.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CLbL9K7v.js → Wallet-BORYDdE1.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DZUelI3V.js → WebAuthn-C0V5S2FM.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-8RPxt4KW.js → WorkflowEditor-2dR0fcHL.js} +1 -1
- package/src/assets/web-panel/assets/{chat-4N4tOA3d.js → chat-BmuAFAn8.js} +1 -1
- package/src/assets/web-panel/assets/{colors-zbbGVrua.js → colors-CN3smMcK.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-D7iMkbnE.js → compact-item-DCnxb4kW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CBzPJv-w.js → createContext-vlR43pHn.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-bHdrQSqZ.js → hasIn-D04tHYFd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbE5D0WL.js → index-0DnaZGkC.js} +1 -1
- package/src/assets/web-panel/assets/{index-308gCGh7.js → index-430S68MN.js} +1 -1
- package/src/assets/web-panel/assets/{index-kmh1TxRa.js → index-B78xL3s2.js} +1 -1
- package/src/assets/web-panel/assets/{index-QFObYeo1.js → index-BTPEZTk1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZb8F8YG.js → index-BXRg8GFQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE9j5YgT.js → index-BasvsWDM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiG1IZao.js → index-BeGDEXFs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bqmx24kh.js → index-BqhASEL4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCr0geV9.js → index-BudvKQfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-S74FpAIn.js → index-C-6NO5il.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyoNgbXl.js → index-C5Sxb1sE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtbbdLnf.js → index-CP9m9Ggr.js} +3 -3
- package/src/assets/web-panel/assets/{index-CkxK86vf.js → index-CTOCnIQ7.js} +1 -1
- package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
- package/src/assets/web-panel/assets/{index-BTIjjXry.js → index-ClhAHDK3.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7b1hGry.js → index-CnXebZLL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPxkoKgA.js → index-CngCC8hC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci009ijp.js → index-Cr-A0FYJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdmMoYN1.js → index-CsAsCPJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBNr77E1.js → index-D28DeAAB.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1s0Bz9O.js → index-D5LZVZ0L.js} +1 -1
- package/src/assets/web-panel/assets/{index-oDKNgCsP.js → index-D65_XseV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QNAG4JtR.js → index-D8vwp4qp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQfu1U78.js → index-DDwLgQY9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnBOpqv3.js → index-DIpY0qM5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ6NcfWz.js → index-DTWg-18U.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4XKpj6c.js → index-DTtRscUa.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1YDh7Wr.js → index-DYAtmqiv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvRMA9uT.js → index-D_n8_vfI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CahryTX0.js → index-DbADHwhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJ4ZGyW0.js → index-DbVsQBOH.js} +1 -1
- package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
- package/src/assets/web-panel/assets/{index-xYCm6W2h.js → index-HhNjnCfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BS4_Mwk6.js → index-ToUh2b1-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DISWAYce.js → index-TslMTwNy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmqlJed-.js → index-_FIkN3jd.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqdPyWm5.js → index-hpvvtAZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsKehmmS.js → index-jzR7Ujuw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUyjUkY7.js → index-ljuBiQW9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BD55yNBt.js → initDefaultProps-C3wUb4je.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B5Bbe77U.js → motion-D-jYFUNA.js} +1 -1
- package/src/assets/web-panel/assets/{move-CTvIqHEH.js → move-tqb8nwJ2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BkNyPQKB.js → mtc-parser-DSOjMJMz.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BaXJXgHA.js → omit-CJ7VimIW.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Bw6-YTxH.js → pickAttrs-qXiG1iT3.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLdbkLqJ.js → placementArrow-CkGBcy1Z.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-IVYkzntU.js → responsiveObserve-D3WvTlLO.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DFMFwwtY.js → slide-BEE2ftge.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CURYRtZ1.js → statusUtils-CsfBpxiG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cfz63s1r.js → styleChecker-xPrIiJPI.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-COJL0KE3.js → useFlexGapSupport-C5Yd-SCR.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-YLLojQQf.js → useFs-D_MDS8HI.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BF_21qUC.js → usePersonalDataHub-BqJh-usA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-9ZpkA7bb.js → vnode-chm_p-gz.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DhtAfhl8.js → zoom-3-YDNz0Z.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/assets/web-panel/remote-session-sw.js +43 -0
- package/src/command-manifest.json +22 -1
- package/src/commands/agent.js +44 -5
- package/src/commands/codeintel.js +252 -0
- package/src/commands/eval.js +248 -0
- package/src/commands/plugin.js +354 -0
- package/src/commands/team.js +456 -0
- package/src/gateways/ws/message-dispatcher.js +45 -2
- package/src/gateways/ws/remote-session-protocol.js +37 -0
- package/src/gateways/ws/ws-server.js +1 -1
- package/src/harness/prompt-compressor.js +27 -1
- package/src/harness/remote-command-ledger.js +158 -0
- package/src/harness/remote-session-push-apns.js +246 -0
- package/src/harness/remote-session-push-errors.js +15 -0
- package/src/harness/remote-session-push-fcm.js +9 -25
- package/src/harness/remote-session-push-huawei.js +174 -0
- package/src/harness/remote-session-push-oppo.js +171 -0
- package/src/harness/remote-session-push-senders.js +42 -0
- package/src/harness/remote-session-push-vivo.js +179 -0
- package/src/harness/remote-session-push-web.js +299 -0
- package/src/harness/remote-session-push-xiaomi.js +99 -0
- package/src/index.js +6 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +161 -5
- package/src/lib/agent-team/task-lease.js +434 -0
- package/src/lib/agent-team/team-budget.js +165 -0
- package/src/lib/agent-team/team-mailbox.js +106 -0
- package/src/lib/agent-team/team-runner.js +266 -0
- package/src/lib/agent-team/team-worktree.js +204 -0
- package/src/lib/agents.js +18 -0
- package/src/lib/async-hook-supervisor.cjs +305 -0
- package/src/lib/eval/runner.js +163 -0
- package/src/lib/eval/tasks.js +456 -0
- package/src/lib/eval/trend.js +185 -0
- package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
- package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
- package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
- package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
- package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
- package/src/lib/lsp/code-intelligence.js +356 -0
- package/src/lib/lsp/jsonrpc-stream.js +139 -0
- package/src/lib/lsp/lsp-client.js +318 -0
- package/src/lib/lsp/lsp-manager.js +323 -0
- package/src/lib/lsp/lsp-server-registry.js +196 -0
- package/src/lib/plugin-monitor-supervisor.js +238 -0
- package/src/lib/plugin-runtime/agents.js +51 -0
- package/src/lib/plugin-runtime/bin.js +100 -0
- package/src/lib/plugin-runtime/hooks.js +100 -0
- package/src/lib/plugin-runtime/install.js +388 -0
- package/src/lib/plugin-runtime/lsp.js +75 -0
- package/src/lib/plugin-runtime/manifest.js +458 -0
- package/src/lib/plugin-runtime/mcp.js +89 -0
- package/src/lib/plugin-runtime/monitors.js +100 -0
- package/src/lib/plugin-runtime/policy.js +144 -0
- package/src/lib/plugin-runtime/reload.js +79 -0
- package/src/lib/plugin-runtime/scopes.js +197 -0
- package/src/lib/plugin-runtime/settings.js +119 -0
- package/src/lib/plugin-runtime/signature.js +107 -0
- package/src/lib/plugin-runtime/skills.js +43 -0
- package/src/lib/plugin-runtime/trust.js +140 -0
- package/src/lib/sandbox-egress-proxy.js +162 -0
- package/src/lib/sandbox-network-policy.js +134 -0
- package/src/lib/sandbox-v2.js +10 -4
- package/src/lib/settings-hook-events.cjs +78 -6
- package/src/lib/settings-hooks.cjs +29 -3
- package/src/lib/settings-loader.cjs +35 -1
- package/src/lib/skill-loader.js +18 -0
- package/src/lib/telemetry/span-recorder.js +237 -0
- package/src/repl/agent-repl.js +400 -4
- package/src/runtime/agent-core.js +546 -52
- package/src/runtime/coding-agent-contract-shared.cjs +82 -8
- package/src/runtime/coding-agent-policy.cjs +32 -7
- package/src/runtime/fallback-model.js +134 -7
- package/src/runtime/headless-runner.js +353 -108
- package/src/runtime/mcp-config.js +46 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DCNfbdlx.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DIp8Xcbd.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dpid9CDg.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BvAK_Zel.js +0 -3
- package/src/assets/web-panel/assets/devWarning-C2Z5LRgq.js +0 -1
- package/src/assets/web-panel/assets/index-BGp6Yr-Y.js +0 -1
- package/src/assets/web-panel/assets/index-BXiw9dQf.js +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSPClient tests — JSON-RPC handshake, request/response correlation,
|
|
3
|
+
* notification dispatch, timeouts and crash propagation. A fake child process
|
|
4
|
+
* is injected through `_deps.spawn`; no real language server is spawned.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import {
|
|
10
|
+
LSPClient,
|
|
11
|
+
_deps,
|
|
12
|
+
pathToFileUri,
|
|
13
|
+
fileUriToPath,
|
|
14
|
+
normalizeUri,
|
|
15
|
+
buildSpawnCommand,
|
|
16
|
+
} from "../lsp-client.js";
|
|
17
|
+
import { encodeMessage, MessageBuffer } from "../jsonrpc-stream.js";
|
|
18
|
+
|
|
19
|
+
/** A fake language server process. Auto-replies to `initialize`; test controls the rest. */
|
|
20
|
+
class FakeChild extends EventEmitter {
|
|
21
|
+
constructor({ autoInit = true } = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.stdout = new EventEmitter();
|
|
24
|
+
this.stderr = new EventEmitter();
|
|
25
|
+
this._inbound = new MessageBuffer();
|
|
26
|
+
this.received = [];
|
|
27
|
+
this.killed = false;
|
|
28
|
+
// Arrow keeps `this` bound to the FakeChild (no this-alias needed).
|
|
29
|
+
this.stdin = {
|
|
30
|
+
writable: true,
|
|
31
|
+
write: (buf) => {
|
|
32
|
+
this._inbound.append(buf);
|
|
33
|
+
for (const msg of this._inbound.readAll()) {
|
|
34
|
+
this.received.push(msg);
|
|
35
|
+
if (autoInit && msg.method === "initialize") {
|
|
36
|
+
this.reply(msg.id, { capabilities: { definitionProvider: true } });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
reply(id, result) {
|
|
44
|
+
this.stdout.emit("data", encodeMessage({ jsonrpc: "2.0", id, result }));
|
|
45
|
+
}
|
|
46
|
+
replyError(id, error) {
|
|
47
|
+
this.stdout.emit("data", encodeMessage({ jsonrpc: "2.0", id, error }));
|
|
48
|
+
}
|
|
49
|
+
notify(method, params) {
|
|
50
|
+
this.stdout.emit("data", encodeMessage({ jsonrpc: "2.0", method, params }));
|
|
51
|
+
}
|
|
52
|
+
kill() {
|
|
53
|
+
this.killed = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let origSpawn;
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
origSpawn = _deps.spawn;
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
_deps.spawn = origSpawn;
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function makeClient(child, opts = {}) {
|
|
67
|
+
_deps.spawn = vi.fn(() => child);
|
|
68
|
+
return new LSPClient({
|
|
69
|
+
command: "fake-lsp",
|
|
70
|
+
args: ["--stdio"],
|
|
71
|
+
rootPath: "/proj",
|
|
72
|
+
...opts,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("LSPClient handshake", () => {
|
|
77
|
+
it("spawns, sends initialize, and captures server capabilities", async () => {
|
|
78
|
+
const child = new FakeChild();
|
|
79
|
+
const client = makeClient(child);
|
|
80
|
+
const caps = await client.start();
|
|
81
|
+
expect(caps).toEqual({ definitionProvider: true });
|
|
82
|
+
expect(client.running).toBe(true);
|
|
83
|
+
// sent initialize then initialized notification
|
|
84
|
+
expect(child.received[0].method).toBe("initialize");
|
|
85
|
+
expect(child.received[1].method).toBe("initialized");
|
|
86
|
+
// spawn used shell:false
|
|
87
|
+
expect(_deps.spawn).toHaveBeenCalledWith(
|
|
88
|
+
"fake-lsp",
|
|
89
|
+
["--stdio"],
|
|
90
|
+
expect.objectContaining({ shell: false }),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("LSPClient request/response", () => {
|
|
96
|
+
it("correlates a request to its response by id", async () => {
|
|
97
|
+
const child = new FakeChild();
|
|
98
|
+
const client = makeClient(child);
|
|
99
|
+
await client.start();
|
|
100
|
+
const p = client.request("textDocument/definition", { x: 1 });
|
|
101
|
+
// server replies to the definition request (id follows initialize=1 → 2)
|
|
102
|
+
const req = child.received.find(
|
|
103
|
+
(m) => m.method === "textDocument/definition",
|
|
104
|
+
);
|
|
105
|
+
child.reply(req.id, [
|
|
106
|
+
{
|
|
107
|
+
uri: "file:///proj/a.ts",
|
|
108
|
+
range: {
|
|
109
|
+
start: { line: 0, character: 0 },
|
|
110
|
+
end: { line: 0, character: 3 },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
const result = await p;
|
|
115
|
+
expect(result[0].uri).toBe("file:///proj/a.ts");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("rejects a request whose response carries an error", async () => {
|
|
119
|
+
const child = new FakeChild();
|
|
120
|
+
const client = makeClient(child);
|
|
121
|
+
await client.start();
|
|
122
|
+
const p = client.request("bad/method", {});
|
|
123
|
+
const req = child.received.find((m) => m.method === "bad/method");
|
|
124
|
+
child.replyError(req.id, { code: -32601, message: "method not found" });
|
|
125
|
+
await expect(p).rejects.toThrow(/method not found/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("times out a request with no response", async () => {
|
|
129
|
+
const child = new FakeChild();
|
|
130
|
+
const client = makeClient(child);
|
|
131
|
+
await client.start();
|
|
132
|
+
await expect(
|
|
133
|
+
client.request("slow/op", {}, { timeoutMs: 20 }),
|
|
134
|
+
).rejects.toThrow(/timed out/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("LSPClient notifications", () => {
|
|
139
|
+
it("emits server notifications by method", async () => {
|
|
140
|
+
const child = new FakeChild();
|
|
141
|
+
const client = makeClient(child);
|
|
142
|
+
await client.start();
|
|
143
|
+
const got = vi.fn();
|
|
144
|
+
client.on("notify:textDocument/publishDiagnostics", got);
|
|
145
|
+
child.notify("textDocument/publishDiagnostics", {
|
|
146
|
+
uri: "file:///proj/a.ts",
|
|
147
|
+
diagnostics: [],
|
|
148
|
+
});
|
|
149
|
+
expect(got).toHaveBeenCalledWith({
|
|
150
|
+
uri: "file:///proj/a.ts",
|
|
151
|
+
diagnostics: [],
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("auto-replies null to server→client requests so the server never blocks", async () => {
|
|
156
|
+
const child = new FakeChild();
|
|
157
|
+
const client = makeClient(child);
|
|
158
|
+
await client.start();
|
|
159
|
+
child.stdout.emit(
|
|
160
|
+
"data",
|
|
161
|
+
encodeMessage({
|
|
162
|
+
jsonrpc: "2.0",
|
|
163
|
+
id: 999,
|
|
164
|
+
method: "workspace/configuration",
|
|
165
|
+
params: {},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
const reply = child.received.find((m) => m.id === 999 && m.method == null);
|
|
169
|
+
expect(reply).toEqual({ jsonrpc: "2.0", id: 999, result: null });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("LSPClient lifecycle", () => {
|
|
174
|
+
it("fails all pending requests when the process exits", async () => {
|
|
175
|
+
const child = new FakeChild();
|
|
176
|
+
const client = makeClient(child);
|
|
177
|
+
await client.start();
|
|
178
|
+
const p = client.request("op", {});
|
|
179
|
+
child.emit("exit", 1, null);
|
|
180
|
+
await expect(p).rejects.toThrow(/exited|stopped/);
|
|
181
|
+
expect(client.running).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("stop() shuts down gracefully and is idempotent", async () => {
|
|
185
|
+
const child = new FakeChild();
|
|
186
|
+
const client = makeClient(child);
|
|
187
|
+
await client.start();
|
|
188
|
+
// auto-reply to shutdown
|
|
189
|
+
child.stdin.write = (buf) => {
|
|
190
|
+
const mb = new MessageBuffer().append(buf);
|
|
191
|
+
for (const m of mb.readAll()) {
|
|
192
|
+
child.received.push(m);
|
|
193
|
+
if (m.method === "shutdown") child.reply(m.id, null);
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
};
|
|
197
|
+
await client.stop();
|
|
198
|
+
await client.stop(); // no throw second time
|
|
199
|
+
expect(child.killed).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("uri <-> path", () => {
|
|
204
|
+
it("round-trips a POSIX path", () => {
|
|
205
|
+
expect(fileUriToPath(pathToFileUri("/home/u/a b.ts"))).toBe(
|
|
206
|
+
process.platform === "win32" ? "\\home\\u\\a b.ts" : "/home/u/a b.ts",
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
it("builds a windows drive URI", () => {
|
|
210
|
+
expect(pathToFileUri("C:\\proj\\a.ts")).toBe("file:///C%3A/proj/a.ts");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("normalizeUri", () => {
|
|
215
|
+
it("lowercases the windows drive letter so client/server keys match", () => {
|
|
216
|
+
// path.resolve yields C:, servers echo c: — must collapse to one key
|
|
217
|
+
expect(normalizeUri("file:///C%3A/proj/a.ts")).toBe(
|
|
218
|
+
"file:///c%3A/proj/a.ts",
|
|
219
|
+
);
|
|
220
|
+
expect(normalizeUri("file:///c%3A/proj/a.ts")).toBe(
|
|
221
|
+
"file:///c%3A/proj/a.ts",
|
|
222
|
+
);
|
|
223
|
+
expect(normalizeUri("file:///C:/proj/a.ts")).toBe("file:///c:/proj/a.ts");
|
|
224
|
+
});
|
|
225
|
+
it("leaves POSIX file URIs unchanged", () => {
|
|
226
|
+
expect(normalizeUri("file:///home/u/a.ts")).toBe("file:///home/u/a.ts");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("buildSpawnCommand", () => {
|
|
231
|
+
const isWin = process.platform === "win32";
|
|
232
|
+
it("passes non-batch commands through verbatim", () => {
|
|
233
|
+
const spec = buildSpawnCommand("/usr/bin/gopls", []);
|
|
234
|
+
expect(spec).toEqual({
|
|
235
|
+
file: "/usr/bin/gopls",
|
|
236
|
+
args: [],
|
|
237
|
+
windowsVerbatimArguments: false,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
it.runIf(isWin)(
|
|
241
|
+
"routes a .cmd shim through cmd.exe /d /s /c with a quoted path",
|
|
242
|
+
() => {
|
|
243
|
+
const spec = buildSpawnCommand(
|
|
244
|
+
"C:\\proj\\node_modules\\.bin\\typescript-language-server.cmd",
|
|
245
|
+
["--stdio"],
|
|
246
|
+
);
|
|
247
|
+
expect(spec.windowsVerbatimArguments).toBe(true);
|
|
248
|
+
expect(spec.args.slice(0, 3)).toEqual(["/d", "/s", "/c"]);
|
|
249
|
+
expect(spec.args[3]).toBe(
|
|
250
|
+
'"C:\\proj\\node_modules\\.bin\\typescript-language-server.cmd" --stdio',
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
it.runIf(isWin)("quotes a path containing spaces", () => {
|
|
255
|
+
const spec = buildSpawnCommand("C:\\Program Files\\svr.cmd", []);
|
|
256
|
+
expect(spec.args[3]).toBe('"C:\\Program Files\\svr.cmd"');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSPManager tests — server pooling, document open/change sync, diagnostics
|
|
3
|
+
* collection, and position conversion. A fake client is injected via
|
|
4
|
+
* `_deps.createClient`; the registry resolves a server via `_deps.existsSync`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import {
|
|
11
|
+
LSPManager,
|
|
12
|
+
toLspPosition,
|
|
13
|
+
fromLspPosition,
|
|
14
|
+
_deps,
|
|
15
|
+
} from "../lsp-manager.js";
|
|
16
|
+
import { _deps as regDeps } from "../lsp-server-registry.js";
|
|
17
|
+
|
|
18
|
+
/** Minimal fake LSPClient: records notifications, canned requests, emits events. */
|
|
19
|
+
class FakeClient extends EventEmitter {
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
super();
|
|
22
|
+
this.rootPath = opts.rootPath;
|
|
23
|
+
this.running = true;
|
|
24
|
+
this.notifications = [];
|
|
25
|
+
this.started = false;
|
|
26
|
+
}
|
|
27
|
+
async start() {
|
|
28
|
+
this.started = true;
|
|
29
|
+
return { definitionProvider: true };
|
|
30
|
+
}
|
|
31
|
+
notify(method, params) {
|
|
32
|
+
this.notifications.push({ method, params });
|
|
33
|
+
}
|
|
34
|
+
async request() {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
async stop() {
|
|
38
|
+
this.running = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let origCreate, origRead, origExistsMgr, origExistsReg;
|
|
43
|
+
let lastClient;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
origCreate = _deps.createClient;
|
|
47
|
+
origRead = _deps.readFileSync;
|
|
48
|
+
origExistsMgr = _deps.existsSync;
|
|
49
|
+
origExistsReg = regDeps.existsSync;
|
|
50
|
+
|
|
51
|
+
_deps.createClient = (opts) => {
|
|
52
|
+
lastClient = new FakeClient(opts);
|
|
53
|
+
return lastClient;
|
|
54
|
+
};
|
|
55
|
+
_deps.readFileSync = () => "const x = 1;\n";
|
|
56
|
+
// root inference: pretend the nearest /proj ancestor has package.json.
|
|
57
|
+
// Suffix match tolerates path.resolve prepending a drive letter on Windows.
|
|
58
|
+
_deps.existsSync = (p) =>
|
|
59
|
+
String(p).replace(/\\/g, "/").endsWith("/proj/package.json");
|
|
60
|
+
// registry: pretend the TS server bin exists in <root>/node_modules/.bin
|
|
61
|
+
const isWin = process.platform === "win32";
|
|
62
|
+
const binName = isWin
|
|
63
|
+
? "typescript-language-server.cmd"
|
|
64
|
+
: "typescript-language-server";
|
|
65
|
+
regDeps.existsSync = (p) =>
|
|
66
|
+
String(p).replace(/\\/g, "/").endsWith(`/node_modules/.bin/${binName}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
_deps.createClient = origCreate;
|
|
71
|
+
_deps.readFileSync = origRead;
|
|
72
|
+
_deps.existsSync = origExistsMgr;
|
|
73
|
+
regDeps.existsSync = origExistsReg;
|
|
74
|
+
vi.restoreAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("position helpers", () => {
|
|
78
|
+
it("converts 1-based ⇄ 0-based", () => {
|
|
79
|
+
expect(toLspPosition({ line: 1, col: 1 })).toEqual({
|
|
80
|
+
line: 0,
|
|
81
|
+
character: 0,
|
|
82
|
+
});
|
|
83
|
+
expect(toLspPosition({ line: 10, col: 5 })).toEqual({
|
|
84
|
+
line: 9,
|
|
85
|
+
character: 4,
|
|
86
|
+
});
|
|
87
|
+
expect(fromLspPosition({ line: 0, character: 0 })).toEqual({
|
|
88
|
+
line: 1,
|
|
89
|
+
col: 1,
|
|
90
|
+
});
|
|
91
|
+
expect(fromLspPosition({ line: 9, character: 4 })).toEqual({
|
|
92
|
+
line: 10,
|
|
93
|
+
col: 5,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it("clamps below 1 to 0 (no negatives to the server)", () => {
|
|
97
|
+
expect(toLspPosition({ line: 0, col: 0 })).toEqual({
|
|
98
|
+
line: 0,
|
|
99
|
+
character: 0,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("ensureFor", () => {
|
|
105
|
+
it("starts a server and sends didOpen the first time", async () => {
|
|
106
|
+
const mgr = new LSPManager();
|
|
107
|
+
const ready = await mgr.ensureFor("/proj/src/a.ts");
|
|
108
|
+
expect(ready.languageId).toBe("typescript");
|
|
109
|
+
expect(ready.root.replace(/\\/g, "/")).toMatch(/\/proj$/);
|
|
110
|
+
expect(lastClient.started).toBe(true);
|
|
111
|
+
const open = lastClient.notifications.find(
|
|
112
|
+
(n) => n.method === "textDocument/didOpen",
|
|
113
|
+
);
|
|
114
|
+
expect(open.params.textDocument.text).toBe("const x = 1;\n");
|
|
115
|
+
await mgr.disposeAll();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("sends didChange (not a second didOpen) on the next access", async () => {
|
|
119
|
+
const mgr = new LSPManager();
|
|
120
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
121
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
122
|
+
const opens = lastClient.notifications.filter(
|
|
123
|
+
(n) => n.method === "textDocument/didOpen",
|
|
124
|
+
);
|
|
125
|
+
const changes = lastClient.notifications.filter(
|
|
126
|
+
(n) => n.method === "textDocument/didChange",
|
|
127
|
+
);
|
|
128
|
+
expect(opens).toHaveLength(1);
|
|
129
|
+
expect(changes).toHaveLength(1);
|
|
130
|
+
expect(changes[0].params.textDocument.version).toBe(2);
|
|
131
|
+
await mgr.disposeAll();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("reuses one server for two files in the same project/language", async () => {
|
|
135
|
+
const mgr = new LSPManager();
|
|
136
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
137
|
+
const first = lastClient;
|
|
138
|
+
await mgr.ensureFor("/proj/src/b.ts");
|
|
139
|
+
expect(lastClient).toBe(first); // same client instance
|
|
140
|
+
expect(mgr.activeServerCount).toBe(1);
|
|
141
|
+
await mgr.disposeAll();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("returns unavailable for an unsupported extension", async () => {
|
|
145
|
+
const mgr = new LSPManager();
|
|
146
|
+
const ready = await mgr.ensureFor("/proj/readme.txt");
|
|
147
|
+
expect(ready.unavailable).toBe(true);
|
|
148
|
+
expect(ready.reason).toMatch(/no language mapping/);
|
|
149
|
+
await mgr.disposeAll();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns unavailable when no server bin is installed", async () => {
|
|
153
|
+
regDeps.existsSync = () => false; // nothing installed
|
|
154
|
+
const mgr = new LSPManager();
|
|
155
|
+
const ready = await mgr.ensureFor("/proj/src/a.ts");
|
|
156
|
+
expect(ready.unavailable).toBe(true);
|
|
157
|
+
expect(ready.reason).toMatch(/no language server installed/);
|
|
158
|
+
await mgr.disposeAll();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("crash-loop restart guard", () => {
|
|
163
|
+
// Simulate a language server process dying: the client marks itself not
|
|
164
|
+
// running and emits `exit`, exactly as the real LSPClient does on child exit.
|
|
165
|
+
function crash(client) {
|
|
166
|
+
client.running = false;
|
|
167
|
+
client.emit("exit", 1, null);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
it("re-spawns a crashed server on the next request (auto-restart)", async () => {
|
|
171
|
+
const mgr = new LSPManager();
|
|
172
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
173
|
+
const first = lastClient;
|
|
174
|
+
crash(first);
|
|
175
|
+
const ready = await mgr.ensureFor("/proj/src/a.ts");
|
|
176
|
+
expect(ready.unavailable).toBeUndefined();
|
|
177
|
+
expect(lastClient).not.toBe(first); // a FRESH client was spawned
|
|
178
|
+
expect(lastClient.started).toBe(true);
|
|
179
|
+
expect(ready.client).toBe(lastClient);
|
|
180
|
+
await mgr.disposeAll();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("quarantines a server that crash-loops past maxRestarts within the window", async () => {
|
|
184
|
+
let t = 1000;
|
|
185
|
+
const mgr = new LSPManager({
|
|
186
|
+
maxRestarts: 3,
|
|
187
|
+
restartWindowMs: 30000,
|
|
188
|
+
now: () => t,
|
|
189
|
+
});
|
|
190
|
+
for (let i = 0; i < 3; i++) {
|
|
191
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
192
|
+
t += 100;
|
|
193
|
+
crash(lastClient);
|
|
194
|
+
}
|
|
195
|
+
// 3 crashes are now inside the window → the next request quarantines the
|
|
196
|
+
// server (degrade to text search) rather than thrash-respawning it.
|
|
197
|
+
const before = lastClient;
|
|
198
|
+
const ready = await mgr.ensureFor("/proj/src/a.ts");
|
|
199
|
+
expect(ready.unavailable).toBe(true);
|
|
200
|
+
expect(ready.quarantined).toBe(true);
|
|
201
|
+
expect(ready.reason).toMatch(/crash-looped/);
|
|
202
|
+
expect(lastClient).toBe(before); // did NOT spawn yet another
|
|
203
|
+
await mgr.disposeAll();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("recovers and re-spawns once the crash window clears", async () => {
|
|
207
|
+
let t = 1000;
|
|
208
|
+
const mgr = new LSPManager({
|
|
209
|
+
maxRestarts: 3,
|
|
210
|
+
restartWindowMs: 30000,
|
|
211
|
+
now: () => t,
|
|
212
|
+
});
|
|
213
|
+
for (let i = 0; i < 3; i++) {
|
|
214
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
215
|
+
t += 100;
|
|
216
|
+
crash(lastClient);
|
|
217
|
+
}
|
|
218
|
+
expect((await mgr.ensureFor("/proj/src/a.ts")).quarantined).toBe(true);
|
|
219
|
+
// Stay healthy past the window → crash history prunes → a fresh start is
|
|
220
|
+
// allowed again (a server isn't quarantined forever).
|
|
221
|
+
t += 30001;
|
|
222
|
+
const quarantined = lastClient;
|
|
223
|
+
const ready = await mgr.ensureFor("/proj/src/a.ts");
|
|
224
|
+
expect(ready.unavailable).toBeUndefined();
|
|
225
|
+
expect(lastClient).not.toBe(quarantined);
|
|
226
|
+
await mgr.disposeAll();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("diagnostics", () => {
|
|
231
|
+
it("collects pushed diagnostics keyed by file", async () => {
|
|
232
|
+
const mgr = new LSPManager();
|
|
233
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
234
|
+
lastClient.emit("notify:textDocument/publishDiagnostics", {
|
|
235
|
+
uri: (await import("../lsp-client.js")).pathToFileUri(
|
|
236
|
+
path.resolve("/proj/src/a.ts"),
|
|
237
|
+
),
|
|
238
|
+
diagnostics: [
|
|
239
|
+
{
|
|
240
|
+
message: "boom",
|
|
241
|
+
severity: 1,
|
|
242
|
+
range: {
|
|
243
|
+
start: { line: 0, character: 0 },
|
|
244
|
+
end: { line: 0, character: 1 },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
const diags = mgr.getDiagnostics("/proj/src/a.ts");
|
|
250
|
+
expect(diags).toHaveLength(1);
|
|
251
|
+
expect(diags[0].message).toBe("boom");
|
|
252
|
+
await mgr.disposeAll();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("disposeAll", () => {
|
|
257
|
+
it("stops all servers and blocks further use", async () => {
|
|
258
|
+
const mgr = new LSPManager();
|
|
259
|
+
await mgr.ensureFor("/proj/src/a.ts");
|
|
260
|
+
await mgr.disposeAll();
|
|
261
|
+
expect(lastClient.running).toBe(false);
|
|
262
|
+
await expect(mgr.ensureFor("/proj/src/a.ts")).rejects.toThrow(/disposed/);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lsp-server-registry tests — language mapping, bin resolution (project-local
|
|
3
|
+
* wins over PATH), plugin server registration, and availability probing. FS
|
|
4
|
+
* existence is faked via `_deps.existsSync`; no real toolchain needed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import {
|
|
10
|
+
languageIdForFile,
|
|
11
|
+
resolveServer,
|
|
12
|
+
resolveBin,
|
|
13
|
+
probeServers,
|
|
14
|
+
registerLanguageServer,
|
|
15
|
+
_resetPluginServers,
|
|
16
|
+
_deps,
|
|
17
|
+
} from "../lsp-server-registry.js";
|
|
18
|
+
|
|
19
|
+
let origExists;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
origExists = _deps.existsSync;
|
|
22
|
+
_resetPluginServers();
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
_deps.existsSync = origExists;
|
|
26
|
+
_resetPluginServers();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** existsSync true only for the given absolute paths (normalized separators). */
|
|
30
|
+
function existsFor(paths) {
|
|
31
|
+
const set = new Set(paths.map((p) => p.replace(/\\/g, "/")));
|
|
32
|
+
_deps.existsSync = (p) => set.has(String(p).replace(/\\/g, "/"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("languageIdForFile", () => {
|
|
36
|
+
it("maps common extensions", () => {
|
|
37
|
+
expect(languageIdForFile("a.ts")).toBe("typescript");
|
|
38
|
+
expect(languageIdForFile("a.TSX")).toBe("typescriptreact");
|
|
39
|
+
expect(languageIdForFile("a.py")).toBe("python");
|
|
40
|
+
expect(languageIdForFile("a.go")).toBe("go");
|
|
41
|
+
expect(languageIdForFile("a.rs")).toBe("rust");
|
|
42
|
+
});
|
|
43
|
+
it("returns null for unsupported extensions", () => {
|
|
44
|
+
expect(languageIdForFile("a.txt")).toBeNull();
|
|
45
|
+
expect(languageIdForFile("noext")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("resolveBin", () => {
|
|
50
|
+
it("prefers project node_modules/.bin over PATH", () => {
|
|
51
|
+
const isWin = process.platform === "win32";
|
|
52
|
+
const binName = isWin
|
|
53
|
+
? "typescript-language-server.cmd"
|
|
54
|
+
: "typescript-language-server";
|
|
55
|
+
const localBin = path.join("/proj", "node_modules", ".bin", binName);
|
|
56
|
+
existsFor([localBin]);
|
|
57
|
+
const resolved = resolveBin("typescript-language-server", "/proj");
|
|
58
|
+
expect(resolved.replace(/\\/g, "/")).toBe(localBin.replace(/\\/g, "/"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns null when nothing resolves", () => {
|
|
62
|
+
existsFor([]);
|
|
63
|
+
expect(resolveBin("nonexistent-server", "/proj")).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("resolveServer", () => {
|
|
68
|
+
it("resolves a TS server command with --stdio args", () => {
|
|
69
|
+
const isWin = process.platform === "win32";
|
|
70
|
+
const binName = isWin
|
|
71
|
+
? "typescript-language-server.cmd"
|
|
72
|
+
: "typescript-language-server";
|
|
73
|
+
existsFor([path.join("/proj", "node_modules", ".bin", binName)]);
|
|
74
|
+
const s = resolveServer("typescript", "/proj");
|
|
75
|
+
expect(s).toMatchObject({
|
|
76
|
+
languageId: "typescript",
|
|
77
|
+
id: "typescript-language-server",
|
|
78
|
+
args: ["--stdio"],
|
|
79
|
+
});
|
|
80
|
+
expect(s.command).toContain("typescript-language-server");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns null for a language with no installed server", () => {
|
|
84
|
+
existsFor([]);
|
|
85
|
+
expect(resolveServer("rust", "/proj")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns null for an unknown language", () => {
|
|
89
|
+
existsFor([]);
|
|
90
|
+
expect(resolveServer("cobol", "/proj")).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("plugin server registration", () => {
|
|
95
|
+
it("registers an extra language server and its extensions", () => {
|
|
96
|
+
registerLanguageServer({
|
|
97
|
+
languageId: "zig",
|
|
98
|
+
extensions: [".zig"],
|
|
99
|
+
command: "zls",
|
|
100
|
+
args: [],
|
|
101
|
+
});
|
|
102
|
+
expect(languageIdForFile("a.zig")).toBe("zig");
|
|
103
|
+
const isWin = process.platform === "win32";
|
|
104
|
+
existsFor([
|
|
105
|
+
path.join("/proj", "node_modules", ".bin", isWin ? "zls.cmd" : "zls"),
|
|
106
|
+
]);
|
|
107
|
+
const s = resolveServer("zig", "/proj");
|
|
108
|
+
expect(s).toMatchObject({ id: "zls" });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("rejects an incomplete registration", () => {
|
|
112
|
+
expect(() => registerLanguageServer({ languageId: "x" })).toThrow(
|
|
113
|
+
/requires/,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("probeServers", () => {
|
|
119
|
+
it("reports availability per server family", () => {
|
|
120
|
+
existsFor([]); // nothing installed
|
|
121
|
+
const rows = probeServers("/proj");
|
|
122
|
+
expect(rows.every((r) => r.available === false)).toBe(true);
|
|
123
|
+
// one row per distinct server id (TS family collapses)
|
|
124
|
+
const ids = rows.map((r) => r.id);
|
|
125
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
126
|
+
expect(ids).toContain("typescript-language-server");
|
|
127
|
+
});
|
|
128
|
+
});
|