@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,397 @@
1
+ /**
2
+ * Tool Schema Tests
3
+ *
4
+ * Validates tool definitions, ToolRegistry, permission tiers, and schema
5
+ * structures for both standard and DevOps tools.
6
+ */
7
+
8
+ import { describe, test, expect, beforeEach } from 'bun:test';
9
+ import { z } from 'zod';
10
+ import {
11
+ ToolRegistry,
12
+ PERMISSION_TIER_ORDER,
13
+ permissionTierIndex,
14
+ type ToolDefinition,
15
+ } from '../tools/schemas/types';
16
+ import { standardTools } from '../tools/schemas/standard';
17
+ import { devopsTools } from '../tools/schemas/devops';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** Create a minimal valid ToolDefinition for testing. */
24
+ function makeTool(name: string, overrides?: Partial<ToolDefinition>): ToolDefinition {
25
+ return {
26
+ name,
27
+ description: `Test tool: ${name}`,
28
+ inputSchema: z.object({}),
29
+ execute: async () => ({ output: 'ok', isError: false }),
30
+ permissionTier: 'auto_allow',
31
+ category: 'standard',
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ // ===========================================================================
37
+ // ToolRegistry
38
+ // ===========================================================================
39
+
40
+ describe('ToolRegistry', () => {
41
+ let registry: ToolRegistry;
42
+
43
+ beforeEach(() => {
44
+ registry = new ToolRegistry();
45
+ });
46
+
47
+ test('register and get a tool', () => {
48
+ const tool = makeTool('alpha');
49
+ registry.register(tool);
50
+ expect(registry.get('alpha')).toBe(tool);
51
+ });
52
+
53
+ test('get returns undefined for unknown tool', () => {
54
+ expect(registry.get('nonexistent')).toBeUndefined();
55
+ });
56
+
57
+ test('getAll returns all registered tools', () => {
58
+ registry.register(makeTool('a'));
59
+ registry.register(makeTool('b'));
60
+ registry.register(makeTool('c'));
61
+ const all = registry.getAll();
62
+ expect(all).toHaveLength(3);
63
+ expect(all.map(t => t.name)).toEqual(['a', 'b', 'c']);
64
+ });
65
+
66
+ test('getByCategory filters correctly', () => {
67
+ registry.register(makeTool('s1', { category: 'standard' }));
68
+ registry.register(makeTool('d1', { category: 'devops' }));
69
+ registry.register(makeTool('s2', { category: 'standard' }));
70
+ expect(registry.getByCategory('standard')).toHaveLength(2);
71
+ expect(registry.getByCategory('devops')).toHaveLength(1);
72
+ expect(registry.getByCategory('mcp')).toHaveLength(0);
73
+ });
74
+
75
+ test('getByPermissionTier filters correctly', () => {
76
+ registry.register(makeTool('t1', { permissionTier: 'auto_allow' }));
77
+ registry.register(makeTool('t2', { permissionTier: 'ask_once' }));
78
+ registry.register(makeTool('t3', { permissionTier: 'auto_allow' }));
79
+ expect(registry.getByPermissionTier('auto_allow')).toHaveLength(2);
80
+ expect(registry.getByPermissionTier('ask_once')).toHaveLength(1);
81
+ expect(registry.getByPermissionTier('always_ask')).toHaveLength(0);
82
+ });
83
+
84
+ test('getNames returns tool names in insertion order', () => {
85
+ registry.register(makeTool('z'));
86
+ registry.register(makeTool('a'));
87
+ registry.register(makeTool('m'));
88
+ expect(registry.getNames()).toEqual(['z', 'a', 'm']);
89
+ });
90
+
91
+ test('unregister removes a tool and returns true', () => {
92
+ registry.register(makeTool('x'));
93
+ expect(registry.unregister('x')).toBe(true);
94
+ expect(registry.get('x')).toBeUndefined();
95
+ expect(registry.size).toBe(0);
96
+ });
97
+
98
+ test('unregister returns false for non-existent tool', () => {
99
+ expect(registry.unregister('nope')).toBe(false);
100
+ });
101
+
102
+ test('clear removes all tools', () => {
103
+ registry.register(makeTool('a'));
104
+ registry.register(makeTool('b'));
105
+ registry.clear();
106
+ expect(registry.size).toBe(0);
107
+ expect(registry.getAll()).toEqual([]);
108
+ });
109
+
110
+ test('size reflects the number of registered tools', () => {
111
+ expect(registry.size).toBe(0);
112
+ registry.register(makeTool('one'));
113
+ expect(registry.size).toBe(1);
114
+ registry.register(makeTool('two'));
115
+ expect(registry.size).toBe(2);
116
+ });
117
+
118
+ test('throws on duplicate registration', () => {
119
+ registry.register(makeTool('dup'));
120
+ expect(() => registry.register(makeTool('dup'))).toThrow(/already registered/);
121
+ });
122
+ });
123
+
124
+ // ===========================================================================
125
+ // PERMISSION_TIER_ORDER & permissionTierIndex
126
+ // ===========================================================================
127
+
128
+ describe('PERMISSION_TIER_ORDER', () => {
129
+ test('is ordered from least to most restrictive', () => {
130
+ expect(PERMISSION_TIER_ORDER).toEqual(['auto_allow', 'ask_once', 'always_ask', 'blocked']);
131
+ });
132
+
133
+ test('has exactly 4 tiers', () => {
134
+ expect(PERMISSION_TIER_ORDER).toHaveLength(4);
135
+ });
136
+ });
137
+
138
+ describe('permissionTierIndex', () => {
139
+ test('returns 0 for auto_allow', () => {
140
+ expect(permissionTierIndex('auto_allow')).toBe(0);
141
+ });
142
+
143
+ test('returns 1 for ask_once', () => {
144
+ expect(permissionTierIndex('ask_once')).toBe(1);
145
+ });
146
+
147
+ test('returns 2 for always_ask', () => {
148
+ expect(permissionTierIndex('always_ask')).toBe(2);
149
+ });
150
+
151
+ test('returns 3 for blocked', () => {
152
+ expect(permissionTierIndex('blocked')).toBe(3);
153
+ });
154
+
155
+ test('indices are strictly ascending', () => {
156
+ const indices = PERMISSION_TIER_ORDER.map(permissionTierIndex);
157
+ for (let i = 1; i < indices.length; i++) {
158
+ expect(indices[i]).toBeGreaterThan(indices[i - 1]);
159
+ }
160
+ });
161
+ });
162
+
163
+ // ===========================================================================
164
+ // Standard Tool Schema Validation
165
+ // ===========================================================================
166
+
167
+ describe('Standard tool schemas', () => {
168
+ /** Helper to find a standard tool by name. */
169
+ function findStandard(name: string): ToolDefinition {
170
+ const t = standardTools.find(t => t.name === name);
171
+ if (!t) {
172
+ throw new Error(`Standard tool '${name}' not found`);
173
+ }
174
+ return t;
175
+ }
176
+
177
+ test('read_file: path is required, offset/limit are optional numbers', () => {
178
+ const schema = findStandard('read_file').inputSchema;
179
+ // Valid with just path
180
+ expect(() => schema.parse({ path: '/tmp/f.txt' })).not.toThrow();
181
+ // Valid with offset and limit
182
+ expect(() => schema.parse({ path: '/tmp/f.txt', offset: 5, limit: 10 })).not.toThrow();
183
+ // Missing path -> error
184
+ expect(() => schema.parse({})).toThrow();
185
+ });
186
+
187
+ test('edit_file: path, old_string, new_string are all required', () => {
188
+ const schema = findStandard('edit_file').inputSchema;
189
+ expect(() => schema.parse({ path: 'f', old_string: 'a', new_string: 'b' })).not.toThrow();
190
+ expect(() => schema.parse({ path: 'f', old_string: 'a' })).toThrow();
191
+ expect(() => schema.parse({ path: 'f' })).toThrow();
192
+ expect(() => schema.parse({})).toThrow();
193
+ });
194
+
195
+ test('multi_edit: edits array structure is validated', () => {
196
+ const schema = findStandard('multi_edit').inputSchema;
197
+ expect(() =>
198
+ schema.parse({
199
+ path: 'f',
200
+ edits: [{ old_string: 'a', new_string: 'b' }],
201
+ })
202
+ ).not.toThrow();
203
+ // edits missing -> error
204
+ expect(() => schema.parse({ path: 'f' })).toThrow();
205
+ // empty edits array is valid
206
+ expect(() => schema.parse({ path: 'f', edits: [] })).not.toThrow();
207
+ });
208
+
209
+ test('write_file: path and content are required', () => {
210
+ const schema = findStandard('write_file').inputSchema;
211
+ expect(() => schema.parse({ path: '/tmp/x', content: 'hello' })).not.toThrow();
212
+ expect(() => schema.parse({ path: '/tmp/x' })).toThrow();
213
+ expect(() => schema.parse({ content: 'hello' })).toThrow();
214
+ });
215
+
216
+ test('bash: command is required, timeout defaults to 120000', () => {
217
+ const schema = findStandard('bash').inputSchema;
218
+ const result = schema.parse({ command: 'echo hi' }) as { command: string; timeout: number };
219
+ expect(result.command).toBe('echo hi');
220
+ expect(result.timeout).toBe(120_000);
221
+ expect(() => schema.parse({})).toThrow();
222
+ });
223
+
224
+ test('glob: pattern is required', () => {
225
+ const schema = findStandard('glob').inputSchema;
226
+ expect(() => schema.parse({ pattern: '**/*.ts' })).not.toThrow();
227
+ expect(() => schema.parse({})).toThrow();
228
+ });
229
+
230
+ test('grep: pattern is required', () => {
231
+ const schema = findStandard('grep').inputSchema;
232
+ expect(() => schema.parse({ pattern: 'TODO' })).not.toThrow();
233
+ expect(() => schema.parse({})).toThrow();
234
+ });
235
+
236
+ test('list_dir: path is required', () => {
237
+ const schema = findStandard('list_dir').inputSchema;
238
+ expect(() => schema.parse({ path: '/tmp' })).not.toThrow();
239
+ expect(() => schema.parse({})).toThrow();
240
+ });
241
+
242
+ test('webfetch: url is required and must be valid URL', () => {
243
+ const schema = findStandard('webfetch').inputSchema;
244
+ expect(() => schema.parse({ url: 'https://example.com' })).not.toThrow();
245
+ expect(() => schema.parse({ url: 'not-a-url' })).toThrow();
246
+ expect(() => schema.parse({})).toThrow();
247
+ });
248
+
249
+ test('todo_read: accepts empty input', () => {
250
+ const schema = findStandard('todo_read').inputSchema;
251
+ expect(() => schema.parse({})).not.toThrow();
252
+ });
253
+
254
+ test('todo_write: validates tasks array with subject and status enum', () => {
255
+ const schema = findStandard('todo_write').inputSchema;
256
+ expect(() =>
257
+ schema.parse({
258
+ tasks: [{ subject: 'Fix bug', status: 'pending' }],
259
+ })
260
+ ).not.toThrow();
261
+ // Invalid status
262
+ expect(() =>
263
+ schema.parse({
264
+ tasks: [{ subject: 'X', status: 'invalid_status' }],
265
+ })
266
+ ).toThrow();
267
+ // Missing tasks
268
+ expect(() => schema.parse({})).toThrow();
269
+ });
270
+ });
271
+
272
+ // ===========================================================================
273
+ // DevOps Tool Schema Validation
274
+ // ===========================================================================
275
+
276
+ describe('DevOps tool schemas', () => {
277
+ /** Helper to find a devops tool by name. */
278
+ function findDevops(name: string): ToolDefinition {
279
+ const t = devopsTools.find(t => t.name === name);
280
+ if (!t) {
281
+ throw new Error(`DevOps tool '${name}' not found`);
282
+ }
283
+ return t;
284
+ }
285
+
286
+ test('terraform: action enum and workdir are required', () => {
287
+ const schema = findDevops('terraform').inputSchema;
288
+ expect(() => schema.parse({ action: 'plan', workdir: '/infra' })).not.toThrow();
289
+ expect(() => schema.parse({ action: 'invalid', workdir: '/infra' })).toThrow();
290
+ expect(() => schema.parse({ action: 'plan' })).toThrow();
291
+ });
292
+
293
+ test('kubectl: action enum is validated', () => {
294
+ const schema = findDevops('kubectl').inputSchema;
295
+ expect(() => schema.parse({ action: 'get' })).not.toThrow();
296
+ expect(() => schema.parse({ action: 'invalid_action' })).toThrow();
297
+ });
298
+
299
+ test('helm: action enum is validated', () => {
300
+ const schema = findDevops('helm').inputSchema;
301
+ expect(() => schema.parse({ action: 'list' })).not.toThrow();
302
+ expect(() => schema.parse({ action: 'invalid_action' })).toThrow();
303
+ });
304
+
305
+ test('cloud_discover: provider enum and resource_type are required', () => {
306
+ const schema = findDevops('cloud_discover').inputSchema;
307
+ expect(() => schema.parse({ provider: 'aws', resource_type: 'ec2' })).not.toThrow();
308
+ expect(() => schema.parse({ provider: 'invalid', resource_type: 'ec2' })).toThrow();
309
+ expect(() => schema.parse({ provider: 'aws' })).toThrow();
310
+ });
311
+
312
+ test('cost_estimate: plan_file and workdir are both optional', () => {
313
+ const schema = findDevops('cost_estimate').inputSchema;
314
+ expect(() => schema.parse({})).not.toThrow();
315
+ expect(() => schema.parse({ plan_file: '/plan.tfplan' })).not.toThrow();
316
+ expect(() => schema.parse({ workdir: '/infra' })).not.toThrow();
317
+ });
318
+
319
+ test('drift_detect: workdir is required', () => {
320
+ const schema = findDevops('drift_detect').inputSchema;
321
+ expect(() => schema.parse({ workdir: '/infra' })).not.toThrow();
322
+ expect(() => schema.parse({})).toThrow();
323
+ });
324
+
325
+ test('deploy_preview: action and workdir are required', () => {
326
+ const schema = findDevops('deploy_preview').inputSchema;
327
+ expect(() => schema.parse({ action: 'terraform apply', workdir: '/infra' })).not.toThrow();
328
+ expect(() => schema.parse({ action: 'terraform apply' })).toThrow();
329
+ expect(() => schema.parse({ workdir: '/infra' })).toThrow();
330
+ });
331
+
332
+ test('git: action enum is validated', () => {
333
+ const schema = findDevops('git').inputSchema;
334
+ expect(() => schema.parse({ action: 'status' })).not.toThrow();
335
+ expect(() => schema.parse({ action: 'bad_action' })).toThrow();
336
+ });
337
+
338
+ test('task: prompt is required, agent enum is optional', () => {
339
+ const schema = findDevops('task').inputSchema;
340
+ expect(() => schema.parse({ prompt: 'do something' })).not.toThrow();
341
+ expect(() => schema.parse({ prompt: 'x', agent: 'explore' })).not.toThrow();
342
+ expect(() => schema.parse({ prompt: 'x', agent: 'invalid' })).toThrow();
343
+ expect(() => schema.parse({})).toThrow();
344
+ });
345
+ });
346
+
347
+ // ===========================================================================
348
+ // Tool Counts and Metadata
349
+ // ===========================================================================
350
+
351
+ describe('Tool counts and metadata', () => {
352
+ test('standardTools has exactly 12 tools', () => {
353
+ expect(standardTools).toHaveLength(12);
354
+ });
355
+
356
+ test('devopsTools has exactly 9 tools', () => {
357
+ expect(devopsTools).toHaveLength(9);
358
+ });
359
+
360
+ test('all standard tools have category "standard"', () => {
361
+ for (const tool of standardTools) {
362
+ expect(tool.category).toBe('standard');
363
+ }
364
+ });
365
+
366
+ test('all devops tools have category "devops"', () => {
367
+ for (const tool of devopsTools) {
368
+ expect(tool.category).toBe('devops');
369
+ }
370
+ });
371
+
372
+ test('all tools have valid permissionTier values', () => {
373
+ const validTiers = new Set<string>(PERMISSION_TIER_ORDER);
374
+ for (const tool of [...standardTools, ...devopsTools]) {
375
+ expect(validTiers.has(tool.permissionTier)).toBe(true);
376
+ }
377
+ });
378
+
379
+ test('all tools have non-empty name and description', () => {
380
+ for (const tool of [...standardTools, ...devopsTools]) {
381
+ expect(tool.name.length).toBeGreaterThan(0);
382
+ expect(tool.description.length).toBeGreaterThan(0);
383
+ }
384
+ });
385
+
386
+ test('all tool names are unique across standard and devops', () => {
387
+ const allNames = [...standardTools, ...devopsTools].map(t => t.name);
388
+ const uniqueNames = new Set(allNames);
389
+ expect(uniqueNames.size).toBe(allNames.length);
390
+ });
391
+
392
+ test('all tools have an execute function', () => {
393
+ for (const tool of [...standardTools, ...devopsTools]) {
394
+ expect(typeof tool.execute).toBe('function');
395
+ }
396
+ });
397
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Tests for embedded tool operation classes:
3
+ * - src/tools/file-ops.ts – FileSystemOperations
4
+ * - src/tools/git-ops.ts – GitOperations
5
+ *
6
+ * These tests verify that the classes can be instantiated and that their
7
+ * fundamental read/exists operations work against the real filesystem.
8
+ * Git network operations (clone, push, pull) are NOT exercised here to keep
9
+ * tests fast and hermetic.
10
+ */
11
+
12
+ import { describe, it, expect } from 'bun:test';
13
+ import { join } from 'node:path';
14
+ import { FileSystemOperations } from '../tools/file-ops';
15
+ import { GitOperations } from '../tools/git-ops';
16
+
17
+ // The repository root is two levels above this test file: src/__tests__/ -> src/ -> repo-root
18
+ const REPO_ROOT = join(import.meta.dir, '..', '..');
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // FileSystemOperations
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe('FileSystemOperations', () => {
25
+ it('can be instantiated with no arguments (defaults to cwd)', () => {
26
+ const ops = new FileSystemOperations();
27
+ expect(ops).toBeInstanceOf(FileSystemOperations);
28
+ });
29
+
30
+ it('can be instantiated with a specific base path', () => {
31
+ const ops = new FileSystemOperations(REPO_ROOT);
32
+ expect(ops).toBeInstanceOf(FileSystemOperations);
33
+ });
34
+
35
+ it('readFile reads the package.json at the repo root', async () => {
36
+ const ops = new FileSystemOperations(REPO_ROOT);
37
+ const content = await ops.readFile('package.json');
38
+ expect(typeof content).toBe('string');
39
+ expect(content.length).toBeGreaterThan(0);
40
+ // Verify it is valid JSON containing expected nimbus fields
41
+ const pkg = JSON.parse(content);
42
+ expect(pkg.name).toBe('@build-astron-co/nimbus');
43
+ });
44
+
45
+ it('exists() returns true for a known file', async () => {
46
+ const ops = new FileSystemOperations(REPO_ROOT);
47
+ const result = await ops.exists('package.json');
48
+ expect(result).toBe(true);
49
+ });
50
+
51
+ it('exists() returns false for a non-existent path', async () => {
52
+ const ops = new FileSystemOperations(REPO_ROOT);
53
+ const result = await ops.exists('this-file-does-not-exist-abc123.txt');
54
+ expect(result).toBe(false);
55
+ });
56
+
57
+ it('stat() returns file statistics for package.json', async () => {
58
+ const ops = new FileSystemOperations(REPO_ROOT);
59
+ const stats = await ops.stat('package.json');
60
+
61
+ expect(stats.isFile).toBe(true);
62
+ expect(stats.isDirectory).toBe(false);
63
+ expect(stats.size).toBeGreaterThan(0);
64
+ expect(stats.modifiedAt).toBeInstanceOf(Date);
65
+ });
66
+
67
+ it('readDir() lists the src directory', async () => {
68
+ const ops = new FileSystemOperations(REPO_ROOT);
69
+ const entries = await ops.readDir('src');
70
+
71
+ expect(Array.isArray(entries)).toBe(true);
72
+ expect(entries.length).toBeGreaterThan(0);
73
+ // src/ contains at least app.ts and version.ts
74
+ const names = entries.map(e => e.name);
75
+ expect(names).toContain('app.ts');
76
+ expect(names).toContain('version.ts');
77
+ });
78
+
79
+ it('readFile throws when attempting to read a sensitive file', async () => {
80
+ const ops = new FileSystemOperations(REPO_ROOT);
81
+ // .env files are blocked by the sensitive-file guard
82
+ await expect(ops.readFile('.env')).rejects.toThrow();
83
+ });
84
+ });
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // GitOperations
88
+ // ---------------------------------------------------------------------------
89
+
90
+ describe('GitOperations', () => {
91
+ it('can be instantiated with no arguments (defaults to cwd)', () => {
92
+ const git = new GitOperations();
93
+ expect(git).toBeInstanceOf(GitOperations);
94
+ });
95
+
96
+ it('can be instantiated with a specific repo path', () => {
97
+ const git = new GitOperations(REPO_ROOT);
98
+ expect(git).toBeInstanceOf(GitOperations);
99
+ });
100
+
101
+ it('isRepo() returns true for the nimbus repository', async () => {
102
+ const git = new GitOperations(REPO_ROOT);
103
+ const result = await git.isRepo();
104
+ expect(result).toBe(true);
105
+ });
106
+
107
+ it('currentBranch() returns a non-empty string', async () => {
108
+ const git = new GitOperations(REPO_ROOT);
109
+ const branch = await git.currentBranch();
110
+ expect(typeof branch).toBe('string');
111
+ expect(branch.length).toBeGreaterThan(0);
112
+ });
113
+
114
+ it('status() returns a status result with expected shape', async () => {
115
+ const git = new GitOperations(REPO_ROOT);
116
+ const status = await git.status();
117
+ // StatusResult from simple-git has a `current` property and isClean()
118
+ expect(typeof status.isClean).toBe('function');
119
+ expect(typeof status.current).toBe('string');
120
+ });
121
+
122
+ it('listBranches() returns current branch and branch list', async () => {
123
+ const git = new GitOperations(REPO_ROOT);
124
+ const { current, branches } = await git.listBranches();
125
+ expect(typeof current).toBe('string');
126
+ expect(Array.isArray(branches)).toBe(true);
127
+ expect(branches.length).toBeGreaterThan(0);
128
+ });
129
+
130
+ it('listTags() returns an array', async () => {
131
+ const git = new GitOperations(REPO_ROOT);
132
+ const tags = await git.listTags();
133
+ expect(Array.isArray(tags)).toBe(true);
134
+ });
135
+
136
+ it('getShortHash() returns a short commit hash string', async () => {
137
+ const git = new GitOperations(REPO_ROOT);
138
+ const hash = await git.getShortHash('HEAD');
139
+ expect(typeof hash).toBe('string');
140
+ // Short hashes are typically 7–12 hex characters
141
+ expect(/^[0-9a-f]{4,12}$/.test(hash)).toBe(true);
142
+ });
143
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tests for src/version.ts
3
+ *
4
+ * Verifies that the module exports a valid semver VERSION string and a
5
+ * non-empty BUILD_DATE string.
6
+ */
7
+
8
+ import { describe, it, expect } from 'bun:test';
9
+ import { VERSION, BUILD_DATE } from '../version';
10
+
11
+ describe('version', () => {
12
+ it('exports VERSION as a non-empty string', () => {
13
+ expect(typeof VERSION).toBe('string');
14
+ expect(VERSION.length).toBeGreaterThan(0);
15
+ });
16
+
17
+ it('VERSION matches semver format (MAJOR.MINOR.PATCH)', () => {
18
+ // A loose semver pattern: digits separated by dots, with optional pre-release
19
+ const semverLoose = /^\d+\.\d+\.\d+/;
20
+ expect(semverLoose.test(VERSION)).toBe(true);
21
+ });
22
+
23
+ it('VERSION major version is a non-negative integer', () => {
24
+ const major = parseInt(VERSION.split('.')[0], 10);
25
+ expect(Number.isInteger(major)).toBe(true);
26
+ expect(major).toBeGreaterThanOrEqual(0);
27
+ });
28
+
29
+ it('VERSION minor and patch are non-negative integers', () => {
30
+ const parts = VERSION.split('.');
31
+ const minor = parseInt(parts[1], 10);
32
+ const patch = parseInt(parts[2], 10);
33
+ expect(minor).toBeGreaterThanOrEqual(0);
34
+ expect(patch).toBeGreaterThanOrEqual(0);
35
+ });
36
+
37
+ it('exports BUILD_DATE as a non-empty string', () => {
38
+ expect(typeof BUILD_DATE).toBe('string');
39
+ expect(BUILD_DATE.length).toBeGreaterThan(0);
40
+ });
41
+
42
+ it('BUILD_DATE is "dev" in development or ISO-like date in builds', () => {
43
+ // In dev mode the placeholder is detected and replaced with 'dev'.
44
+ // After a compiled build it is an ISO 8601 date string (YYYY-MM-DD).
45
+ const isDev = BUILD_DATE === 'dev';
46
+ const isISODate = /^\d{4}-\d{2}-\d{2}/.test(BUILD_DATE);
47
+ expect(isDev || isISODate).toBe(true);
48
+ });
49
+ });