@build-astron-co/nimbus 0.2.0

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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Subagent Tests
3
+ *
4
+ * Validates subagent creation, configuration, tool restrictions, and
5
+ * the @agent mention parser.
6
+ */
7
+
8
+ import { describe, test, expect } from 'bun:test';
9
+ import {
10
+ createSubagent,
11
+ parseAgentMention,
12
+ type SubagentType,
13
+ exploreConfig,
14
+ infraConfig,
15
+ securityConfig,
16
+ costConfig,
17
+ generalConfig,
18
+ Subagent,
19
+ } from '../agent/subagents/index';
20
+
21
+ // ===========================================================================
22
+ // createSubagent
23
+ // ===========================================================================
24
+
25
+ describe('createSubagent', () => {
26
+ test('createSubagent("explore") returns Subagent with correct config', () => {
27
+ const agent = createSubagent('explore');
28
+ expect(agent).toBeInstanceOf(Subagent);
29
+ expect(agent.config.name).toBe('explore');
30
+ expect(agent.config.model).toBe('anthropic/claude-haiku-4-5');
31
+ expect(agent.config.maxTurns).toBe(15);
32
+ });
33
+
34
+ test('createSubagent("infra") returns Subagent with correct tools', () => {
35
+ const agent = createSubagent('infra');
36
+ expect(agent.config.name).toBe('infra');
37
+ const toolNames = agent.config.tools.map(t => t.name);
38
+ expect(toolNames).toContain('read_file');
39
+ expect(toolNames).toContain('cloud_discover');
40
+ expect(toolNames).toContain('cost_estimate');
41
+ expect(toolNames).toContain('drift_detect');
42
+ });
43
+
44
+ test('createSubagent("security") returns Subagent with security prompt', () => {
45
+ const agent = createSubagent('security');
46
+ expect(agent.config.name).toBe('security');
47
+ expect(agent.config.systemPrompt).toContain('security');
48
+ expect(agent.config.systemPrompt).toContain('CRITICAL');
49
+ });
50
+
51
+ test('createSubagent("cost") uses haiku model', () => {
52
+ const agent = createSubagent('cost');
53
+ expect(agent.config.model).toContain('haiku');
54
+ });
55
+
56
+ test('createSubagent("general") has bash and webfetch tools', () => {
57
+ const agent = createSubagent('general');
58
+ const toolNames = agent.config.tools.map(t => t.name);
59
+ expect(toolNames).toContain('bash');
60
+ expect(toolNames).toContain('webfetch');
61
+ });
62
+
63
+ test('all subagent types are valid and produce Subagent instances', () => {
64
+ const types: SubagentType[] = ['explore', 'infra', 'security', 'cost', 'general'];
65
+ for (const type of types) {
66
+ const agent = createSubagent(type);
67
+ expect(agent).toBeInstanceOf(Subagent);
68
+ expect(agent.config.name).toBe(type);
69
+ }
70
+ });
71
+ });
72
+
73
+ // ===========================================================================
74
+ // parseAgentMention
75
+ // ===========================================================================
76
+
77
+ describe('parseAgentMention', () => {
78
+ test('parses "@explore find TODOs" correctly', () => {
79
+ const result = parseAgentMention('@explore find TODOs');
80
+ expect(result).not.toBeNull();
81
+ expect(result!.agent).toBe('explore');
82
+ expect(result!.prompt).toBe('find TODOs');
83
+ });
84
+
85
+ test('parses "@infra check EKS" correctly', () => {
86
+ const result = parseAgentMention('@infra check EKS');
87
+ expect(result).not.toBeNull();
88
+ expect(result!.agent).toBe('infra');
89
+ expect(result!.prompt).toBe('check EKS');
90
+ });
91
+
92
+ test('parses "@security scan for secrets" correctly', () => {
93
+ const result = parseAgentMention('@security scan for secrets');
94
+ expect(result).not.toBeNull();
95
+ expect(result!.agent).toBe('security');
96
+ expect(result!.prompt).toBe('scan for secrets');
97
+ });
98
+
99
+ test('parses "@cost estimate monthly spend" correctly', () => {
100
+ const result = parseAgentMention('@cost estimate monthly spend');
101
+ expect(result).not.toBeNull();
102
+ expect(result!.agent).toBe('cost');
103
+ });
104
+
105
+ test('parses "@general research topic" correctly', () => {
106
+ const result = parseAgentMention('@general research topic');
107
+ expect(result).not.toBeNull();
108
+ expect(result!.agent).toBe('general');
109
+ });
110
+
111
+ test('returns null for normal messages', () => {
112
+ expect(parseAgentMention('normal message without @mention')).toBeNull();
113
+ expect(parseAgentMention('fix the bug')).toBeNull();
114
+ });
115
+
116
+ test('returns null for empty string', () => {
117
+ expect(parseAgentMention('')).toBeNull();
118
+ });
119
+
120
+ test('returns null for unknown @agent prefix', () => {
121
+ expect(parseAgentMention('@unknown do something')).toBeNull();
122
+ expect(parseAgentMention('@deploy run it')).toBeNull();
123
+ });
124
+
125
+ test('returns null when @agent has no prompt', () => {
126
+ // The regex requires at least one character after the agent name
127
+ expect(parseAgentMention('@explore')).toBeNull();
128
+ });
129
+ });
130
+
131
+ // ===========================================================================
132
+ // Subagent Tool Restrictions
133
+ // ===========================================================================
134
+
135
+ describe('Subagent tool restrictions', () => {
136
+ test('all subagent configs exclude the "task" tool (no nesting)', () => {
137
+ const configs = [exploreConfig, infraConfig, securityConfig, costConfig, generalConfig];
138
+ for (const config of configs) {
139
+ const hasTask = config.tools.some(t => t.name === 'task');
140
+ expect(hasTask).toBe(false);
141
+ }
142
+ });
143
+
144
+ test('explore subagent only has read-only tools', () => {
145
+ const toolNames = exploreConfig.tools.map(t => t.name);
146
+ expect(toolNames).toEqual(['read_file', 'glob', 'grep', 'list_dir']);
147
+ // None of these are destructive
148
+ for (const tool of exploreConfig.tools) {
149
+ expect(tool.isDestructive).toBeFalsy();
150
+ }
151
+ });
152
+
153
+ test('security subagent only has read-only tools', () => {
154
+ const toolNames = securityConfig.tools.map(t => t.name);
155
+ expect(toolNames).toEqual(['read_file', 'glob', 'grep', 'list_dir']);
156
+ for (const tool of securityConfig.tools) {
157
+ expect(tool.isDestructive).toBeFalsy();
158
+ }
159
+ });
160
+
161
+ test('infra subagent has cloud discovery tools', () => {
162
+ const toolNames = infraConfig.tools.map(t => t.name);
163
+ expect(toolNames).toContain('cloud_discover');
164
+ expect(toolNames).toContain('cost_estimate');
165
+ expect(toolNames).toContain('drift_detect');
166
+ });
167
+
168
+ test('cost subagent has cost_estimate and cloud_discover', () => {
169
+ const toolNames = costConfig.tools.map(t => t.name);
170
+ expect(toolNames).toContain('cost_estimate');
171
+ expect(toolNames).toContain('cloud_discover');
172
+ // But should not have destructive tools
173
+ expect(toolNames).not.toContain('terraform');
174
+ expect(toolNames).not.toContain('kubectl');
175
+ });
176
+ });
@@ -0,0 +1,169 @@
1
+ /**
2
+ * System Prompt Tests
3
+ *
4
+ * Validates that buildSystemPrompt assembles the correct prompt sections
5
+ * based on mode, tools, NIMBUS.md, subagent state, and environment context.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+ import { buildSystemPrompt, loadNimbusMd } from '../agent/system-prompt';
13
+ import type { ToolDefinition } from '../tools/schemas/types';
14
+ import { z } from 'zod';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Create a minimal ToolDefinition for prompt tests. */
21
+ function makeTool(name: string): ToolDefinition {
22
+ return {
23
+ name,
24
+ description: `Description of ${name}`,
25
+ inputSchema: z.object({}),
26
+ execute: async () => ({ output: 'ok', isError: false }),
27
+ permissionTier: 'auto_allow',
28
+ category: 'standard',
29
+ };
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Temp directory for NIMBUS.md tests
34
+ // ---------------------------------------------------------------------------
35
+
36
+ let tmpDir: string;
37
+
38
+ beforeEach(() => {
39
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nimbus-prompt-test-'));
40
+ });
41
+
42
+ afterEach(() => {
43
+ fs.rmSync(tmpDir, { recursive: true, force: true });
44
+ });
45
+
46
+ // ===========================================================================
47
+ // buildSystemPrompt
48
+ // ===========================================================================
49
+
50
+ describe('buildSystemPrompt', () => {
51
+ test('includes base identity', () => {
52
+ const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
53
+ expect(prompt).toContain('You are Nimbus');
54
+ expect(prompt).toContain('AI-powered DevOps');
55
+ });
56
+
57
+ test('includes mode-specific instructions for "plan"', () => {
58
+ const prompt = buildSystemPrompt({ mode: 'plan', tools: [] });
59
+ expect(prompt).toContain('Mode: PLAN');
60
+ expect(prompt).toContain('NOT allowed');
61
+ });
62
+
63
+ test('includes mode-specific instructions for "build"', () => {
64
+ const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
65
+ expect(prompt).toContain('Mode: BUILD');
66
+ expect(prompt).toContain('Edit and create files');
67
+ });
68
+
69
+ test('includes mode-specific instructions for "deploy"', () => {
70
+ const prompt = buildSystemPrompt({ mode: 'deploy', tools: [] });
71
+ expect(prompt).toContain('Mode: DEPLOY');
72
+ expect(prompt).toContain('terraform apply');
73
+ });
74
+
75
+ test('includes tool-use guidelines', () => {
76
+ const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
77
+ expect(prompt).toContain('Tool-Use Guidelines');
78
+ expect(prompt).toContain('read_file');
79
+ });
80
+
81
+ test('includes tools summary with correct count', () => {
82
+ const tools = [makeTool('alpha'), makeTool('beta'), makeTool('gamma')];
83
+ const prompt = buildSystemPrompt({ mode: 'build', tools });
84
+ expect(prompt).toContain('Available Tools (3)');
85
+ expect(prompt).toContain('**alpha**');
86
+ expect(prompt).toContain('**beta**');
87
+ expect(prompt).toContain('**gamma**');
88
+ });
89
+
90
+ test('includes NIMBUS.md content when provided', () => {
91
+ const prompt = buildSystemPrompt({
92
+ mode: 'build',
93
+ tools: [],
94
+ nimbusInstructions: 'Always use TypeScript strict mode.',
95
+ });
96
+ expect(prompt).toContain('Project Instructions (NIMBUS.md)');
97
+ expect(prompt).toContain('Always use TypeScript strict mode.');
98
+ });
99
+
100
+ test('includes environment context', () => {
101
+ const prompt = buildSystemPrompt({
102
+ mode: 'build',
103
+ tools: [],
104
+ cwd: tmpDir,
105
+ });
106
+ expect(prompt).toContain('# Environment');
107
+ expect(prompt).toContain(`Working directory: ${tmpDir}`);
108
+ expect(prompt).toContain(`Platform: ${process.platform}`);
109
+ });
110
+
111
+ test('includes subagent instructions when activeSubagent set', () => {
112
+ const prompt = buildSystemPrompt({
113
+ mode: 'build',
114
+ tools: [],
115
+ activeSubagent: 'explore',
116
+ });
117
+ expect(prompt).toContain('Subagent Mode: explore');
118
+ expect(prompt).toContain('Do NOT spawn further subagents');
119
+ });
120
+
121
+ test('does not include subagent section when activeSubagent is not set', () => {
122
+ const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
123
+ expect(prompt).not.toContain('Subagent Mode');
124
+ });
125
+
126
+ test('includes date in environment context', () => {
127
+ const prompt = buildSystemPrompt({ mode: 'build', tools: [] });
128
+ // Should contain a date-like string YYYY-MM-DD
129
+ expect(prompt).toMatch(/Date: \d{4}-\d{2}-\d{2}/);
130
+ });
131
+ });
132
+
133
+ // ===========================================================================
134
+ // loadNimbusMd
135
+ // ===========================================================================
136
+
137
+ describe('loadNimbusMd', () => {
138
+ test('returns null when no file exists', () => {
139
+ const result = loadNimbusMd(tmpDir);
140
+ expect(result).toBeNull();
141
+ });
142
+
143
+ test('loads NIMBUS.md from cwd', () => {
144
+ const content = '# Custom Instructions\nDo the thing.';
145
+ fs.writeFileSync(path.join(tmpDir, 'NIMBUS.md'), content, 'utf-8');
146
+ const result = loadNimbusMd(tmpDir);
147
+ expect(result).toBe(content);
148
+ });
149
+
150
+ test('loads NIMBUS.md from .nimbus subdirectory', () => {
151
+ const nimbusDir = path.join(tmpDir, '.nimbus');
152
+ fs.mkdirSync(nimbusDir, { recursive: true });
153
+ const content = 'Sub-dir instructions';
154
+ fs.writeFileSync(path.join(nimbusDir, 'NIMBUS.md'), content, 'utf-8');
155
+ const result = loadNimbusMd(tmpDir);
156
+ expect(result).toBe(content);
157
+ });
158
+
159
+ test('prefers cwd NIMBUS.md over .nimbus subdirectory', () => {
160
+ // Write both
161
+ fs.writeFileSync(path.join(tmpDir, 'NIMBUS.md'), 'root-level', 'utf-8');
162
+ const nimbusDir = path.join(tmpDir, '.nimbus');
163
+ fs.mkdirSync(nimbusDir, { recursive: true });
164
+ fs.writeFileSync(path.join(nimbusDir, 'NIMBUS.md'), 'sub-level', 'utf-8');
165
+
166
+ const result = loadNimbusMd(tmpDir);
167
+ expect(result).toBe('root-level');
168
+ });
169
+ });
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Tool Converter Tests
3
+ *
4
+ * Validates the Zod-to-JSON-Schema converter and the provider-specific
5
+ * format converters (Anthropic, OpenAI, Google).
6
+ */
7
+
8
+ import { describe, test, expect } from 'bun:test';
9
+ import { z } from 'zod';
10
+ import {
11
+ zodToJsonSchema,
12
+ toAnthropicTool,
13
+ toOpenAITool,
14
+ toGoogleTool,
15
+ type ToolDefinition,
16
+ } from '../tools/schemas/types';
17
+ import {
18
+ toAnthropicFormat,
19
+ toOpenAIFormat,
20
+ toGoogleFormat,
21
+ toProviderFormat,
22
+ } from '../tools/schemas/converter';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Create a minimal ToolDefinition for conversion tests. */
29
+ function makeTool(name: string, schema: z.ZodType<unknown>): ToolDefinition {
30
+ return {
31
+ name,
32
+ description: `Description of ${name}`,
33
+ inputSchema: schema,
34
+ execute: async () => ({ output: 'ok', isError: false }),
35
+ permissionTier: 'auto_allow',
36
+ category: 'standard',
37
+ };
38
+ }
39
+
40
+ // ===========================================================================
41
+ // zodToJsonSchema — Primitive Types
42
+ // ===========================================================================
43
+
44
+ describe('zodToJsonSchema', () => {
45
+ test('converts z.string() to { type: "string" }', () => {
46
+ const result = zodToJsonSchema(z.string());
47
+ expect(result).toEqual({ type: 'string' });
48
+ });
49
+
50
+ test('converts z.number() to { type: "number" }', () => {
51
+ const result = zodToJsonSchema(z.number());
52
+ expect(result).toEqual({ type: 'number' });
53
+ });
54
+
55
+ test('converts z.boolean() to { type: "boolean" }', () => {
56
+ const result = zodToJsonSchema(z.boolean());
57
+ expect(result).toEqual({ type: 'boolean' });
58
+ });
59
+
60
+ test('converts z.object({ name: z.string() }) with required', () => {
61
+ const result = zodToJsonSchema(z.object({ name: z.string() }));
62
+ expect(result.type).toBe('object');
63
+ expect(result.properties).toBeDefined();
64
+ expect((result.properties as Record<string, unknown>).name).toEqual({ type: 'string' });
65
+ expect(result.required).toEqual(['name']);
66
+ });
67
+
68
+ test('handles optional fields (not in required array)', () => {
69
+ const result = zodToJsonSchema(
70
+ z.object({
71
+ required_field: z.string(),
72
+ optional_field: z.string().optional(),
73
+ })
74
+ );
75
+ expect(result.required).toEqual(['required_field']);
76
+ expect((result.properties as Record<string, unknown>).optional_field).toBeDefined();
77
+ });
78
+
79
+ test('handles z.enum(["a", "b"]) as { type: "string", enum: ["a", "b"] }', () => {
80
+ const result = zodToJsonSchema(z.enum(['a', 'b']));
81
+ expect(result.type).toBe('string');
82
+ expect(result.enum).toEqual(['a', 'b']);
83
+ });
84
+
85
+ test('handles z.array(z.string()) as { type: "array", items: { type: "string" } }', () => {
86
+ const result = zodToJsonSchema(z.array(z.string()));
87
+ expect(result.type).toBe('array');
88
+ expect(result.items).toEqual({ type: 'string' });
89
+ });
90
+
91
+ test('handles nested objects', () => {
92
+ const result = zodToJsonSchema(
93
+ z.object({
94
+ inner: z.object({
95
+ value: z.number(),
96
+ }),
97
+ })
98
+ );
99
+ expect(result.type).toBe('object');
100
+ const inner = (result.properties as Record<string, any>).inner;
101
+ expect(inner.type).toBe('object');
102
+ expect(inner.properties.value).toEqual({ type: 'number' });
103
+ });
104
+
105
+ test('handles z.number().optional().default() with default value', () => {
106
+ const result = zodToJsonSchema(z.number().optional().default(42));
107
+ expect(result.type).toBe('number');
108
+ expect(result.default).toBe(42);
109
+ });
110
+ });
111
+
112
+ // ===========================================================================
113
+ // toAnthropicTool
114
+ // ===========================================================================
115
+
116
+ describe('toAnthropicTool', () => {
117
+ test('produces correct format with input_schema.type = "object"', () => {
118
+ const tool = makeTool('test_tool', z.object({ x: z.string() }));
119
+ const result = toAnthropicTool(tool);
120
+
121
+ expect(result.name).toBe('test_tool');
122
+ expect(result.description).toBe('Description of test_tool');
123
+ expect(result.input_schema).toBeDefined();
124
+ expect(result.input_schema.type).toBe('object');
125
+ expect(result.input_schema.properties).toBeDefined();
126
+ });
127
+ });
128
+
129
+ // ===========================================================================
130
+ // toOpenAITool
131
+ // ===========================================================================
132
+
133
+ describe('toOpenAITool', () => {
134
+ test('produces correct format with type = "function"', () => {
135
+ const tool = makeTool('test_tool', z.object({ y: z.number() }));
136
+ const result = toOpenAITool(tool);
137
+
138
+ expect(result.type).toBe('function');
139
+ expect(result.function.name).toBe('test_tool');
140
+ expect(result.function.description).toBe('Description of test_tool');
141
+ expect(result.function.parameters).toBeDefined();
142
+ expect(result.function.parameters.type).toBe('object');
143
+ });
144
+ });
145
+
146
+ // ===========================================================================
147
+ // toGoogleTool
148
+ // ===========================================================================
149
+
150
+ describe('toGoogleTool', () => {
151
+ test('produces correct format with functionDeclarations', () => {
152
+ const tool1 = makeTool('tool_a', z.object({ a: z.string() }));
153
+ const tool2 = makeTool('tool_b', z.object({ b: z.number() }));
154
+ const result = toGoogleTool([tool1, tool2]);
155
+
156
+ expect(result.functionDeclarations).toBeDefined();
157
+ expect(result.functionDeclarations).toHaveLength(2);
158
+ expect(result.functionDeclarations[0].name).toBe('tool_a');
159
+ expect(result.functionDeclarations[1].name).toBe('tool_b');
160
+ expect(result.functionDeclarations[0].parameters.type).toBe('OBJECT');
161
+ });
162
+ });
163
+
164
+ // ===========================================================================
165
+ // Batch Conversion Functions
166
+ // ===========================================================================
167
+
168
+ describe('toAnthropicFormat', () => {
169
+ test('converts array of tools to Anthropic format', () => {
170
+ const tools = [makeTool('a', z.object({})), makeTool('b', z.object({}))];
171
+ const result = toAnthropicFormat(tools);
172
+ expect(result).toHaveLength(2);
173
+ expect(result[0].name).toBe('a');
174
+ expect(result[1].name).toBe('b');
175
+ expect(result[0].input_schema.type).toBe('object');
176
+ });
177
+ });
178
+
179
+ describe('toOpenAIFormat', () => {
180
+ test('converts array of tools to OpenAI format', () => {
181
+ const tools = [makeTool('a', z.object({})), makeTool('b', z.object({}))];
182
+ const result = toOpenAIFormat(tools);
183
+ expect(result).toHaveLength(2);
184
+ expect(result[0].type).toBe('function');
185
+ expect(result[1].function.name).toBe('b');
186
+ });
187
+ });
188
+
189
+ describe('toGoogleFormat', () => {
190
+ test('produces single tool object with all declarations', () => {
191
+ const tools = [
192
+ makeTool('x', z.object({})),
193
+ makeTool('y', z.object({})),
194
+ makeTool('z', z.object({})),
195
+ ];
196
+ const result = toGoogleFormat(tools);
197
+ expect(result.functionDeclarations).toHaveLength(3);
198
+ expect(result.functionDeclarations.map(d => d.name)).toEqual(['x', 'y', 'z']);
199
+ });
200
+ });
201
+
202
+ // ===========================================================================
203
+ // toProviderFormat
204
+ // ===========================================================================
205
+
206
+ describe('toProviderFormat', () => {
207
+ const tools = [makeTool('t', z.object({ a: z.string() }))];
208
+
209
+ test('dispatches to Anthropic for "anthropic"', () => {
210
+ const result = toProviderFormat(tools, 'anthropic');
211
+ expect(Array.isArray(result)).toBe(true);
212
+ const arr = result as any[];
213
+ expect(arr[0].input_schema).toBeDefined();
214
+ });
215
+
216
+ test('dispatches to OpenAI for "openai"', () => {
217
+ const result = toProviderFormat(tools, 'openai');
218
+ expect(Array.isArray(result)).toBe(true);
219
+ const arr = result as any[];
220
+ expect(arr[0].type).toBe('function');
221
+ });
222
+
223
+ test('dispatches to Google for "google"', () => {
224
+ const result = toProviderFormat(tools, 'google');
225
+ expect(Array.isArray(result)).toBe(false);
226
+ expect((result as any).functionDeclarations).toBeDefined();
227
+ });
228
+
229
+ test('dispatches to OpenAI for "ollama"', () => {
230
+ const result = toProviderFormat(tools, 'ollama');
231
+ expect(Array.isArray(result)).toBe(true);
232
+ const arr = result as any[];
233
+ expect(arr[0].type).toBe('function');
234
+ });
235
+
236
+ test('dispatches to OpenAI for "openrouter"', () => {
237
+ const result = toProviderFormat(tools, 'openrouter');
238
+ expect(Array.isArray(result)).toBe(true);
239
+ const arr = result as any[];
240
+ expect(arr[0].type).toBe('function');
241
+ });
242
+
243
+ test('dispatches to Anthropic for "bedrock"', () => {
244
+ const result = toProviderFormat(tools, 'bedrock');
245
+ expect(Array.isArray(result)).toBe(true);
246
+ const arr = result as any[];
247
+ expect(arr[0].input_schema).toBeDefined();
248
+ });
249
+
250
+ test('dispatches to OpenAI for "openai-compatible"', () => {
251
+ const result = toProviderFormat(tools, 'openai-compatible');
252
+ expect(Array.isArray(result)).toBe(true);
253
+ const arr = result as any[];
254
+ expect(arr[0].type).toBe('function');
255
+ });
256
+ });