@build-astron-co/nimbus 0.4.2 → 0.4.3

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 (430) hide show
  1. package/dist/src/agent/compaction-agent.js +24 -12
  2. package/dist/src/agent/context-manager.js +2 -1
  3. package/dist/src/agent/expand-files.js +2 -1
  4. package/dist/src/agent/loop.js +71 -33
  5. package/dist/src/agent/permissions.js +4 -2
  6. package/dist/src/agent/system-prompt.js +34 -17
  7. package/dist/src/app.js +1 -1
  8. package/dist/src/auth/keychain.js +8 -4
  9. package/dist/src/auth/store.js +70 -107
  10. package/dist/src/cli/init.js +35 -19
  11. package/dist/src/cli/run.js +18 -10
  12. package/dist/src/cli/serve.js +4 -2
  13. package/dist/src/cli.js +52 -11
  14. package/dist/src/commands/alias.js +5 -3
  15. package/dist/src/commands/audit/index.js +2 -1
  16. package/dist/src/commands/aws-terraform.js +36 -18
  17. package/dist/src/commands/completions.js +1 -1
  18. package/dist/src/commands/config.js +3 -2
  19. package/dist/src/commands/connect-github.js +92 -0
  20. package/dist/src/commands/cost/index.js +3 -2
  21. package/dist/src/commands/deploy.js +15 -10
  22. package/dist/src/commands/doctor.js +6 -3
  23. package/dist/src/commands/drift/index.js +2 -1
  24. package/dist/src/commands/export.js +5 -3
  25. package/dist/src/commands/generate-terraform.js +110 -2
  26. package/dist/src/commands/import.js +3 -3
  27. package/dist/src/commands/incident.js +10 -5
  28. package/dist/src/commands/login.js +8 -93
  29. package/dist/src/commands/logs.js +16 -8
  30. package/dist/src/commands/onboarding.js +6 -4
  31. package/dist/src/commands/pipeline.js +6 -3
  32. package/dist/src/commands/plugin.js +3 -2
  33. package/dist/src/commands/profile.js +27 -14
  34. package/dist/src/commands/questionnaire.js +1 -1
  35. package/dist/src/commands/rollback.js +3 -2
  36. package/dist/src/commands/rollout.js +5 -3
  37. package/dist/src/commands/runbook.js +17 -10
  38. package/dist/src/commands/schedule.js +10 -5
  39. package/dist/src/commands/status.js +2 -1
  40. package/dist/src/commands/team-context.js +12 -7
  41. package/dist/src/commands/template.js +1 -1
  42. package/dist/src/commands/tf/index.js +6 -3
  43. package/dist/src/commands/version.js +6 -3
  44. package/dist/src/commands/watch.js +6 -3
  45. package/dist/src/compat/sqlite.js +5 -3
  46. package/dist/src/config/mode-store.js +2 -1
  47. package/dist/src/config/profiles.js +4 -2
  48. package/dist/src/config/types.js +2 -1
  49. package/dist/src/engine/executor.js +8 -4
  50. package/dist/src/engine/planner.js +9 -5
  51. package/dist/src/llm/providers/anthropic.js +6 -3
  52. package/dist/src/llm/providers/ollama.js +1 -1
  53. package/dist/src/llm/router.js +22 -7
  54. package/dist/src/sessions/manager.js +6 -3
  55. package/dist/src/sharing/viewer.js +2 -1
  56. package/dist/src/tools/file-ops.js +1 -2
  57. package/dist/src/tools/schemas/devops.js +197 -108
  58. package/dist/src/tools/schemas/standard.js +1 -1
  59. package/dist/src/ui/App.js +25 -13
  60. package/dist/src/ui/FileDiffModal.js +22 -11
  61. package/dist/src/ui/HelpModal.js +2 -1
  62. package/dist/src/ui/InputBox.js +6 -3
  63. package/dist/src/ui/MessageList.js +40 -20
  64. package/dist/src/ui/TerminalPane.js +2 -1
  65. package/dist/src/ui/ToolCallDisplay.js +12 -6
  66. package/dist/src/ui/TreePane.js +2 -1
  67. package/dist/src/ui/ink/index.js +37 -21
  68. package/dist/src/watcher/index.js +8 -4
  69. package/package.json +3 -5
  70. package/src/__tests__/alias.test.ts +0 -133
  71. package/src/__tests__/app.test.ts +0 -76
  72. package/src/__tests__/audit.test.ts +0 -877
  73. package/src/__tests__/circuit-breaker.test.ts +0 -116
  74. package/src/__tests__/cli-run.test.ts +0 -351
  75. package/src/__tests__/compat-sqlite.test.ts +0 -68
  76. package/src/__tests__/context-manager.test.ts +0 -632
  77. package/src/__tests__/context.test.ts +0 -242
  78. package/src/__tests__/devops-terminal-gaps.test.ts +0 -718
  79. package/src/__tests__/doctor.test.ts +0 -48
  80. package/src/__tests__/enterprise.test.ts +0 -401
  81. package/src/__tests__/export.test.ts +0 -236
  82. package/src/__tests__/gap-11-18-20.test.ts +0 -958
  83. package/src/__tests__/generator.test.ts +0 -433
  84. package/src/__tests__/helm-streaming.test.ts +0 -127
  85. package/src/__tests__/hooks.test.ts +0 -582
  86. package/src/__tests__/incident.test.ts +0 -179
  87. package/src/__tests__/init.test.ts +0 -487
  88. package/src/__tests__/intent-parser.test.ts +0 -229
  89. package/src/__tests__/llm-router.test.ts +0 -209
  90. package/src/__tests__/logs.test.ts +0 -107
  91. package/src/__tests__/loop-errors.test.ts +0 -244
  92. package/src/__tests__/lsp.test.ts +0 -293
  93. package/src/__tests__/modes.test.ts +0 -336
  94. package/src/__tests__/perf-optimizations.test.ts +0 -847
  95. package/src/__tests__/permissions.test.ts +0 -338
  96. package/src/__tests__/pipeline.test.ts +0 -50
  97. package/src/__tests__/polish-phase3.test.ts +0 -340
  98. package/src/__tests__/profile.test.ts +0 -237
  99. package/src/__tests__/rollback.test.ts +0 -83
  100. package/src/__tests__/runbook.test.ts +0 -219
  101. package/src/__tests__/schedule.test.ts +0 -206
  102. package/src/__tests__/serve.test.ts +0 -275
  103. package/src/__tests__/sessions.test.ts +0 -322
  104. package/src/__tests__/sharing.test.ts +0 -340
  105. package/src/__tests__/snapshots.test.ts +0 -581
  106. package/src/__tests__/standalone-migration.test.ts +0 -199
  107. package/src/__tests__/state-db.test.ts +0 -334
  108. package/src/__tests__/status.test.ts +0 -158
  109. package/src/__tests__/stream-with-tools.test.ts +0 -778
  110. package/src/__tests__/subagents.test.ts +0 -176
  111. package/src/__tests__/system-prompt.test.ts +0 -248
  112. package/src/__tests__/terminal-gap-v2.test.ts +0 -395
  113. package/src/__tests__/terminal-parity.test.ts +0 -393
  114. package/src/__tests__/tf-apply.test.ts +0 -187
  115. package/src/__tests__/tool-converter.test.ts +0 -256
  116. package/src/__tests__/tool-schemas.test.ts +0 -602
  117. package/src/__tests__/tools.test.ts +0 -144
  118. package/src/__tests__/version-json.test.ts +0 -184
  119. package/src/__tests__/version.test.ts +0 -49
  120. package/src/__tests__/watch.test.ts +0 -129
  121. package/src/agent/compaction-agent.ts +0 -266
  122. package/src/agent/context-manager.ts +0 -499
  123. package/src/agent/context.ts +0 -427
  124. package/src/agent/deploy-preview.ts +0 -487
  125. package/src/agent/expand-files.ts +0 -108
  126. package/src/agent/index.ts +0 -68
  127. package/src/agent/loop.ts +0 -1998
  128. package/src/agent/modes.ts +0 -429
  129. package/src/agent/permissions.ts +0 -513
  130. package/src/agent/subagents/base.ts +0 -116
  131. package/src/agent/subagents/cost.ts +0 -51
  132. package/src/agent/subagents/explore.ts +0 -42
  133. package/src/agent/subagents/general.ts +0 -54
  134. package/src/agent/subagents/index.ts +0 -102
  135. package/src/agent/subagents/infra.ts +0 -59
  136. package/src/agent/subagents/security.ts +0 -69
  137. package/src/agent/system-prompt.ts +0 -990
  138. package/src/app.ts +0 -180
  139. package/src/audit/activity-log.ts +0 -290
  140. package/src/audit/compliance-checker.ts +0 -540
  141. package/src/audit/cost-tracker.ts +0 -318
  142. package/src/audit/index.ts +0 -23
  143. package/src/audit/security-scanner.ts +0 -641
  144. package/src/auth/guard.ts +0 -75
  145. package/src/auth/index.ts +0 -56
  146. package/src/auth/keychain.ts +0 -82
  147. package/src/auth/oauth.ts +0 -465
  148. package/src/auth/providers.ts +0 -470
  149. package/src/auth/sso.ts +0 -113
  150. package/src/auth/store.ts +0 -505
  151. package/src/auth/types.ts +0 -187
  152. package/src/build.ts +0 -141
  153. package/src/cli/index.ts +0 -16
  154. package/src/cli/init.ts +0 -1227
  155. package/src/cli/openapi-spec.ts +0 -356
  156. package/src/cli/run.ts +0 -628
  157. package/src/cli/serve-auth.ts +0 -80
  158. package/src/cli/serve.ts +0 -539
  159. package/src/cli/web.ts +0 -71
  160. package/src/cli.ts +0 -1728
  161. package/src/clients/core-engine-client.ts +0 -227
  162. package/src/clients/enterprise-client.ts +0 -334
  163. package/src/clients/generator-client.ts +0 -351
  164. package/src/clients/git-client.ts +0 -627
  165. package/src/clients/github-client.ts +0 -410
  166. package/src/clients/helm-client.ts +0 -504
  167. package/src/clients/index.ts +0 -80
  168. package/src/clients/k8s-client.ts +0 -497
  169. package/src/clients/llm-client.ts +0 -161
  170. package/src/clients/rest-client.ts +0 -130
  171. package/src/clients/service-discovery.ts +0 -38
  172. package/src/clients/terraform-client.ts +0 -482
  173. package/src/clients/tools-client.ts +0 -1843
  174. package/src/clients/ws-client.ts +0 -115
  175. package/src/commands/alias.ts +0 -100
  176. package/src/commands/analyze/index.ts +0 -352
  177. package/src/commands/apply/helm.ts +0 -473
  178. package/src/commands/apply/index.ts +0 -213
  179. package/src/commands/apply/k8s.ts +0 -454
  180. package/src/commands/apply/terraform.ts +0 -582
  181. package/src/commands/ask.ts +0 -167
  182. package/src/commands/audit/index.ts +0 -357
  183. package/src/commands/auth-cloud.ts +0 -407
  184. package/src/commands/auth-list.ts +0 -134
  185. package/src/commands/auth-profile.ts +0 -121
  186. package/src/commands/auth-refresh.ts +0 -187
  187. package/src/commands/auth-status.ts +0 -141
  188. package/src/commands/aws/ec2.ts +0 -501
  189. package/src/commands/aws/iam.ts +0 -397
  190. package/src/commands/aws/index.ts +0 -133
  191. package/src/commands/aws/lambda.ts +0 -396
  192. package/src/commands/aws/rds.ts +0 -439
  193. package/src/commands/aws/s3.ts +0 -439
  194. package/src/commands/aws/vpc.ts +0 -393
  195. package/src/commands/aws-discover.ts +0 -542
  196. package/src/commands/aws-terraform.ts +0 -755
  197. package/src/commands/azure/aks.ts +0 -376
  198. package/src/commands/azure/functions.ts +0 -253
  199. package/src/commands/azure/index.ts +0 -116
  200. package/src/commands/azure/storage.ts +0 -478
  201. package/src/commands/azure/vm.ts +0 -355
  202. package/src/commands/billing/index.ts +0 -256
  203. package/src/commands/chat.ts +0 -320
  204. package/src/commands/completions.ts +0 -268
  205. package/src/commands/config.ts +0 -372
  206. package/src/commands/cost/cloud-cost-estimator.ts +0 -266
  207. package/src/commands/cost/estimator.ts +0 -79
  208. package/src/commands/cost/index.ts +0 -810
  209. package/src/commands/cost/parsers/terraform.ts +0 -273
  210. package/src/commands/cost/parsers/types.ts +0 -25
  211. package/src/commands/cost/pricing/aws.ts +0 -544
  212. package/src/commands/cost/pricing/azure.ts +0 -499
  213. package/src/commands/cost/pricing/gcp.ts +0 -396
  214. package/src/commands/cost/pricing/index.ts +0 -40
  215. package/src/commands/demo.ts +0 -250
  216. package/src/commands/deploy.ts +0 -260
  217. package/src/commands/doctor.ts +0 -1386
  218. package/src/commands/drift/index.ts +0 -787
  219. package/src/commands/explain.ts +0 -277
  220. package/src/commands/export.ts +0 -146
  221. package/src/commands/feedback.ts +0 -389
  222. package/src/commands/fix.ts +0 -324
  223. package/src/commands/fs/index.ts +0 -402
  224. package/src/commands/gcp/compute.ts +0 -325
  225. package/src/commands/gcp/functions.ts +0 -271
  226. package/src/commands/gcp/gke.ts +0 -438
  227. package/src/commands/gcp/iam.ts +0 -344
  228. package/src/commands/gcp/index.ts +0 -129
  229. package/src/commands/gcp/storage.ts +0 -284
  230. package/src/commands/generate-helm.ts +0 -1249
  231. package/src/commands/generate-k8s.ts +0 -1508
  232. package/src/commands/generate-terraform.ts +0 -1202
  233. package/src/commands/gh/index.ts +0 -863
  234. package/src/commands/git/index.ts +0 -1343
  235. package/src/commands/helm/index.ts +0 -1126
  236. package/src/commands/help.ts +0 -715
  237. package/src/commands/history.ts +0 -149
  238. package/src/commands/import.ts +0 -868
  239. package/src/commands/incident.ts +0 -166
  240. package/src/commands/index.ts +0 -367
  241. package/src/commands/init.ts +0 -1051
  242. package/src/commands/k8s/index.ts +0 -1137
  243. package/src/commands/login.ts +0 -716
  244. package/src/commands/logout.ts +0 -83
  245. package/src/commands/logs.ts +0 -167
  246. package/src/commands/onboarding.ts +0 -405
  247. package/src/commands/pipeline.ts +0 -186
  248. package/src/commands/plan/display.ts +0 -279
  249. package/src/commands/plan/index.ts +0 -599
  250. package/src/commands/plugin.ts +0 -398
  251. package/src/commands/preview.ts +0 -452
  252. package/src/commands/profile.ts +0 -342
  253. package/src/commands/questionnaire.ts +0 -1172
  254. package/src/commands/resume.ts +0 -47
  255. package/src/commands/rollback.ts +0 -315
  256. package/src/commands/rollout.ts +0 -88
  257. package/src/commands/runbook.ts +0 -346
  258. package/src/commands/schedule.ts +0 -236
  259. package/src/commands/status.ts +0 -252
  260. package/src/commands/team/index.ts +0 -346
  261. package/src/commands/team-context.ts +0 -220
  262. package/src/commands/template.ts +0 -233
  263. package/src/commands/tf/index.ts +0 -1093
  264. package/src/commands/upgrade.ts +0 -609
  265. package/src/commands/usage/index.ts +0 -134
  266. package/src/commands/version.ts +0 -174
  267. package/src/commands/watch.ts +0 -153
  268. package/src/compat/index.ts +0 -2
  269. package/src/compat/runtime.ts +0 -12
  270. package/src/compat/sqlite.ts +0 -177
  271. package/src/config/index.ts +0 -17
  272. package/src/config/manager.ts +0 -530
  273. package/src/config/mode-store.ts +0 -62
  274. package/src/config/profiles.ts +0 -84
  275. package/src/config/safety-policy.ts +0 -358
  276. package/src/config/schema.ts +0 -125
  277. package/src/config/types.ts +0 -609
  278. package/src/config/workspace-state.ts +0 -53
  279. package/src/context/context-db.ts +0 -199
  280. package/src/demo/index.ts +0 -349
  281. package/src/demo/scenarios/full-journey.ts +0 -229
  282. package/src/demo/scenarios/getting-started.ts +0 -127
  283. package/src/demo/scenarios/helm-release.ts +0 -341
  284. package/src/demo/scenarios/k8s-deployment.ts +0 -194
  285. package/src/demo/scenarios/terraform-vpc.ts +0 -170
  286. package/src/demo/types.ts +0 -92
  287. package/src/engine/cost-estimator.ts +0 -480
  288. package/src/engine/diagram-generator.ts +0 -256
  289. package/src/engine/drift-detector.ts +0 -902
  290. package/src/engine/executor.ts +0 -1066
  291. package/src/engine/index.ts +0 -76
  292. package/src/engine/orchestrator.ts +0 -636
  293. package/src/engine/planner.ts +0 -787
  294. package/src/engine/safety.ts +0 -743
  295. package/src/engine/verifier.ts +0 -770
  296. package/src/enterprise/audit.ts +0 -348
  297. package/src/enterprise/auth.ts +0 -270
  298. package/src/enterprise/billing.ts +0 -822
  299. package/src/enterprise/index.ts +0 -17
  300. package/src/enterprise/teams.ts +0 -443
  301. package/src/generator/best-practices.ts +0 -1608
  302. package/src/generator/helm.ts +0 -630
  303. package/src/generator/index.ts +0 -37
  304. package/src/generator/intent-parser.ts +0 -514
  305. package/src/generator/kubernetes.ts +0 -976
  306. package/src/generator/terraform.ts +0 -1875
  307. package/src/history/index.ts +0 -8
  308. package/src/history/manager.ts +0 -250
  309. package/src/history/types.ts +0 -34
  310. package/src/hooks/config.ts +0 -432
  311. package/src/hooks/engine.ts +0 -392
  312. package/src/hooks/index.ts +0 -4
  313. package/src/llm/auth-bridge.ts +0 -198
  314. package/src/llm/circuit-breaker.ts +0 -140
  315. package/src/llm/config-loader.ts +0 -201
  316. package/src/llm/cost-calculator.ts +0 -171
  317. package/src/llm/index.ts +0 -8
  318. package/src/llm/model-aliases.ts +0 -115
  319. package/src/llm/provider-registry.ts +0 -63
  320. package/src/llm/providers/anthropic.ts +0 -462
  321. package/src/llm/providers/bedrock.ts +0 -477
  322. package/src/llm/providers/google.ts +0 -405
  323. package/src/llm/providers/ollama.ts +0 -767
  324. package/src/llm/providers/openai-compatible.ts +0 -340
  325. package/src/llm/providers/openai.ts +0 -328
  326. package/src/llm/providers/openrouter.ts +0 -338
  327. package/src/llm/router.ts +0 -1104
  328. package/src/llm/types.ts +0 -232
  329. package/src/lsp/client.ts +0 -298
  330. package/src/lsp/languages.ts +0 -119
  331. package/src/lsp/manager.ts +0 -294
  332. package/src/mcp/client.ts +0 -402
  333. package/src/mcp/index.ts +0 -5
  334. package/src/mcp/manager.ts +0 -133
  335. package/src/nimbus.ts +0 -234
  336. package/src/plugins/index.ts +0 -27
  337. package/src/plugins/loader.ts +0 -334
  338. package/src/plugins/manager.ts +0 -376
  339. package/src/plugins/types.ts +0 -284
  340. package/src/scanners/cicd-scanner.ts +0 -258
  341. package/src/scanners/cloud-scanner.ts +0 -466
  342. package/src/scanners/framework-scanner.ts +0 -469
  343. package/src/scanners/iac-scanner.ts +0 -388
  344. package/src/scanners/index.ts +0 -539
  345. package/src/scanners/language-scanner.ts +0 -276
  346. package/src/scanners/package-manager-scanner.ts +0 -277
  347. package/src/scanners/types.ts +0 -172
  348. package/src/sessions/manager.ts +0 -472
  349. package/src/sessions/types.ts +0 -44
  350. package/src/sharing/sync.ts +0 -300
  351. package/src/sharing/viewer.ts +0 -163
  352. package/src/snapshots/index.ts +0 -2
  353. package/src/snapshots/manager.ts +0 -530
  354. package/src/state/artifacts.ts +0 -147
  355. package/src/state/audit.ts +0 -137
  356. package/src/state/billing.ts +0 -240
  357. package/src/state/checkpoints.ts +0 -117
  358. package/src/state/config.ts +0 -67
  359. package/src/state/conversations.ts +0 -14
  360. package/src/state/credentials.ts +0 -154
  361. package/src/state/db.ts +0 -58
  362. package/src/state/index.ts +0 -26
  363. package/src/state/messages.ts +0 -115
  364. package/src/state/projects.ts +0 -123
  365. package/src/state/schema.ts +0 -236
  366. package/src/state/sessions.ts +0 -147
  367. package/src/state/teams.ts +0 -200
  368. package/src/telemetry.ts +0 -108
  369. package/src/tools/aws-ops.ts +0 -952
  370. package/src/tools/azure-ops.ts +0 -579
  371. package/src/tools/file-ops.ts +0 -615
  372. package/src/tools/gcp-ops.ts +0 -625
  373. package/src/tools/git-ops.ts +0 -773
  374. package/src/tools/github-ops.ts +0 -799
  375. package/src/tools/helm-ops.ts +0 -943
  376. package/src/tools/index.ts +0 -17
  377. package/src/tools/k8s-ops.ts +0 -819
  378. package/src/tools/schemas/converter.ts +0 -184
  379. package/src/tools/schemas/devops.ts +0 -3502
  380. package/src/tools/schemas/index.ts +0 -73
  381. package/src/tools/schemas/standard.ts +0 -1148
  382. package/src/tools/schemas/types.ts +0 -735
  383. package/src/tools/spawn-exec.ts +0 -148
  384. package/src/tools/terraform-ops.ts +0 -862
  385. package/src/types/ambient.d.ts +0 -193
  386. package/src/types/config.ts +0 -83
  387. package/src/types/drift.ts +0 -116
  388. package/src/types/enterprise.ts +0 -335
  389. package/src/types/index.ts +0 -20
  390. package/src/types/plan.ts +0 -44
  391. package/src/types/request.ts +0 -65
  392. package/src/types/response.ts +0 -54
  393. package/src/types/service.ts +0 -51
  394. package/src/ui/App.tsx +0 -2114
  395. package/src/ui/DeployPreview.tsx +0 -174
  396. package/src/ui/FileDiffModal.tsx +0 -162
  397. package/src/ui/Header.tsx +0 -131
  398. package/src/ui/HelpModal.tsx +0 -57
  399. package/src/ui/InputBox.tsx +0 -503
  400. package/src/ui/MessageList.tsx +0 -1032
  401. package/src/ui/PermissionPrompt.tsx +0 -163
  402. package/src/ui/StatusBar.tsx +0 -277
  403. package/src/ui/TerminalPane.tsx +0 -84
  404. package/src/ui/ToolCallDisplay.tsx +0 -643
  405. package/src/ui/TreePane.tsx +0 -132
  406. package/src/ui/chat-ui.ts +0 -850
  407. package/src/ui/index.ts +0 -33
  408. package/src/ui/ink/index.ts +0 -1444
  409. package/src/ui/streaming.ts +0 -176
  410. package/src/ui/theme.ts +0 -104
  411. package/src/ui/types.ts +0 -75
  412. package/src/utils/analytics.ts +0 -72
  413. package/src/utils/cost-warning.ts +0 -27
  414. package/src/utils/env.ts +0 -46
  415. package/src/utils/errors.ts +0 -69
  416. package/src/utils/event-bus.ts +0 -38
  417. package/src/utils/index.ts +0 -24
  418. package/src/utils/logger.ts +0 -171
  419. package/src/utils/rate-limiter.ts +0 -121
  420. package/src/utils/service-auth.ts +0 -49
  421. package/src/utils/validation.ts +0 -53
  422. package/src/version.ts +0 -4
  423. package/src/watcher/index.ts +0 -214
  424. package/src/wizard/approval.ts +0 -383
  425. package/src/wizard/index.ts +0 -25
  426. package/src/wizard/prompts.ts +0 -338
  427. package/src/wizard/types.ts +0 -172
  428. package/src/wizard/ui.ts +0 -556
  429. package/src/wizard/wizard.ts +0 -304
  430. package/tsconfig.json +0 -24
@@ -1,958 +0,0 @@
1
- /**
2
- * GAP-11, GAP-18, GAP-20 Tests
3
- *
4
- * GAP-11: Terraform plan → FileDiffBatch wiring
5
- * - parseTerraformPlanOutput with various plan outputs
6
- * - buildFileDiffBatchFromPlan output shape
7
- * - requestFileDiff callback called after terraform plan
8
- *
9
- * GAP-18: IaC validation after writing .tf files
10
- * - .tf file detection from tool name and path
11
- * - terraform validate error injection into tool result
12
- *
13
- * GAP-20: Per-tool timeout from NIMBUS.md
14
- * - parseToolTimeouts function parsing
15
- * - timeout propagation to ToolExecuteContext
16
- * - NIMBUS.md ## Tool Timeouts section parsing
17
- */
18
-
19
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
20
- import {
21
- parseTerraformPlanOutput,
22
- buildFileDiffBatchFromPlan,
23
- type ResourceChange,
24
- type DeployPreview,
25
- } from '../agent/deploy-preview';
26
- import { ToolRegistry } from '../tools/schemas/types';
27
- import type { ToolDefinition, ToolExecuteContext } from '../tools/schemas/types';
28
- import type { FileDiffDecision } from '../agent/loop';
29
- import { z } from 'zod';
30
-
31
- // ---------------------------------------------------------------------------
32
- // GAP-11 Tests: parseTerraformPlanOutput
33
- // ---------------------------------------------------------------------------
34
-
35
- describe('GAP-11 — parseTerraformPlanOutput', () => {
36
- it('parses a create resource line', () => {
37
- const output = ` # aws_instance.web will be created`;
38
- const changes = parseTerraformPlanOutput(output);
39
- expect(changes).toHaveLength(1);
40
- expect(changes[0].resource).toBe('aws_instance.web');
41
- expect(changes[0].action).toBe('create');
42
- });
43
-
44
- it('parses an update resource line', () => {
45
- const output = ` # aws_s3_bucket.data will be updated in-place`;
46
- const changes = parseTerraformPlanOutput(output);
47
- expect(changes).toHaveLength(1);
48
- expect(changes[0].resource).toBe('aws_s3_bucket.data');
49
- expect(changes[0].action).toBe('update');
50
- });
51
-
52
- it('parses a destroy resource line', () => {
53
- const output = ` # aws_security_group.old will be destroyed`;
54
- const changes = parseTerraformPlanOutput(output);
55
- expect(changes).toHaveLength(1);
56
- expect(changes[0].resource).toBe('aws_security_group.old');
57
- expect(changes[0].action).toBe('destroy');
58
- });
59
-
60
- it('parses a replace resource line', () => {
61
- const output = ` # aws_instance.app must be replaced`;
62
- const changes = parseTerraformPlanOutput(output);
63
- expect(changes).toHaveLength(1);
64
- expect(changes[0].resource).toBe('aws_instance.app');
65
- expect(changes[0].action).toBe('replace');
66
- });
67
-
68
- it('parses multiple resources in a plan output', () => {
69
- const output = [
70
- ' # aws_vpc.main will be created',
71
- ' # aws_subnet.public will be created',
72
- ' # aws_instance.old will be destroyed',
73
- ' # aws_instance.web will be updated in-place',
74
- ' # aws_rds_cluster.db must be replaced',
75
- ].join('\n');
76
- const changes = parseTerraformPlanOutput(output);
77
- expect(changes).toHaveLength(5);
78
- expect(changes.filter(c => c.action === 'create')).toHaveLength(2);
79
- expect(changes.filter(c => c.action === 'destroy')).toHaveLength(1);
80
- expect(changes.filter(c => c.action === 'update')).toHaveLength(1);
81
- expect(changes.filter(c => c.action === 'replace')).toHaveLength(1);
82
- });
83
-
84
- it('falls back to summary line when no resource lines present', () => {
85
- const output = `Plan: 2 to add, 1 to change, 1 to destroy.`;
86
- const changes = parseTerraformPlanOutput(output);
87
- expect(changes).toHaveLength(4);
88
- expect(changes.filter(c => c.action === 'create')).toHaveLength(2);
89
- expect(changes.filter(c => c.action === 'update')).toHaveLength(1);
90
- expect(changes.filter(c => c.action === 'destroy')).toHaveLength(1);
91
- });
92
-
93
- it('returns empty array for plan with no changes', () => {
94
- const output = `No changes. Your infrastructure matches the configuration.`;
95
- const changes = parseTerraformPlanOutput(output);
96
- expect(changes).toHaveLength(0);
97
- });
98
-
99
- it('ignores the summary line if individual resource lines were already parsed', () => {
100
- const output = [
101
- ' # aws_instance.web will be created',
102
- 'Plan: 3 to add, 0 to change, 0 to destroy.',
103
- ].join('\n');
104
- const changes = parseTerraformPlanOutput(output);
105
- // Should only have 1 entry from the resource line, not 3+1
106
- expect(changes).toHaveLength(1);
107
- expect(changes[0].resource).toBe('aws_instance.web');
108
- });
109
-
110
- it('handles module-prefixed resource names', () => {
111
- const output = ` # module.vpc.aws_vpc.main will be created`;
112
- const changes = parseTerraformPlanOutput(output);
113
- expect(changes).toHaveLength(1);
114
- expect(changes[0].resource).toBe('module.vpc.aws_vpc.main');
115
- });
116
-
117
- it('parses plan with only destroy actions', () => {
118
- const output = [
119
- ' # aws_route53_record.old will be destroyed',
120
- ' # aws_iam_role.legacy will be destroyed',
121
- ].join('\n');
122
- const changes = parseTerraformPlanOutput(output);
123
- expect(changes).toHaveLength(2);
124
- expect(changes.every(c => c.action === 'destroy')).toBe(true);
125
- });
126
- });
127
-
128
- // ---------------------------------------------------------------------------
129
- // GAP-11 Tests: buildFileDiffBatchFromPlan
130
- // ---------------------------------------------------------------------------
131
-
132
- describe('GAP-11 — buildFileDiffBatchFromPlan', () => {
133
- function makePreview(changes: ResourceChange[]): DeployPreview {
134
- return {
135
- tool: 'terraform',
136
- action: 'plan',
137
- workdir: '/tmp/infra',
138
- changes,
139
- summary: {
140
- toCreate: changes.filter(c => c.action === 'create').length,
141
- toUpdate: changes.filter(c => c.action === 'update').length,
142
- toDestroy: changes.filter(c => c.action === 'destroy').length,
143
- toReplace: changes.filter(c => c.action === 'replace').length,
144
- unchanged: 0,
145
- },
146
- rawOutput: '',
147
- success: true,
148
- };
149
- }
150
-
151
- it('returns one entry per resource change', () => {
152
- const preview = makePreview([
153
- { resource: 'aws_instance.web', action: 'create' },
154
- { resource: 'aws_s3_bucket.data', action: 'update' },
155
- ]);
156
- const batch = buildFileDiffBatchFromPlan(preview);
157
- expect(batch).toHaveLength(2);
158
- });
159
-
160
- it('each entry has filePath, diff, and toolName', () => {
161
- const preview = makePreview([{ resource: 'aws_vpc.main', action: 'create' }]);
162
- const batch = buildFileDiffBatchFromPlan(preview);
163
- expect(batch[0]).toHaveProperty('filePath');
164
- expect(batch[0]).toHaveProperty('diff');
165
- expect(batch[0]).toHaveProperty('toolName');
166
- });
167
-
168
- it('filePath equals the resource name', () => {
169
- const preview = makePreview([{ resource: 'aws_instance.app', action: 'destroy' }]);
170
- const batch = buildFileDiffBatchFromPlan(preview);
171
- expect(batch[0].filePath).toBe('aws_instance.app');
172
- });
173
-
174
- it('toolName is "terraform" for all entries', () => {
175
- const preview = makePreview([
176
- { resource: 'aws_vpc.main', action: 'create' },
177
- { resource: 'aws_subnet.pub', action: 'update' },
178
- ]);
179
- const batch = buildFileDiffBatchFromPlan(preview);
180
- expect(batch.every(b => b.toolName === 'terraform')).toBe(true);
181
- });
182
-
183
- it('diff contains a unified diff header', () => {
184
- const preview = makePreview([{ resource: 'aws_instance.web', action: 'create' }]);
185
- const batch = buildFileDiffBatchFromPlan(preview);
186
- expect(batch[0].diff).toContain('--- a/aws_instance.web');
187
- expect(batch[0].diff).toContain('+++ b/aws_instance.web');
188
- });
189
-
190
- it('diff uses "+" symbol for create action', () => {
191
- const preview = makePreview([{ resource: 'aws_lambda.fn', action: 'create' }]);
192
- const batch = buildFileDiffBatchFromPlan(preview);
193
- expect(batch[0].diff).toContain('+ aws_lambda.fn');
194
- });
195
-
196
- it('diff uses "-" symbol for destroy action', () => {
197
- const preview = makePreview([{ resource: 'aws_iam_role.old', action: 'destroy' }]);
198
- const batch = buildFileDiffBatchFromPlan(preview);
199
- expect(batch[0].diff).toContain('- aws_iam_role.old');
200
- });
201
-
202
- it('diff uses "~" symbol for update action', () => {
203
- const preview = makePreview([{ resource: 'aws_rds_instance.db', action: 'update' }]);
204
- const batch = buildFileDiffBatchFromPlan(preview);
205
- expect(batch[0].diff).toContain('~ aws_rds_instance.db');
206
- });
207
-
208
- it('diff includes details when present', () => {
209
- const preview = makePreview([
210
- { resource: 'aws_instance.web', action: 'create', details: 'ami changed' },
211
- ]);
212
- const batch = buildFileDiffBatchFromPlan(preview);
213
- expect(batch[0].diff).toContain('ami changed');
214
- });
215
-
216
- it('returns empty array for plan with no changes', () => {
217
- const preview = makePreview([]);
218
- const batch = buildFileDiffBatchFromPlan(preview);
219
- expect(batch).toHaveLength(0);
220
- });
221
-
222
- it('handles replace action correctly', () => {
223
- const preview = makePreview([{ resource: 'aws_instance.app', action: 'replace' }]);
224
- const batch = buildFileDiffBatchFromPlan(preview);
225
- expect(batch[0].diff).toContain('+/-');
226
- });
227
- });
228
-
229
- // ---------------------------------------------------------------------------
230
- // GAP-11 Tests: requestFileDiff callback integration
231
- // ---------------------------------------------------------------------------
232
-
233
- describe('GAP-11 — requestFileDiff callback with terraform plan', () => {
234
- it('parseTerraformPlanOutput + buildFileDiffBatchFromPlan together produce requestable diffs', () => {
235
- const planOutput = [
236
- ' # aws_instance.web will be created',
237
- ' # aws_security_group.main will be updated in-place',
238
- ].join('\n');
239
-
240
- const changes = parseTerraformPlanOutput(planOutput);
241
- expect(changes).toHaveLength(2);
242
-
243
- // Build a minimal preview to feed buildFileDiffBatchFromPlan
244
- const preview: DeployPreview = {
245
- tool: 'terraform',
246
- action: 'plan',
247
- workdir: '/tmp',
248
- changes,
249
- summary: { toCreate: 1, toUpdate: 1, toDestroy: 0, toReplace: 0, unchanged: 0 },
250
- rawOutput: planOutput,
251
- success: true,
252
- };
253
-
254
- const batch = buildFileDiffBatchFromPlan(preview);
255
- expect(batch).toHaveLength(2);
256
-
257
- // Simulate the requestFileDiff callback being called for each
258
- const calls: Array<[string, string, string]> = [];
259
- const fakeRequestFileDiff = async (path: string, toolName: string, diff: string): Promise<FileDiffDecision> => {
260
- calls.push([path, toolName, diff]);
261
- return 'apply';
262
- };
263
-
264
- // Run through the batch as loop.ts would
265
- const runBatch = async () => {
266
- for (const file of batch) {
267
- const decision = await fakeRequestFileDiff(file.filePath, file.toolName ?? 'terraform', file.diff ?? '');
268
- if (decision === 'reject-all') break;
269
- }
270
- };
271
-
272
- return runBatch().then(() => {
273
- expect(calls).toHaveLength(2);
274
- expect(calls[0][0]).toBe('aws_instance.web');
275
- expect(calls[0][1]).toBe('terraform');
276
- expect(calls[1][0]).toBe('aws_security_group.main');
277
- });
278
- });
279
-
280
- it('stops iteration on reject-all decision', async () => {
281
- const changes: ResourceChange[] = [
282
- { resource: 'aws_vpc.main', action: 'create' },
283
- { resource: 'aws_subnet.pub', action: 'create' },
284
- { resource: 'aws_sg.main', action: 'create' },
285
- ];
286
- const preview: DeployPreview = {
287
- tool: 'terraform', action: 'plan', workdir: '/tmp',
288
- changes, summary: { toCreate: 3, toUpdate: 0, toDestroy: 0, toReplace: 0, unchanged: 0 },
289
- rawOutput: '', success: true,
290
- };
291
-
292
- const batch = buildFileDiffBatchFromPlan(preview);
293
- const callCount = { n: 0 };
294
- const fakeRequestFileDiff = async (_path: string, _toolName: string, _diff: string): Promise<FileDiffDecision> => {
295
- callCount.n++;
296
- return callCount.n === 1 ? 'reject-all' : 'apply';
297
- };
298
-
299
- for (const file of batch) {
300
- const decision = await fakeRequestFileDiff(file.filePath, file.toolName ?? 'terraform', file.diff ?? '');
301
- if (decision === 'reject-all') break;
302
- }
303
-
304
- // Only 1 call should have been made before reject-all stopped iteration
305
- expect(callCount.n).toBe(1);
306
- });
307
- });
308
-
309
- // ---------------------------------------------------------------------------
310
- // GAP-18 Tests: .tf file detection
311
- // ---------------------------------------------------------------------------
312
-
313
- describe('GAP-18 — .tf file detection logic', () => {
314
- const FILE_WRITING_TOOLS = ['write_file', 'edit_file', 'multi_edit'];
315
-
316
- function shouldValidateTf(toolName: string, filePath: string): boolean {
317
- return FILE_WRITING_TOOLS.includes(toolName) && filePath.endsWith('.tf');
318
- }
319
-
320
- it('detects .tf extension for write_file with path', () => {
321
- expect(shouldValidateTf('write_file', 'main.tf')).toBe(true);
322
- expect(shouldValidateTf('write_file', '/infra/main.tf')).toBe(true);
323
- });
324
-
325
- it('detects .tf extension for edit_file', () => {
326
- expect(shouldValidateTf('edit_file', 'variables.tf')).toBe(true);
327
- });
328
-
329
- it('detects .tf extension for multi_edit', () => {
330
- expect(shouldValidateTf('multi_edit', 'outputs.tf')).toBe(true);
331
- });
332
-
333
- it('does NOT trigger for non-.tf files', () => {
334
- expect(shouldValidateTf('write_file', 'main.py')).toBe(false);
335
- expect(shouldValidateTf('write_file', 'values.yaml')).toBe(false);
336
- expect(shouldValidateTf('write_file', 'Dockerfile')).toBe(false);
337
- expect(shouldValidateTf('write_file', 'main.tfvars')).toBe(false);
338
- });
339
-
340
- it('does NOT trigger for non-file tools even with .tf in name', () => {
341
- expect(shouldValidateTf('bash', 'something.tf')).toBe(false);
342
- expect(shouldValidateTf('terraform', 'main.tf')).toBe(false);
343
- expect(shouldValidateTf('read_file', 'main.tf')).toBe(false);
344
- });
345
-
346
- it('handles empty path without error', () => {
347
- expect(shouldValidateTf('write_file', '')).toBe(false);
348
- });
349
-
350
- it('recognizes nested .tf paths', () => {
351
- expect(shouldValidateTf('write_file', '/home/user/project/modules/vpc/main.tf')).toBe(true);
352
- });
353
-
354
- it('does NOT match .tf.bak or other .tf-prefixed extensions', () => {
355
- expect(shouldValidateTf('write_file', 'main.tf.bak')).toBe(false);
356
- });
357
- });
358
-
359
- // ---------------------------------------------------------------------------
360
- // GAP-18 Tests: terraform validate error injection
361
- // ---------------------------------------------------------------------------
362
-
363
- describe('GAP-18 — terraform validate error injection', () => {
364
- it('produces correct error string from diagnostics', () => {
365
- const diagnostics = [
366
- { severity: 'error', summary: 'Missing required argument', detail: '"name" is required' },
367
- { severity: 'error', summary: 'Invalid value', detail: '"region" must be a string' },
368
- ];
369
- const errors = diagnostics
370
- .filter(d => d.severity === 'error')
371
- .map(d => ` ${d.summary}: ${d.detail}`)
372
- .join('\n');
373
- const suffix = `\n\nTerraform validation errors (please fix):\n${errors}`;
374
- expect(suffix).toContain('Missing required argument');
375
- expect(suffix).toContain('"name" is required');
376
- expect(suffix).toContain('Invalid value');
377
- expect(suffix).toContain('"region" must be a string');
378
- });
379
-
380
- it('filters out warning-level diagnostics', () => {
381
- const diagnostics = [
382
- { severity: 'error', summary: 'Missing required argument', detail: '"name" is required' },
383
- { severity: 'warning', summary: 'Deprecated', detail: 'Use newer syntax' },
384
- ];
385
- const errors = diagnostics
386
- .filter(d => d.severity === 'error')
387
- .map(d => ` ${d.summary}: ${d.detail}`)
388
- .join('\n');
389
- expect(errors).toContain('Missing required argument');
390
- expect(errors).not.toContain('Deprecated');
391
- });
392
-
393
- it('produces no suffix when valid is true', () => {
394
- const parsed = { valid: true, diagnostics: [] };
395
- let toolContent = 'Success';
396
- if (!parsed.valid && parsed.diagnostics && parsed.diagnostics.length > 0) {
397
- toolContent += '\n\nTerraform validation errors (please fix):';
398
- }
399
- expect(toolContent).toBe('Success');
400
- });
401
-
402
- it('produces suffix when valid is false and errors exist', () => {
403
- const parsed = {
404
- valid: false,
405
- diagnostics: [{ severity: 'error', summary: 'Error', detail: 'Something wrong' }],
406
- };
407
- let toolContent = 'File written';
408
- if (!parsed.valid && parsed.diagnostics && parsed.diagnostics.length > 0) {
409
- const errors = parsed.diagnostics
410
- .filter(d => d.severity === 'error')
411
- .map(d => ` ${d.summary}: ${d.detail}`)
412
- .join('\n');
413
- toolContent += `\n\nTerraform validation errors (please fix):\n${errors}`;
414
- }
415
- expect(toolContent).toContain('Terraform validation errors (please fix)');
416
- expect(toolContent).toContain('File written');
417
- expect(toolContent).toContain('Something wrong');
418
- });
419
-
420
- it('handles diagnostics with empty detail gracefully', () => {
421
- const diagnostics = [
422
- { severity: 'error', summary: 'Syntax error', detail: '' },
423
- ];
424
- const errors = diagnostics
425
- .filter(d => d.severity === 'error')
426
- .map(d => ` ${d.summary}: ${d.detail}`)
427
- .join('\n');
428
- expect(errors).toContain('Syntax error');
429
- expect(errors).toContain(':');
430
- });
431
-
432
- it('handles empty diagnostics array without suffix', () => {
433
- const parsed = { valid: false, diagnostics: [] as Array<{severity: string; summary: string; detail: string}> };
434
- let toolContent = 'File written';
435
- if (!parsed.valid && parsed.diagnostics && parsed.diagnostics.length > 0) {
436
- const errors = parsed.diagnostics
437
- .filter(d => d.severity === 'error')
438
- .map(d => ` ${d.summary}: ${d.detail}`)
439
- .join('\n');
440
- toolContent += `\n\nTerraform validation errors (please fix):\n${errors}`;
441
- }
442
- // No errors even though valid is false (empty diagnostics)
443
- expect(toolContent).toBe('File written');
444
- });
445
- });
446
-
447
- // ---------------------------------------------------------------------------
448
- // GAP-20 Tests: parseToolTimeouts function
449
- // ---------------------------------------------------------------------------
450
-
451
- // Inline reproduction of parseToolTimeouts from ink/index.ts so we can unit-test it
452
- function parseToolTimeouts(nimbusMd: string): Record<string, number> {
453
- const result: Record<string, number> = {};
454
- const match = nimbusMd.match(/##\s+Tool Timeouts\s*\n([\s\S]*?)(?=##|$)/);
455
- if (!match) return result;
456
- for (const line of match[1].split('\n')) {
457
- const m = line.match(/^\s*([a-z_]+)\s*:\s*(\d+)\s*$/);
458
- if (m) result[m[1]] = parseInt(m[2], 10);
459
- }
460
- return result;
461
- }
462
-
463
- describe('GAP-20 — parseToolTimeouts', () => {
464
- it('returns empty object when no Tool Timeouts section', () => {
465
- const nimbusMd = `## Project\nThis is a test project.\n\n## Instructions\nDo stuff.`;
466
- const result = parseToolTimeouts(nimbusMd);
467
- expect(result).toEqual({});
468
- });
469
-
470
- it('parses a single tool timeout', () => {
471
- const nimbusMd = `## Tool Timeouts\nterraform: 300000\n`;
472
- const result = parseToolTimeouts(nimbusMd);
473
- expect(result).toEqual({ terraform: 300000 });
474
- });
475
-
476
- it('parses multiple tool timeouts', () => {
477
- const nimbusMd = `## Tool Timeouts\nterraform: 600000\nkubectl: 120000\nhelm: 300000\n`;
478
- const result = parseToolTimeouts(nimbusMd);
479
- expect(result).toEqual({ terraform: 600000, kubectl: 120000, helm: 300000 });
480
- });
481
-
482
- it('parses tool timeouts with leading whitespace', () => {
483
- const nimbusMd = `## Tool Timeouts\n terraform: 300000\n kubectl: 60000\n`;
484
- const result = parseToolTimeouts(nimbusMd);
485
- expect(result).toEqual({ terraform: 300000, kubectl: 60000 });
486
- });
487
-
488
- it('stops at the next ## section', () => {
489
- const nimbusMd = `## Tool Timeouts\nterraform: 300000\n\n## Other Section\nkubectl: 999999\n`;
490
- const result = parseToolTimeouts(nimbusMd);
491
- // Only terraform should be parsed; kubectl appears after the next ##
492
- expect(result).toHaveProperty('terraform', 300000);
493
- expect(result).not.toHaveProperty('kubectl');
494
- });
495
-
496
- it('ignores non-matching lines in the section', () => {
497
- const nimbusMd = `## Tool Timeouts\n# comment line\nterraform: 300000\nsome_text without colon\n`;
498
- const result = parseToolTimeouts(nimbusMd);
499
- expect(result).toEqual({ terraform: 300000 });
500
- });
501
-
502
- it('ignores lines with non-numeric values', () => {
503
- const nimbusMd = `## Tool Timeouts\nterraform: fast\nkubectl: 120000\n`;
504
- const result = parseToolTimeouts(nimbusMd);
505
- // Only kubectl has a numeric value
506
- expect(result).toEqual({ kubectl: 120000 });
507
- });
508
-
509
- it('ignores tool names with uppercase letters', () => {
510
- const nimbusMd = `## Tool Timeouts\nTerraform: 300000\nkubectl: 120000\n`;
511
- const result = parseToolTimeouts(nimbusMd);
512
- // Terraform (capital T) doesn't match [a-z_]+ pattern
513
- expect(result).toEqual({ kubectl: 120000 });
514
- });
515
-
516
- it('parses tool names with underscores', () => {
517
- const nimbusMd = `## Tool Timeouts\ncloud_discover: 30000\nkubectl_context: 60000\n`;
518
- const result = parseToolTimeouts(nimbusMd);
519
- expect(result).toEqual({ cloud_discover: 30000, kubectl_context: 60000 });
520
- });
521
-
522
- it('handles Tool Timeouts section at end of file without trailing ##', () => {
523
- const nimbusMd = `## Project\nMy project.\n\n## Tool Timeouts\nterraform: 450000`;
524
- const result = parseToolTimeouts(nimbusMd);
525
- expect(result).toEqual({ terraform: 450000 });
526
- });
527
-
528
- it('returns integer values (not floats)', () => {
529
- const nimbusMd = `## Tool Timeouts\nterraform: 300000\n`;
530
- const result = parseToolTimeouts(nimbusMd);
531
- expect(Number.isInteger(result.terraform)).toBe(true);
532
- });
533
- });
534
-
535
- // ---------------------------------------------------------------------------
536
- // GAP-20 Tests: ToolExecuteContext timeout field
537
- // ---------------------------------------------------------------------------
538
-
539
- describe('GAP-20 — ToolExecuteContext.timeout field', () => {
540
- it('ToolExecuteContext accepts a timeout field', async () => {
541
- const { z } = await import('zod');
542
- let capturedCtx: ToolExecuteContext | undefined;
543
-
544
- const tool: ToolDefinition = {
545
- name: 'test_timeout_tool',
546
- description: 'A test tool that captures context',
547
- inputSchema: z.object({ value: z.string() }),
548
- permissionTier: 'auto_allow',
549
- category: 'devops',
550
- execute: async (_input: unknown, ctx?: ToolExecuteContext) => {
551
- capturedCtx = ctx;
552
- return { output: 'ok', isError: false };
553
- },
554
- };
555
-
556
- const ctx: ToolExecuteContext = { timeout: 30000 };
557
- await tool.execute({ value: 'test' }, ctx);
558
- expect(capturedCtx?.timeout).toBe(30000);
559
- });
560
-
561
- it('ToolExecuteContext.timeout is optional', async () => {
562
- const { z } = await import('zod');
563
- let capturedCtx: ToolExecuteContext | undefined;
564
-
565
- const tool: ToolDefinition = {
566
- name: 'test_no_timeout_tool',
567
- description: 'A test tool',
568
- inputSchema: z.object({ value: z.string() }),
569
- permissionTier: 'auto_allow',
570
- category: 'devops',
571
- execute: async (_input: unknown, ctx?: ToolExecuteContext) => {
572
- capturedCtx = ctx;
573
- return { output: 'ok', isError: false };
574
- },
575
- };
576
-
577
- await tool.execute({ value: 'test' }, {});
578
- expect(capturedCtx?.timeout).toBeUndefined();
579
- });
580
-
581
- it('ToolExecuteContext can have both onProgress and timeout', async () => {
582
- const { z } = await import('zod');
583
- let capturedCtx: ToolExecuteContext | undefined;
584
-
585
- const tool: ToolDefinition = {
586
- name: 'test_full_ctx_tool',
587
- description: 'A test tool',
588
- inputSchema: z.object({}),
589
- permissionTier: 'auto_allow',
590
- category: 'standard',
591
- execute: async (_input: unknown, ctx?: ToolExecuteContext) => {
592
- capturedCtx = ctx;
593
- return { output: 'ok', isError: false };
594
- },
595
- };
596
-
597
- const chunks: string[] = [];
598
- const ctx: ToolExecuteContext = {
599
- onProgress: (chunk) => chunks.push(chunk),
600
- timeout: 45000,
601
- };
602
- await tool.execute({}, ctx);
603
- expect(capturedCtx?.timeout).toBe(45000);
604
- expect(capturedCtx?.onProgress).toBeDefined();
605
- });
606
- });
607
-
608
- // ---------------------------------------------------------------------------
609
- // GAP-20 Tests: toolTimeouts propagation in AgentLoopOptions
610
- // ---------------------------------------------------------------------------
611
-
612
- describe('GAP-20 — toolTimeouts in AgentLoopOptions (type check)', () => {
613
- it('AgentLoopOptions type includes toolTimeouts field', async () => {
614
- const { readFileSync } = await import('node:fs');
615
- const { join } = await import('node:path');
616
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
617
- // Verify the toolTimeouts field is defined in AgentLoopOptions
618
- expect(loopSrc).toContain('toolTimeouts?: Record<string, number>');
619
- });
620
-
621
- it('executeToolCall signature includes toolTimeouts parameter', async () => {
622
- const { readFileSync } = await import('node:fs');
623
- const { join } = await import('node:path');
624
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
625
- // Verify toolTimeouts is a parameter of executeToolCall
626
- expect(loopSrc).toContain('toolTimeouts?: Record<string, number>');
627
- // Verify the GAP-20 comment is present
628
- expect(loopSrc).toContain('GAP-20');
629
- });
630
-
631
- it('loop.ts passes options.toolTimeouts to executeToolCall', async () => {
632
- const { readFileSync } = await import('node:fs');
633
- const { join } = await import('node:path');
634
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
635
- expect(loopSrc).toContain('options.toolTimeouts');
636
- });
637
-
638
- it('loop.ts builds toolCtx with timeout field', async () => {
639
- const { readFileSync } = await import('node:fs');
640
- const { join } = await import('node:path');
641
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
642
- expect(loopSrc).toContain('toolTimeouts?.[toolName]');
643
- });
644
- });
645
-
646
- // ---------------------------------------------------------------------------
647
- // GAP-20 Tests: devops.ts DEFAULT_TIMEOUT usage
648
- // ---------------------------------------------------------------------------
649
-
650
- describe('GAP-20 — devops.ts DEFAULT_TIMEOUT constant', () => {
651
- it('DEFAULT_TIMEOUT constant is defined in devops.ts', async () => {
652
- const { readFileSync } = await import('node:fs');
653
- const { join } = await import('node:path');
654
- const devopsSrc = readFileSync(join(__dirname, '..', 'tools', 'schemas', 'devops.ts'), 'utf-8');
655
- expect(devopsSrc).toContain('const DEFAULT_TIMEOUT = 600_000');
656
- });
657
-
658
- it('terraform spawnExec uses ctx?.timeout ?? DEFAULT_TIMEOUT', async () => {
659
- const { readFileSync } = await import('node:fs');
660
- const { join } = await import('node:path');
661
- const devopsSrc = readFileSync(join(__dirname, '..', 'tools', 'schemas', 'devops.ts'), 'utf-8');
662
- expect(devopsSrc).toContain('ctx?.timeout ?? DEFAULT_TIMEOUT');
663
- });
664
-
665
- it('kubectl spawnExec uses ctx?.timeout override', async () => {
666
- const { readFileSync } = await import('node:fs');
667
- const { join } = await import('node:path');
668
- const devopsSrc = readFileSync(join(__dirname, '..', 'tools', 'schemas', 'devops.ts'), 'utf-8');
669
- // kubectl uses ctx?.timeout ?? defaultKubectlTimeoutMs
670
- expect(devopsSrc).toContain('ctx?.timeout ?? defaultKubectlTimeoutMs');
671
- });
672
-
673
- it('helm spawnExec uses ctx?.timeout ?? DEFAULT_TIMEOUT', async () => {
674
- const { readFileSync } = await import('node:fs');
675
- const { join } = await import('node:path');
676
- const devopsSrc = readFileSync(join(__dirname, '..', 'tools', 'schemas', 'devops.ts'), 'utf-8');
677
- // There should be at least 2 occurrences of ctx?.timeout ?? DEFAULT_TIMEOUT (terraform + helm)
678
- const occurrences = (devopsSrc.match(/ctx\?\.timeout \?\? DEFAULT_TIMEOUT/g) ?? []).length;
679
- expect(occurrences).toBeGreaterThanOrEqual(2);
680
- });
681
- });
682
-
683
- // ---------------------------------------------------------------------------
684
- // GAP-20 Tests: ink/index.ts parseToolTimeouts integration
685
- // ---------------------------------------------------------------------------
686
-
687
- describe('GAP-20 — ink/index.ts parseToolTimeouts integration', () => {
688
- it('parseToolTimeouts function is defined in ink/index.ts source', async () => {
689
- const { readFileSync } = await import('node:fs');
690
- const { join } = await import('node:path');
691
- const inkSrc = readFileSync(join(__dirname, '..', 'ui', 'ink', 'index.ts'), 'utf-8');
692
- expect(inkSrc).toContain('function parseToolTimeouts');
693
- });
694
-
695
- it('ink/index.ts passes toolTimeouts to runAgentLoop', async () => {
696
- const { readFileSync } = await import('node:fs');
697
- const { join } = await import('node:path');
698
- const inkSrc = readFileSync(join(__dirname, '..', 'ui', 'ink', 'index.ts'), 'utf-8');
699
- expect(inkSrc).toContain('toolTimeouts:');
700
- expect(inkSrc).toContain('parseToolTimeouts(nimbusInstructions)');
701
- });
702
-
703
- it('parseToolTimeouts uses the ## Tool Timeouts section regex', async () => {
704
- const { readFileSync } = await import('node:fs');
705
- const { join } = await import('node:path');
706
- const inkSrc = readFileSync(join(__dirname, '..', 'ui', 'ink', 'index.ts'), 'utf-8');
707
- expect(inkSrc).toContain('Tool Timeouts');
708
- expect(inkSrc).toContain('[a-z_]+');
709
- });
710
- });
711
-
712
- // ---------------------------------------------------------------------------
713
- // GAP-11 Tests: loop.ts FileDiff wiring source check
714
- // ---------------------------------------------------------------------------
715
-
716
- describe('GAP-11 — loop.ts FileDiff wiring', () => {
717
- it('loop.ts contains GAP-11 comment for FileDiff trigger', async () => {
718
- const { readFileSync } = await import('node:fs');
719
- const { join } = await import('node:path');
720
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
721
- expect(loopSrc).toContain('GAP-11');
722
- });
723
-
724
- it('loop.ts imports parseTerraformPlanOutput from deploy-preview', async () => {
725
- const { readFileSync } = await import('node:fs');
726
- const { join } = await import('node:path');
727
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
728
- expect(loopSrc).toContain('parseTerraformPlanOutput');
729
- expect(loopSrc).toContain('buildFileDiffBatchFromPlan');
730
- expect(loopSrc).toContain('./deploy-preview');
731
- });
732
-
733
- it('loop.ts checks for terraform plan action before calling FileDiff', async () => {
734
- const { readFileSync } = await import('node:fs');
735
- const { join } = await import('node:path');
736
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
737
- expect(loopSrc).toContain("action === 'plan'");
738
- expect(loopSrc).toContain('options.requestFileDiff');
739
- });
740
-
741
- it('loop.ts breaks on reject-all decision in FileDiff loop', async () => {
742
- const { readFileSync } = await import('node:fs');
743
- const { join } = await import('node:path');
744
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
745
- // The GAP-11 block should break on reject-all
746
- expect(loopSrc).toContain("decision === 'reject-all'");
747
- expect(loopSrc).toContain('break');
748
- });
749
- });
750
-
751
- // ---------------------------------------------------------------------------
752
- // GAP-18 Tests: loop.ts IaC validation wiring source check
753
- // ---------------------------------------------------------------------------
754
-
755
- describe('GAP-18 — loop.ts IaC validation wiring', () => {
756
- it('loop.ts contains GAP-18 comment for terraform validate', async () => {
757
- const { readFileSync } = await import('node:fs');
758
- const { join } = await import('node:path');
759
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
760
- expect(loopSrc).toContain('GAP-18');
761
- });
762
-
763
- it('loop.ts checks for .tf file extension', async () => {
764
- const { readFileSync } = await import('node:fs');
765
- const { join } = await import('node:path');
766
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
767
- expect(loopSrc).toContain(".endsWith('.tf')");
768
- });
769
-
770
- it('loop.ts runs terraform validate -json', async () => {
771
- const { readFileSync } = await import('node:fs');
772
- const { join } = await import('node:path');
773
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
774
- expect(loopSrc).toContain('terraform validate -json');
775
- });
776
-
777
- it('loop.ts checks for write_file, edit_file, multi_edit tool names', async () => {
778
- const { readFileSync } = await import('node:fs');
779
- const { join } = await import('node:path');
780
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
781
- expect(loopSrc).toContain('write_file');
782
- expect(loopSrc).toContain('edit_file');
783
- expect(loopSrc).toContain('multi_edit');
784
- });
785
-
786
- it('loop.ts appends validation errors to toolContent', async () => {
787
- const { readFileSync } = await import('node:fs');
788
- const { join } = await import('node:path');
789
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
790
- expect(loopSrc).toContain('Terraform validation errors (please fix)');
791
- });
792
-
793
- it('loop.ts uses 10 second timeout for validate command', async () => {
794
- const { readFileSync } = await import('node:fs');
795
- const { join } = await import('node:path');
796
- const loopSrc = readFileSync(join(__dirname, '..', 'agent', 'loop.ts'), 'utf-8');
797
- expect(loopSrc).toContain('timeout: 10_000');
798
- });
799
- });
800
-
801
- // ---------------------------------------------------------------------------
802
- // Integration: parseToolTimeouts with a realistic NIMBUS.md
803
- // ---------------------------------------------------------------------------
804
-
805
- describe('GAP-20 — parseToolTimeouts with realistic NIMBUS.md', () => {
806
- const realisticNimbusMd = `# NIMBUS.md
807
-
808
- ## Project
809
- This is an AWS infrastructure project using Terraform and Kubernetes.
810
-
811
- ## Cloud Context
812
- - AWS Account: 123456789012
813
- - Region: us-east-1
814
-
815
- ## Tool Timeouts
816
- terraform: 900000
817
- kubectl: 180000
818
- helm: 600000
819
- cloud_discover: 30000
820
-
821
- ## Instructions
822
- Always run terraform plan before apply.
823
- `;
824
-
825
- it('parses all tool timeouts from a realistic NIMBUS.md', () => {
826
- const result = parseToolTimeouts(realisticNimbusMd);
827
- expect(result.terraform).toBe(900000);
828
- expect(result.kubectl).toBe(180000);
829
- expect(result.helm).toBe(600000);
830
- expect(result.cloud_discover).toBe(30000);
831
- });
832
-
833
- it('does not include keys from other sections', () => {
834
- const result = parseToolTimeouts(realisticNimbusMd);
835
- expect(Object.keys(result)).toHaveLength(4);
836
- });
837
-
838
- it('returns correct types (numbers not strings)', () => {
839
- const result = parseToolTimeouts(realisticNimbusMd);
840
- for (const value of Object.values(result)) {
841
- expect(typeof value).toBe('number');
842
- }
843
- });
844
-
845
- it('handles NIMBUS.md with no Tool Timeouts section gracefully', () => {
846
- const minimal = `# NIMBUS.md\n\n## Project\nSome project.\n`;
847
- const result = parseToolTimeouts(minimal);
848
- expect(result).toEqual({});
849
- });
850
-
851
- it('handles NIMBUS.md where Tool Timeouts is the last section', () => {
852
- const lastSection = `# NIMBUS.md\n\n## Project\nSome project.\n\n## Tool Timeouts\nbash: 60000\n`;
853
- const result = parseToolTimeouts(lastSection);
854
- expect(result.bash).toBe(60000);
855
- });
856
- });
857
-
858
- // ---------------------------------------------------------------------------
859
- // H2: Parallel read-only tool dispatch
860
- // ---------------------------------------------------------------------------
861
-
862
- describe('parallel read-only tool dispatch (H2)', () => {
863
- it('loop.ts contains READ_ONLY_TOOLS set', async () => {
864
- const { readFileSync } = await import('node:fs');
865
- const { join } = await import('node:path');
866
- const src = readFileSync(join(process.cwd(), 'src/agent/loop.ts'), 'utf-8');
867
- expect(src).toContain('READ_ONLY_TOOLS');
868
- });
869
-
870
- it('READ_ONLY_TOOLS includes cloud_discover and read_file', async () => {
871
- const { readFileSync } = await import('node:fs');
872
- const { join } = await import('node:path');
873
- const src = readFileSync(join(process.cwd(), 'src/agent/loop.ts'), 'utf-8');
874
- expect(src).toContain("'cloud_discover'");
875
- expect(src).toContain("'read_file'");
876
- });
877
-
878
- it('parallel dispatch uses Promise.allSettled', async () => {
879
- const { readFileSync } = await import('node:fs');
880
- const { join } = await import('node:path');
881
- const src = readFileSync(join(process.cwd(), 'src/agent/loop.ts'), 'utf-8');
882
- expect(src).toContain('Promise.allSettled');
883
- });
884
-
885
- it('allReadOnly check requires length > 1', async () => {
886
- const { readFileSync } = await import('node:fs');
887
- const { join } = await import('node:path');
888
- const src = readFileSync(join(process.cwd(), 'src/agent/loop.ts'), 'utf-8');
889
- expect(src).toContain('allReadOnly && responseToolCalls.length > 1');
890
- });
891
-
892
- it('cloudDiscoverTool schema has regions array field', async () => {
893
- const { devopsTools } = await import('../tools/schemas/devops');
894
- const tool = devopsTools.find(t => t.name === 'cloud_discover');
895
- expect(tool).toBeDefined();
896
- // Schema should have regions field
897
- const { readFileSync } = await import('node:fs');
898
- const { join } = await import('node:path');
899
- const src = readFileSync(join(process.cwd(), 'src/tools/schemas/devops.ts'), 'utf-8');
900
- expect(src).toContain('regions: z.array(z.string())');
901
- });
902
-
903
- it('parallel dispatch uses continue to skip sequential loop', async () => {
904
- const { readFileSync } = await import('node:fs');
905
- const { join } = await import('node:path');
906
- const src = readFileSync(join(process.cwd(), 'src/agent/loop.ts'), 'utf-8');
907
- expect(src).toContain('Skip sequential processing');
908
- });
909
-
910
- it('READ_ONLY_TOOLS includes kubectl_context', async () => {
911
- const { readFileSync } = await import('node:fs');
912
- const { join } = await import('node:path');
913
- const src = readFileSync(join(process.cwd(), 'src/agent/loop.ts'), 'utf-8');
914
- expect(src).toContain("'kubectl_context'");
915
- });
916
-
917
- it('READ_ONLY_TOOLS includes helm_values', async () => {
918
- const { readFileSync } = await import('node:fs');
919
- const { join } = await import('node:path');
920
- const src = readFileSync(join(process.cwd(), 'src/agent/loop.ts'), 'utf-8');
921
- expect(src).toContain("'helm_values'");
922
- });
923
- });
924
-
925
- // ---------------------------------------------------------------------------
926
- // H1: LIVE streaming indicator
927
- // ---------------------------------------------------------------------------
928
-
929
- describe('LIVE streaming indicator (H1)', () => {
930
- it('ToolCallDisplay has LIVE indicator for logs tool', async () => {
931
- const { readFileSync } = await import('node:fs');
932
- const { join } = await import('node:path');
933
- const src = readFileSync(join(process.cwd(), 'src/ui/ToolCallDisplay.tsx'), 'utf-8');
934
- expect(src).toContain('● LIVE');
935
- });
936
-
937
- it('StatusBar has showStreamingHint prop', async () => {
938
- const { readFileSync } = await import('node:fs');
939
- const { join } = await import('node:path');
940
- const src = readFileSync(join(process.cwd(), 'src/ui/StatusBar.tsx'), 'utf-8');
941
- expect(src).toContain('showStreamingHint');
942
- });
943
-
944
- it('StatusBar shows Esc:stop stream when streaming hint active', async () => {
945
- const { readFileSync } = await import('node:fs');
946
- const { join } = await import('node:path');
947
- const src = readFileSync(join(process.cwd(), 'src/ui/StatusBar.tsx'), 'utf-8');
948
- expect(src).toContain('Esc:stop stream');
949
- });
950
-
951
- it('ToolCallDisplay streaming window is 40 for generic tools (M1: increased from 20)', async () => {
952
- const { readFileSync } = await import('node:fs');
953
- const { join } = await import('node:path');
954
- const src = readFileSync(join(process.cwd(), 'src/ui/ToolCallDisplay.tsx'), 'utf-8');
955
- // M1: Streaming window was increased — 60 lines for terraform/kubectl/logs, 40 for other tools
956
- expect(src).toContain('windowSize = isTerraformOrKubectl ? 60 : 40');
957
- });
958
- });