onbuzz 3.4.0 → 3.6.2

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 (562) hide show
  1. package/package.json +1 -1
  2. package/scripts/bump-version.js +116 -0
  3. package/src/__test-utils__/fixtures/malformedJson.js +31 -0
  4. package/src/__test-utils__/globalSetup.js +9 -0
  5. package/src/__test-utils__/globalTeardown.js +12 -0
  6. package/src/__test-utils__/mockFactories.js +101 -0
  7. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
  8. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
  9. package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
  10. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
  11. package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
  12. package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
  13. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
  14. package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
  15. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
  16. package/src/core/__tests__/agentPool.test.js +601 -0
  17. package/src/core/__tests__/agentScheduler.test.js +576 -0
  18. package/src/core/__tests__/contextManager.test.js +252 -0
  19. package/src/core/__tests__/flowExecutor.test.js +262 -0
  20. package/src/core/__tests__/messageProcessor.test.js +627 -0
  21. package/src/core/__tests__/orchestrator.test.js +257 -0
  22. package/src/core/__tests__/stateManager.test.js +375 -0
  23. package/src/core/agentPool.js +26 -4
  24. package/src/core/agentScheduler.js +79 -21
  25. package/src/core/messageProcessor.js +110 -2
  26. package/src/index.js +27 -11
  27. package/src/interfaces/__tests__/imageServing.test.js +228 -0
  28. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
  29. package/src/interfaces/webServer.js +97 -13
  30. package/src/services/__tests__/agentActivityService.test.js +319 -0
  31. package/src/services/__tests__/apiKeyManager.test.js +206 -0
  32. package/src/services/__tests__/benchmarkService.test.js +184 -0
  33. package/src/services/__tests__/budgetService.test.js +211 -0
  34. package/src/services/__tests__/contextInjectionService.test.js +205 -0
  35. package/src/services/__tests__/conversationCompactionService.test.js +280 -0
  36. package/src/services/__tests__/credentialVault.test.js +469 -0
  37. package/src/services/__tests__/errorHandler.test.js +314 -0
  38. package/src/services/__tests__/fileAttachmentService.test.js +278 -0
  39. package/src/services/__tests__/flowContextService.test.js +199 -0
  40. package/src/services/__tests__/memoryService.test.js +450 -0
  41. package/src/services/__tests__/modelRouterService.test.js +388 -0
  42. package/src/services/__tests__/modelsService.test.js +261 -0
  43. package/src/services/__tests__/portRegistry.test.js +123 -0
  44. package/src/services/__tests__/projectDetector.test.js +34 -0
  45. package/src/services/__tests__/promptService.test.js +242 -0
  46. package/src/services/__tests__/qualityInspector.test.js +97 -0
  47. package/src/services/__tests__/scheduleService.test.js +308 -0
  48. package/src/services/__tests__/serviceRegistry.test.js +74 -0
  49. package/src/services/__tests__/skillsService.test.js +402 -0
  50. package/src/services/__tests__/tokenCountingService.test.js +48 -0
  51. package/src/services/conversationCompactionService.js +2 -2
  52. package/src/services/visualEditorServer.js +26 -7
  53. package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
  54. package/src/tools/__tests__/agentDelayTool.test.js +342 -0
  55. package/src/tools/__tests__/asyncToolManager.test.js +344 -0
  56. package/src/tools/__tests__/baseTool.test.js +420 -0
  57. package/src/tools/__tests__/codeMapTool.test.js +348 -0
  58. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
  59. package/src/tools/__tests__/fileTreeTool.test.js +274 -0
  60. package/src/tools/__tests__/filesystemTool.test.js +717 -0
  61. package/src/tools/__tests__/helpTool.test.js +204 -0
  62. package/src/tools/__tests__/jobDoneTool.test.js +296 -0
  63. package/src/tools/__tests__/memoryTool.test.js +297 -0
  64. package/src/tools/__tests__/seekTool.test.js +282 -0
  65. package/src/tools/__tests__/skillsTool.test.js +226 -0
  66. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
  67. package/src/tools/__tests__/taskManagerTool.test.js +725 -0
  68. package/src/tools/__tests__/terminalTool.test.js +384 -0
  69. package/src/tools/__tests__/userPromptTool.test.js +297 -0
  70. package/src/tools/__tests__/webTool.e2e.test.js +25 -11
  71. package/src/tools/imageTool.js +41 -5
  72. package/src/tools/webTool.js +161 -48
  73. package/src/types/__tests__/agent.test.js +499 -0
  74. package/src/types/__tests__/contextReference.test.js +606 -0
  75. package/src/types/__tests__/conversation.test.js +555 -0
  76. package/src/types/__tests__/toolCommand.test.js +584 -0
  77. package/src/types/contextReference.js +1 -1
  78. package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
  79. package/src/utilities/__tests__/configManager.test.js +397 -0
  80. package/src/utilities/__tests__/constants.test.js +49 -0
  81. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
  82. package/src/utilities/__tests__/fileProcessor.test.js +104 -0
  83. package/src/utilities/__tests__/jsonRepair.test.js +104 -0
  84. package/src/utilities/__tests__/logger.test.js +129 -0
  85. package/src/utilities/__tests__/platformUtils.test.js +87 -0
  86. package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
  87. package/src/utilities/__tests__/tagParser.test.js +887 -0
  88. package/src/utilities/__tests__/toolConstants.test.js +94 -0
  89. package/src/utilities/tagParser.js +2 -2
  90. package/web-ui/build/index.html +2 -2
  91. package/web-ui/build/static/1c-8PZzOTzp.js +1 -0
  92. package/web-ui/build/static/abap-Bcx_Au1F.js +1 -0
  93. package/web-ui/build/static/abnf-BKTLqpWA.js +1 -0
  94. package/web-ui/build/static/abnf-J05BAvJt.js +1 -0
  95. package/web-ui/build/static/accesslog-Cp8_lqVY.js +1 -0
  96. package/web-ui/build/static/actionscript-BK0UaMrm.js +1 -0
  97. package/web-ui/build/static/actionscript-CyqZUddh.js +1 -0
  98. package/web-ui/build/static/ada-BNirS6Nr.js +1 -0
  99. package/web-ui/build/static/ada-BSFWcT1O.js +1 -0
  100. package/web-ui/build/static/agda-D0NJDJg7.js +1 -0
  101. package/web-ui/build/static/al-rWARKtwb.js +1 -0
  102. package/web-ui/build/static/angelscript-fCehtOYk.js +1 -0
  103. package/web-ui/build/static/antlr4-Dn9jrnZN.js +1 -0
  104. package/web-ui/build/static/apache-DaQCsvNW.js +1 -0
  105. package/web-ui/build/static/apacheconf-dY4i0Xvz.js +1 -0
  106. package/web-ui/build/static/apex-vhS4SI46.js +1 -0
  107. package/web-ui/build/static/apl-CKRkxH90.js +1 -0
  108. package/web-ui/build/static/applescript-CWmpQIEB.js +1 -0
  109. package/web-ui/build/static/applescript-DBaX7Uqo.js +1 -0
  110. package/web-ui/build/static/aql-8s41lrIa.js +1 -0
  111. package/web-ui/build/static/arcade-w2_RhAcq.js +1 -0
  112. package/web-ui/build/static/arduino-I7BtZTu6.js +1 -0
  113. package/web-ui/build/static/arduino-h2LZErKQ.js +1 -0
  114. package/web-ui/build/static/arff-C543-5a1.js +1 -0
  115. package/web-ui/build/static/armasm-DyZdFOzz.js +1 -0
  116. package/web-ui/build/static/asciidoc-ZzENlACu.js +1 -0
  117. package/web-ui/build/static/asciidoc-_j9x9bUz.js +1 -0
  118. package/web-ui/build/static/asm6502-CsNsmBfq.js +1 -0
  119. package/web-ui/build/static/asmatmel-CkIVf_tD.js +1 -0
  120. package/web-ui/build/static/aspectj-C6AQLme_.js +1 -0
  121. package/web-ui/build/static/aspnet-5AkdiVyL.js +1 -0
  122. package/web-ui/build/static/autohotkey-BRZVABiS.js +1 -0
  123. package/web-ui/build/static/autohotkey-DVTmfk_f.js +1 -0
  124. package/web-ui/build/static/autoit-3UEcWu5a.js +1 -0
  125. package/web-ui/build/static/autoit-BDByIKSH.js +1 -0
  126. package/web-ui/build/static/avisynth-BHc4uUkP.js +1 -0
  127. package/web-ui/build/static/avrasm-BAPq8_aI.js +1 -0
  128. package/web-ui/build/static/avro-idl-BKEBYUtv.js +1 -0
  129. package/web-ui/build/static/awk-CBCkArRT.js +1 -0
  130. package/web-ui/build/static/axapta-DlOgnXSZ.js +1 -0
  131. package/web-ui/build/static/bash-C6Brp5OE.js +1 -0
  132. package/web-ui/build/static/bash-DkEO7JRq.js +1 -0
  133. package/web-ui/build/static/basic-DG6TYB0R.js +1 -0
  134. package/web-ui/build/static/basic-DRPcNfAn.js +1 -0
  135. package/web-ui/build/static/batch-DdjZ5KC1.js +1 -0
  136. package/web-ui/build/static/bbcode-DCXEEs2w.js +1 -0
  137. package/web-ui/build/static/bicep-CpLhfOwt.js +1 -0
  138. package/web-ui/build/static/birb-DNWkqgQm.js +1 -0
  139. package/web-ui/build/static/bison-DwxbQHJ9.js +1 -0
  140. package/web-ui/build/static/bnf-Cgnt7npj.js +1 -0
  141. package/web-ui/build/static/bnf-DSTq_eu9.js +1 -0
  142. package/web-ui/build/static/brainfuck-Bi8mGutW.js +1 -0
  143. package/web-ui/build/static/brainfuck-DOWfqVtR.js +1 -0
  144. package/web-ui/build/static/brightscript-D95pbP-v.js +1 -0
  145. package/web-ui/build/static/bro-BrDVwXeg.js +1 -0
  146. package/web-ui/build/static/bsl-BMoXI84g.js +1 -0
  147. package/web-ui/build/static/c-CKH4C7-Z.js +1 -0
  148. package/web-ui/build/static/c-Z0txyaeJ.js +1 -0
  149. package/web-ui/build/static/c-like-Dzm9dMmR.js +1 -0
  150. package/web-ui/build/static/cal-DoyAwiUt.js +1 -0
  151. package/web-ui/build/static/capnproto-DeIi9LOH.js +1 -0
  152. package/web-ui/build/static/ceylon-Coim6DIe.js +1 -0
  153. package/web-ui/build/static/cfscript-CwsndC-j.js +1 -0
  154. package/web-ui/build/static/chaiscript-D6Aq-PSv.js +1 -0
  155. package/web-ui/build/static/cil-vi56VRk_.js +1 -0
  156. package/web-ui/build/static/clean-BfpKrTdp.js +1 -0
  157. package/web-ui/build/static/clojure-DUtl6BaB.js +1 -0
  158. package/web-ui/build/static/clojure-DXJHtDlY.js +1 -0
  159. package/web-ui/build/static/clojure-repl-BxwP5C3g.js +1 -0
  160. package/web-ui/build/static/cmake-C9_VZ1vH.js +1 -0
  161. package/web-ui/build/static/cmake-dplO-PGD.js +1 -0
  162. package/web-ui/build/static/cobol-DsZhu02V.js +1 -0
  163. package/web-ui/build/static/coffeescript-Cw9jtGNP.js +1 -0
  164. package/web-ui/build/static/coffeescript-DvDt4T2l.js +1 -0
  165. package/web-ui/build/static/concurnas-Bzc_Dcdd.js +1 -0
  166. package/web-ui/build/static/coq-Cv-5BqGo.js +1 -0
  167. package/web-ui/build/static/coq-DWFe2ssK.js +1 -0
  168. package/web-ui/build/static/cos-D6Lc6Cah.js +1 -0
  169. package/web-ui/build/static/cpp-BFmLjd76.js +1 -0
  170. package/web-ui/build/static/cpp-DVQgbHji.js +1 -0
  171. package/web-ui/build/static/crmsh-Cqveth9p.js +1 -0
  172. package/web-ui/build/static/crystal-0syYaH4Y.js +1 -0
  173. package/web-ui/build/static/crystal-Noptp-kr.js +1 -0
  174. package/web-ui/build/static/csharp-B799cFMH.js +1 -0
  175. package/web-ui/build/static/csharp-_HlvMZzJ.js +1 -0
  176. package/web-ui/build/static/cshtml-CnwOXlhP.js +1 -0
  177. package/web-ui/build/static/csp-1ffxIG_-.js +1 -0
  178. package/web-ui/build/static/csp-Bws60bPu.js +1 -0
  179. package/web-ui/build/static/css-BGdwXzpm.js +1 -0
  180. package/web-ui/build/static/css-extras-DZCECiOa.js +1 -0
  181. package/web-ui/build/static/csv-FMFGT0T4.js +1 -0
  182. package/web-ui/build/static/cypher-DnXoEwRp.js +1 -0
  183. package/web-ui/build/static/d-CBrts1xB.js +1 -0
  184. package/web-ui/build/static/d-qrJLxk2L.js +1 -0
  185. package/web-ui/build/static/dart-3vBSXJVV.js +1 -0
  186. package/web-ui/build/static/dart-Cj5b7BV9.js +1 -0
  187. package/web-ui/build/static/dataweave-BuFf63rk.js +1 -0
  188. package/web-ui/build/static/dax-Cl-se1JI.js +1 -0
  189. package/web-ui/build/static/delphi-CLkRb26y.js +1 -0
  190. package/web-ui/build/static/dhall--TIL2Z--.js +1 -0
  191. package/web-ui/build/static/diff-Bn-XL2om.js +1 -0
  192. package/web-ui/build/static/diff-BsTwly4w.js +1 -0
  193. package/web-ui/build/static/django-BfRtHnTS.js +1 -0
  194. package/web-ui/build/static/django-Dm9O4e3A.js +1 -0
  195. package/web-ui/build/static/dns-BIVEp3uD.js +1 -0
  196. package/web-ui/build/static/dns-zone-file-CO7LnOdh.js +1 -0
  197. package/web-ui/build/static/docker-BhwMip1R.js +1 -0
  198. package/web-ui/build/static/dockerfile-8Tjw9_jF.js +1 -0
  199. package/web-ui/build/static/dos-CRMiAo46.js +1 -0
  200. package/web-ui/build/static/dot-DPpW7LrJ.js +1 -0
  201. package/web-ui/build/static/dsconfig-D0zbYilI.js +1 -0
  202. package/web-ui/build/static/dts-C_-yqWEL.js +1 -0
  203. package/web-ui/build/static/dust-CucJgqnE.js +1 -0
  204. package/web-ui/build/static/ebnf-CCHK0H6j.js +1 -0
  205. package/web-ui/build/static/ebnf-CDdAcveH.js +1 -0
  206. package/web-ui/build/static/editorconfig-f-5b95UM.js +1 -0
  207. package/web-ui/build/static/eiffel-MmghFce7.js +1 -0
  208. package/web-ui/build/static/ejs-CPMN4-jk.js +1 -0
  209. package/web-ui/build/static/elixir-57Ldyw8h.js +1 -0
  210. package/web-ui/build/static/elixir-DTxnmhIt.js +1 -0
  211. package/web-ui/build/static/elm-ClV9zQoT.js +1 -0
  212. package/web-ui/build/static/elm-pX-i6o7U.js +1 -0
  213. package/web-ui/build/static/erb-BPeO9smT.js +1 -0
  214. package/web-ui/build/static/erb-BnZQ4STz.js +1 -0
  215. package/web-ui/build/static/erlang-DPtI7VK_.js +1 -0
  216. package/web-ui/build/static/erlang-KG5mg5wN.js +1 -0
  217. package/web-ui/build/static/erlang-repl-BqPVXZ3Y.js +1 -0
  218. package/web-ui/build/static/etlua-BRc0Qbbs.js +1 -0
  219. package/web-ui/build/static/excel-BAlZ9Hkj.js +1 -0
  220. package/web-ui/build/static/excel-formula-BOW-bnHh.js +1 -0
  221. package/web-ui/build/static/factor-DCCsCpGM.js +1 -0
  222. package/web-ui/build/static/false-CnqnCzBU.js +1 -0
  223. package/web-ui/build/static/firestore-security-rules-DtkQ3uEq.js +1 -0
  224. package/web-ui/build/static/fix-CfPjl4Xr.js +1 -0
  225. package/web-ui/build/static/flix-BCA3BceS.js +1 -0
  226. package/web-ui/build/static/flow-C-5ewqYW.js +1 -0
  227. package/web-ui/build/static/fortran-CfDtl8An.js +1 -0
  228. package/web-ui/build/static/fortran-DQ_knNPt.js +1 -0
  229. package/web-ui/build/static/fsharp-CcVQ3IqX.js +1 -0
  230. package/web-ui/build/static/fsharp-olQ6ojCa.js +1 -0
  231. package/web-ui/build/static/ftl-DIWHDyWt.js +1 -0
  232. package/web-ui/build/static/gams-_NVFTSj5.js +1 -0
  233. package/web-ui/build/static/gap-DIG5TV2b.js +1 -0
  234. package/web-ui/build/static/gauss-C8rjPjTG.js +1 -0
  235. package/web-ui/build/static/gcode-8wJu4gcL.js +1 -0
  236. package/web-ui/build/static/gcode-DjHf417I.js +1 -0
  237. package/web-ui/build/static/gdscript-BZdmRCYu.js +1 -0
  238. package/web-ui/build/static/gedcom-BVFJ8C_Q.js +1 -0
  239. package/web-ui/build/static/gherkin-7NQkoaub.js +1 -0
  240. package/web-ui/build/static/gherkin-bSpNbJ48.js +1 -0
  241. package/web-ui/build/static/git-BRY_UXsc.js +1 -0
  242. package/web-ui/build/static/glsl-Ck6ShGRC.js +1 -0
  243. package/web-ui/build/static/glsl-jwCJ0Pmg.js +1 -0
  244. package/web-ui/build/static/gml-BAjG4Kr2.js +1 -0
  245. package/web-ui/build/static/gml-BnhKb5Da.js +1 -0
  246. package/web-ui/build/static/gn-Dux09iX8.js +1 -0
  247. package/web-ui/build/static/go-BOG-9Cqk.js +1 -0
  248. package/web-ui/build/static/go-BWZB_3b5.js +1 -0
  249. package/web-ui/build/static/go-module-BVLW7KBE.js +1 -0
  250. package/web-ui/build/static/golo-BD_SOfwL.js +1 -0
  251. package/web-ui/build/static/gradle-zSadWOD2.js +1 -0
  252. package/web-ui/build/static/graphql-BQlyj78B.js +1 -0
  253. package/web-ui/build/static/groovy-BurRMqQS.js +1 -0
  254. package/web-ui/build/static/groovy-SP58zwlE.js +1 -0
  255. package/web-ui/build/static/haml-eZ5ah5KY.js +1 -0
  256. package/web-ui/build/static/haml-lFC47IZb.js +1 -0
  257. package/web-ui/build/static/handlebars-B4UXrB-Q.js +1 -0
  258. package/web-ui/build/static/handlebars-CQ-Q5HnC.js +1 -0
  259. package/web-ui/build/static/haskell-AQrRyTSy.js +1 -0
  260. package/web-ui/build/static/haskell-BZTSbaB_.js +1 -0
  261. package/web-ui/build/static/haxe-5l1X6ESp.js +1 -0
  262. package/web-ui/build/static/haxe-DBn90muG.js +1 -0
  263. package/web-ui/build/static/hcl-CnMewPLM.js +1 -0
  264. package/web-ui/build/static/hlsl-RAtuBzr5.js +1 -0
  265. package/web-ui/build/static/hoon-CSqRU_4M.js +1 -0
  266. package/web-ui/build/static/hpkp-_gNbXcHt.js +1 -0
  267. package/web-ui/build/static/hsp-DY1V4au3.js +1 -0
  268. package/web-ui/build/static/hsts-j5z2jJo8.js +1 -0
  269. package/web-ui/build/static/htmlbars-C5EHvatr.js +1 -0
  270. package/web-ui/build/static/http-DgWgQrZh.js +1 -0
  271. package/web-ui/build/static/http-DphJL0q2.js +1 -0
  272. package/web-ui/build/static/hy-DnBqjPsB.js +1 -0
  273. package/web-ui/build/static/ichigojam-DeiCOKyF.js +1 -0
  274. package/web-ui/build/static/icon-CWANFWY5.js +1 -0
  275. package/web-ui/build/static/icu-message-format-C7w3vpgC.js +1 -0
  276. package/web-ui/build/static/idris-BeD8eULz.js +1 -0
  277. package/web-ui/build/static/iecst-BIznHXqY.js +1 -0
  278. package/web-ui/build/static/ignore-BcFgcNaS.js +1 -0
  279. package/web-ui/build/static/index-D8uVofpo.js +13 -0
  280. package/web-ui/build/static/index-DPFadqM6.css +1 -0
  281. package/web-ui/build/static/index-SkOgWEAU.js +1 -0
  282. package/web-ui/build/static/index-Vd3WlhtC.js +747 -0
  283. package/web-ui/build/static/inform7-CkQD_jz-.js +1 -0
  284. package/web-ui/build/static/inform7-phQiuDty.js +1 -0
  285. package/web-ui/build/static/ini-Bw_QAbzV.js +1 -0
  286. package/web-ui/build/static/ini-CB8ZxX7y.js +1 -0
  287. package/web-ui/build/static/io-D6IgpCmL.js +1 -0
  288. package/web-ui/build/static/irpf90-Ctj0koST.js +1 -0
  289. package/web-ui/build/static/isbl-D2mGcH87.js +1 -0
  290. package/web-ui/build/static/j-KvHmDBWH.js +1 -0
  291. package/web-ui/build/static/java-BeBIdo5i.js +1 -0
  292. package/web-ui/build/static/java-llFZkHLe.js +1 -0
  293. package/web-ui/build/static/javadoc-DXvQGu0s.js +1 -0
  294. package/web-ui/build/static/javadoclike-B5qdA9KZ.js +1 -0
  295. package/web-ui/build/static/javascript-De6HzHvc.js +1 -0
  296. package/web-ui/build/static/javastacktrace-C5MolKiP.js +1 -0
  297. package/web-ui/build/static/jboss-cli-2TXd54zo.js +1 -0
  298. package/web-ui/build/static/jexl-W4AVA9fi.js +1 -0
  299. package/web-ui/build/static/jolie-DTJKRMZN.js +1 -0
  300. package/web-ui/build/static/jq-BYg-QQKh.js +1 -0
  301. package/web-ui/build/static/js-extras-BrYWd2VE.js +1 -0
  302. package/web-ui/build/static/js-templates-DorYpbHq.js +1 -0
  303. package/web-ui/build/static/jsdoc-CRF8n9pZ.js +1 -0
  304. package/web-ui/build/static/json-Dlcd7rla.js +1 -0
  305. package/web-ui/build/static/json-rhOJZzzZ.js +1 -0
  306. package/web-ui/build/static/json5-hTq1nNIW.js +1 -0
  307. package/web-ui/build/static/jsonp-CMg9Xvol.js +1 -0
  308. package/web-ui/build/static/jsstacktrace-hEeYxHtB.js +1 -0
  309. package/web-ui/build/static/jsx-B7PtA8WD.js +1 -0
  310. package/web-ui/build/static/julia-CNiEEY-n.js +1 -0
  311. package/web-ui/build/static/julia-eE0_SJlc.js +1 -0
  312. package/web-ui/build/static/julia-repl-CUJTTT4C.js +1 -0
  313. package/web-ui/build/static/keepalived-GUWJBqyD.js +1 -0
  314. package/web-ui/build/static/keyman-Bdf9MJyu.js +1 -0
  315. package/web-ui/build/static/kotlin-Ch6Bej5W.js +1 -0
  316. package/web-ui/build/static/kotlin-DFJ7D7Lw.js +1 -0
  317. package/web-ui/build/static/kumir-DJLIjcCs.js +1 -0
  318. package/web-ui/build/static/kusto-BM0YTwU4.js +1 -0
  319. package/web-ui/build/static/lasso-Z1DVS84K.js +1 -0
  320. package/web-ui/build/static/latex-BWbw71RK.js +1 -0
  321. package/web-ui/build/static/latex-CMzqmbhw.js +1 -0
  322. package/web-ui/build/static/latte-C1g8_grc.js +1 -0
  323. package/web-ui/build/static/ldif-C6QH3OIL.js +1 -0
  324. package/web-ui/build/static/leaf-CbR--ZsH.js +1 -0
  325. package/web-ui/build/static/less-ClVrKh2Z.js +1 -0
  326. package/web-ui/build/static/less-DNSxm8UA.js +1 -0
  327. package/web-ui/build/static/lilypond-lTzf_sWt.js +1 -0
  328. package/web-ui/build/static/liquid-Bn91mVfC.js +1 -0
  329. package/web-ui/build/static/lisp-CU3bHohQ.js +1 -0
  330. package/web-ui/build/static/lisp-DbgzE9W8.js +1 -0
  331. package/web-ui/build/static/livecodeserver-Cse1Uz3H.js +1 -0
  332. package/web-ui/build/static/livescript-BaxbgzWP.js +1 -0
  333. package/web-ui/build/static/livescript-nJt61DBy.js +1 -0
  334. package/web-ui/build/static/llvm-DBboo6UI.js +1 -0
  335. package/web-ui/build/static/llvm-vIy7XYVy.js +1 -0
  336. package/web-ui/build/static/log-CT7nfoDW.js +1 -0
  337. package/web-ui/build/static/lolcode-CUKVytZh.js +1 -0
  338. package/web-ui/build/static/lsl-CsAOlGF2.js +1 -0
  339. package/web-ui/build/static/lua-BuU2FFxP.js +1 -0
  340. package/web-ui/build/static/lua-CiDuKQaa.js +1 -0
  341. package/web-ui/build/static/magma-7vR0zcmS.js +1 -0
  342. package/web-ui/build/static/makefile-Buiz-Msh.js +1 -0
  343. package/web-ui/build/static/makefile-DXW_-6OY.js +1 -0
  344. package/web-ui/build/static/markdown-Bk5DUoGY.js +1 -0
  345. package/web-ui/build/static/markdown-CRS5W_Ai.js +1 -0
  346. package/web-ui/build/static/markup-templating-24odpmF3.js +1 -0
  347. package/web-ui/build/static/mathematica-BxcwhJUp.js +1 -0
  348. package/web-ui/build/static/matlab-3pJYx_Fb.js +1 -0
  349. package/web-ui/build/static/matlab-BqlRrzMf.js +1 -0
  350. package/web-ui/build/static/maxima-DlCfUpcj.js +1 -0
  351. package/web-ui/build/static/maxscript-Cu_gCaFU.js +1 -0
  352. package/web-ui/build/static/mel-D7iQ-5Up.js +1 -0
  353. package/web-ui/build/static/mel-DzBKNpoN.js +1 -0
  354. package/web-ui/build/static/mercury-Dfrb-i8A.js +1 -0
  355. package/web-ui/build/static/mermaid-WN7V2_Eq.js +1 -0
  356. package/web-ui/build/static/mipsasm-CcijzL0q.js +1 -0
  357. package/web-ui/build/static/mizar-Bk68zACP.js +1 -0
  358. package/web-ui/build/static/mizar-Twc2-iZ4.js +1 -0
  359. package/web-ui/build/static/mojolicious-DBbo2S7X.js +1 -0
  360. package/web-ui/build/static/mongodb-2RsFIjgg.js +1 -0
  361. package/web-ui/build/static/monkey-CPXtQ0Bf.js +1 -0
  362. package/web-ui/build/static/monkey-DjV7Wcek.js +1 -0
  363. package/web-ui/build/static/moonscript-B5M5as70.js +1 -0
  364. package/web-ui/build/static/moonscript-D1BHW4Il.js +1 -0
  365. package/web-ui/build/static/n1ql-D0heNDBD.js +1 -0
  366. package/web-ui/build/static/n1ql-DfHqXQD7.js +1 -0
  367. package/web-ui/build/static/n4js-CaPf44Dz.js +1 -0
  368. package/web-ui/build/static/nand2tetris-hdl-D1nf9mn4.js +1 -0
  369. package/web-ui/build/static/naniscript-DnCnu5ZX.js +1 -0
  370. package/web-ui/build/static/nasm-BZrSaMsK.js +1 -0
  371. package/web-ui/build/static/neon-D29Grm2v.js +1 -0
  372. package/web-ui/build/static/nevod-DgSNbQkE.js +1 -0
  373. package/web-ui/build/static/nginx-BAaDGDfT.js +1 -0
  374. package/web-ui/build/static/nginx-BvJ1lrHX.js +1 -0
  375. package/web-ui/build/static/nim--9zzVe5F.js +1 -0
  376. package/web-ui/build/static/nim-Br1relpU.js +1 -0
  377. package/web-ui/build/static/nix--0ftErCy.js +1 -0
  378. package/web-ui/build/static/nix-104ztQqr.js +1 -0
  379. package/web-ui/build/static/node-repl-BUMAf7_p.js +1 -0
  380. package/web-ui/build/static/nsis-BaeKybNA.js +1 -0
  381. package/web-ui/build/static/nsis-CdZEv2iA.js +1 -0
  382. package/web-ui/build/static/objectivec-DBB4ymdg.js +1 -0
  383. package/web-ui/build/static/objectivec-kFYXC6g4.js +1 -0
  384. package/web-ui/build/static/ocaml-D1GXvN7c.js +1 -0
  385. package/web-ui/build/static/ocaml-D80jRMPE.js +1 -0
  386. package/web-ui/build/static/opencl-fb7BfRdO.js +1 -0
  387. package/web-ui/build/static/openqasm-CWUBrR2w.js +1 -0
  388. package/web-ui/build/static/openscad-Dim7ILSL.js +1 -0
  389. package/web-ui/build/static/oxygene-BSwApkwz.js +1 -0
  390. package/web-ui/build/static/oz-CMtRoi5F.js +1 -0
  391. package/web-ui/build/static/parigp-AH8cZ38D.js +1 -0
  392. package/web-ui/build/static/parser-bBNjuhG3.js +1 -0
  393. package/web-ui/build/static/parser3-DUtoWEAd.js +1 -0
  394. package/web-ui/build/static/pascal-Cr3DPIYT.js +1 -0
  395. package/web-ui/build/static/pascaligo-pWW12jfs.js +1 -0
  396. package/web-ui/build/static/pcaxis-DBw9rxmr.js +1 -0
  397. package/web-ui/build/static/peoplecode-aCpMPm_s.js +1 -0
  398. package/web-ui/build/static/perl-BpZ7GmJ3.js +1 -0
  399. package/web-ui/build/static/perl-fnHTrqJL.js +1 -0
  400. package/web-ui/build/static/pf-Dz352ty7.js +1 -0
  401. package/web-ui/build/static/pgsql-CHPUdlI_.js +1 -0
  402. package/web-ui/build/static/php-BRwItjmS.js +1 -0
  403. package/web-ui/build/static/php-CrX_kswO.js +1 -0
  404. package/web-ui/build/static/php-extras-BmeRXDSO.js +1 -0
  405. package/web-ui/build/static/php-template-B0MFJ9RR.js +1 -0
  406. package/web-ui/build/static/phpdoc-wAPkJj9X.js +1 -0
  407. package/web-ui/build/static/plaintext-CmPk1gvP.js +1 -0
  408. package/web-ui/build/static/plsql-pWVw0sCJ.js +1 -0
  409. package/web-ui/build/static/pony-B4SXhyDS.js +1 -0
  410. package/web-ui/build/static/powerquery-ZJ28bdRR.js +1 -0
  411. package/web-ui/build/static/powershell-CWg1ca6z.js +1 -0
  412. package/web-ui/build/static/powershell-Dnl0aBXc.js +1 -0
  413. package/web-ui/build/static/processing-CbYVU7hZ.js +1 -0
  414. package/web-ui/build/static/processing-DnroirEw.js +1 -0
  415. package/web-ui/build/static/profile-DLNc-MTA.js +1 -0
  416. package/web-ui/build/static/prolog-4KlPFQus.js +1 -0
  417. package/web-ui/build/static/prolog-CtUicY87.js +1 -0
  418. package/web-ui/build/static/promql-C_i6OJVg.js +1 -0
  419. package/web-ui/build/static/properties-Cj0lBOSP.js +1 -0
  420. package/web-ui/build/static/properties-vGFibcz9.js +1 -0
  421. package/web-ui/build/static/protobuf-BOIGxbSP.js +1 -0
  422. package/web-ui/build/static/protobuf-CQ3su-J7.js +1 -0
  423. package/web-ui/build/static/psl-DeG5_YUF.js +1 -0
  424. package/web-ui/build/static/pug-BieVVXYz.js +1 -0
  425. package/web-ui/build/static/puppet-Ba40SVKU.js +1 -0
  426. package/web-ui/build/static/puppet-D7BzrcGt.js +1 -0
  427. package/web-ui/build/static/pure-DZnkz1iT.js +1 -0
  428. package/web-ui/build/static/purebasic-CLLZW_6G.js +1 -0
  429. package/web-ui/build/static/purebasic-CYPZo_H6.js +1 -0
  430. package/web-ui/build/static/purescript-Dyjfu5Id.js +1 -0
  431. package/web-ui/build/static/python-BdIWKxdN.js +1 -0
  432. package/web-ui/build/static/python-ofKsqxv7.js +1 -0
  433. package/web-ui/build/static/python-repl-DiTYb1xK.js +1 -0
  434. package/web-ui/build/static/q-B4P0If_I.js +1 -0
  435. package/web-ui/build/static/q-t_17xfY8.js +1 -0
  436. package/web-ui/build/static/qml-B5WhiN48.js +1 -0
  437. package/web-ui/build/static/qml-Dq0cESXJ.js +1 -0
  438. package/web-ui/build/static/qore-DCx30XRf.js +1 -0
  439. package/web-ui/build/static/qsharp-UrBScekp.js +1 -0
  440. package/web-ui/build/static/r-B0Ty1RKQ.js +1 -0
  441. package/web-ui/build/static/r-B0za8QKS.js +1 -0
  442. package/web-ui/build/static/racket-Dj6WEyhS.js +1 -0
  443. package/web-ui/build/static/reason-dj9hJSfr.js +1 -0
  444. package/web-ui/build/static/reasonml-B-q5_wag.js +1 -0
  445. package/web-ui/build/static/regex-4HEc5C1m.js +1 -0
  446. package/web-ui/build/static/rego-BdQe18RK.js +1 -0
  447. package/web-ui/build/static/renpy-CVMA2llL.js +1 -0
  448. package/web-ui/build/static/rest-9B4JWVGr.js +1 -0
  449. package/web-ui/build/static/rib-DR-U8OaT.js +1 -0
  450. package/web-ui/build/static/rip-Bu2t_rFZ.js +1 -0
  451. package/web-ui/build/static/roboconf-CJeXD5-I.js +1 -0
  452. package/web-ui/build/static/roboconf-DzDTVrdM.js +1 -0
  453. package/web-ui/build/static/robotframework-CR7KyPpN.js +1 -0
  454. package/web-ui/build/static/routeros-B2741z2k.js +1 -0
  455. package/web-ui/build/static/rsl-B9F_ZCgv.js +1 -0
  456. package/web-ui/build/static/ruby-I2JTNgyY.js +1 -0
  457. package/web-ui/build/static/ruby-QGDPOmJX.js +1 -0
  458. package/web-ui/build/static/ruleslanguage-CGzXEUCO.js +1 -0
  459. package/web-ui/build/static/rust-BxW5-WOm.js +1 -0
  460. package/web-ui/build/static/rust-CSOA43di.js +1 -0
  461. package/web-ui/build/static/sas-Bclfx4g3.js +1 -0
  462. package/web-ui/build/static/sas-xbQaiYjT.js +1 -0
  463. package/web-ui/build/static/sass-DJPbdNwd.js +1 -0
  464. package/web-ui/build/static/scala-Bo18NtHQ.js +1 -0
  465. package/web-ui/build/static/scala-Cy0JH-SG.js +1 -0
  466. package/web-ui/build/static/scheme-BjcWWjIx.js +1 -0
  467. package/web-ui/build/static/scheme-DQdj8PzN.js +1 -0
  468. package/web-ui/build/static/scilab-Bn1KHdK-.js +1 -0
  469. package/web-ui/build/static/scss-B1twkZBz.js +1 -0
  470. package/web-ui/build/static/scss-DmOuMI4v.js +1 -0
  471. package/web-ui/build/static/shell-BUlkJG0S.js +1 -0
  472. package/web-ui/build/static/shell-session-Bke-svxA.js +1 -0
  473. package/web-ui/build/static/smali-Ch9S16HV.js +1 -0
  474. package/web-ui/build/static/smali-D_yDr_Aj.js +1 -0
  475. package/web-ui/build/static/smalltalk-B9TfQ5Md.js +1 -0
  476. package/web-ui/build/static/smalltalk-EwbZxZsR.js +1 -0
  477. package/web-ui/build/static/smarty-9kDPpeSm.js +1 -0
  478. package/web-ui/build/static/sml-2fEfT7rd.js +1 -0
  479. package/web-ui/build/static/sml-BiwoLNk7.js +1 -0
  480. package/web-ui/build/static/solidity-n_x8Oe0h.js +1 -0
  481. package/web-ui/build/static/solution-file-B2mvjI3e.js +1 -0
  482. package/web-ui/build/static/soy-DPkgKBIS.js +1 -0
  483. package/web-ui/build/static/sparql-Cy95tds0.js +1 -0
  484. package/web-ui/build/static/splunk-spl-Ym3z9ouN.js +1 -0
  485. package/web-ui/build/static/sqf-CXZTG8WE.js +1 -0
  486. package/web-ui/build/static/sqf-Cwi3yg7f.js +1 -0
  487. package/web-ui/build/static/sql-DPxSQY4S.js +1 -0
  488. package/web-ui/build/static/sql-peh7ijGj.js +1 -0
  489. package/web-ui/build/static/sql_more-0YAbAuPw.js +1 -0
  490. package/web-ui/build/static/squirrel-CphzjV0e.js +1 -0
  491. package/web-ui/build/static/stan-0-xZ95-O.js +1 -0
  492. package/web-ui/build/static/stan-CaI4__2g.js +1 -0
  493. package/web-ui/build/static/stata-BrbzrGSs.js +1 -0
  494. package/web-ui/build/static/step21-C_qeyVLw.js +1 -0
  495. package/web-ui/build/static/stylus-Btycb2sZ.js +1 -0
  496. package/web-ui/build/static/stylus-FoBJ7jki.js +1 -0
  497. package/web-ui/build/static/subunit-Dpg-m04-.js +1 -0
  498. package/web-ui/build/static/swift-Cr9uZmgb.js +1 -0
  499. package/web-ui/build/static/swift-hGLFtD7e.js +1 -0
  500. package/web-ui/build/static/systemd-Bls2D9Vj.js +1 -0
  501. package/web-ui/build/static/t4-cs-C4qDO-jJ.js +1 -0
  502. package/web-ui/build/static/t4-templating-BbCFPMPO.js +1 -0
  503. package/web-ui/build/static/t4-vb-D1zoEccT.js +1 -0
  504. package/web-ui/build/static/taggerscript-CWHk9Gih.js +1 -0
  505. package/web-ui/build/static/tap-Bjt0UnzV.js +1 -0
  506. package/web-ui/build/static/tap-BnHKwLQs.js +1 -0
  507. package/web-ui/build/static/tcl-Zo9kx4y-.js +1 -0
  508. package/web-ui/build/static/tcl-fzLmefkt.js +1 -0
  509. package/web-ui/build/static/textile-9lIlUPH5.js +1 -0
  510. package/web-ui/build/static/thrift-M3K6r5Cy.js +1 -0
  511. package/web-ui/build/static/toml-HpaKqckc.js +1 -0
  512. package/web-ui/build/static/tp-DFKuxrKj.js +1 -0
  513. package/web-ui/build/static/tremor-D4_bUtMB.js +1 -0
  514. package/web-ui/build/static/tsx-o1RT-T90.js +1 -0
  515. package/web-ui/build/static/tt2-1xDqcN_2.js +1 -0
  516. package/web-ui/build/static/turtle-Dlt-aGky.js +1 -0
  517. package/web-ui/build/static/twig-CJ_BnGSR.js +1 -0
  518. package/web-ui/build/static/twig-CjsiSQb6.js +1 -0
  519. package/web-ui/build/static/typescript-B8B9zUn-.js +1 -0
  520. package/web-ui/build/static/typescript-D0Jgo8O7.js +1 -0
  521. package/web-ui/build/static/typoscript-C8Qke4ZB.js +1 -0
  522. package/web-ui/build/static/unrealscript-YxJdDNZ3.js +1 -0
  523. package/web-ui/build/static/uorazor-CtEVnqBv.js +1 -0
  524. package/web-ui/build/static/uri-YdaiQl4c.js +1 -0
  525. package/web-ui/build/static/v-CIyttMDD.js +1 -0
  526. package/web-ui/build/static/vala-DGslcym_.js +1 -0
  527. package/web-ui/build/static/vala-GFPx3uEJ.js +1 -0
  528. package/web-ui/build/static/vbnet-B20itab-.js +1 -0
  529. package/web-ui/build/static/vbnet-BdoN6egk.js +1 -0
  530. package/web-ui/build/static/vbscript-PHVh6Fp_.js +1 -0
  531. package/web-ui/build/static/vbscript-html-woH1VZ7U.js +1 -0
  532. package/web-ui/build/static/velocity-DtVfCZeg.js +1 -0
  533. package/web-ui/build/static/verilog-Bt6edXvM.js +1 -0
  534. package/web-ui/build/static/verilog-k_7lr9Zq.js +1 -0
  535. package/web-ui/build/static/vhdl-BMzOgOeK.js +1 -0
  536. package/web-ui/build/static/vhdl-BcAbtPG6.js +1 -0
  537. package/web-ui/build/static/vim-DrinG9a4.js +1 -0
  538. package/web-ui/build/static/vim-WihLATJL.js +1 -0
  539. package/web-ui/build/static/visual-basic-CJnvgPjM.js +1 -0
  540. package/web-ui/build/static/warpscript-zMlbUoZs.js +1 -0
  541. package/web-ui/build/static/wasm-GUnfTBUL.js +1 -0
  542. package/web-ui/build/static/web-idl-CfaLTG_r.js +1 -0
  543. package/web-ui/build/static/wiki-13AlLoOc.js +1 -0
  544. package/web-ui/build/static/wolfram-zHocYNXW.js +1 -0
  545. package/web-ui/build/static/wren-Byq862Iu.js +1 -0
  546. package/web-ui/build/static/x86asm-CLcOnePY.js +1 -0
  547. package/web-ui/build/static/xeora-BVHqWOFS.js +1 -0
  548. package/web-ui/build/static/xl-lXi8OYfr.js +1 -0
  549. package/web-ui/build/static/xml-KZjGBKxi.js +1 -0
  550. package/web-ui/build/static/xml-doc-DrQSDcEW.js +1 -0
  551. package/web-ui/build/static/xojo-DosHeFXU.js +1 -0
  552. package/web-ui/build/static/xquery-BZN1F14Q.js +1 -0
  553. package/web-ui/build/static/xquery-Cnz7ZLFr.js +1 -0
  554. package/web-ui/build/static/yaml-BzXOcy9u.js +1 -0
  555. package/web-ui/build/static/yaml-C207y5bt.js +1 -0
  556. package/web-ui/build/static/yang-ByrBdDIg.js +1 -0
  557. package/web-ui/build/static/zephir-bahTa7of.js +1 -0
  558. package/web-ui/build/static/zig-BlFYhdtC.js +1 -0
  559. package/src/tools/browserTool.js +0 -897
  560. package/src/utilities/platformUtils.test.js +0 -98
  561. package/web-ui/build/static/index-SmQFfvBs.js +0 -746
  562. package/web-ui/build/static/index-V2ySwjHp.css +0 -1
@@ -0,0 +1,402 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock fs and userDataDir
5
+ const mockFs = {
6
+ readFile: jest.fn(),
7
+ writeFile: jest.fn().mockResolvedValue(undefined),
8
+ mkdir: jest.fn().mockResolvedValue(undefined),
9
+ rm: jest.fn().mockResolvedValue(undefined),
10
+ readdir: jest.fn().mockResolvedValue([]),
11
+ stat: jest.fn(),
12
+ access: jest.fn().mockResolvedValue(undefined),
13
+ copyFile: jest.fn().mockResolvedValue(undefined)
14
+ };
15
+
16
+ jest.unstable_mockModule('fs', () => ({
17
+ promises: mockFs
18
+ }));
19
+
20
+ jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
21
+ getUserDataPaths: jest.fn(() => ({
22
+ settings: '/fake/settings',
23
+ attachments: '/fake/attachments',
24
+ skills: '/fake/skills'
25
+ })),
26
+ ensureUserDataDirs: jest.fn(async () => {})
27
+ }));
28
+
29
+ const { SkillsService } = await import('../skillsService.js');
30
+
31
+ describe('SkillsService', () => {
32
+ let service;
33
+ let logger;
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ logger = createMockLogger();
38
+ service = new SkillsService(logger);
39
+ service.initialized = false;
40
+ service.indexCache = null;
41
+ });
42
+
43
+ test('constructor initializes with default state', () => {
44
+ expect(service.initialized).toBe(false);
45
+ expect(service.indexCache).toBeNull();
46
+ });
47
+
48
+ test('initialize sets up skillsDir and marks initialized', async () => {
49
+ await service.initialize();
50
+ expect(service.initialized).toBe(true);
51
+ expect(service.skillsDir).toBe('/fake/skills');
52
+ });
53
+
54
+ test('initialize only runs once', async () => {
55
+ await service.initialize();
56
+ const { ensureUserDataDirs } = await import('../../utilities/userDataDir.js');
57
+ const callCount = ensureUserDataDirs.mock.calls.length;
58
+ await service.initialize();
59
+ expect(ensureUserDataDirs.mock.calls.length).toBe(callCount);
60
+ });
61
+
62
+ test('initialize throws on failure', async () => {
63
+ const { ensureUserDataDirs } = await import('../../utilities/userDataDir.js');
64
+ service.initialized = false;
65
+ ensureUserDataDirs.mockRejectedValueOnce(new Error('disk error'));
66
+ await expect(service.initialize()).rejects.toThrow('disk error');
67
+ });
68
+
69
+ describe('validation', () => {
70
+ test('_validateSkillName rejects empty names', () => {
71
+ expect(() => service._validateSkillName('')).toThrow('required');
72
+ expect(() => service._validateSkillName(null)).toThrow('required');
73
+ });
74
+
75
+ test('_validateSkillName rejects too long names', () => {
76
+ expect(() => service._validateSkillName('a'.repeat(51))).toThrow('50 characters');
77
+ });
78
+
79
+ test('_validateSkillName rejects non-kebab-case', () => {
80
+ expect(() => service._validateSkillName('MySkill')).toThrow('kebab-case');
81
+ expect(() => service._validateSkillName('my_skill')).toThrow('kebab-case');
82
+ });
83
+
84
+ test('_validateSkillName accepts valid names', () => {
85
+ expect(() => service._validateSkillName('my-skill')).not.toThrow();
86
+ expect(() => service._validateSkillName('code-review')).not.toThrow();
87
+ expect(() => service._validateSkillName('a1-b2')).not.toThrow();
88
+ });
89
+
90
+ test('_validatePathSafe rejects traversal paths', () => {
91
+ service.skillsDir = '/fake/skills';
92
+ expect(() => service._validatePathSafe('my-skill', '../../etc/passwd')).toThrow('within the skill directory');
93
+ });
94
+
95
+ test('_validatePathSafe accepts valid paths', () => {
96
+ service.skillsDir = '/fake/skills';
97
+ const result = service._validatePathSafe('my-skill', 'subdir/file.txt');
98
+ expect(result).toContain('my-skill');
99
+ });
100
+ });
101
+
102
+ describe('content analysis', () => {
103
+ test('_extractDescription returns first non-heading line', () => {
104
+ const content = '# Title\n\nThis is the description.\nMore text.';
105
+ expect(service._extractDescription(content)).toBe('This is the description.');
106
+ });
107
+
108
+ test('_extractDescription returns empty for null', () => {
109
+ expect(service._extractDescription(null)).toBe('');
110
+ });
111
+
112
+ test('_extractDescription truncates long descriptions', () => {
113
+ const content = 'x'.repeat(250);
114
+ const desc = service._extractDescription(content);
115
+ expect(desc.length).toBeLessThanOrEqual(200);
116
+ expect(desc).toContain('...');
117
+ });
118
+
119
+ test('_extractSections finds ## headings', () => {
120
+ const content = '# Title\nIntro\n## Section A\nContent A\n## Section B\nContent B';
121
+ const sections = service._extractSections(content);
122
+ expect(sections).toHaveLength(2);
123
+ expect(sections[0].heading).toBe('## Section A');
124
+ expect(sections[1].heading).toBe('## Section B');
125
+ });
126
+
127
+ test('_extractSections returns empty for null', () => {
128
+ expect(service._extractSections(null)).toEqual([]);
129
+ });
130
+
131
+ test('_computeSize counts bytes and lines', () => {
132
+ const result = service._computeSize('line1\nline2\nline3');
133
+ expect(result.lineCount).toBe(3);
134
+ expect(result.sizeBytes).toBeGreaterThan(0);
135
+ });
136
+
137
+ test('_computeSize handles null', () => {
138
+ const result = service._computeSize(null);
139
+ expect(result.lineCount).toBe(0);
140
+ });
141
+ });
142
+
143
+ describe('CRUD operations', () => {
144
+ beforeEach(async () => {
145
+ // Pre-initialize
146
+ service.initialized = true;
147
+ service.skillsDir = '/fake/skills';
148
+ });
149
+
150
+ test('listSkills returns mapped skill summaries', async () => {
151
+ service.indexCache = {
152
+ skills: {
153
+ 'my-skill': {
154
+ name: 'my-skill',
155
+ description: 'A skill',
156
+ sections: ['## Setup'],
157
+ sizeBytes: 100,
158
+ lineCount: 10,
159
+ files: ['skill.md'],
160
+ createdAt: '2024-01-01',
161
+ updatedAt: '2024-01-02'
162
+ }
163
+ }
164
+ };
165
+ const list = await service.listSkills();
166
+ expect(list).toHaveLength(1);
167
+ expect(list[0].name).toBe('my-skill');
168
+ expect(list[0].fileCount).toBe(1);
169
+ });
170
+
171
+ test('listSkills loads index when cache empty', async () => {
172
+ service.indexCache = null;
173
+ mockFs.readFile.mockRejectedValueOnce(new Error('ENOENT'));
174
+ const list = await service.listSkills();
175
+ expect(list).toEqual([]);
176
+ });
177
+
178
+ test('describeSkill returns detailed info', async () => {
179
+ service.indexCache = {
180
+ skills: {
181
+ 'my-skill': {
182
+ name: 'my-skill',
183
+ description: 'Desc',
184
+ sections: ['## Setup'],
185
+ files: ['skill.md'],
186
+ createdAt: '2024-01-01',
187
+ updatedAt: '2024-01-02'
188
+ }
189
+ }
190
+ };
191
+ mockFs.readFile.mockResolvedValueOnce('# Title\n## Setup\nContent');
192
+
193
+ const info = await service.describeSkill('my-skill');
194
+ expect(info.name).toBe('my-skill');
195
+ expect(info.sections).toHaveLength(1);
196
+ });
197
+
198
+ test('describeSkill throws for unknown skill', async () => {
199
+ service.indexCache = { skills: {} };
200
+ await expect(service.describeSkill('unknown')).rejects.toThrow('not found');
201
+ });
202
+
203
+ test('readSkill returns content and files', async () => {
204
+ service.indexCache = { skills: { 'my-skill': { description: 'Desc' } } };
205
+ mockFs.readFile.mockResolvedValueOnce('# Skill content');
206
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
207
+
208
+ const result = await service.readSkill('my-skill');
209
+ expect(result.content).toBe('# Skill content');
210
+ expect(result.name).toBe('my-skill');
211
+ });
212
+
213
+ test('readSkill throws for unknown skill', async () => {
214
+ service.indexCache = { skills: {} };
215
+ await expect(service.readSkill('unknown')).rejects.toThrow('not found');
216
+ });
217
+
218
+ test('readSkillSection returns matching section', async () => {
219
+ service.indexCache = { skills: { 'my-skill': {} } };
220
+ mockFs.readFile.mockResolvedValueOnce('# Title\nIntro\n## Setup\nSetup content\n## Usage\nUsage content');
221
+
222
+ const result = await service.readSkillSection('my-skill', 'Setup');
223
+ expect(result.section).toBe('## Setup');
224
+ expect(result.content).toContain('Setup content');
225
+ });
226
+
227
+ test('readSkillSection throws for missing section', async () => {
228
+ service.indexCache = { skills: { 'my-skill': {} } };
229
+ mockFs.readFile.mockResolvedValueOnce('# Title\n## Setup\nContent');
230
+
231
+ await expect(service.readSkillSection('my-skill', 'Missing')).rejects.toThrow('Section not found');
232
+ });
233
+
234
+ test('readSkillFile reads a file within skill directory', async () => {
235
+ service.indexCache = { skills: { 'my-skill': {} } };
236
+ mockFs.readFile.mockResolvedValueOnce('file content');
237
+
238
+ const result = await service.readSkillFile('my-skill', 'data.json');
239
+ expect(result.content).toBe('file content');
240
+ });
241
+
242
+ test('readSkillFile throws for missing file', async () => {
243
+ service.indexCache = { skills: { 'my-skill': {} } };
244
+ mockFs.readFile.mockRejectedValueOnce(new Error('ENOENT'));
245
+
246
+ await expect(service.readSkillFile('my-skill', 'missing.txt')).rejects.toThrow('File not found');
247
+ });
248
+
249
+ test('createSkill creates directory and writes files', async () => {
250
+ service.indexCache = { skills: {} };
251
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
252
+
253
+ const entry = await service.createSkill('new-skill', '# New Skill\nDescription text');
254
+ expect(mockFs.mkdir).toHaveBeenCalled();
255
+ expect(mockFs.writeFile).toHaveBeenCalled();
256
+ expect(entry.name).toBe('new-skill');
257
+ });
258
+
259
+ test('createSkill throws for existing skill', async () => {
260
+ service.indexCache = { skills: { 'existing': {} } };
261
+ await expect(service.createSkill('existing', 'content')).rejects.toThrow('already exists');
262
+ });
263
+
264
+ test('createSkill handles additional files', async () => {
265
+ service.indexCache = { skills: {} };
266
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
267
+
268
+ await service.createSkill('my-skill', '# Skill', [
269
+ { path: 'data.json', content: '{}' }
270
+ ]);
271
+ // writeFile called for skill.md, data.json, and index
272
+ expect(mockFs.writeFile.mock.calls.length).toBeGreaterThanOrEqual(3);
273
+ });
274
+
275
+ test('updateSkill updates content', async () => {
276
+ service.indexCache = {
277
+ skills: {
278
+ 'my-skill': { name: 'my-skill', createdAt: '2024-01-01', description: 'Old' }
279
+ }
280
+ };
281
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
282
+
283
+ const entry = await service.updateSkill('my-skill', '# Updated');
284
+ expect(entry.createdAt).toBe('2024-01-01'); // Preserved
285
+ });
286
+
287
+ test('updateSkill reads existing content when not provided', async () => {
288
+ service.indexCache = { skills: { 'my-skill': { createdAt: '2024-01-01', description: 'D' } } };
289
+ mockFs.readFile.mockResolvedValueOnce('# Existing content');
290
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
291
+
292
+ await service.updateSkill('my-skill');
293
+ expect(mockFs.readFile).toHaveBeenCalled();
294
+ });
295
+
296
+ test('updateSkill throws for unknown skill', async () => {
297
+ service.indexCache = { skills: {} };
298
+ await expect(service.updateSkill('unknown', 'content')).rejects.toThrow('not found');
299
+ });
300
+
301
+ test('deleteSkill removes directory and index entry', async () => {
302
+ service.indexCache = { skills: { 'my-skill': {} } };
303
+ await service.deleteSkill('my-skill');
304
+ expect(mockFs.rm).toHaveBeenCalled();
305
+ expect(service.indexCache.skills['my-skill']).toBeUndefined();
306
+ });
307
+
308
+ test('deleteSkill throws for unknown skill', async () => {
309
+ service.indexCache = { skills: {} };
310
+ await expect(service.deleteSkill('unknown')).rejects.toThrow('not found');
311
+ });
312
+
313
+ test('getSkillSummaries returns matching summaries', async () => {
314
+ service.indexCache = {
315
+ skills: {
316
+ 'skill-a': { name: 'skill-a', description: 'A', sections: ['## S1'], lineCount: 5 },
317
+ 'skill-b': { name: 'skill-b', description: 'B', sections: [], lineCount: 10 }
318
+ }
319
+ };
320
+
321
+ const summaries = await service.getSkillSummaries(['skill-a', 'skill-c']);
322
+ expect(summaries).toHaveLength(1);
323
+ expect(summaries[0].name).toBe('skill-a');
324
+ });
325
+ });
326
+
327
+ describe('importSkill', () => {
328
+ beforeEach(() => {
329
+ service.initialized = true;
330
+ service.skillsDir = '/fake/skills';
331
+ });
332
+
333
+ test('imports a single file as skill.md', async () => {
334
+ service.indexCache = { skills: {} };
335
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => false });
336
+ mockFs.readFile.mockResolvedValueOnce('# Imported skill content');
337
+ // second readFile for _buildIndexEntry
338
+ mockFs.readFile.mockResolvedValueOnce('# Imported skill content');
339
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
340
+
341
+ const entry = await service.importSkill('/path/to/my-file.md', 'imported-skill');
342
+ expect(entry.name).toBe('imported-skill');
343
+ });
344
+
345
+ test('throws for non-existent source', async () => {
346
+ service.indexCache = { skills: {} };
347
+ mockFs.stat.mockRejectedValueOnce(new Error('ENOENT'));
348
+
349
+ await expect(service.importSkill('/missing/path')).rejects.toThrow('not found');
350
+ });
351
+
352
+ test('throws for duplicate skill name', async () => {
353
+ service.indexCache = { skills: { 'existing': {} } };
354
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => false });
355
+
356
+ await expect(service.importSkill('/path/file.md', 'existing')).rejects.toThrow('already exists');
357
+ });
358
+
359
+ test('derives skill name from source path', async () => {
360
+ service.indexCache = { skills: {} };
361
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => false });
362
+ mockFs.readFile.mockResolvedValueOnce('content');
363
+ mockFs.readFile.mockResolvedValueOnce('content');
364
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
365
+
366
+ const entry = await service.importSkill('/path/to/My Cool Skill.md');
367
+ // Should be kebab-cased
368
+ expect(entry.name).toMatch(/^[a-z0-9-]+$/);
369
+ });
370
+
371
+ test('imports directory with skill.md', async () => {
372
+ service.indexCache = { skills: {} };
373
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => true });
374
+ // _copyDir
375
+ mockFs.readdir.mockResolvedValueOnce([
376
+ { name: 'skill.md', isFile: () => true, isDirectory: () => false },
377
+ { name: 'data.json', isFile: () => true, isDirectory: () => false }
378
+ ]);
379
+ // access check
380
+ mockFs.access.mockResolvedValueOnce(undefined);
381
+ // readFile for _buildIndexEntry
382
+ mockFs.readFile.mockResolvedValueOnce('# Dir skill');
383
+ // _listSkillFiles
384
+ mockFs.readdir.mockResolvedValueOnce([
385
+ { name: 'skill.md', isFile: () => true },
386
+ { name: 'data.json', isFile: () => true }
387
+ ]);
388
+
389
+ const entry = await service.importSkill('/path/skill-dir', 'dir-skill');
390
+ expect(entry.name).toBe('dir-skill');
391
+ });
392
+
393
+ test('imports directory without skill.md throws', async () => {
394
+ service.indexCache = { skills: {} };
395
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => true });
396
+ mockFs.readdir.mockResolvedValueOnce([]);
397
+ mockFs.access.mockRejectedValueOnce(new Error('ENOENT'));
398
+
399
+ await expect(service.importSkill('/path/no-skill', 'bad-import')).rejects.toThrow('must contain a skill.md');
400
+ });
401
+ });
402
+ });
@@ -0,0 +1,48 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+ import TokenCountingService from '../tokenCountingService.js';
4
+
5
+ describe('TokenCountingService', () => {
6
+ let logger;
7
+
8
+ beforeEach(() => {
9
+ logger = createMockLogger();
10
+ });
11
+
12
+ test('constructor creates instance', () => {
13
+ const service = new TokenCountingService(logger);
14
+ expect(service).toBeInstanceOf(TokenCountingService);
15
+ expect(service.logger).toBe(logger);
16
+ });
17
+
18
+ test('getModelContextWindow returns positive number for known model', () => {
19
+ const service = new TokenCountingService(logger);
20
+ // Use a fallback model name that exists in the hardcoded map
21
+ const contextWindow = service.getModelContextWindow('gpt-4');
22
+ expect(typeof contextWindow).toBe('number');
23
+ expect(contextWindow).toBeGreaterThan(0);
24
+ });
25
+
26
+ test('shouldTriggerCompaction returns true when near limit', () => {
27
+ const service = new TokenCountingService(logger);
28
+ // currentTokens + maxOutputTokens >= threshold * contextWindow
29
+ // 90000 + 8192 = 98192 >= 0.7 * 128000 = 89600 => true
30
+ const result = service.shouldTriggerCompaction(90000, 8192, 128000);
31
+ expect(result).toBe(true);
32
+ });
33
+
34
+ test('shouldTriggerCompaction returns false when well under limit', () => {
35
+ const service = new TokenCountingService(logger);
36
+ // 10000 + 8192 = 18192 < 0.7 * 128000 = 89600 => false
37
+ const result = service.shouldTriggerCompaction(10000, 8192, 128000);
38
+ expect(result).toBe(false);
39
+ });
40
+
41
+ test('calculateTargetTokenCount returns positive number', () => {
42
+ const service = new TokenCountingService(logger);
43
+ const target = service.calculateTargetTokenCount(128000);
44
+ expect(typeof target).toBe('number');
45
+ expect(target).toBeGreaterThan(0);
46
+ expect(target).toBeLessThanOrEqual(128000);
47
+ });
48
+ });
@@ -825,12 +825,12 @@ class ConversationCompactionService {
825
825
  const filteredMessages = allMessages.filter(m => {
826
826
  if (m === mainSystemMsg) return true;
827
827
  if (m.role === 'system') return false;
828
- if (m.type === 'tool_result' || m.role === 'tool') return false;
828
+ if (m.type === 'tool_result' || m.type === 'tool-result' || m.role === 'tool') return false;
829
829
  return true;
830
830
  });
831
831
 
832
832
  const removedSystemCount = allMessages.filter(m => m.role === 'system' && m !== mainSystemMsg).length;
833
- const removedToolCount = allMessages.filter(m => m.type === 'tool_result' || m.role === 'tool').length;
833
+ const removedToolCount = allMessages.filter(m => m.type === 'tool_result' || m.type === 'tool-result' || m.role === 'tool').length;
834
834
 
835
835
  // 3. Apply sandwich using segment identification
836
836
  const segments = this._identifySegments(filteredMessages);
@@ -155,10 +155,10 @@ class VisualEditorServer {
155
155
  return { success: true, port: this.port, message: 'Already running' };
156
156
  }
157
157
 
158
- // Find a free port starting from the preferred port
158
+ // Find a free port by checking on 0.0.0.0 (matching the actual bind address)
159
159
  const preferredPort = this.port;
160
160
  try {
161
- const actualPort = await findFreePort(preferredPort);
161
+ const actualPort = await findFreePort(preferredPort, 100, '0.0.0.0');
162
162
 
163
163
  if (actualPort !== preferredPort) {
164
164
  this.logger.info?.(`[VisualEditorServer] Port ${preferredPort} taken, using ${actualPort}`) ||
@@ -170,6 +170,15 @@ class VisualEditorServer {
170
170
  throw err;
171
171
  }
172
172
 
173
+ return this._tryListen(this.port, 10);
174
+ }
175
+
176
+ /**
177
+ * Try to listen on a port, retrying on EADDRINUSE up to maxRetries times.
178
+ * Handles the TOCTOU race between findFreePort and actual listen().
179
+ */
180
+ async _tryListen(port, maxRetries) {
181
+ this.port = port;
173
182
  this.app = express();
174
183
  this._setupMiddleware();
175
184
  this._setupRoutes();
@@ -197,9 +206,19 @@ class VisualEditorServer {
197
206
  });
198
207
 
199
208
  this.server.on('error', (err) => {
200
- // This shouldn't happen since we checked with findFreePort, but handle anyway
201
- this.logger.error?.(`[VisualEditorServer] Server error: ${err.message}`);
202
- reject(err);
209
+ if (err.code === 'EADDRINUSE' && maxRetries > 0) {
210
+ const nextPort = this.port + 1;
211
+ this.logger.info?.(`[VisualEditorServer] Port ${this.port} in use, trying ${nextPort}...`) ||
212
+ console.log(`[VisualEditorServer] Port ${this.port} in use, trying ${nextPort}...`);
213
+ // Clean up and retry on next port
214
+ try { this.server.close(); } catch {}
215
+ this.app = null;
216
+ this.server = null;
217
+ resolve(this._tryListen(nextPort, maxRetries - 1));
218
+ } else {
219
+ this.logger.error?.(`[VisualEditorServer] Server error: ${err.message}`);
220
+ reject(err);
221
+ }
203
222
  });
204
223
  });
205
224
  }
@@ -1158,8 +1177,8 @@ class VisualEditorServer {
1158
1177
 
1159
1178
  // Listen for commands from parent (Loxia Web-UI) or WebSocket
1160
1179
  window.addEventListener('message', (e) => {
1161
- if (e.data && (e.data.type === 'highlight' || e.data.type === 'scroll-to')) {
1162
- // Forward to app iframe
1180
+ if (e.data && (e.data.type === 'highlight' || e.data.type === 'scroll-to' || e.data.type === 'toggle')) {
1181
+ // Forward to app iframe (including toggle for Select/Preview mode switching)
1163
1182
  iframe.contentWindow.postMessage(e.data, '*');
1164
1183
  }
1165
1184
  });