@sonde/packs 0.1.0 → 0.1.1

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 (359) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-test.log +814 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/CHANGELOG.md +10 -0
  5. package/dist/index.d.ts +16 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +40 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/citrix.d.ts +13 -0
  10. package/dist/integrations/citrix.d.ts.map +1 -0
  11. package/dist/integrations/citrix.js +420 -0
  12. package/dist/integrations/citrix.js.map +1 -0
  13. package/dist/integrations/citrix.test.d.ts +2 -0
  14. package/dist/integrations/citrix.test.d.ts.map +1 -0
  15. package/dist/integrations/citrix.test.js +464 -0
  16. package/dist/integrations/citrix.test.js.map +1 -0
  17. package/dist/integrations/graph.d.ts +9 -0
  18. package/dist/integrations/graph.d.ts.map +1 -0
  19. package/dist/integrations/graph.js +290 -0
  20. package/dist/integrations/graph.js.map +1 -0
  21. package/dist/integrations/graph.test.d.ts +2 -0
  22. package/dist/integrations/graph.test.d.ts.map +1 -0
  23. package/dist/integrations/graph.test.js +356 -0
  24. package/dist/integrations/graph.test.js.map +1 -0
  25. package/dist/integrations/httpbin.d.ts +3 -0
  26. package/dist/integrations/httpbin.d.ts.map +1 -0
  27. package/dist/integrations/httpbin.js +70 -0
  28. package/dist/integrations/httpbin.js.map +1 -0
  29. package/dist/integrations/nutanix.d.ts +18 -0
  30. package/dist/integrations/nutanix.d.ts.map +1 -0
  31. package/dist/integrations/nutanix.js +1121 -0
  32. package/dist/integrations/nutanix.js.map +1 -0
  33. package/dist/integrations/nutanix.test.d.ts +2 -0
  34. package/dist/integrations/nutanix.test.d.ts.map +1 -0
  35. package/dist/integrations/nutanix.test.js +978 -0
  36. package/dist/integrations/nutanix.test.js.map +1 -0
  37. package/dist/integrations/proxmox.d.ts +12 -0
  38. package/dist/integrations/proxmox.d.ts.map +1 -0
  39. package/dist/integrations/proxmox.js +733 -0
  40. package/dist/integrations/proxmox.js.map +1 -0
  41. package/dist/integrations/proxmox.test.d.ts +2 -0
  42. package/dist/integrations/proxmox.test.d.ts.map +1 -0
  43. package/dist/integrations/proxmox.test.js +697 -0
  44. package/dist/integrations/proxmox.test.js.map +1 -0
  45. package/dist/integrations/servicenow.d.ts +3 -0
  46. package/dist/integrations/servicenow.d.ts.map +1 -0
  47. package/dist/integrations/servicenow.js +257 -0
  48. package/dist/integrations/servicenow.js.map +1 -0
  49. package/dist/integrations/servicenow.test.d.ts +2 -0
  50. package/dist/integrations/servicenow.test.d.ts.map +1 -0
  51. package/dist/integrations/servicenow.test.js +217 -0
  52. package/dist/integrations/servicenow.test.js.map +1 -0
  53. package/dist/integrations/splunk.d.ts +9 -0
  54. package/dist/integrations/splunk.d.ts.map +1 -0
  55. package/dist/integrations/splunk.js +242 -0
  56. package/dist/integrations/splunk.js.map +1 -0
  57. package/dist/integrations/splunk.test.d.ts +2 -0
  58. package/dist/integrations/splunk.test.d.ts.map +1 -0
  59. package/dist/integrations/splunk.test.js +323 -0
  60. package/dist/integrations/splunk.test.js.map +1 -0
  61. package/dist/mysql/index.d.ts +3 -0
  62. package/dist/mysql/index.d.ts.map +1 -0
  63. package/dist/mysql/index.js +13 -0
  64. package/dist/mysql/index.js.map +1 -0
  65. package/dist/mysql/manifest.d.ts +3 -0
  66. package/dist/mysql/manifest.d.ts.map +1 -0
  67. package/dist/mysql/manifest.js +69 -0
  68. package/dist/mysql/manifest.js.map +1 -0
  69. package/dist/mysql/probes/databases-list.d.ts +13 -0
  70. package/dist/mysql/probes/databases-list.d.ts.map +1 -0
  71. package/dist/mysql/probes/databases-list.js +31 -0
  72. package/dist/mysql/probes/databases-list.js.map +1 -0
  73. package/dist/mysql/probes/databases-list.test.d.ts +2 -0
  74. package/dist/mysql/probes/databases-list.test.d.ts.map +1 -0
  75. package/dist/mysql/probes/databases-list.test.js +54 -0
  76. package/dist/mysql/probes/databases-list.test.js.map +1 -0
  77. package/dist/mysql/probes/processlist.d.ts +18 -0
  78. package/dist/mysql/probes/processlist.d.ts.map +1 -0
  79. package/dist/mysql/probes/processlist.js +36 -0
  80. package/dist/mysql/probes/processlist.js.map +1 -0
  81. package/dist/mysql/probes/processlist.test.d.ts +2 -0
  82. package/dist/mysql/probes/processlist.test.d.ts.map +1 -0
  83. package/dist/mysql/probes/processlist.test.js +41 -0
  84. package/dist/mysql/probes/processlist.test.js.map +1 -0
  85. package/dist/mysql/probes/status.d.ts +14 -0
  86. package/dist/mysql/probes/status.d.ts.map +1 -0
  87. package/dist/mysql/probes/status.js +40 -0
  88. package/dist/mysql/probes/status.js.map +1 -0
  89. package/dist/mysql/probes/status.test.d.ts +2 -0
  90. package/dist/mysql/probes/status.test.d.ts.map +1 -0
  91. package/dist/mysql/probes/status.test.js +43 -0
  92. package/dist/mysql/probes/status.test.js.map +1 -0
  93. package/dist/nginx/index.d.ts +3 -0
  94. package/dist/nginx/index.d.ts.map +1 -0
  95. package/dist/nginx/index.js +13 -0
  96. package/dist/nginx/index.js.map +1 -0
  97. package/dist/nginx/manifest.d.ts +3 -0
  98. package/dist/nginx/manifest.d.ts.map +1 -0
  99. package/dist/nginx/manifest.js +68 -0
  100. package/dist/nginx/manifest.js.map +1 -0
  101. package/dist/nginx/probes/access-log-tail.d.ts +9 -0
  102. package/dist/nginx/probes/access-log-tail.d.ts.map +1 -0
  103. package/dist/nginx/probes/access-log-tail.js +14 -0
  104. package/dist/nginx/probes/access-log-tail.js.map +1 -0
  105. package/dist/nginx/probes/access-log-tail.test.d.ts +2 -0
  106. package/dist/nginx/probes/access-log-tail.test.d.ts.map +1 -0
  107. package/dist/nginx/probes/access-log-tail.test.js +40 -0
  108. package/dist/nginx/probes/access-log-tail.test.js.map +1 -0
  109. package/dist/nginx/probes/config-test.d.ts +8 -0
  110. package/dist/nginx/probes/config-test.d.ts.map +1 -0
  111. package/dist/nginx/probes/config-test.js +18 -0
  112. package/dist/nginx/probes/config-test.js.map +1 -0
  113. package/dist/nginx/probes/config-test.test.d.ts +2 -0
  114. package/dist/nginx/probes/config-test.test.d.ts.map +1 -0
  115. package/dist/nginx/probes/config-test.test.js +35 -0
  116. package/dist/nginx/probes/config-test.test.js.map +1 -0
  117. package/dist/nginx/probes/error-log-tail.d.ts +9 -0
  118. package/dist/nginx/probes/error-log-tail.d.ts.map +1 -0
  119. package/dist/nginx/probes/error-log-tail.js +14 -0
  120. package/dist/nginx/probes/error-log-tail.js.map +1 -0
  121. package/dist/nginx/probes/error-log-tail.test.d.ts +2 -0
  122. package/dist/nginx/probes/error-log-tail.test.d.ts.map +1 -0
  123. package/dist/nginx/probes/error-log-tail.test.js +34 -0
  124. package/dist/nginx/probes/error-log-tail.test.js.map +1 -0
  125. package/dist/postgres/index.d.ts +3 -0
  126. package/dist/postgres/index.d.ts.map +1 -0
  127. package/dist/postgres/index.js +13 -0
  128. package/dist/postgres/index.js.map +1 -0
  129. package/dist/postgres/manifest.d.ts +3 -0
  130. package/dist/postgres/manifest.d.ts.map +1 -0
  131. package/dist/postgres/manifest.js +90 -0
  132. package/dist/postgres/manifest.js.map +1 -0
  133. package/dist/postgres/probes/connections-active.d.ts +17 -0
  134. package/dist/postgres/probes/connections-active.d.ts.map +1 -0
  135. package/dist/postgres/probes/connections-active.js +37 -0
  136. package/dist/postgres/probes/connections-active.js.map +1 -0
  137. package/dist/postgres/probes/connections-active.test.d.ts +2 -0
  138. package/dist/postgres/probes/connections-active.test.d.ts.map +1 -0
  139. package/dist/postgres/probes/connections-active.test.js +36 -0
  140. package/dist/postgres/probes/connections-active.test.js.map +1 -0
  141. package/dist/postgres/probes/databases-list.d.ts +14 -0
  142. package/dist/postgres/probes/databases-list.d.ts.map +1 -0
  143. package/dist/postgres/probes/databases-list.js +34 -0
  144. package/dist/postgres/probes/databases-list.js.map +1 -0
  145. package/dist/postgres/probes/databases-list.test.d.ts +2 -0
  146. package/dist/postgres/probes/databases-list.test.d.ts.map +1 -0
  147. package/dist/postgres/probes/databases-list.test.js +49 -0
  148. package/dist/postgres/probes/databases-list.test.js.map +1 -0
  149. package/dist/postgres/probes/query-slow.d.ts +17 -0
  150. package/dist/postgres/probes/query-slow.d.ts.map +1 -0
  151. package/dist/postgres/probes/query-slow.js +37 -0
  152. package/dist/postgres/probes/query-slow.js.map +1 -0
  153. package/dist/postgres/probes/query-slow.test.d.ts +2 -0
  154. package/dist/postgres/probes/query-slow.test.d.ts.map +1 -0
  155. package/dist/postgres/probes/query-slow.test.js +30 -0
  156. package/dist/postgres/probes/query-slow.test.js.map +1 -0
  157. package/dist/proxmox/index.d.ts +3 -0
  158. package/dist/proxmox/index.d.ts.map +1 -0
  159. package/dist/proxmox/index.js +23 -0
  160. package/dist/proxmox/index.js.map +1 -0
  161. package/dist/proxmox/manifest.d.ts +3 -0
  162. package/dist/proxmox/manifest.d.ts.map +1 -0
  163. package/dist/proxmox/manifest.js +75 -0
  164. package/dist/proxmox/manifest.js.map +1 -0
  165. package/dist/proxmox/probes/ceph-status.d.ts +36 -0
  166. package/dist/proxmox/probes/ceph-status.d.ts.map +1 -0
  167. package/dist/proxmox/probes/ceph-status.js +71 -0
  168. package/dist/proxmox/probes/ceph-status.js.map +1 -0
  169. package/dist/proxmox/probes/ceph-status.test.d.ts +2 -0
  170. package/dist/proxmox/probes/ceph-status.test.d.ts.map +1 -0
  171. package/dist/proxmox/probes/ceph-status.test.js +115 -0
  172. package/dist/proxmox/probes/ceph-status.test.js.map +1 -0
  173. package/dist/proxmox/probes/cluster-config.d.ts +31 -0
  174. package/dist/proxmox/probes/cluster-config.d.ts.map +1 -0
  175. package/dist/proxmox/probes/cluster-config.js +72 -0
  176. package/dist/proxmox/probes/cluster-config.js.map +1 -0
  177. package/dist/proxmox/probes/cluster-config.test.d.ts +2 -0
  178. package/dist/proxmox/probes/cluster-config.test.d.ts.map +1 -0
  179. package/dist/proxmox/probes/cluster-config.test.js +107 -0
  180. package/dist/proxmox/probes/cluster-config.test.js.map +1 -0
  181. package/dist/proxmox/probes/ha-status.d.ts +18 -0
  182. package/dist/proxmox/probes/ha-status.d.ts.map +1 -0
  183. package/dist/proxmox/probes/ha-status.js +38 -0
  184. package/dist/proxmox/probes/ha-status.js.map +1 -0
  185. package/dist/proxmox/probes/ha-status.test.d.ts +2 -0
  186. package/dist/proxmox/probes/ha-status.test.d.ts.map +1 -0
  187. package/dist/proxmox/probes/ha-status.test.js +66 -0
  188. package/dist/proxmox/probes/ha-status.test.js.map +1 -0
  189. package/dist/proxmox/probes/lvm.d.ts +35 -0
  190. package/dist/proxmox/probes/lvm.d.ts.map +1 -0
  191. package/dist/proxmox/probes/lvm.js +75 -0
  192. package/dist/proxmox/probes/lvm.js.map +1 -0
  193. package/dist/proxmox/probes/lvm.test.d.ts +2 -0
  194. package/dist/proxmox/probes/lvm.test.d.ts.map +1 -0
  195. package/dist/proxmox/probes/lvm.test.js +128 -0
  196. package/dist/proxmox/probes/lvm.test.js.map +1 -0
  197. package/dist/proxmox/probes/lxc-config.d.ts +29 -0
  198. package/dist/proxmox/probes/lxc-config.d.ts.map +1 -0
  199. package/dist/proxmox/probes/lxc-config.js +67 -0
  200. package/dist/proxmox/probes/lxc-config.js.map +1 -0
  201. package/dist/proxmox/probes/lxc-config.test.d.ts +2 -0
  202. package/dist/proxmox/probes/lxc-config.test.d.ts.map +1 -0
  203. package/dist/proxmox/probes/lxc-config.test.js +77 -0
  204. package/dist/proxmox/probes/lxc-config.test.js.map +1 -0
  205. package/dist/proxmox/probes/lxc-list.d.ts +20 -0
  206. package/dist/proxmox/probes/lxc-list.d.ts.map +1 -0
  207. package/dist/proxmox/probes/lxc-list.js +49 -0
  208. package/dist/proxmox/probes/lxc-list.js.map +1 -0
  209. package/dist/proxmox/probes/lxc-list.test.d.ts +2 -0
  210. package/dist/proxmox/probes/lxc-list.test.d.ts.map +1 -0
  211. package/dist/proxmox/probes/lxc-list.test.js +51 -0
  212. package/dist/proxmox/probes/lxc-list.test.js.map +1 -0
  213. package/dist/proxmox/probes/vm-config.d.ts +21 -0
  214. package/dist/proxmox/probes/vm-config.d.ts.map +1 -0
  215. package/dist/proxmox/probes/vm-config.js +58 -0
  216. package/dist/proxmox/probes/vm-config.js.map +1 -0
  217. package/dist/proxmox/probes/vm-config.test.d.ts +2 -0
  218. package/dist/proxmox/probes/vm-config.test.d.ts.map +1 -0
  219. package/dist/proxmox/probes/vm-config.test.js +80 -0
  220. package/dist/proxmox/probes/vm-config.test.js.map +1 -0
  221. package/dist/proxmox/probes/vm-locks.d.ts +16 -0
  222. package/dist/proxmox/probes/vm-locks.d.ts.map +1 -0
  223. package/dist/proxmox/probes/vm-locks.js +35 -0
  224. package/dist/proxmox/probes/vm-locks.js.map +1 -0
  225. package/dist/proxmox/probes/vm-locks.test.d.ts +2 -0
  226. package/dist/proxmox/probes/vm-locks.test.d.ts.map +1 -0
  227. package/dist/proxmox/probes/vm-locks.test.js +54 -0
  228. package/dist/proxmox/probes/vm-locks.test.js.map +1 -0
  229. package/dist/redis/index.d.ts +3 -0
  230. package/dist/redis/index.d.ts.map +1 -0
  231. package/dist/redis/index.js +13 -0
  232. package/dist/redis/index.js.map +1 -0
  233. package/dist/redis/manifest.d.ts +3 -0
  234. package/dist/redis/manifest.d.ts.map +1 -0
  235. package/dist/redis/manifest.js +51 -0
  236. package/dist/redis/manifest.js.map +1 -0
  237. package/dist/redis/probes/info.d.ts +15 -0
  238. package/dist/redis/probes/info.d.ts.map +1 -0
  239. package/dist/redis/probes/info.js +32 -0
  240. package/dist/redis/probes/info.js.map +1 -0
  241. package/dist/redis/probes/info.test.d.ts +2 -0
  242. package/dist/redis/probes/info.test.d.ts.map +1 -0
  243. package/dist/redis/probes/info.test.js +64 -0
  244. package/dist/redis/probes/info.test.js.map +1 -0
  245. package/dist/redis/probes/keys-count.d.ts +13 -0
  246. package/dist/redis/probes/keys-count.d.ts.map +1 -0
  247. package/dist/redis/probes/keys-count.js +24 -0
  248. package/dist/redis/probes/keys-count.js.map +1 -0
  249. package/dist/redis/probes/keys-count.test.d.ts +2 -0
  250. package/dist/redis/probes/keys-count.test.d.ts.map +1 -0
  251. package/dist/redis/probes/keys-count.test.js +37 -0
  252. package/dist/redis/probes/keys-count.test.js.map +1 -0
  253. package/dist/redis/probes/memory-usage.d.ts +16 -0
  254. package/dist/redis/probes/memory-usage.d.ts.map +1 -0
  255. package/dist/redis/probes/memory-usage.js +31 -0
  256. package/dist/redis/probes/memory-usage.js.map +1 -0
  257. package/dist/redis/probes/memory-usage.test.d.ts +2 -0
  258. package/dist/redis/probes/memory-usage.test.d.ts.map +1 -0
  259. package/dist/redis/probes/memory-usage.test.js +48 -0
  260. package/dist/redis/probes/memory-usage.test.js.map +1 -0
  261. package/dist/runbooks/nutanix.d.ts +3 -0
  262. package/dist/runbooks/nutanix.d.ts.map +1 -0
  263. package/dist/runbooks/nutanix.js +619 -0
  264. package/dist/runbooks/nutanix.js.map +1 -0
  265. package/dist/runbooks/nutanix.test.d.ts +2 -0
  266. package/dist/runbooks/nutanix.test.d.ts.map +1 -0
  267. package/dist/runbooks/nutanix.test.js +971 -0
  268. package/dist/runbooks/nutanix.test.js.map +1 -0
  269. package/dist/runbooks/proxmox.d.ts +3 -0
  270. package/dist/runbooks/proxmox.d.ts.map +1 -0
  271. package/dist/runbooks/proxmox.js +451 -0
  272. package/dist/runbooks/proxmox.js.map +1 -0
  273. package/dist/runbooks/proxmox.test.d.ts +2 -0
  274. package/dist/runbooks/proxmox.test.d.ts.map +1 -0
  275. package/dist/runbooks/proxmox.test.js +700 -0
  276. package/dist/runbooks/proxmox.test.js.map +1 -0
  277. package/dist/signatures.d.ts +2 -0
  278. package/dist/signatures.d.ts.map +1 -0
  279. package/dist/signatures.js +2 -0
  280. package/dist/signatures.js.map +1 -0
  281. package/dist/types.d.ts +53 -0
  282. package/dist/types.d.ts.map +1 -1
  283. package/dist/validation.d.ts +6 -1
  284. package/dist/validation.d.ts.map +1 -1
  285. package/dist/validation.js +10 -1
  286. package/dist/validation.js.map +1 -1
  287. package/package.json +1 -1
  288. package/src/index.ts +60 -6
  289. package/src/integrations/citrix.test.ts +592 -0
  290. package/src/integrations/citrix.ts +557 -0
  291. package/src/integrations/graph.test.ts +478 -0
  292. package/src/integrations/graph.ts +413 -0
  293. package/src/integrations/httpbin.ts +72 -0
  294. package/src/integrations/nutanix.test.ts +1508 -0
  295. package/src/integrations/nutanix.ts +1460 -0
  296. package/src/integrations/proxmox.test.ts +1020 -0
  297. package/src/integrations/proxmox.ts +989 -0
  298. package/src/integrations/servicenow.test.ts +314 -0
  299. package/src/integrations/servicenow.ts +285 -0
  300. package/src/integrations/splunk.test.ts +440 -0
  301. package/src/integrations/splunk.ts +356 -0
  302. package/src/mysql/index.ts +14 -0
  303. package/src/mysql/manifest.ts +70 -0
  304. package/src/mysql/probes/databases-list.test.ts +62 -0
  305. package/src/mysql/probes/databases-list.ts +45 -0
  306. package/src/mysql/probes/processlist.test.ts +47 -0
  307. package/src/mysql/probes/processlist.ts +55 -0
  308. package/src/mysql/probes/status.test.ts +50 -0
  309. package/src/mysql/probes/status.ts +56 -0
  310. package/src/nginx/index.ts +14 -0
  311. package/src/nginx/manifest.ts +69 -0
  312. package/src/nginx/probes/access-log-tail.test.ts +51 -0
  313. package/src/nginx/probes/access-log-tail.ts +23 -0
  314. package/src/nginx/probes/config-test.test.ts +47 -0
  315. package/src/nginx/probes/config-test.ts +24 -0
  316. package/src/nginx/probes/error-log-tail.test.ts +44 -0
  317. package/src/nginx/probes/error-log-tail.ts +23 -0
  318. package/src/postgres/index.ts +14 -0
  319. package/src/postgres/manifest.ts +91 -0
  320. package/src/postgres/probes/connections-active.test.ts +42 -0
  321. package/src/postgres/probes/connections-active.ts +55 -0
  322. package/src/postgres/probes/databases-list.test.ts +57 -0
  323. package/src/postgres/probes/databases-list.ts +49 -0
  324. package/src/postgres/probes/query-slow.test.ts +37 -0
  325. package/src/postgres/probes/query-slow.ts +55 -0
  326. package/src/proxmox/index.ts +24 -0
  327. package/src/proxmox/manifest.ts +76 -0
  328. package/src/proxmox/probes/ceph-status.test.ts +126 -0
  329. package/src/proxmox/probes/ceph-status.ts +116 -0
  330. package/src/proxmox/probes/cluster-config.test.ts +118 -0
  331. package/src/proxmox/probes/cluster-config.ts +97 -0
  332. package/src/proxmox/probes/ha-status.test.ts +76 -0
  333. package/src/proxmox/probes/ha-status.ts +56 -0
  334. package/src/proxmox/probes/lvm.test.ts +140 -0
  335. package/src/proxmox/probes/lvm.ts +121 -0
  336. package/src/proxmox/probes/lxc-config.test.ts +89 -0
  337. package/src/proxmox/probes/lxc-config.ts +90 -0
  338. package/src/proxmox/probes/lxc-list.test.ts +60 -0
  339. package/src/proxmox/probes/lxc-list.ts +67 -0
  340. package/src/proxmox/probes/vm-config.test.ts +93 -0
  341. package/src/proxmox/probes/vm-config.ts +77 -0
  342. package/src/proxmox/probes/vm-locks.test.ts +63 -0
  343. package/src/proxmox/probes/vm-locks.ts +49 -0
  344. package/src/redis/index.ts +14 -0
  345. package/src/redis/manifest.ts +52 -0
  346. package/src/redis/probes/info.test.ts +73 -0
  347. package/src/redis/probes/info.ts +46 -0
  348. package/src/redis/probes/keys-count.test.ts +44 -0
  349. package/src/redis/probes/keys-count.ts +38 -0
  350. package/src/redis/probes/memory-usage.test.ts +54 -0
  351. package/src/redis/probes/memory-usage.ts +46 -0
  352. package/src/runbooks/nutanix.test.ts +1138 -0
  353. package/src/runbooks/nutanix.ts +941 -0
  354. package/src/runbooks/proxmox.test.ts +838 -0
  355. package/src/runbooks/proxmox.ts +626 -0
  356. package/src/signatures.ts +1 -0
  357. package/src/types.ts +62 -0
  358. package/src/validation.ts +21 -1
  359. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,1020 @@
1
+ import type { IntegrationConfig, IntegrationCredentials } from '@sonde/shared';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { buildAuthHeaders, proxmoxGet, proxmoxPack, resolveNode } from './proxmox.js';
4
+
5
+ const pveConfig: IntegrationConfig = {
6
+ endpoint: 'https://pve01.local:8006',
7
+ };
8
+
9
+ const pveCreds: IntegrationCredentials = {
10
+ packName: 'proxmox',
11
+ authMethod: 'api_key',
12
+ credentials: { tokenId: 'sonde@pve!sonde-token', tokenSecret: 'secret-uuid' },
13
+ };
14
+
15
+ const handler = (name: string) => {
16
+ const h = proxmoxPack.handlers[name];
17
+ if (!h) throw new Error(`Handler ${name} not found`);
18
+ return h;
19
+ };
20
+
21
+ function callArgs(fn: ReturnType<typeof vi.fn>, index: number): unknown[] {
22
+ const args = fn.mock.calls[index];
23
+ if (!args) throw new Error(`No call at index ${index}`);
24
+ return args;
25
+ }
26
+
27
+ function mockPveResponse(body: unknown, status = 200) {
28
+ return vi.fn().mockResolvedValue(
29
+ new Response(JSON.stringify(body), {
30
+ status,
31
+ headers: { 'Content-Type': 'application/json' },
32
+ }),
33
+ );
34
+ }
35
+
36
+ function mockFetchError(status: number) {
37
+ return vi.fn().mockResolvedValue(new Response('Error', { status, statusText: 'Error' }));
38
+ }
39
+
40
+ describe('proxmox pack', () => {
41
+ describe('auth headers', () => {
42
+ it('builds correct PVEAPIToken header', () => {
43
+ const headers = buildAuthHeaders(pveCreds);
44
+ expect(headers.Authorization).toBe('PVEAPIToken=sonde@pve!sonde-token=secret-uuid');
45
+ });
46
+
47
+ it('handles missing credentials gracefully', () => {
48
+ const emptyCreds: IntegrationCredentials = {
49
+ packName: 'proxmox',
50
+ authMethod: 'api_key',
51
+ credentials: {},
52
+ };
53
+ const headers = buildAuthHeaders(emptyCreds);
54
+ expect(headers.Authorization).toBe('PVEAPIToken==');
55
+ });
56
+ });
57
+
58
+ describe('proxmoxGet', () => {
59
+ it('constructs correct URL with /api2/json prefix', async () => {
60
+ const fetchFn = mockPveResponse({ data: {} });
61
+ await proxmoxGet('/cluster/status', pveConfig, pveCreds, fetchFn);
62
+
63
+ const [url] = callArgs(fetchFn, 0);
64
+ expect(url).toContain('https://pve01.local:8006/api2/json/cluster/status');
65
+ });
66
+
67
+ it('includes query params', async () => {
68
+ const fetchFn = mockPveResponse({ data: [] });
69
+ await proxmoxGet('/cluster/resources', pveConfig, pveCreds, fetchFn, { type: 'vm' });
70
+
71
+ const [url] = callArgs(fetchFn, 0);
72
+ expect(url).toContain('type=vm');
73
+ });
74
+
75
+ it('throws on non-200 response', async () => {
76
+ const fetchFn = mockFetchError(401);
77
+ await expect(proxmoxGet('/version', pveConfig, pveCreds, fetchFn)).rejects.toThrow(
78
+ 'Proxmox API returned 401',
79
+ );
80
+ });
81
+ });
82
+
83
+ describe('testConnection', () => {
84
+ it('returns true on GET /version success', async () => {
85
+ const fetchFn = mockPveResponse({ data: { version: '8.1', release: '1', repoid: 'abc' } });
86
+ const result = await proxmoxPack.testConnection(pveConfig, pveCreds, fetchFn);
87
+ expect(result).toBe(true);
88
+
89
+ const [url] = callArgs(fetchFn, 0);
90
+ expect(url).toContain('/api2/json/version');
91
+ });
92
+
93
+ it('returns false on non-200', async () => {
94
+ const fetchFn = mockFetchError(401);
95
+ const result = await proxmoxPack.testConnection(pveConfig, pveCreds, fetchFn);
96
+ expect(result).toBe(false);
97
+ });
98
+
99
+ it('returns false on network error', async () => {
100
+ const fetchFn = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
101
+ const result = await proxmoxPack.testConnection(pveConfig, pveCreds, fetchFn);
102
+ expect(result).toBe(false);
103
+ });
104
+ });
105
+
106
+ describe('resolveNode', () => {
107
+ it('finds a qemu VM', async () => {
108
+ const fetchFn = mockPveResponse({
109
+ data: [
110
+ { vmid: 100, node: 'pve01', type: 'qemu' },
111
+ { vmid: 200, node: 'pve02', type: 'lxc' },
112
+ ],
113
+ });
114
+ const result = await resolveNode(100, pveConfig, pveCreds, fetchFn);
115
+ expect(result).toEqual({ node: 'pve01', type: 'qemu' });
116
+ });
117
+
118
+ it('finds an LXC container', async () => {
119
+ const fetchFn = mockPveResponse({
120
+ data: [
121
+ { vmid: 100, node: 'pve01', type: 'qemu' },
122
+ { vmid: 200, node: 'pve02', type: 'lxc' },
123
+ ],
124
+ });
125
+ const result = await resolveNode(200, pveConfig, pveCreds, fetchFn);
126
+ expect(result).toEqual({ node: 'pve02', type: 'lxc' });
127
+ });
128
+
129
+ it('throws when VMID not found', async () => {
130
+ const fetchFn = mockPveResponse({ data: [] });
131
+ await expect(resolveNode(999, pveConfig, pveCreds, fetchFn)).rejects.toThrow(
132
+ 'VM/container 999 not found in cluster',
133
+ );
134
+ });
135
+ });
136
+
137
+ describe('cluster.status', () => {
138
+ it('returns cluster info with quorum', async () => {
139
+ const fetchFn = mockPveResponse({
140
+ data: [
141
+ { name: 'mycluster', type: 'cluster', quorate: 1 },
142
+ { name: 'pve01', type: 'node', online: 1, ip: '10.0.0.1' },
143
+ { name: 'pve02', type: 'node', online: 1, ip: '10.0.0.2' },
144
+ ],
145
+ });
146
+
147
+ const result = (await handler('cluster.status')({}, pveConfig, pveCreds, fetchFn)) as {
148
+ clusterName: string;
149
+ quorate: boolean;
150
+ nodes: Array<{ name: string; online: boolean }>;
151
+ warnings: string[];
152
+ };
153
+
154
+ expect(result.clusterName).toBe('mycluster');
155
+ expect(result.quorate).toBe(true);
156
+ expect(result.nodes).toHaveLength(2);
157
+ expect(result.warnings).toHaveLength(0);
158
+ });
159
+
160
+ it('flags offline nodes and lost quorum', async () => {
161
+ const fetchFn = mockPveResponse({
162
+ data: [
163
+ { name: 'mycluster', type: 'cluster', quorate: 0 },
164
+ { name: 'pve01', type: 'node', online: 1 },
165
+ { name: 'pve02', type: 'node', online: 0 },
166
+ ],
167
+ });
168
+
169
+ const result = (await handler('cluster.status')({}, pveConfig, pveCreds, fetchFn)) as {
170
+ warnings: string[];
171
+ };
172
+
173
+ expect(result.warnings).toContain('Cluster has lost quorum');
174
+ expect(result.warnings).toContain('Node pve02 is offline');
175
+ });
176
+ });
177
+
178
+ describe('cluster.ha.status', () => {
179
+ it('returns HA status and resources', async () => {
180
+ let callCount = 0;
181
+ const fetchFn = vi.fn().mockImplementation(() => {
182
+ callCount++;
183
+ if (callCount === 1) {
184
+ return Promise.resolve(
185
+ new Response(
186
+ JSON.stringify({
187
+ data: [{ id: 'manager', type: 'manager', status: 'active' }],
188
+ }),
189
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
190
+ ),
191
+ );
192
+ }
193
+ return Promise.resolve(
194
+ new Response(
195
+ JSON.stringify({
196
+ data: [{ sid: 'vm:100', state: 'started', node: 'pve01', type: 'vm' }],
197
+ }),
198
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
199
+ ),
200
+ );
201
+ });
202
+
203
+ const result = (await handler('cluster.ha.status')({}, pveConfig, pveCreds, fetchFn)) as {
204
+ managerStatus: string;
205
+ resources: Array<{ sid: string; state: string }>;
206
+ warnings: string[];
207
+ };
208
+
209
+ expect(result.managerStatus).toBe('active');
210
+ expect(result.resources).toHaveLength(1);
211
+ expect(result.warnings).toHaveLength(0);
212
+ });
213
+
214
+ it('flags error/fence states', async () => {
215
+ let callCount = 0;
216
+ const fetchFn = vi.fn().mockImplementation(() => {
217
+ callCount++;
218
+ if (callCount === 1) {
219
+ return Promise.resolve(
220
+ new Response(
221
+ JSON.stringify({ data: [{ id: 'manager', type: 'manager', status: 'active' }] }),
222
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
223
+ ),
224
+ );
225
+ }
226
+ return Promise.resolve(
227
+ new Response(
228
+ JSON.stringify({
229
+ data: [
230
+ { sid: 'vm:100', state: 'error', node: 'pve01', type: 'vm' },
231
+ { sid: 'vm:101', state: 'fence', node: 'pve02', type: 'vm' },
232
+ ],
233
+ }),
234
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
235
+ ),
236
+ );
237
+ });
238
+
239
+ const result = (await handler('cluster.ha.status')({}, pveConfig, pveCreds, fetchFn)) as {
240
+ warnings: string[];
241
+ };
242
+
243
+ expect(result.warnings).toContain('HA resource vm:100 in error state');
244
+ expect(result.warnings).toContain('HA resource vm:101 in fence state');
245
+ });
246
+ });
247
+
248
+ describe('nodes.list', () => {
249
+ it('returns node data', async () => {
250
+ const fetchFn = mockPveResponse({
251
+ data: [
252
+ {
253
+ node: 'pve01',
254
+ status: 'online',
255
+ uptime: 86400,
256
+ cpu: 0.25,
257
+ maxcpu: 8,
258
+ mem: 4e9,
259
+ maxmem: 16e9,
260
+ },
261
+ {
262
+ node: 'pve02',
263
+ status: 'online',
264
+ uptime: 172800,
265
+ cpu: 0.5,
266
+ maxcpu: 8,
267
+ mem: 8e9,
268
+ maxmem: 16e9,
269
+ },
270
+ ],
271
+ });
272
+
273
+ const result = (await handler('nodes.list')({}, pveConfig, pveCreds, fetchFn)) as {
274
+ nodes: Array<{ node: string; status: string }>;
275
+ warnings: string[];
276
+ };
277
+
278
+ expect(result.nodes).toHaveLength(2);
279
+ expect(result.warnings).toHaveLength(0);
280
+ });
281
+
282
+ it('flags offline nodes and high resource usage', async () => {
283
+ const fetchFn = mockPveResponse({
284
+ data: [
285
+ { node: 'pve01', status: 'offline', cpu: 0.1, mem: 1e9, maxmem: 16e9 },
286
+ { node: 'pve02', status: 'online', cpu: 0.95, mem: 15e9, maxmem: 16e9 },
287
+ ],
288
+ });
289
+
290
+ const result = (await handler('nodes.list')({}, pveConfig, pveCreds, fetchFn)) as {
291
+ warnings: string[];
292
+ };
293
+
294
+ expect(result.warnings).toContain('Node pve01 is offline');
295
+ expect(result.warnings.some((w: string) => w.includes('pve02 CPU'))).toBe(true);
296
+ expect(result.warnings.some((w: string) => w.includes('pve02 memory'))).toBe(true);
297
+ });
298
+ });
299
+
300
+ describe('node.storage', () => {
301
+ it('returns storage pools', async () => {
302
+ const fetchFn = mockPveResponse({
303
+ data: [
304
+ {
305
+ storage: 'local',
306
+ type: 'dir',
307
+ total: 100e9,
308
+ used: 40e9,
309
+ avail: 60e9,
310
+ enabled: 1,
311
+ active: 1,
312
+ },
313
+ {
314
+ storage: 'ceph-pool',
315
+ type: 'rbd',
316
+ total: 1e12,
317
+ used: 200e9,
318
+ avail: 800e9,
319
+ shared: 1,
320
+ enabled: 1,
321
+ active: 1,
322
+ },
323
+ ],
324
+ });
325
+
326
+ const result = (await handler('node.storage')(
327
+ { node: 'pve01' },
328
+ pveConfig,
329
+ pveCreds,
330
+ fetchFn,
331
+ )) as {
332
+ storages: Array<{ storage: string; shared: boolean }>;
333
+ warnings: string[];
334
+ };
335
+
336
+ expect(result.storages).toHaveLength(2);
337
+ expect(result.storages[1]?.shared).toBe(true);
338
+ expect(result.warnings).toHaveLength(0);
339
+ });
340
+
341
+ it('requires node parameter', async () => {
342
+ const fetchFn = mockPveResponse({});
343
+ await expect(handler('node.storage')({}, pveConfig, pveCreds, fetchFn)).rejects.toThrow(
344
+ 'node parameter is required',
345
+ );
346
+ });
347
+
348
+ it('flags high usage and disabled storage', async () => {
349
+ const fetchFn = mockPveResponse({
350
+ data: [
351
+ {
352
+ storage: 'local',
353
+ type: 'dir',
354
+ total: 100e9,
355
+ used: 90e9,
356
+ avail: 10e9,
357
+ enabled: 1,
358
+ active: 1,
359
+ },
360
+ {
361
+ storage: 'nfs',
362
+ type: 'nfs',
363
+ total: 100e9,
364
+ used: 10e9,
365
+ avail: 90e9,
366
+ enabled: 0,
367
+ active: 0,
368
+ },
369
+ ],
370
+ });
371
+
372
+ const result = (await handler('node.storage')(
373
+ { node: 'pve01' },
374
+ pveConfig,
375
+ pveCreds,
376
+ fetchFn,
377
+ )) as {
378
+ warnings: string[];
379
+ };
380
+
381
+ expect(result.warnings.some((w: string) => w.includes('local') && w.includes('90%'))).toBe(
382
+ true,
383
+ );
384
+ expect(result.warnings).toContain('Storage nfs is disabled');
385
+ });
386
+ });
387
+
388
+ describe('vm.status', () => {
389
+ it('returns VM status', async () => {
390
+ const fetchFn = mockPveResponse({
391
+ data: [
392
+ {
393
+ vmid: 100,
394
+ name: 'web-01',
395
+ status: 'running',
396
+ node: 'pve01',
397
+ type: 'qemu',
398
+ uptime: 3600,
399
+ cpu: 0.1,
400
+ mem: 2e9,
401
+ maxmem: 4e9,
402
+ },
403
+ ],
404
+ });
405
+
406
+ const result = (await handler('vm.status')({ vmid: 100 }, pveConfig, pveCreds, fetchFn)) as {
407
+ vmid: number;
408
+ name: string;
409
+ status: string;
410
+ warnings: string[];
411
+ };
412
+
413
+ expect(result.vmid).toBe(100);
414
+ expect(result.name).toBe('web-01');
415
+ expect(result.warnings).toHaveLength(0);
416
+ });
417
+
418
+ it('throws when VM not found', async () => {
419
+ const fetchFn = mockPveResponse({ data: [] });
420
+ await expect(
421
+ handler('vm.status')({ vmid: 999 }, pveConfig, pveCreds, fetchFn),
422
+ ).rejects.toThrow('VM/container 999 not found');
423
+ });
424
+
425
+ it('requires vmid parameter', async () => {
426
+ const fetchFn = mockPveResponse({});
427
+ await expect(handler('vm.status')({}, pveConfig, pveCreds, fetchFn)).rejects.toThrow(
428
+ 'vmid parameter is required',
429
+ );
430
+ });
431
+
432
+ it('flags stopped, locked, and HA error VMs', async () => {
433
+ const fetchFn = mockPveResponse({
434
+ data: [
435
+ {
436
+ vmid: 100,
437
+ name: 'web-01',
438
+ status: 'stopped',
439
+ node: 'pve01',
440
+ type: 'qemu',
441
+ lock: 'backup',
442
+ hastate: 'error',
443
+ },
444
+ ],
445
+ });
446
+
447
+ const result = (await handler('vm.status')({ vmid: 100 }, pveConfig, pveCreds, fetchFn)) as {
448
+ warnings: string[];
449
+ };
450
+
451
+ expect(result.warnings).toContain('VM is stopped');
452
+ expect(result.warnings).toContain('VM has lock: backup');
453
+ expect(result.warnings).toContain('HA state: error');
454
+ });
455
+ });
456
+
457
+ describe('vm.config', () => {
458
+ it('parses disk entries from config', async () => {
459
+ // First call: resolveNode
460
+ // Second call: config
461
+ let callCount = 0;
462
+ const fetchFn = vi.fn().mockImplementation(() => {
463
+ callCount++;
464
+ if (callCount === 1) {
465
+ return Promise.resolve(
466
+ new Response(JSON.stringify({ data: [{ vmid: 100, node: 'pve01', type: 'qemu' }] }), {
467
+ status: 200,
468
+ headers: { 'Content-Type': 'application/json' },
469
+ }),
470
+ );
471
+ }
472
+ return Promise.resolve(
473
+ new Response(
474
+ JSON.stringify({
475
+ data: {
476
+ name: 'web-01',
477
+ memory: 4096,
478
+ cores: 2,
479
+ scsi0: 'local-lvm:vm-100-disk-0,size=32G',
480
+ ide2: 'none,media=cdrom',
481
+ net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0',
482
+ },
483
+ }),
484
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
485
+ ),
486
+ );
487
+ });
488
+
489
+ const result = (await handler('vm.config')({ vmid: 100 }, pveConfig, pveCreds, fetchFn)) as {
490
+ vmid: number;
491
+ node: string;
492
+ disks: Array<{ key: string; storage: string; size: string }>;
493
+ warnings: string[];
494
+ };
495
+
496
+ expect(result.vmid).toBe(100);
497
+ expect(result.node).toBe('pve01');
498
+ expect(result.disks).toHaveLength(1);
499
+ expect(result.disks[0]?.storage).toBe('local-lvm');
500
+ expect(result.disks[0]?.size).toBe('32G');
501
+ expect(result.warnings).toContain(
502
+ 'Disk scsi0 uses local storage (local-lvm) — not shared for HA',
503
+ );
504
+ });
505
+
506
+ it('uses provided node without resolving', async () => {
507
+ const fetchFn = mockPveResponse({
508
+ data: { name: 'web-01', memory: 4096 },
509
+ });
510
+
511
+ const result = (await handler('vm.config')(
512
+ { vmid: 100, node: 'pve01' },
513
+ pveConfig,
514
+ pveCreds,
515
+ fetchFn,
516
+ )) as { node: string };
517
+
518
+ expect(result.node).toBe('pve01');
519
+ // Only 1 call (no resolveNode)
520
+ expect(fetchFn).toHaveBeenCalledTimes(1);
521
+ });
522
+ });
523
+
524
+ describe('vm.snapshots', () => {
525
+ it('returns snapshots and filters "current"', async () => {
526
+ const now = Math.floor(Date.now() / 1000);
527
+ const fetchFn = mockPveResponse({
528
+ data: [
529
+ { name: 'current', description: 'You are here!' },
530
+ {
531
+ name: 'pre-upgrade',
532
+ description: 'Before upgrade',
533
+ snaptime: now - 3600,
534
+ parent: 'current',
535
+ },
536
+ ],
537
+ });
538
+
539
+ const result = (await handler('vm.snapshots')(
540
+ { vmid: 100, node: 'pve01' },
541
+ pveConfig,
542
+ pveCreds,
543
+ fetchFn,
544
+ )) as {
545
+ snapshots: Array<{ name: string }>;
546
+ warnings: string[];
547
+ };
548
+
549
+ expect(result.snapshots).toHaveLength(1);
550
+ expect(result.snapshots[0]?.name).toBe('pre-upgrade');
551
+ expect(result.warnings).toHaveLength(0);
552
+ });
553
+
554
+ it('flags old snapshots', async () => {
555
+ const eightDaysAgo = Math.floor(Date.now() / 1000) - 8 * 24 * 60 * 60;
556
+ const fetchFn = mockPveResponse({
557
+ data: [{ name: 'old-snap', description: 'Old one', snaptime: eightDaysAgo }],
558
+ });
559
+
560
+ const result = (await handler('vm.snapshots')(
561
+ { vmid: 100, node: 'pve01' },
562
+ pveConfig,
563
+ pveCreds,
564
+ fetchFn,
565
+ )) as { warnings: string[] };
566
+
567
+ expect(result.warnings.some((w: string) => w.includes('older than 7 days'))).toBe(true);
568
+ });
569
+ });
570
+
571
+ describe('storage.content', () => {
572
+ it('returns volumes', async () => {
573
+ const fetchFn = mockPveResponse({
574
+ data: [
575
+ { volid: 'local-lvm:vm-100-disk-0', vmid: 100, size: 32e9, format: 'raw' },
576
+ { volid: 'local-lvm:vm-101-disk-0', vmid: 101, size: 64e9, format: 'raw' },
577
+ ],
578
+ });
579
+
580
+ const result = (await handler('storage.content')(
581
+ { node: 'pve01', storage: 'local-lvm' },
582
+ pveConfig,
583
+ pveCreds,
584
+ fetchFn,
585
+ )) as { volumes: unknown[]; count: number };
586
+
587
+ expect(result.count).toBe(2);
588
+ });
589
+
590
+ it('filters by vmid', async () => {
591
+ const fetchFn = mockPveResponse({
592
+ data: [
593
+ { volid: 'local-lvm:vm-100-disk-0', vmid: 100, size: 32e9 },
594
+ { volid: 'local-lvm:vm-101-disk-0', vmid: 101, size: 64e9 },
595
+ ],
596
+ });
597
+
598
+ const result = (await handler('storage.content')(
599
+ { node: 'pve01', storage: 'local-lvm', vmid: 100 },
600
+ pveConfig,
601
+ pveCreds,
602
+ fetchFn,
603
+ )) as { count: number };
604
+
605
+ expect(result.count).toBe(1);
606
+ });
607
+
608
+ it('requires node and storage', async () => {
609
+ const fetchFn = mockPveResponse({});
610
+ await expect(handler('storage.content')({}, pveConfig, pveCreds, fetchFn)).rejects.toThrow(
611
+ 'node parameter is required',
612
+ );
613
+
614
+ await expect(
615
+ handler('storage.content')({ node: 'pve01' }, pveConfig, pveCreds, fetchFn),
616
+ ).rejects.toThrow('storage parameter is required');
617
+ });
618
+ });
619
+
620
+ describe('cluster.tasks', () => {
621
+ it('returns tasks', async () => {
622
+ const fetchFn = mockPveResponse({
623
+ data: [
624
+ {
625
+ upid: 'UPID:pve01:001',
626
+ type: 'qmstart',
627
+ status: 'OK',
628
+ starttime: 1000,
629
+ endtime: 1010,
630
+ node: 'pve01',
631
+ user: 'root@pam',
632
+ id: '100',
633
+ },
634
+ ],
635
+ });
636
+
637
+ const result = (await handler('cluster.tasks')({}, pveConfig, pveCreds, fetchFn)) as {
638
+ tasks: Array<{ type: string }>;
639
+ warnings: string[];
640
+ };
641
+
642
+ expect(result.tasks).toHaveLength(1);
643
+ expect(result.warnings).toHaveLength(0);
644
+ });
645
+
646
+ it('filters by vmid', async () => {
647
+ const fetchFn = mockPveResponse({
648
+ data: [
649
+ { upid: 'UPID:1', type: 'qmstart', status: 'OK', node: 'pve01', id: '100', endtime: 1 },
650
+ { upid: 'UPID:2', type: 'qmstart', status: 'OK', node: 'pve01', id: '101', endtime: 2 },
651
+ ],
652
+ });
653
+
654
+ const result = (await handler('cluster.tasks')(
655
+ { vmid: 100 },
656
+ pveConfig,
657
+ pveCreds,
658
+ fetchFn,
659
+ )) as { tasks: unknown[] };
660
+
661
+ expect(result.tasks).toHaveLength(1);
662
+ });
663
+
664
+ it('flags failed tasks and running migrations', async () => {
665
+ const fetchFn = mockPveResponse({
666
+ data: [
667
+ {
668
+ upid: 'UPID:1',
669
+ type: 'qmstart',
670
+ status: 'WARNINGS: something',
671
+ node: 'pve01',
672
+ endtime: 1,
673
+ },
674
+ { upid: 'UPID:2', type: 'qmigrate', status: '', node: 'pve02' },
675
+ ],
676
+ });
677
+
678
+ const result = (await handler('cluster.tasks')({}, pveConfig, pveCreds, fetchFn)) as {
679
+ warnings: string[];
680
+ };
681
+
682
+ expect(result.warnings.some((w: string) => w.includes('failed'))).toBe(true);
683
+ expect(result.warnings.some((w: string) => w.includes('Migration in progress'))).toBe(true);
684
+ });
685
+ });
686
+
687
+ describe('node.lvm', () => {
688
+ it('returns volume groups', async () => {
689
+ const fetchFn = mockPveResponse({
690
+ data: [{ name: 'pve', size: 500e9, free: 200e9, pvs: 1, lvs: 3 }],
691
+ });
692
+
693
+ const result = (await handler('node.lvm')(
694
+ { node: 'pve01' },
695
+ pveConfig,
696
+ pveCreds,
697
+ fetchFn,
698
+ )) as {
699
+ volumeGroups: Array<{ name: string }>;
700
+ warnings: string[];
701
+ };
702
+
703
+ expect(result.volumeGroups).toHaveLength(1);
704
+ expect(result.warnings).toHaveLength(0);
705
+ });
706
+
707
+ it('flags VGs with no free space', async () => {
708
+ const fetchFn = mockPveResponse({
709
+ data: [{ name: 'pve', size: 500e9, free: 0, pvs: 1, lvs: 5 }],
710
+ });
711
+
712
+ const result = (await handler('node.lvm')(
713
+ { node: 'pve01' },
714
+ pveConfig,
715
+ pveCreds,
716
+ fetchFn,
717
+ )) as {
718
+ warnings: string[];
719
+ };
720
+
721
+ expect(result.warnings).toContain('Volume group pve has no free space');
722
+ });
723
+
724
+ it('requires node parameter', async () => {
725
+ const fetchFn = mockPveResponse({});
726
+ await expect(handler('node.lvm')({}, pveConfig, pveCreds, fetchFn)).rejects.toThrow(
727
+ 'node parameter is required',
728
+ );
729
+ });
730
+ });
731
+
732
+ describe('lxc.status', () => {
733
+ it('returns container status', async () => {
734
+ // resolveNode call
735
+ let callCount = 0;
736
+ const fetchFn = vi.fn().mockImplementation(() => {
737
+ callCount++;
738
+ if (callCount === 1) {
739
+ return Promise.resolve(
740
+ new Response(JSON.stringify({ data: [{ vmid: 200, node: 'pve02', type: 'lxc' }] }), {
741
+ status: 200,
742
+ headers: { 'Content-Type': 'application/json' },
743
+ }),
744
+ );
745
+ }
746
+ return Promise.resolve(
747
+ new Response(
748
+ JSON.stringify({
749
+ data: {
750
+ vmid: 200,
751
+ name: 'ct-nginx',
752
+ status: 'running',
753
+ uptime: 7200,
754
+ cpu: 0.05,
755
+ mem: 256e6,
756
+ maxmem: 512e6,
757
+ disk: 1e9,
758
+ maxdisk: 8e9,
759
+ swap: 0,
760
+ maxswap: 512e6,
761
+ },
762
+ }),
763
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
764
+ ),
765
+ );
766
+ });
767
+
768
+ const result = (await handler('lxc.status')({ vmid: 200 }, pveConfig, pveCreds, fetchFn)) as {
769
+ vmid: number;
770
+ name: string;
771
+ status: string;
772
+ node: string;
773
+ warnings: string[];
774
+ };
775
+
776
+ expect(result.vmid).toBe(200);
777
+ expect(result.name).toBe('ct-nginx');
778
+ expect(result.node).toBe('pve02');
779
+ expect(result.warnings).toHaveLength(0);
780
+ });
781
+
782
+ it('rejects qemu VMID', async () => {
783
+ const fetchFn = mockPveResponse({
784
+ data: [{ vmid: 100, node: 'pve01', type: 'qemu' }],
785
+ });
786
+
787
+ await expect(
788
+ handler('lxc.status')({ vmid: 100 }, pveConfig, pveCreds, fetchFn),
789
+ ).rejects.toThrow('not an LXC container');
790
+ });
791
+ });
792
+
793
+ describe('lxc.config', () => {
794
+ it('parses rootfs and mountpoints', async () => {
795
+ const fetchFn = mockPveResponse({
796
+ data: {
797
+ hostname: 'ct-nginx',
798
+ rootfs: 'local-lvm:subvol-200-disk-0,size=8G',
799
+ mp0: 'local-lvm:subvol-200-disk-1,mp=/data,size=20G',
800
+ cores: 2,
801
+ memory: 512,
802
+ },
803
+ });
804
+
805
+ const result = (await handler('lxc.config')(
806
+ { vmid: 200, node: 'pve02' },
807
+ pveConfig,
808
+ pveCreds,
809
+ fetchFn,
810
+ )) as {
811
+ rootfs: { storage: string; size: string };
812
+ mountpoints: Array<{ key: string; storage: string; mountpoint: string; size: string }>;
813
+ };
814
+
815
+ expect(result.rootfs).toEqual({ storage: 'local-lvm', size: '8G' });
816
+ expect(result.mountpoints).toHaveLength(1);
817
+ expect(result.mountpoints[0]?.mountpoint).toBe('/data');
818
+ expect(result.mountpoints[0]?.size).toBe('20G');
819
+ });
820
+ });
821
+
822
+ describe('ceph.status', () => {
823
+ it('returns ceph health and OSDs', async () => {
824
+ let callCount = 0;
825
+ const fetchFn = vi.fn().mockImplementation(() => {
826
+ callCount++;
827
+ if (callCount === 1) {
828
+ // /cluster/ceph/status
829
+ return Promise.resolve(
830
+ new Response(
831
+ JSON.stringify({
832
+ data: {
833
+ health: { status: 'HEALTH_OK' },
834
+ osdmap: { osdmap: { num_osds: 6, num_up_osds: 6, num_in_osds: 6 } },
835
+ pgmap: {
836
+ pgs_by_state: [{ state_name: 'active+clean', count: 256 }],
837
+ bytes_total: 6e12,
838
+ bytes_used: 2e12,
839
+ bytes_avail: 4e12,
840
+ },
841
+ },
842
+ }),
843
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
844
+ ),
845
+ );
846
+ }
847
+ if (callCount === 2) {
848
+ // /nodes
849
+ return Promise.resolve(
850
+ new Response(JSON.stringify({ data: [{ node: 'pve01', status: 'online' }] }), {
851
+ status: 200,
852
+ headers: { 'Content-Type': 'application/json' },
853
+ }),
854
+ );
855
+ }
856
+ // /nodes/pve01/ceph/osd
857
+ return Promise.resolve(
858
+ new Response(
859
+ JSON.stringify({
860
+ data: [
861
+ { id: 0, name: 'osd.0', up: 1 },
862
+ { id: 1, name: 'osd.1', up: 1 },
863
+ ],
864
+ }),
865
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
866
+ ),
867
+ );
868
+ });
869
+
870
+ const result = (await handler('ceph.status')({}, pveConfig, pveCreds, fetchFn)) as {
871
+ available: boolean;
872
+ health: string;
873
+ osdCount: number;
874
+ osdUp: number;
875
+ osds: Array<{ id: number; status: string }>;
876
+ warnings: string[];
877
+ };
878
+
879
+ expect(result.available).toBe(true);
880
+ expect(result.health).toBe('HEALTH_OK');
881
+ expect(result.osdCount).toBe(6);
882
+ expect(result.osds).toHaveLength(2);
883
+ expect(result.warnings).toHaveLength(0);
884
+ });
885
+
886
+ it('handles degraded ceph', async () => {
887
+ const fetchFn = vi.fn().mockImplementation(() => {
888
+ return Promise.resolve(
889
+ new Response(
890
+ JSON.stringify({
891
+ data: {
892
+ health: { status: 'HEALTH_WARN' },
893
+ osdmap: { osdmap: { num_osds: 6, num_up_osds: 4, num_in_osds: 6 } },
894
+ pgmap: { bytes_total: 6e12, bytes_used: 2e12, bytes_avail: 4e12 },
895
+ },
896
+ }),
897
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
898
+ ),
899
+ );
900
+ });
901
+
902
+ const result = (await handler('ceph.status')({}, pveConfig, pveCreds, fetchFn)) as {
903
+ warnings: string[];
904
+ };
905
+
906
+ expect(result.warnings.some((w: string) => w.includes('HEALTH_WARN'))).toBe(true);
907
+ expect(result.warnings.some((w: string) => w.includes('2 OSD(s) down'))).toBe(true);
908
+ });
909
+
910
+ it('handles no ceph (404)', async () => {
911
+ const fetchFn = vi
912
+ .fn()
913
+ .mockResolvedValue(new Response('Not Found', { status: 404, statusText: 'Not Found' }));
914
+
915
+ const result = (await handler('ceph.status')({}, pveConfig, pveCreds, fetchFn)) as {
916
+ available: boolean;
917
+ warnings: string[];
918
+ };
919
+
920
+ expect(result.available).toBe(false);
921
+ expect(result.warnings).toContain('Ceph is not configured on this cluster');
922
+ });
923
+ });
924
+
925
+ describe('cluster.resources', () => {
926
+ it('returns all VMs and containers', async () => {
927
+ const fetchFn = mockPveResponse({
928
+ data: [
929
+ {
930
+ vmid: 100,
931
+ name: 'web-01',
932
+ node: 'pve01',
933
+ type: 'qemu',
934
+ status: 'running',
935
+ uptime: 3600,
936
+ cpu: 0.1,
937
+ mem: 2e9,
938
+ maxmem: 4e9,
939
+ },
940
+ {
941
+ vmid: 200,
942
+ name: 'ct-db',
943
+ node: 'pve02',
944
+ type: 'lxc',
945
+ status: 'running',
946
+ uptime: 7200,
947
+ cpu: 0.05,
948
+ mem: 512e6,
949
+ maxmem: 1e9,
950
+ },
951
+ { vmid: 101, name: 'stopped-vm', node: 'pve01', type: 'qemu', status: 'stopped' },
952
+ ],
953
+ });
954
+
955
+ const result = (await handler('cluster.resources')({}, pveConfig, pveCreds, fetchFn)) as {
956
+ resources: Array<{
957
+ vmid: number;
958
+ name: string | null;
959
+ node: string;
960
+ type: string;
961
+ status: string;
962
+ }>;
963
+ };
964
+
965
+ expect(result.resources).toHaveLength(3);
966
+ expect(result.resources[0]?.vmid).toBe(100);
967
+ expect(result.resources[0]?.type).toBe('qemu');
968
+ expect(result.resources[1]?.type).toBe('lxc');
969
+ });
970
+
971
+ it('calls /cluster/resources?type=vm', async () => {
972
+ const fetchFn = mockPveResponse({ data: [] });
973
+ await handler('cluster.resources')({}, pveConfig, pveCreds, fetchFn);
974
+
975
+ const [url] = callArgs(fetchFn, 0);
976
+ expect(url).toContain('/cluster/resources');
977
+ expect(url).toContain('type=vm');
978
+ });
979
+ });
980
+
981
+ describe('manifest', () => {
982
+ it('has correct name and 14 probes', () => {
983
+ expect(proxmoxPack.manifest.name).toBe('proxmox');
984
+ expect(proxmoxPack.manifest.probes).toHaveLength(14);
985
+ });
986
+
987
+ it('all handlers match manifest probes', () => {
988
+ const probeNames = proxmoxPack.manifest.probes.map((p) => p.name);
989
+ const handlerNames = Object.keys(proxmoxPack.handlers);
990
+ expect(handlerNames.sort()).toEqual(probeNames.sort());
991
+ });
992
+
993
+ it('has correct timeouts (30s for ceph, 15s for others)', () => {
994
+ const probeMap = new Map(proxmoxPack.manifest.probes.map((p) => [p.name, p.timeout]));
995
+ expect(probeMap.get('ceph.status')).toBe(30000);
996
+ for (const [name, timeout] of probeMap) {
997
+ if (name !== 'ceph.status') {
998
+ expect(timeout).toBe(15000);
999
+ }
1000
+ }
1001
+ });
1002
+
1003
+ it('has virtualization runbook', () => {
1004
+ expect(proxmoxPack.manifest.runbook).toEqual({
1005
+ category: 'virtualization',
1006
+ probes: ['cluster.status', 'nodes.list', 'ceph.status'],
1007
+ parallel: true,
1008
+ });
1009
+ });
1010
+ });
1011
+
1012
+ describe('error handling', () => {
1013
+ it('throws on non-200 API response for probes', async () => {
1014
+ const fetchFn = mockFetchError(403);
1015
+ await expect(handler('nodes.list')({}, pveConfig, pveCreds, fetchFn)).rejects.toThrow(
1016
+ 'Proxmox API returned 403',
1017
+ );
1018
+ });
1019
+ });
1020
+ });