chainlesschain 0.162.101 → 0.162.103

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.
Files changed (214) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/scripts/postinstall.mjs +33 -3
  4. package/src/assets/web-panel/assets/{AIOps-NeD6mA1O.js → AIOps-BpJ5yCcV.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Bhp7A7vO.js → ActionButton-B1uBfxnA.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-DgdI84Jy.js → Analytics-0ubw5quf.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-Qw29caa7.js → AppLayout-go-GyyM_.js} +4 -4
  8. package/src/assets/web-panel/assets/{Audit-DgkzhBjz.js → Audit-CdUfoUnM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-CiWEQoW5.js → Backup-CBn0xtsa.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BWNbkeDF.js → BaseInput-a9fqTkvL.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CUcj4Vyd.js → Chat-D-d_IatD.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-bAMNCgEj.js → ChatBubbleRenderer-Co9h0SbA.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-B0uZTW8f.js → Checkbox-CiKEjtXm.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BPk7b-Xa.js → Codegen-OQxW_9Z2.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-C2cV_dw4.js → Col-BZHXUEnZ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CDN6Yggb.js → Community-D3xno9fj.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-C4WPtW_V.js → Compact-D62jpZk_.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-BGUfeH-3.js → Compliance-D4jFTAcQ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-CsnVgF-o.js → Cowork-BOU_wir4.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron--rGanASk.js → Cron-Cr9BlJA7.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-D_CLrd-0.js → Crosschain-L9M--CXb.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-iYkbVc8W.js → DID-xFu3-VWT.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CuFMU98t.js → Dashboard--ax48T3j.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BPSjv_Ye.js → Dropdown-CTOS8daR.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DBmk7czO.js → EmailListRenderer-CNLSX81U.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BaCArN11.js → FamilyGuardDashboard-5EjFynBc.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DsTV-8Ca.js → Federation-CoPyIWIu.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-C6fRr_6F.js → FormItemContext-BbzCNbYy.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-AfuZCfyv.js → GenericCardRenderer-8_9_uXY0.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-CAFIrGhb.js → Git-Bz_qlVPg.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-C-NaxDs4.js → Governance-7eepNCeD.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BPfeNZP7.js → Inference-DlBx5l1g.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DDUBte_m.js → KnowledgeGraph-BPkc--rM.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DWysA6JP.js → Logs-BQw4nPvS.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-CAK68hXc.js → Marketplace-Cil3SdgN.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-CNmmoDAk.js → McpTools-Bnch7KrG.js} +3 -3
  37. package/src/assets/web-panel/assets/{Memory-D_KmFoui.js → Memory-assQ3Suc.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CaY-6kjj.js → MobileBridge-D5mE8c-z.js} +2 -2
  39. package/src/assets/web-panel/assets/MobileProjects-C4MiDXw1.js +1 -0
  40. package/src/assets/web-panel/assets/{Mtc-YnLrtuXT.js → Mtc-CumojeLk.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-D38v836u.js → MtcAudit-3tTBc_fw.js} +4 -4
  42. package/src/assets/web-panel/assets/{Multisig-Dv3ad9S4.js → Multisig-Dl4IJBsl.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-MYTh2eWC.js → NLProgramming-DHN_cV9x.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-DYO-0Hva.js → Notes-DeyTvwYk.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-D39gOb3h.js → NotificationSettings-BUkj8mTj.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-CXdO0z3I.js → OrderTableRenderer-BMpEXTki.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-DOYTwQwt.js → Organization-B6Ogmkpz.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-Cm-go0yU.js → Overflow-DRUE-2Wz.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-CakGtorw.js → P2P-CnMUkd64.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-BlmbHJnb.js → PdhVaultBrowser-B3nFxT3b.js} +4 -4
  51. package/src/assets/web-panel/assets/{Permissions-C_podo04.js → Permissions-CyG1FR7v.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-nBbehIKp.js → PersonalDataHub-BmN6YMb5.js} +3 -3
  53. package/src/assets/web-panel/assets/{Pipeline-CGzQUoKv.js → Pipeline-BZcamAnH.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-BuD-Dd9m.js → Privacy-D7_zsaY3.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-CLcFmGXU.js → ProjectInit-DNa7XQjg.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-BxXTR0dH.js → ProjectSettings-KH2Dm09G.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-DkpBjAfd.js → Projects-T-WqR3D6.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-DwHK42ht.js → Providers-8GtQ6GgR.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-idJPKErk.js → QuickAsk-BuBM5Zpg.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-BvJj0Nu2.js → Recommend-DvH95W1b.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-BK9l4Rcf.js → Reputation-BHCpMEP6.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-rLxg7xk5.js → Row-BnMPysBm.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Cw6ZcHBY.js → RssFeed-C-T8B5VU.js} +3 -3
  64. package/src/assets/web-panel/assets/{Search-D8mKp506.js → Search-CioiZGkX.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-CXunojmQ.js → Security-zSApKqdd.js} +4 -4
  66. package/src/assets/web-panel/assets/{Services-BE5DgmPl.js → Services-CQh0UDXC.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-CssL5wmE.js → Skeleton-C0dRaEVG.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-zyS_1YrT.js → Skills-BgQRArsa.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-DIVn2Roa.js → Sla-BNn7F3jc.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CjNdZWdj.js → SpeechSettings-kvVfKfqv.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-BCbu5WQT.js → SyncSettings-BwIkkX1E.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-BjFrgXge.js → Tasks-BkcvhBZw.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-Cl4Y3lKN.js → Templates-COK0o2pL.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-k1T0F_eH.js → Tenant-DpK87fqo.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-i0tHF8fy.js → Terminal-2g2-XPT_.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-FmPaNyqK.js → TimelineRenderer-B4SJqyqf.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-BUWEiyab.js → Tokens-C3iSR8fH.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-9srMXwpo.js → Trigger-D1Oo58Yo.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-hmGor_R2.js → Trust-CUhy4DDc.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-Dst7K10a.js → UkeySign-r-kTqSw8.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-D_utUv1u.js → VideoEditing-B8MyBfCX.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-BpYTTseI.js → Wallet-CquTfSv0.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-CEIw8QrF.js → WebAuthn-BC7TP1H3.js} +5 -5
  84. package/src/assets/web-panel/assets/{WorkflowEditor-Bkb4bUZ_.js → WorkflowEditor-CELBSNbP.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-CTqDgGRH.js → chat-d66mbO2i.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-CnJHVFLk.js → colors-D0WRCg20.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-D4-v0ahl.js → compact-item-4__-8TIi.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-CYDbnHZX.js → createContext-Dqsf52js.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-DUAmX0aE.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-CbYwlEz5.js → hasIn-ClTZOS8I.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-DVKfa-z4.js → index-0Axwx1nN.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-Cl7-mbsb.js → index-81SaeU6t.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CK8nROWH.js → index-B1TGSkBt.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BygyTsjr.js → index-B3vrmn5v.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-B78d_-pR.js → index-BCOpbfAH.js} +1 -1
  96. package/src/assets/web-panel/assets/index-BLCwqv8j.js +1 -0
  97. package/src/assets/web-panel/assets/{index-YuO8gxKb.js → index-BNKECBZm.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-tmNsJzQU.js → index-BwozLU75.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-dKpwhR5x.js → index-C4_XhCJS.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BNz3yht2.js → index-C5l6tSky.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-4767GEls.js → index-CFwkeSun.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BCQ4kcL3.js → index-CGf3oqM0.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-xDe5BkjU.js → index-CJKyF-y8.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DjFQuno4.js → index-Cg8Qw3Jw.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Dl_NdY39.js → index-Cops66s2.js} +3 -3
  106. package/src/assets/web-panel/assets/{index-D8MYVsVB.js → index-Ct5SJenr.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CY971z_9.js → index-CxoWS8hl.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CVc_5aBF.js → index-D-ya3Ohe.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C4chfOjw.js → index-D68tyHYa.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BR4facv0.js → index-D9x6i7iV.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CxKdSB-p.js → index-DDLjwgva.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-B-hiXmVK.js → index-DORg0Hps.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BR0FfrCL.js → index-DQaGurrQ.js} +1 -1
  114. package/src/assets/web-panel/assets/index-Db5JRQ6E.js +1 -0
  115. package/src/assets/web-panel/assets/{index-5NXdAIZK.js → index-DbY1lv1z.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BOUSaCsn.js → index-Dm7CYK3j.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DxvFrsxl.js → index-DpLhC0nx.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-qNwkSspX.js → index-P4X9zSF2.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-OqmUv5QO.js → index-PAiWA2un.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-B8uhiRML.js → index-PMJ4tBAA.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CfshMjFN.js → index-Yi5Lr63I.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-wxz2s6uW.js → index-ZZpfHd4J.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DdRtpQFq.js → index-b7a5aEX9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C2fICr5d.js → index-e5zJLrJQ.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BGUOVPjO.js → index-fi2ikHoc.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-8cmtr1jC.js → index-jAPphbJL.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DMKNVIqJ.js → index-leuwRsbf.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CP-UKwu8.js → index-luisYDBz.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DaCCLe7i.js → index-we9-1IbF.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-qCQgpAty.js → initDefaultProps-zqDf7qWv.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-SkVSGdwu.js → motion-DRqTooll.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-BpWxaW_x.js → move-N8tkRqS4.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-DlZWM2H4.js → omit-Cy01f2vw.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DobtUnVe.js → pickAttrs-C2ycNXNi.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-CD6mYRZi.js → placementArrow-D60Xb1j9.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-DvIIMTD_.js → responsiveObserve-BF8Y-1HQ.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-CVgyUfaA.js → slide-DDi6zYZf.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-DrUDnjy_.js → statusUtils-CWPq6UzA.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-BNYQS60m.js → styleChecker-D4zVyb4w.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-DYv7Tvk0.js → useFlexGapSupport-BEMNO3oR.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-DmAD2atJ.js → useFs-DBLO8VR7.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-DbLVKA3I.js → usePersonalDataHub-C83ZIrPY.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-C3WEt6_P.js → vnode-Dcfyve-7.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-CRoSX_oZ.js → zoom-Dg6uoWSc.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/__tests__/hub-health.test.js +105 -0
  147. package/src/commands/__tests__/hub-stats.test.js +106 -0
  148. package/src/commands/a2a.js +2 -1
  149. package/src/commands/bi.js +4 -3
  150. package/src/commands/browse.js +3 -2
  151. package/src/commands/codegen.js +7 -6
  152. package/src/commands/collab.js +2 -1
  153. package/src/commands/crosschain.js +5 -4
  154. package/src/commands/dao.js +10 -10
  155. package/src/commands/dbevo.js +3 -2
  156. package/src/commands/economy.js +8 -6
  157. package/src/commands/evolution.js +10 -8
  158. package/src/commands/export.js +2 -1
  159. package/src/commands/federation.js +8 -7
  160. package/src/commands/fusion.js +4 -3
  161. package/src/commands/git.js +2 -1
  162. package/src/commands/governance.js +6 -5
  163. package/src/commands/hardening.js +3 -2
  164. package/src/commands/hmemory.js +2 -1
  165. package/src/commands/hub.js +111 -32
  166. package/src/commands/inference.js +9 -8
  167. package/src/commands/infra.js +9 -8
  168. package/src/commands/instinct.js +66 -0
  169. package/src/commands/learning.js +66 -0
  170. package/src/commands/marketplace.js +5 -4
  171. package/src/commands/matrix.js +2 -1
  172. package/src/commands/memory.js +66 -0
  173. package/src/commands/nlprog.js +4 -3
  174. package/src/commands/nostr.js +4 -3
  175. package/src/commands/note.js +2 -1
  176. package/src/commands/ops.js +3 -2
  177. package/src/commands/perception.js +6 -5
  178. package/src/commands/privacy.js +13 -12
  179. package/src/commands/quantization.js +3 -2
  180. package/src/commands/recommend.js +8 -7
  181. package/src/commands/reputation.js +4 -3
  182. package/src/commands/runtime.js +6 -5
  183. package/src/commands/sandbox.js +5 -4
  184. package/src/commands/session.js +2 -1
  185. package/src/commands/sla.js +6 -5
  186. package/src/commands/social.js +6 -5
  187. package/src/commands/stress.js +7 -6
  188. package/src/commands/sync.js +2 -1
  189. package/src/commands/tenant.js +3 -2
  190. package/src/commands/trust.js +7 -6
  191. package/src/commands/wallet.js +2 -1
  192. package/src/gateways/ws/personal-data-hub-protocol.js +16 -3
  193. package/src/lib/__tests__/agents.test.js +225 -0
  194. package/src/lib/__tests__/cowork-observe-html.test.js +141 -0
  195. package/src/lib/__tests__/logger.test.js +110 -0
  196. package/src/lib/__tests__/project-inventory.test.js +280 -0
  197. package/src/lib/__tests__/project-root.test.js +96 -0
  198. package/src/lib/__tests__/provider-stream.test.js +190 -0
  199. package/src/lib/cli-arg.js +41 -0
  200. package/src/lib/cli-numeric.js +12 -3
  201. package/src/lib/instinct-manager.js +64 -0
  202. package/src/lib/learning/learning-tables.js +83 -0
  203. package/src/lib/mcp-oauth.js +10 -2
  204. package/src/lib/memory-manager.js +67 -0
  205. package/src/lib/personal-data-hub-wiring.js +135 -7
  206. package/src/lib/plan-mode.js +3 -2
  207. package/src/repl/__tests__/mcp-prompt.test.js +155 -0
  208. package/src/repl/__tests__/slash-macro.test.js +81 -0
  209. package/src/runtime/__tests__/coding-agent-contract-shared.test.js +136 -0
  210. package/src/runtime/__tests__/coding-agent-events.test.js +263 -0
  211. package/src/assets/web-panel/assets/MobileProjects-DhDxrWze.js +0 -1
  212. package/src/assets/web-panel/assets/devWarning-OhxeajH6.js +0 -1
  213. package/src/assets/web-panel/assets/index-Bbtoi_47.js +0 -1
  214. package/src/assets/web-panel/assets/index-Cb8RWwaB.js +0 -1
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * logger tests (previously untested) — the cc CLI's console logger. Small but
5
+ * the gating semantics matter: quiet suppresses info/success/log/newline but
6
+ * must NEVER suppress warn/error (otherwise failures would be hidden), and
7
+ * verbose only emits when enabled. console is spied; module-level verbose/quiet
8
+ * flags are reset between tests.
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
12
+ import {
13
+ setVerbose,
14
+ setQuiet,
15
+ info,
16
+ success,
17
+ warn,
18
+ error,
19
+ verbose,
20
+ log,
21
+ newline,
22
+ logger,
23
+ } from "../logger.js";
24
+
25
+ let logSpy;
26
+ let errSpy;
27
+
28
+ beforeEach(() => {
29
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
30
+ errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
31
+ setQuiet(false);
32
+ setVerbose(false);
33
+ });
34
+
35
+ afterEach(() => {
36
+ setQuiet(false);
37
+ setVerbose(false);
38
+ vi.restoreAllMocks();
39
+ });
40
+
41
+ describe("default (not quiet, not verbose)", () => {
42
+ it("info/success/log/newline write to console.log", () => {
43
+ info("i");
44
+ success("s");
45
+ log("l");
46
+ newline();
47
+ expect(logSpy).toHaveBeenCalledTimes(4);
48
+ });
49
+
50
+ it("verbose is suppressed unless enabled", () => {
51
+ verbose("v");
52
+ expect(logSpy).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("error writes to console.error, not console.log", () => {
56
+ error("boom");
57
+ expect(errSpy).toHaveBeenCalledTimes(1);
58
+ expect(logSpy).not.toHaveBeenCalled();
59
+ });
60
+ });
61
+
62
+ describe("quiet mode", () => {
63
+ beforeEach(() => setQuiet(true));
64
+
65
+ it("suppresses info/success/log/newline", () => {
66
+ info("i");
67
+ success("s");
68
+ log("l");
69
+ newline();
70
+ expect(logSpy).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it("does NOT suppress warn (still console.log)", () => {
74
+ warn("careful");
75
+ expect(logSpy).toHaveBeenCalledTimes(1);
76
+ });
77
+
78
+ it("does NOT suppress error", () => {
79
+ error("boom");
80
+ expect(errSpy).toHaveBeenCalledTimes(1);
81
+ });
82
+ });
83
+
84
+ describe("verbose mode", () => {
85
+ it("emits verbose only after setVerbose(true)", () => {
86
+ verbose("before");
87
+ expect(logSpy).not.toHaveBeenCalled();
88
+ setVerbose(true);
89
+ verbose("after");
90
+ expect(logSpy).toHaveBeenCalledTimes(1);
91
+ });
92
+ });
93
+
94
+ describe("logger object", () => {
95
+ it("exposes the full method surface", () => {
96
+ for (const m of [
97
+ "info",
98
+ "success",
99
+ "warn",
100
+ "error",
101
+ "verbose",
102
+ "log",
103
+ "newline",
104
+ "setVerbose",
105
+ "setQuiet",
106
+ ]) {
107
+ expect(typeof logger[m]).toBe("function");
108
+ }
109
+ });
110
+ });
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * project-inventory tests — the offline census behind `cc init --memory`
5
+ * (previously untested). Covers readmeSynopsis (markdown synopsis extraction),
6
+ * inventoryProject (package.json / lockfile→PM / config markers / CI count /
7
+ * language census / SKIP_DIRS / workspaces both shapes / existingMemory) and
8
+ * renderMemoryFile (the generated cc.md). Driven by an in-memory fake fs over
9
+ * POSIX paths via the deps seam — no real I/O.
10
+ */
11
+
12
+ import { describe, it, expect } from "vitest";
13
+ import pathDefault from "path";
14
+ import {
15
+ readmeSynopsis,
16
+ inventoryProject,
17
+ renderMemoryFile,
18
+ SKIP_DIRS,
19
+ } from "../project-inventory.js";
20
+
21
+ const P = pathDefault.posix;
22
+
23
+ /** Build a fake fs from a { absPosixPath: content } map. Dirs are derived. */
24
+ function makeFs(files) {
25
+ const contents = new Map(Object.entries(files));
26
+ const fileSet = new Set(contents.keys());
27
+ const dirSet = new Set(["/"]);
28
+ for (const f of fileSet) {
29
+ let d = P.dirname(f);
30
+ while (d && !dirSet.has(d)) {
31
+ dirSet.add(d);
32
+ d = P.dirname(d);
33
+ }
34
+ }
35
+ const childrenOf = (dir) => {
36
+ const out = new Map(); // name -> isDir
37
+ for (const f of fileSet)
38
+ if (P.dirname(f) === dir) out.set(P.basename(f), false);
39
+ for (const d of dirSet)
40
+ if (d !== "/" && P.dirname(d) === dir) out.set(P.basename(d), true);
41
+ return out;
42
+ };
43
+ return {
44
+ readFileSync(p) {
45
+ if (!fileSet.has(p)) {
46
+ const e = new Error("ENOENT");
47
+ e.code = "ENOENT";
48
+ throw e;
49
+ }
50
+ return contents.get(p);
51
+ },
52
+ existsSync(p) {
53
+ return fileSet.has(p) || dirSet.has(p);
54
+ },
55
+ readdirSync(p, opts) {
56
+ if (!dirSet.has(p)) {
57
+ const e = new Error("ENOENT");
58
+ e.code = "ENOENT";
59
+ throw e;
60
+ }
61
+ const kids = childrenOf(p);
62
+ if (opts && opts.withFileTypes) {
63
+ return [...kids].map(([name, isDir]) => ({
64
+ name,
65
+ isDirectory: () => isDir,
66
+ isFile: () => !isDir,
67
+ }));
68
+ }
69
+ return [...kids.keys()];
70
+ },
71
+ };
72
+ }
73
+
74
+ const inv = (files, opts = {}) =>
75
+ inventoryProject("/proj", { deps: { fs: makeFs(files), path: P }, ...opts });
76
+
77
+ // --------------------------------------------------------------------------- //
78
+ // readmeSynopsis
79
+ // --------------------------------------------------------------------------- //
80
+ describe("readmeSynopsis", () => {
81
+ it("returns null for empty input", () => {
82
+ expect(readmeSynopsis(null)).toBe(null);
83
+ expect(readmeSynopsis("")).toBe(null);
84
+ });
85
+
86
+ it("skips headings, badges and bare HTML, returns first real paragraph", () => {
87
+ const md = [
88
+ "# My Project",
89
+ "![badge](x.svg)",
90
+ "[![ci](a)](b)",
91
+ "<p>html</p>",
92
+ "",
93
+ "This is the real summary line.",
94
+ "Second line of the same paragraph.",
95
+ "",
96
+ "Later paragraph ignored.",
97
+ ].join("\n");
98
+ expect(readmeSynopsis(md)).toBe(
99
+ "This is the real summary line. Second line of the same paragraph.",
100
+ );
101
+ });
102
+
103
+ it("returns null when there is no prose (only headings)", () => {
104
+ expect(readmeSynopsis("# Title\n## Sub")).toBe(null);
105
+ });
106
+
107
+ it("truncates to maxChars with an ellipsis", () => {
108
+ const out = readmeSynopsis("x".repeat(50), 10);
109
+ expect(out).toBe("xxxxxxxxxx…");
110
+ expect(out.length).toBe(11);
111
+ });
112
+ });
113
+
114
+ // --------------------------------------------------------------------------- //
115
+ // inventoryProject
116
+ // --------------------------------------------------------------------------- //
117
+ describe("inventoryProject", () => {
118
+ it("reads name/description from package.json", () => {
119
+ const r = inv({
120
+ "/proj/package.json": JSON.stringify({ name: "acme", description: "d" }),
121
+ });
122
+ expect(r.name).toBe("acme");
123
+ expect(r.description).toBe("d");
124
+ });
125
+
126
+ it("falls back to folder basename when no package.json", () => {
127
+ expect(inv({ "/proj/foo.txt": "x" }).name).toBe("proj");
128
+ });
129
+
130
+ it("detects package manager from lockfile (first match wins)", () => {
131
+ expect(inv({ "/proj/pnpm-lock.yaml": "" }).packageManager).toBe("pnpm");
132
+ expect(inv({ "/proj/yarn.lock": "" }).packageManager).toBe("yarn");
133
+ expect(inv({ "/proj/foo.txt": "" }).packageManager).toBe(null);
134
+ });
135
+
136
+ it("collects deduped tooling config markers", () => {
137
+ const r = inv({
138
+ "/proj/tsconfig.json": "{}",
139
+ "/proj/vite.config.js": "",
140
+ "/proj/vite.config.ts": "", // same label "Vite" — deduped
141
+ });
142
+ expect(r.configs).toContain("TypeScript");
143
+ expect(r.configs.filter((c) => c === "Vite").length).toBe(1);
144
+ });
145
+
146
+ it("counts CI workflow files", () => {
147
+ const r = inv({
148
+ "/proj/.github/workflows/ci.yml": "",
149
+ "/proj/.github/workflows/release.yaml": "",
150
+ "/proj/.github/workflows/notes.md": "", // not yaml
151
+ });
152
+ expect(r.ciWorkflows).toBe(2);
153
+ });
154
+
155
+ it("censuses languages by extension, sorted desc, skipping SKIP_DIRS", () => {
156
+ const r = inv({
157
+ "/proj/a.ts": "",
158
+ "/proj/src/b.ts": "",
159
+ "/proj/src/c.js": "",
160
+ "/proj/node_modules/d.ts": "", // skipped
161
+ });
162
+ const langs = Object.fromEntries(r.languages);
163
+ expect(langs.TypeScript).toBe(2); // a.ts + src/b.ts, NOT node_modules
164
+ expect(langs.JavaScript).toBe(1);
165
+ expect(r.languages[0][0]).toBe("TypeScript"); // sorted desc
166
+ });
167
+
168
+ it("tracks top-level directories with file counts", () => {
169
+ const r = inv({
170
+ "/proj/src/a.js": "",
171
+ "/proj/src/b.js": "",
172
+ "/proj/docs/c.md": "",
173
+ });
174
+ const dirs = Object.fromEntries(r.topDirs);
175
+ expect(dirs.src).toBe(2);
176
+ expect(dirs.docs).toBe(1);
177
+ });
178
+
179
+ it("reads workspaces in both array and {packages} shapes", () => {
180
+ expect(
181
+ inv({ "/proj/package.json": JSON.stringify({ workspaces: ["a", "b"] }) })
182
+ .workspaces,
183
+ ).toEqual(["a", "b"]);
184
+ expect(
185
+ inv({
186
+ "/proj/package.json": JSON.stringify({
187
+ workspaces: { packages: ["x"] },
188
+ }),
189
+ }).workspaces,
190
+ ).toEqual(["x"]);
191
+ });
192
+
193
+ it("lists scripts and existing memory files", () => {
194
+ const r = inv({
195
+ "/proj/package.json": JSON.stringify({
196
+ scripts: { build: "tsc", test: "vitest" },
197
+ }),
198
+ "/proj/CLAUDE.md": "x",
199
+ });
200
+ expect(r.scripts).toEqual([
201
+ ["build", "tsc"],
202
+ ["test", "vitest"],
203
+ ]);
204
+ expect(r.existingMemory).toContain("CLAUDE.md");
205
+ });
206
+
207
+ it("derives synopsis from README.md", () => {
208
+ const r = inv({ "/proj/README.md": "# T\n\nHello world synopsis." });
209
+ expect(r.synopsis).toBe("Hello world synopsis.");
210
+ });
211
+
212
+ it("respects maxDepth (deep files beyond cap not counted)", () => {
213
+ const r = inv({ "/proj/a/b/c/deep.ts": "" }, { maxDepth: 1 });
214
+ expect(Object.fromEntries(r.languages).TypeScript).toBeUndefined();
215
+ });
216
+
217
+ it("SKIP_DIRS includes the usual heavy directories", () => {
218
+ expect(SKIP_DIRS.has("node_modules")).toBe(true);
219
+ expect(SKIP_DIRS.has("target")).toBe(true);
220
+ });
221
+ });
222
+
223
+ // --------------------------------------------------------------------------- //
224
+ // renderMemoryFile
225
+ // --------------------------------------------------------------------------- //
226
+ describe("renderMemoryFile", () => {
227
+ const base = {
228
+ name: "acme",
229
+ description: null,
230
+ synopsis: "A cool tool.",
231
+ packageManager: "npm",
232
+ configs: ["TypeScript", "Vitest"],
233
+ ciWorkflows: 3,
234
+ workspaces: ["packages/*"],
235
+ scripts: [
236
+ ["build", "tsc"],
237
+ ["test", "vitest"],
238
+ ],
239
+ languages: [
240
+ ["TypeScript", 10],
241
+ ["JavaScript", 2],
242
+ ],
243
+ topDirs: [["src", 10]],
244
+ truncated: false,
245
+ existingMemory: ["CLAUDE.md"],
246
+ };
247
+
248
+ it("renders title, overview, stack, commands and layout", () => {
249
+ const md = renderMemoryFile(base, { date: "2026-06-22" });
250
+ expect(md).toContain("# acme — Project Memory");
251
+ expect(md).toContain("2026-06-22");
252
+ expect(md).toContain("A cool tool.");
253
+ expect(md).toContain("TypeScript (10), JavaScript (2)");
254
+ expect(md).toContain("Package manager: npm");
255
+ expect(md).toContain("CI: GitHub Actions (3 workflows)");
256
+ expect(md).toContain("`npm run build` — `tsc`");
257
+ expect(md).toContain("Workspaces: packages/*");
258
+ expect(md).toContain("`src/` — 10 files");
259
+ });
260
+
261
+ it("imports existing memory files via @-include (excluding cc.md)", () => {
262
+ const md = renderMemoryFile({
263
+ ...base,
264
+ existingMemory: ["cc.md", "CLAUDE.md"],
265
+ });
266
+ expect(md).toContain("@CLAUDE.md");
267
+ expect(md).not.toContain("@cc.md");
268
+ });
269
+
270
+ it("caps the script list at 12 with a remainder note", () => {
271
+ const scripts = Array.from({ length: 15 }, (_, i) => [`s${i}`, `cmd${i}`]);
272
+ const md = renderMemoryFile({ ...base, scripts });
273
+ expect(md).toContain("… 3 more scripts");
274
+ });
275
+
276
+ it("notes a truncated scan", () => {
277
+ const md = renderMemoryFile({ ...base, truncated: true });
278
+ expect(md).toContain("scan truncated");
279
+ });
280
+ });
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * project-root tests (previously untested) — the git-root walk shared by the
5
+ * .claude/.chainlesschain discovery layers (permission rules, output-styles,
6
+ * slash-commands, sub-agents). A bug here means a subdir run silently drops the
7
+ * project's config (a safety gap), so the walk + defensive bail-to-null branches
8
+ * are pinned here. Fake fs/path via the deps seam; no real I/O.
9
+ */
10
+
11
+ import { describe, it, expect } from "vitest";
12
+ import pathDefault from "path";
13
+ import { createRequire } from "module";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { findGitProjectRoot, projectRootBase } = require("../project-root.cjs");
17
+
18
+ const P = pathDefault.posix;
19
+
20
+ /** fs whose existsSync(`${dir}/.git`) is true for dirs in the given set. */
21
+ function fsWithGit(gitDirs, { throwAt } = {}) {
22
+ const set = new Set(gitDirs);
23
+ return {
24
+ existsSync(p) {
25
+ if (throwAt && p === throwAt) throw new Error("EACCES");
26
+ return p.endsWith("/.git") && set.has(p.slice(0, -"/.git".length));
27
+ },
28
+ };
29
+ }
30
+
31
+ describe("findGitProjectRoot", () => {
32
+ it("returns cwd when .git is right there", () => {
33
+ expect(
34
+ findGitProjectRoot("/proj", { fs: fsWithGit(["/proj"]), path: P }),
35
+ ).toBe("/proj");
36
+ });
37
+
38
+ it("walks up to the nearest ancestor containing .git", () => {
39
+ expect(
40
+ findGitProjectRoot("/proj/src/app", {
41
+ fs: fsWithGit(["/proj"]),
42
+ path: P,
43
+ }),
44
+ ).toBe("/proj");
45
+ });
46
+
47
+ it("returns null when no .git is found", () => {
48
+ expect(findGitProjectRoot("/a/b/c", { fs: fsWithGit([]), path: P })).toBe(
49
+ null,
50
+ );
51
+ });
52
+
53
+ it("keeps walking when existsSync throws on an intermediate dir", () => {
54
+ // .git lives at /proj; checking /proj/src/.git throws but the walk continues
55
+ const fs = fsWithGit(["/proj"], { throwAt: "/proj/src/.git" });
56
+ expect(findGitProjectRoot("/proj/src/app", { fs, path: P })).toBe("/proj");
57
+ });
58
+
59
+ it("bails to null when fs has no existsSync", () => {
60
+ expect(findGitProjectRoot("/proj", { fs: {}, path: P })).toBe(null);
61
+ });
62
+
63
+ it("bails to null when path lacks dirname/join", () => {
64
+ expect(
65
+ findGitProjectRoot("/proj", { fs: fsWithGit(["/proj"]), path: {} }),
66
+ ).toBe(null);
67
+ });
68
+
69
+ it("works without path.resolve (falls back to String(cwd))", () => {
70
+ const noResolve = { dirname: P.dirname, join: P.join };
71
+ expect(
72
+ findGitProjectRoot("/proj/sub", {
73
+ fs: fsWithGit(["/proj"]),
74
+ path: noResolve,
75
+ }),
76
+ ).toBe("/proj");
77
+ });
78
+ });
79
+
80
+ describe("projectRootBase", () => {
81
+ it("returns the root when cwd is a strict subdirectory", () => {
82
+ expect(
83
+ projectRootBase("/proj/src", { fs: fsWithGit(["/proj"]), path: P }),
84
+ ).toBe("/proj");
85
+ });
86
+
87
+ it("returns null when cwd IS the root (own dirs already cover it)", () => {
88
+ expect(
89
+ projectRootBase("/proj", { fs: fsWithGit(["/proj"]), path: P }),
90
+ ).toBe(null);
91
+ });
92
+
93
+ it("returns null when no root is found", () => {
94
+ expect(projectRootBase("/x/y", { fs: fsWithGit([]), path: P })).toBe(null);
95
+ });
96
+ });
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * provider-stream tests (previously untested) — the token-stream parsers behind
5
+ * `cc stream` and the Hosted Session stream.run route. Pins the bug-prone
6
+ * streaming parse logic: Ollama NDJSON (yield response, stop on done, line
7
+ * buffering across chunk boundaries, skip malformed) and OpenAI-style SSE
8
+ * (yield delta.content, stop on [DONE], skip non-data/empty), error on non-ok,
9
+ * plus buildProviderSource routing (ollama defaults / known-provider endpoint +
10
+ * Authorization / unsupported / missing-key). fetch is stubbed; no network.
11
+ */
12
+
13
+ import { describe, it, expect, afterEach, vi } from "vitest";
14
+ import {
15
+ ollamaTokenStream,
16
+ openAIStream,
17
+ buildProviderSource,
18
+ } from "../provider-stream.js";
19
+
20
+ afterEach(() => {
21
+ vi.unstubAllGlobals();
22
+ vi.unstubAllEnvs();
23
+ });
24
+
25
+ /** A fake fetch whose response body streams the given string chunks. */
26
+ function mockFetch(
27
+ chunks,
28
+ { ok = true, status = 200, statusText = "OK" } = {},
29
+ ) {
30
+ return vi.fn(async () => {
31
+ const enc = new TextEncoder();
32
+ let i = 0;
33
+ return {
34
+ ok,
35
+ status,
36
+ statusText,
37
+ body: {
38
+ getReader: () => ({
39
+ read: async () =>
40
+ i < chunks.length
41
+ ? { value: enc.encode(chunks[i++]), done: false }
42
+ : { value: undefined, done: true },
43
+ }),
44
+ },
45
+ };
46
+ });
47
+ }
48
+
49
+ async function collect(gen) {
50
+ const out = [];
51
+ for await (const t of gen) out.push(t);
52
+ return out;
53
+ }
54
+
55
+ // --------------------------------------------------------------------------- //
56
+ // ollamaTokenStream
57
+ // --------------------------------------------------------------------------- //
58
+ describe("ollamaTokenStream", () => {
59
+ it("yields responses across NDJSON lines and stops on done", async () => {
60
+ vi.stubGlobal(
61
+ "fetch",
62
+ mockFetch([
63
+ '{"response":"He"}\n{"response":"llo"}\n',
64
+ '{"response":"!"}\n{"done":true}\n',
65
+ '{"response":"AFTER"}\n', // never read — returned on done
66
+ ]),
67
+ );
68
+ const out = await collect(
69
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
70
+ );
71
+ expect(out).toEqual(["He", "llo", "!"]);
72
+ });
73
+
74
+ it("buffers a JSON line split across chunk boundaries", async () => {
75
+ vi.stubGlobal(
76
+ "fetch",
77
+ mockFetch(['{"resp', 'onse":"hi"}\n{"done":true}\n']),
78
+ );
79
+ expect(
80
+ await collect(
81
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
82
+ ),
83
+ ).toEqual(["hi"]);
84
+ });
85
+
86
+ it("skips malformed JSON lines", async () => {
87
+ vi.stubGlobal(
88
+ "fetch",
89
+ mockFetch(['not json\n{"response":"ok"}\n{"done":true}\n']),
90
+ );
91
+ expect(
92
+ await collect(
93
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
94
+ ),
95
+ ).toEqual(["ok"]);
96
+ });
97
+
98
+ it("throws on a non-ok response", async () => {
99
+ vi.stubGlobal(
100
+ "fetch",
101
+ mockFetch([], { ok: false, status: 500, statusText: "Err" }),
102
+ );
103
+ await expect(
104
+ collect(
105
+ ollamaTokenStream({ baseUrl: "http://x", model: "m", prompt: "p" }),
106
+ ),
107
+ ).rejects.toThrow(/500/);
108
+ });
109
+ });
110
+
111
+ // --------------------------------------------------------------------------- //
112
+ // openAIStream
113
+ // --------------------------------------------------------------------------- //
114
+ describe("openAIStream", () => {
115
+ const args = { baseUrl: "http://x", apiKey: "k", model: "m", prompt: "p" };
116
+
117
+ it("yields delta.content from data lines and stops on [DONE]", async () => {
118
+ vi.stubGlobal(
119
+ "fetch",
120
+ mockFetch([
121
+ 'data: {"choices":[{"delta":{"content":"A"}}]}\n',
122
+ 'data: {"choices":[{"delta":{"content":"B"}}]}\ndata: [DONE]\n',
123
+ 'data: {"choices":[{"delta":{"content":"AFTER"}}]}\n',
124
+ ]),
125
+ );
126
+ expect(await collect(openAIStream(args))).toEqual(["A", "B"]);
127
+ });
128
+
129
+ it("skips non-data, empty and content-less delta lines", async () => {
130
+ vi.stubGlobal(
131
+ "fetch",
132
+ mockFetch([
133
+ ": comment\n\n",
134
+ 'data: {"choices":[{"delta":{}}]}\n', // no content
135
+ 'data: {"choices":[{"delta":{"content":"x"}}]}\ndata: [DONE]\n',
136
+ ]),
137
+ );
138
+ expect(await collect(openAIStream(args))).toEqual(["x"]);
139
+ });
140
+
141
+ it("throws on a non-ok response", async () => {
142
+ vi.stubGlobal(
143
+ "fetch",
144
+ mockFetch([], { ok: false, status: 401, statusText: "No" }),
145
+ );
146
+ await expect(collect(openAIStream(args))).rejects.toThrow(/401/);
147
+ });
148
+ });
149
+
150
+ // --------------------------------------------------------------------------- //
151
+ // buildProviderSource
152
+ // --------------------------------------------------------------------------- //
153
+ describe("buildProviderSource", () => {
154
+ it("routes ollama to /api/generate with localhost default", async () => {
155
+ const fetchMock = mockFetch(['{"done":true}\n']);
156
+ vi.stubGlobal("fetch", fetchMock);
157
+ await collect(buildProviderSource("ollama", { prompt: "hi" }));
158
+ expect(fetchMock).toHaveBeenCalledWith(
159
+ "http://localhost:11434/api/generate",
160
+ expect.objectContaining({ method: "POST" }),
161
+ );
162
+ });
163
+
164
+ it("routes a known provider to /chat/completions with a Bearer key", async () => {
165
+ const fetchMock = mockFetch(["data: [DONE]\n"]);
166
+ vi.stubGlobal("fetch", fetchMock);
167
+ await collect(
168
+ buildProviderSource("deepseek", { apiKey: "sk-x", prompt: "hi" }),
169
+ );
170
+ expect(fetchMock).toHaveBeenCalledWith(
171
+ "https://api.deepseek.com/v1/chat/completions",
172
+ expect.objectContaining({
173
+ headers: expect.objectContaining({ Authorization: "Bearer sk-x" }),
174
+ }),
175
+ );
176
+ });
177
+
178
+ it("throws for an unsupported provider", () => {
179
+ expect(() => buildProviderSource("nope", { prompt: "x" })).toThrow(
180
+ /Unsupported provider/,
181
+ );
182
+ });
183
+
184
+ it("throws when a known provider has no api key", () => {
185
+ vi.stubEnv("DEEPSEEK_API_KEY", "");
186
+ expect(() => buildProviderSource("deepseek", { prompt: "x" })).toThrow(
187
+ /API key/,
188
+ );
189
+ });
190
+ });