chainlesschain 0.162.101 → 0.162.102
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-NeD6mA1O.js → AIOps-C1k00Gsz.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Bhp7A7vO.js → ActionButton-BinI6ZDq.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DgdI84Jy.js → Analytics-Cg0jo_cd.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Qw29caa7.js → AppLayout-CClMs_AA.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-DgkzhBjz.js → Audit-BtQgvFdM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CiWEQoW5.js → Backup-43FRi7ZI.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BWNbkeDF.js → BaseInput-Cfza-FZU.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CUcj4Vyd.js → Chat-7AjHSiSC.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-bAMNCgEj.js → ChatBubbleRenderer-Bh1YNdtx.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-B0uZTW8f.js → Checkbox-DgjfqlWW.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BPk7b-Xa.js → Codegen-CkwOBThj.js} +1 -1
- package/src/assets/web-panel/assets/{Col-C2cV_dw4.js → Col-Cohk5WYM.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CDN6Yggb.js → Community-D3LMGh3F.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-C4WPtW_V.js → Compact-DqoMJmh0.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BGUfeH-3.js → Compliance-BVjWZlEj.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CsnVgF-o.js → Cowork-C0lqQPqu.js} +2 -2
- package/src/assets/web-panel/assets/{Cron--rGanASk.js → Cron-DHd_PJZS.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-D_CLrd-0.js → Crosschain-BphK2Q9u.js} +1 -1
- package/src/assets/web-panel/assets/{DID-iYkbVc8W.js → DID-CWIw0aG4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CuFMU98t.js → Dashboard-CKgDU9U6.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BPSjv_Ye.js → Dropdown-DkIlDUXF.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DBmk7czO.js → EmailListRenderer-5sXA9hwZ.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BaCArN11.js → FamilyGuardDashboard-CgcdJLH7.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsTV-8Ca.js → Federation-DPOcJISA.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-C6fRr_6F.js → FormItemContext-DxOutBMJ.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-AfuZCfyv.js → GenericCardRenderer-BYVNtkcd.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CAFIrGhb.js → Git-B37LKxOu.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C-NaxDs4.js → Governance-CnKcx5un.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BPfeNZP7.js → Inference-CbRAWN3_.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DDUBte_m.js → KnowledgeGraph-D0w6RfE_.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DWysA6JP.js → Logs-DxBDKpzq.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CAK68hXc.js → Marketplace-DNLi6O_3.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CNmmoDAk.js → McpTools-s0sBCPXw.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-D_KmFoui.js → Memory-BdSEgpmP.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CaY-6kjj.js → MobileBridge-Dz7nz4K9.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-DakdA2PC.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-YnLrtuXT.js → Mtc-Be9OBypk.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-D38v836u.js → MtcAudit-KI2ih4Tl.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-Dv3ad9S4.js → Multisig-Cg3jHFjL.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-MYTh2eWC.js → NLProgramming-BepHx8fU.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DYO-0Hva.js → Notes-2f05HyqN.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-D39gOb3h.js → NotificationSettings-Blwio_G5.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CXdO0z3I.js → OrderTableRenderer-D7HJfvo_.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DOYTwQwt.js → Organization-Uihlz1cI.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cm-go0yU.js → Overflow-gZx8kADL.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CakGtorw.js → P2P-DqEEGqba.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-BHUi1nb5.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-C_podo04.js → Permissions-ERF2ZQ3G.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-nBbehIKp.js → PersonalDataHub-CzDy9CFB.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-CGzQUoKv.js → Pipeline-Dy4x9Ejw.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuD-Dd9m.js → Privacy-C6UJys52.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CLcFmGXU.js → ProjectInit-Bmi027fa.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BxXTR0dH.js → ProjectSettings-NinSZmGA.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DkpBjAfd.js → Projects-DeW-p-hT.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DwHK42ht.js → Providers-4hxrBTLo.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-idJPKErk.js → QuickAsk-B5inZlW8.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BvJj0Nu2.js → Recommend-7lpBm00-.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-BK9l4Rcf.js → Reputation-QfdGZxYO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-rLxg7xk5.js → Row-DqHC7BCT.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Cw6ZcHBY.js → RssFeed-B3F1aGP6.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D8mKp506.js → Search-BXtjuoYO.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CXunojmQ.js → Security-Iy0MOV-A.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BE5DgmPl.js → Services-C_puMp_6.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CssL5wmE.js → Skeleton-Bg6mTVZU.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-zyS_1YrT.js → Skills-D4wXhanR.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DIVn2Roa.js → Sla-Cgr2B9R6.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CjNdZWdj.js → SpeechSettings-ctYqZEY5.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BCbu5WQT.js → SyncSettings-C3qWh9y4.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BjFrgXge.js → Tasks-D3AAgUx1.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Cl4Y3lKN.js → Templates-Bm_Dyz8T.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-k1T0F_eH.js → Tenant-DOSrOw9X.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-DrG0bEOS.js +3 -0
- package/src/assets/web-panel/assets/TimelineRenderer-BB8Qq6Cx.js +1 -0
- package/src/assets/web-panel/assets/{Tokens-BUWEiyab.js → Tokens-DqyF_Flh.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-9srMXwpo.js → Trigger-ClwQC2hP.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-hmGor_R2.js → Trust-DGQSLPQ0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Dst7K10a.js → UkeySign-BAkeYsUO.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-D_utUv1u.js → VideoEditing-WyUxS65Y.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BpYTTseI.js → Wallet-Br7GdjEg.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CEIw8QrF.js → WebAuthn-bKs1dahj.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Bkb4bUZ_.js → WorkflowEditor-CVqAALYe.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CTqDgGRH.js → chat-BDGDRbab.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CnJHVFLk.js → colors-DIUxZwT8.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-D4-v0ahl.js → compact-item-C2iM4GmN.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CYDbnHZX.js → createContext-D27NeHwO.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Daz0PHhS.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CbYwlEz5.js → hasIn-D1lXdsFj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BR4facv0.js → index-B5ZB5lsV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMKNVIqJ.js → index-B6kc4-nD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cl7-mbsb.js → index-B8POWlbO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVc_5aBF.js → index-BE9bNvdn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCQ4kcL3.js → index-BIVYPlpl.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8uhiRML.js → index-BTUFhfHs.js} +1 -1
- package/src/assets/web-panel/assets/{index-qNwkSspX.js → index-BTedVe_v.js} +1 -1
- package/src/assets/web-panel/assets/{index-OqmUv5QO.js → index-Bi8u5GsP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CP-UKwu8.js → index-Bil9t91t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfshMjFN.js → index-BqbjsJcS.js} +1 -1
- package/src/assets/web-panel/assets/{index-tmNsJzQU.js → index-CAnMUhHO.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOUSaCsn.js → index-CDzSUTpa.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dl_NdY39.js → index-CEO9vhv7.js} +3 -3
- package/src/assets/web-panel/assets/{index-DxvFrsxl.js → index-CFEwtE3q.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2fICr5d.js → index-CFY1KjB1.js} +1 -1
- package/src/assets/web-panel/assets/{index-5NXdAIZK.js → index-CKMDgjfh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BR0FfrCL.js → index-CNk98bb1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaCCLe7i.js → index-Ca8n97h9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGUOVPjO.js → index-Cb9u9MMo.js} +1 -1
- package/src/assets/web-panel/assets/index-Cbe_eJAR.js +1 -0
- package/src/assets/web-panel/assets/{index-CxKdSB-p.js → index-Cbqr8kW6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CY971z_9.js → index-CfR3Pvsu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVKfa-z4.js → index-Cgf3zMwq.js} +1 -1
- package/src/assets/web-panel/assets/{index-4767GEls.js → index-CiA-L5ld.js} +1 -1
- package/src/assets/web-panel/assets/{index-CK8nROWH.js → index-CoygRreE.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-hiXmVK.js → index-CsIw-YQF.js} +1 -1
- package/src/assets/web-panel/assets/{index-wxz2s6uW.js → index-DFhdJm7M.js} +1 -1
- package/src/assets/web-panel/assets/{index-8cmtr1jC.js → index-DK0jgmJg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BygyTsjr.js → index-Dc4k4V1u.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8MYVsVB.js → index-DfSav-yQ.js} +1 -1
- package/src/assets/web-panel/assets/index-DgcPjW1c.js +1 -0
- package/src/assets/web-panel/assets/{index-xDe5BkjU.js → index-DkOu8er7.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4chfOjw.js → index-DnCVI295.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNz3yht2.js → index-Do13fQ5W.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdRtpQFq.js → index-DrS-Mrm-.js} +1 -1
- package/src/assets/web-panel/assets/{index-B78d_-pR.js → index-ET5LYk9B.js} +1 -1
- package/src/assets/web-panel/assets/{index-YuO8gxKb.js → index-HZGu1woY.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjFQuno4.js → index-Y1rkRas5.js} +1 -1
- package/src/assets/web-panel/assets/{index-dKpwhR5x.js → index-yIM-KxVm.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-qCQgpAty.js → initDefaultProps-C0Yrc6yY.js} +1 -1
- package/src/assets/web-panel/assets/{motion-SkVSGdwu.js → motion-DnOI85ek.js} +1 -1
- package/src/assets/web-panel/assets/{move-BpWxaW_x.js → move-DT9zQPZp.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DlZWM2H4.js → omit-DFtDi5Wd.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DobtUnVe.js → pickAttrs-Cobkb_rd.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CD6mYRZi.js → placementArrow-DWQDu4i-.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DvIIMTD_.js → responsiveObserve-daoQcWcX.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CVgyUfaA.js → slide-BLkzd2OV.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DrUDnjy_.js → statusUtils-Uyxi81M0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BNYQS60m.js → styleChecker-Dtw5lUcs.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DYv7Tvk0.js → useFlexGapSupport-_JpMgpdt.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DmAD2atJ.js → useFs-huwAlPD-.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DbLVKA3I.js → usePersonalDataHub-7cafj8_f.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C3WEt6_P.js → vnode-DYUSfRt7.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CRoSX_oZ.js → zoom-B-tjvmer.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/a2a.js +2 -1
- package/src/commands/bi.js +4 -3
- package/src/commands/browse.js +3 -2
- package/src/commands/codegen.js +7 -6
- package/src/commands/collab.js +2 -1
- package/src/commands/crosschain.js +5 -4
- package/src/commands/dao.js +10 -10
- package/src/commands/dbevo.js +3 -2
- package/src/commands/economy.js +8 -6
- package/src/commands/evolution.js +10 -8
- package/src/commands/export.js +2 -1
- package/src/commands/federation.js +8 -7
- package/src/commands/fusion.js +4 -3
- package/src/commands/git.js +2 -1
- package/src/commands/governance.js +6 -5
- package/src/commands/hardening.js +3 -2
- package/src/commands/hmemory.js +2 -1
- package/src/commands/inference.js +9 -8
- package/src/commands/infra.js +9 -8
- package/src/commands/instinct.js +66 -0
- package/src/commands/learning.js +66 -0
- package/src/commands/marketplace.js +5 -4
- package/src/commands/matrix.js +2 -1
- package/src/commands/memory.js +66 -0
- package/src/commands/nlprog.js +4 -3
- package/src/commands/nostr.js +4 -3
- package/src/commands/note.js +2 -1
- package/src/commands/ops.js +3 -2
- package/src/commands/perception.js +6 -5
- package/src/commands/privacy.js +13 -12
- package/src/commands/quantization.js +3 -2
- package/src/commands/recommend.js +8 -7
- package/src/commands/reputation.js +4 -3
- package/src/commands/runtime.js +6 -5
- package/src/commands/sandbox.js +5 -4
- package/src/commands/session.js +2 -1
- package/src/commands/sla.js +6 -5
- package/src/commands/social.js +6 -5
- package/src/commands/stress.js +7 -6
- package/src/commands/sync.js +2 -1
- package/src/commands/tenant.js +3 -2
- package/src/commands/trust.js +7 -6
- package/src/commands/wallet.js +2 -1
- package/src/lib/__tests__/agents.test.js +225 -0
- package/src/lib/__tests__/cowork-observe-html.test.js +141 -0
- package/src/lib/__tests__/logger.test.js +110 -0
- package/src/lib/__tests__/project-inventory.test.js +280 -0
- package/src/lib/__tests__/project-root.test.js +96 -0
- package/src/lib/__tests__/provider-stream.test.js +190 -0
- package/src/lib/cli-arg.js +41 -0
- package/src/lib/cli-numeric.js +12 -3
- package/src/lib/instinct-manager.js +64 -0
- package/src/lib/learning/learning-tables.js +83 -0
- package/src/lib/mcp-oauth.js +10 -2
- package/src/lib/memory-manager.js +67 -0
- package/src/lib/plan-mode.js +3 -2
- package/src/repl/__tests__/mcp-prompt.test.js +155 -0
- package/src/repl/__tests__/slash-macro.test.js +81 -0
- package/src/runtime/__tests__/coding-agent-contract-shared.test.js +136 -0
- package/src/runtime/__tests__/coding-agent-events.test.js +263 -0
- package/src/assets/web-panel/assets/MobileProjects-DhDxrWze.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-BlmbHJnb.js +0 -7
- package/src/assets/web-panel/assets/Terminal-i0tHF8fy.js +0 -3
- package/src/assets/web-panel/assets/TimelineRenderer-FmPaNyqK.js +0 -1
- package/src/assets/web-panel/assets/devWarning-OhxeajH6.js +0 -1
- package/src/assets/web-panel/assets/index-Bbtoi_47.js +0 -1
- package/src/assets/web-panel/assets/index-Cb8RWwaB.js +0 -1
|
@@ -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
|
+
"",
|
|
90
|
+
"[](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
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-arg — validating commander option coercers.
|
|
3
|
+
*
|
|
4
|
+
* Bare `parseInt` / `parseFloat` / `Number` used as a commander coercer
|
|
5
|
+
* (`.option("--limit <n>", "...", parseInt)`) silently yield NaN on bad input
|
|
6
|
+
* (`--limit abc`), threading NaN into whatever the action does with the option.
|
|
7
|
+
* These factories validate at parse time and raise a commander
|
|
8
|
+
* `InvalidArgumentError`, so a typo fails with the standard
|
|
9
|
+
* `error: option '--limit <n>' argument 'abc' is invalid` message and exit 1 —
|
|
10
|
+
* instead of a silently-corrupt run.
|
|
11
|
+
*
|
|
12
|
+
* Drop-in replacements:
|
|
13
|
+
* .option("--limit <n>", "...", parseInt) -> intArg("--limit")
|
|
14
|
+
* .option("--rate <n>", "...", parseFloat) -> floatArg("--rate")
|
|
15
|
+
* .option("--eps <n>", "...", Number) -> floatArg("--eps")
|
|
16
|
+
*
|
|
17
|
+
* Behaviour for valid and omitted values is identical to the bare coercer
|
|
18
|
+
* (the coercer only runs when the option is supplied with a value); only the
|
|
19
|
+
* NaN-on-bad-input case changes.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} name option label for the message (e.g. "--limit")
|
|
22
|
+
* @param {{min?: number, max?: number}} [opts]
|
|
23
|
+
*/
|
|
24
|
+
import { InvalidArgumentError } from "commander";
|
|
25
|
+
import { numericOption } from "./cli-numeric.js";
|
|
26
|
+
|
|
27
|
+
function makeArg(integer) {
|
|
28
|
+
return (name, opts = {}) =>
|
|
29
|
+
(raw) => {
|
|
30
|
+
try {
|
|
31
|
+
return numericOption(raw, { ...opts, name, integer });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new InvalidArgumentError(err.message);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const intArg = makeArg(true);
|
|
39
|
+
export const floatArg = makeArg(false);
|
|
40
|
+
|
|
41
|
+
export default { intArg, floatArg };
|
package/src/lib/cli-numeric.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* @param {*} raw the option value (string from commander, or undefined)
|
|
11
11
|
* @param {object} [opts]
|
|
12
12
|
* @param {string} [opts.name="value"] label for error messages (e.g. "--limit")
|
|
13
|
-
* @param {boolean} [opts.integer=false]
|
|
13
|
+
* @param {boolean} [opts.integer=false] require a whole number (rejects 1.5);
|
|
14
|
+
* parsing is strict either way (Number,
|
|
15
|
+
* not parseInt — "12abc" is rejected)
|
|
14
16
|
* @param {number} [opts.min] inclusive lower bound
|
|
15
17
|
* @param {number} [opts.max] inclusive upper bound
|
|
16
18
|
* @param {number} [opts.fallback] returned for missing/invalid input; when
|
|
@@ -26,8 +28,15 @@ export function numericOption(raw, opts = {}) {
|
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
if (raw == null || raw === "") return fail(`${name} is required`);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if (typeof raw === "string" && raw.trim() === "")
|
|
32
|
+
return fail(`${name} is required`);
|
|
33
|
+
// Number() (NOT parseInt) so trailing junk is rejected instead of silently
|
|
34
|
+
// truncated: parseInt("12abc") === 12 and parseInt("0x10", 10) === 0 would
|
|
35
|
+
// slip a corrupt value through. Number("12abc") is NaN; whole numbers given
|
|
36
|
+
// via exponent ("1e3" -> 1000) still parse, and a fractional value is
|
|
37
|
+
// rejected in integer mode.
|
|
38
|
+
const n = Number(raw);
|
|
39
|
+
if (!Number.isFinite(n) || (integer && !Number.isInteger(n))) {
|
|
31
40
|
return fail(
|
|
32
41
|
`${name} must be a ${integer ? "whole " : ""}number (got ${JSON.stringify(String(raw))})`,
|
|
33
42
|
);
|