@celilo/cli 0.1.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 (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. package/src/variables/types.ts +59 -0
@@ -0,0 +1,458 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { resolveTemplate, resolveVariable } from './resolver';
3
+ import type { ResolutionContext, VariableReference } from './types';
4
+
5
+ // Mock database for tests (capability secrets not tested here - see integration tests)
6
+ const mockDb = {
7
+ $client: {
8
+ prepare: (_query: string) => ({
9
+ get: () => {
10
+ // Return null for capability/module lookups (non-secret in unit tests)
11
+ // This makes isCapabilityFieldSecret() return false
12
+ return null;
13
+ },
14
+ all: () => [],
15
+ run: () => ({ changes: 0 }),
16
+ }),
17
+ },
18
+ select: () => ({
19
+ from: () => ({
20
+ where: () => ({
21
+ get: () => null,
22
+ all: () => [],
23
+ }),
24
+ }),
25
+ }),
26
+ // biome-ignore lint/suspicious/noExplicitAny: mock database for testing, full type not needed
27
+ } as any;
28
+
29
+ const createContext = (overrides?: Partial<ResolutionContext>): ResolutionContext => ({
30
+ moduleId: 'test-module',
31
+ selfConfig: {
32
+ container_ip: '192.168.0.50',
33
+ hostname: 'homebridge',
34
+ cores: '2',
35
+ 'resources.machine.cpu': '2',
36
+ 'resources.machine.memory': '2048',
37
+ 'resources.machine.disk': '20',
38
+ 'resources.machine.zone': 'app',
39
+ // Auto-derived inventory variables
40
+ 'inventory.ansible_host': '192.168.0.50',
41
+ 'inventory.ansible_user': 'root',
42
+ 'inventory.groups': 'homebridge',
43
+ },
44
+ systemConfig: {
45
+ 'management.ip': '192.168.0.10',
46
+ 'network.domain': 'homelab.local',
47
+ },
48
+ systemSecrets: {},
49
+ secrets: {
50
+ api_key: 'secret123',
51
+ db_password: 'pass456',
52
+ },
53
+ capabilities: {
54
+ dns_external: {
55
+ nameserver: 'ns1.example.com',
56
+ zone: 'example.com',
57
+ config: {
58
+ port: 53,
59
+ },
60
+ },
61
+ idp: {
62
+ issuer: 'https://auth.example.com',
63
+ },
64
+ },
65
+ ...overrides,
66
+ });
67
+
68
+ describe('resolveVariable', () => {
69
+ test('should resolve self variable', async () => {
70
+ const variable: VariableReference = {
71
+ type: 'self',
72
+ path: 'container_ip',
73
+ raw: '$self:container_ip',
74
+ };
75
+ const context = createContext();
76
+
77
+ const result = await resolveVariable(variable, context, mockDb);
78
+
79
+ expect(result.success).toBe(true);
80
+ if (result.success) {
81
+ expect(result.value).toBe('192.168.0.50');
82
+ }
83
+ });
84
+
85
+ test('should resolve system variable', async () => {
86
+ const variable: VariableReference = {
87
+ type: 'system',
88
+ path: 'management.ip',
89
+ raw: '$system:management.ip',
90
+ };
91
+ const context = createContext();
92
+
93
+ const result = await resolveVariable(variable, context, mockDb);
94
+
95
+ expect(result.success).toBe(true);
96
+ if (result.success) {
97
+ expect(result.value).toBe('192.168.0.10');
98
+ }
99
+ });
100
+
101
+ test('should resolve secret variable', async () => {
102
+ const variable: VariableReference = {
103
+ type: 'secret',
104
+ path: 'api_key',
105
+ raw: '$secret:api_key',
106
+ };
107
+ const context = createContext();
108
+
109
+ const result = await resolveVariable(variable, context, mockDb);
110
+
111
+ expect(result.success).toBe(true);
112
+ if (result.success) {
113
+ expect(result.value).toBe('secret123');
114
+ }
115
+ });
116
+
117
+ test('should resolve capability variable', async () => {
118
+ const variable: VariableReference = {
119
+ type: 'capability',
120
+ path: 'dns_external.nameserver',
121
+ raw: '$capability:dns_external.nameserver',
122
+ };
123
+ const context = createContext();
124
+
125
+ const result = await resolveVariable(variable, context, mockDb);
126
+
127
+ expect(result.success).toBe(true);
128
+ if (result.success) {
129
+ expect(result.value).toBe('ns1.example.com');
130
+ }
131
+ });
132
+
133
+ test('should resolve nested capability variable', async () => {
134
+ const variable: VariableReference = {
135
+ type: 'capability',
136
+ path: 'dns_external.config.port',
137
+ raw: '$capability:dns_external.config.port',
138
+ };
139
+ const context = createContext();
140
+
141
+ const result = await resolveVariable(variable, context, mockDb);
142
+
143
+ expect(result.success).toBe(true);
144
+ if (result.success) {
145
+ expect(result.value).toBe('53');
146
+ }
147
+ });
148
+
149
+ test('should return error for missing self variable', async () => {
150
+ const variable: VariableReference = {
151
+ type: 'self',
152
+ path: 'missing_var',
153
+ raw: '$self:missing_var',
154
+ };
155
+ const context = createContext();
156
+
157
+ const result = await resolveVariable(variable, context, mockDb);
158
+
159
+ expect(result.success).toBe(false);
160
+ if (!result.success) {
161
+ expect(result.error).toContain('not found in module configuration');
162
+ }
163
+ });
164
+
165
+ test('should return error for missing system variable', async () => {
166
+ const variable: VariableReference = {
167
+ type: 'system',
168
+ path: 'missing.var',
169
+ raw: '$system:missing.var',
170
+ };
171
+ const context = createContext();
172
+
173
+ const result = await resolveVariable(variable, context, mockDb);
174
+
175
+ expect(result.success).toBe(false);
176
+ if (!result.success) {
177
+ expect(result.error).toContain('not found');
178
+ }
179
+ });
180
+
181
+ test('should return error for missing secret', async () => {
182
+ const variable: VariableReference = {
183
+ type: 'secret',
184
+ path: 'missing_secret',
185
+ raw: '$secret:missing_secret',
186
+ };
187
+ const context = createContext();
188
+
189
+ const result = await resolveVariable(variable, context, mockDb);
190
+
191
+ expect(result.success).toBe(false);
192
+ if (!result.success) {
193
+ expect(result.error).toContain('not found');
194
+ }
195
+ });
196
+
197
+ test('should return error for missing capability', async () => {
198
+ const variable: VariableReference = {
199
+ type: 'capability',
200
+ path: 'missing_capability.value',
201
+ raw: '$capability:missing_capability.value',
202
+ };
203
+ const context = createContext();
204
+
205
+ const result = await resolveVariable(variable, context, mockDb);
206
+
207
+ expect(result.success).toBe(false);
208
+ if (!result.success) {
209
+ expect(result.error).toContain('not found or not registered');
210
+ }
211
+ });
212
+
213
+ test('should return error for capability variable without path', async () => {
214
+ const variable: VariableReference = {
215
+ type: 'capability',
216
+ path: 'dns_external',
217
+ raw: '$capability:dns_external',
218
+ };
219
+ const context = createContext();
220
+
221
+ const result = await resolveVariable(variable, context, mockDb);
222
+
223
+ expect(result.success).toBe(false);
224
+ if (!result.success) {
225
+ expect(result.error).toContain('must specify data path');
226
+ }
227
+ });
228
+
229
+ test('should return error for missing capability data path', async () => {
230
+ const variable: VariableReference = {
231
+ type: 'capability',
232
+ path: 'dns_external.missing_field',
233
+ raw: '$capability:dns_external.missing_field',
234
+ };
235
+ const context = createContext();
236
+
237
+ const result = await resolveVariable(variable, context, mockDb);
238
+
239
+ expect(result.success).toBe(false);
240
+ if (!result.success) {
241
+ expect(result.error).toContain('not found in');
242
+ }
243
+ });
244
+ });
245
+
246
+ describe('resolveTemplate', () => {
247
+ test('should resolve template with single variable', async () => {
248
+ const template = 'ip: $self:container_ip';
249
+ const context = createContext();
250
+
251
+ const result = await resolveTemplate(template, context, mockDb);
252
+
253
+ expect(result.success).toBe(true);
254
+ if (result.success) {
255
+ expect(result.content).toBe('ip: 192.168.0.50');
256
+ }
257
+ });
258
+
259
+ test('should resolve template with multiple variables', async () => {
260
+ const template = `
261
+ hostname: $self:hostname
262
+ ip: $self:container_ip
263
+ domain: $system:network.domain
264
+ `;
265
+ const context = createContext();
266
+
267
+ const result = await resolveTemplate(template, context, mockDb);
268
+
269
+ expect(result.success).toBe(true);
270
+ if (result.success) {
271
+ expect(result.content).toContain('hostname: homebridge');
272
+ expect(result.content).toContain('ip: 192.168.0.50');
273
+ expect(result.content).toContain('domain: homelab.local');
274
+ }
275
+ });
276
+
277
+ test('should resolve template with mixed variable types', async () => {
278
+ const template = `
279
+ container_ip: $self:container_ip
280
+ management_ip: $system:management.ip
281
+ dns_server: $capability:dns_external.nameserver
282
+ api_key: $secret:api_key
283
+ `;
284
+ const context = createContext();
285
+
286
+ const result = await resolveTemplate(template, context, mockDb);
287
+
288
+ expect(result.success).toBe(true);
289
+ if (result.success) {
290
+ expect(result.content).toContain('container_ip: 192.168.0.50');
291
+ expect(result.content).toContain('management_ip: 192.168.0.10');
292
+ expect(result.content).toContain('dns_server: ns1.example.com');
293
+ expect(result.content).toContain('api_key: secret123');
294
+ }
295
+ });
296
+
297
+ test('should resolve VM resource variables from manifest', async () => {
298
+ const template = `
299
+ resource "proxmox_lxc" "grafana" {
300
+ cores = $self:resources.machine.cpu
301
+ memory = $self:resources.machine.memory
302
+ rootfs {
303
+ size = "$self:resources.machine.disk"
304
+ storage = "$self:resources.machine.zone"
305
+ }
306
+ }
307
+ `;
308
+ const context = createContext();
309
+
310
+ const result = await resolveTemplate(template, context, mockDb);
311
+
312
+ expect(result.success).toBe(true);
313
+ if (result.success) {
314
+ expect(result.content).toContain('cores = 2');
315
+ expect(result.content).toContain('memory = 2048');
316
+ expect(result.content).toContain('size = "20"');
317
+ expect(result.content).toContain('storage = "app"');
318
+ }
319
+ });
320
+
321
+ test('should resolve template with variables in complex structure', async () => {
322
+ const template = `
323
+ resource "proxmox_lxc" "container" {
324
+ hostname = "$self:hostname"
325
+ cores = $self:cores
326
+ network {
327
+ ip = "$self:container_ip/24"
328
+ gateway = "$system:management.ip"
329
+ }
330
+ environment = {
331
+ DNS_SERVER = "$capability:dns_external.nameserver"
332
+ API_KEY = "$secret:api_key"
333
+ }
334
+ }
335
+ `;
336
+ const context = createContext();
337
+
338
+ const result = await resolveTemplate(template, context, mockDb);
339
+
340
+ expect(result.success).toBe(true);
341
+ if (result.success) {
342
+ expect(result.content).toContain('hostname = "homebridge"');
343
+ expect(result.content).toContain('cores = 2');
344
+ expect(result.content).toContain('ip = "192.168.0.50/24"');
345
+ expect(result.content).toContain('gateway = "192.168.0.10"');
346
+ expect(result.content).toContain('DNS_SERVER = "ns1.example.com"');
347
+ expect(result.content).toContain('API_KEY = "secret123"');
348
+ }
349
+ });
350
+
351
+ test('should return template unchanged if no variables', async () => {
352
+ const template = 'no variables here';
353
+ const context = createContext();
354
+
355
+ const result = await resolveTemplate(template, context, mockDb);
356
+
357
+ expect(result.success).toBe(true);
358
+ if (result.success) {
359
+ expect(result.content).toBe(template);
360
+ }
361
+ });
362
+
363
+ test('should return errors for missing variables', async () => {
364
+ const template = `
365
+ ip: $self:container_ip
366
+ missing: $self:missing_var
367
+ another_missing: $secret:missing_secret
368
+ `;
369
+ const context = createContext();
370
+
371
+ const result = await resolveTemplate(template, context, mockDb);
372
+
373
+ expect(result.success).toBe(false);
374
+ if (!result.success) {
375
+ expect(result.errors).toHaveLength(2);
376
+ expect(result.errors[0]?.variable).toBe('$self:missing_var');
377
+ expect(result.errors[1]?.variable).toBe('$secret:missing_secret');
378
+ }
379
+ });
380
+
381
+ test('should handle repeated variables', async () => {
382
+ const template = `
383
+ ip1: $self:container_ip
384
+ ip2: $self:container_ip
385
+ ip3: $self:container_ip
386
+ `;
387
+ const context = createContext();
388
+
389
+ const result = await resolveTemplate(template, context, mockDb);
390
+
391
+ expect(result.success).toBe(true);
392
+ if (result.success) {
393
+ expect(result.content).toContain('ip1: 192.168.0.50');
394
+ expect(result.content).toContain('ip2: 192.168.0.50');
395
+ expect(result.content).toContain('ip3: 192.168.0.50');
396
+ expect(result.content).not.toContain('$self:container_ip');
397
+ }
398
+ });
399
+
400
+ test('should resolve auto-derived inventory variables', async () => {
401
+ const template = `
402
+ [all]
403
+ $self:hostname ansible_host=$self:inventory.ansible_host ansible_user=$self:inventory.ansible_user
404
+
405
+ [homebridge]
406
+ $self:hostname
407
+ `;
408
+ const context = createContext();
409
+
410
+ const result = await resolveTemplate(template, context, mockDb);
411
+
412
+ expect(result.success).toBe(true);
413
+ if (result.success) {
414
+ expect(result.content).toContain('homebridge ansible_host=192.168.0.50 ansible_user=root');
415
+ expect(result.content).toContain('[homebridge]');
416
+ }
417
+ });
418
+
419
+ test('should use stripped IP in ansible_host', async () => {
420
+ const template = 'ansible_host: $self:inventory.ansible_host';
421
+ const context = createContext({
422
+ selfConfig: {
423
+ container_ip: '10.0.10.10/24',
424
+ 'inventory.ansible_host': '10.0.10.10', // Auto-derived (CIDR stripped)
425
+ 'inventory.ansible_user': 'root',
426
+ 'inventory.groups': 'test',
427
+ },
428
+ });
429
+
430
+ const result = await resolveTemplate(template, context, mockDb);
431
+
432
+ expect(result.success).toBe(true);
433
+ if (result.success) {
434
+ expect(result.content).toContain('ansible_host: 10.0.10.10');
435
+ expect(result.content).not.toContain('/24');
436
+ }
437
+ });
438
+
439
+ test('should resolve inventory.groups from module ID', async () => {
440
+ const template = `
441
+ ---
442
+ all:
443
+ children:
444
+ $self:inventory.groups:
445
+ hosts:
446
+ $self:hostname:
447
+ `;
448
+ const context = createContext();
449
+
450
+ const result = await resolveTemplate(template, context, mockDb);
451
+
452
+ expect(result.success).toBe(true);
453
+ if (result.success) {
454
+ expect(result.content).toContain('homebridge:');
455
+ expect(result.content).toContain('hosts:');
456
+ }
457
+ });
458
+ });