@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,405 @@
1
+ /**
2
+ * Unit tests for machine pool operations
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
6
+ import { mkdtempSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { eq } from 'drizzle-orm';
10
+ import { closeDb, createDbClient } from '../db/client';
11
+ import { runMigrations } from '../db/migrate';
12
+ import { machines } from '../db/schema';
13
+ import {
14
+ addMachine,
15
+ assignModuleToMachine,
16
+ getMachine,
17
+ getMachineByHostname,
18
+ getMachineSshKey,
19
+ listMachines,
20
+ removeMachine,
21
+ unassignModuleFromMachine,
22
+ } from './machine-pool';
23
+
24
+ describe('machine-pool', () => {
25
+ let testDbPath: string;
26
+ let testDir: string;
27
+
28
+ beforeEach(async () => {
29
+ // Create temp directory for test database
30
+ testDir = mkdtempSync(join(tmpdir(), 'celilo-test-'));
31
+ testDbPath = join(testDir, 'test.db');
32
+
33
+ // Set environment variable for database path
34
+ process.env.CELILO_DB_PATH = testDbPath;
35
+
36
+ // Initialize database and run migrations
37
+ await runMigrations(testDbPath);
38
+
39
+ // Create a dummy master key for encryption
40
+ const masterKeyPath = join(testDir, 'master.key');
41
+ process.env.CELILO_MASTER_KEY_PATH = masterKeyPath;
42
+ const fs = await import('node:fs/promises');
43
+ await fs.writeFile(masterKeyPath, 'a'.repeat(64), 'utf8');
44
+ });
45
+
46
+ afterEach(() => {
47
+ // Close database connection
48
+ closeDb();
49
+
50
+ // Clean up test directory
51
+ if (testDir) {
52
+ rmSync(testDir, { recursive: true, force: true });
53
+ }
54
+
55
+ // Clear environment variables
56
+ process.env.CELILO_DB_PATH = undefined;
57
+ process.env.CELILO_MASTER_KEY_PATH = undefined;
58
+ });
59
+
60
+ describe('addMachine', () => {
61
+ it('creates a machine with hardware specs', async () => {
62
+ const machine = await addMachine({
63
+ hostname: 'rpi4-lr',
64
+ zone: 'internal',
65
+ ipAddress: '192.168.1.100',
66
+ sshUser: 'ubuntu',
67
+ sshKey: 'ssh-ed25519 AAAA...',
68
+ hardware: {
69
+ cpu_cores: 4,
70
+ memory_mb: 4096,
71
+ disk_gb: 128,
72
+ },
73
+ role: 'host',
74
+ interfaces: [],
75
+ assignedModuleIds: [],
76
+ });
77
+
78
+ expect(machine.id).toBeDefined();
79
+ expect(machine.hostname).toBe('rpi4-lr');
80
+ expect(machine.zone).toBe('internal');
81
+ expect(machine.ipAddress).toBe('192.168.1.100');
82
+ expect(machine.hardware.cpu_cores).toBe(4);
83
+ expect(machine.assignedModuleIds).toEqual([]);
84
+ expect(machine.createdAt).toBeInstanceOf(Date);
85
+ });
86
+
87
+ it('encrypts SSH key', async () => {
88
+ const machine = await addMachine({
89
+ hostname: 'test-machine',
90
+ zone: 'dmz',
91
+ ipAddress: '10.0.10.100',
92
+ sshUser: 'root',
93
+ sshKey: 'ssh-ed25519 AAAA_SECRET_KEY_12345',
94
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
95
+ role: 'host',
96
+ interfaces: [],
97
+ assignedModuleIds: [],
98
+ });
99
+
100
+ // Verify key is encrypted in database
101
+ const db = createDbClient({ path: testDbPath });
102
+ const dbRecord = await db.select().from(machines).where(eq(machines.id, machine.id)).get();
103
+
104
+ expect(dbRecord?.sshKeyEncrypted).toBeDefined();
105
+ expect(dbRecord?.sshKeyEncrypted).not.toContain('AAAA_SECRET_KEY_12345');
106
+ });
107
+
108
+ it('supports different zones', async () => {
109
+ const dmzMachine = await addMachine({
110
+ hostname: 'dmz-server',
111
+ zone: 'dmz',
112
+ ipAddress: '10.0.10.100',
113
+ sshUser: 'root',
114
+ sshKey: 'test-key',
115
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
116
+ role: 'host',
117
+ interfaces: [],
118
+ assignedModuleIds: [],
119
+ });
120
+
121
+ const externalMachine = await addMachine({
122
+ hostname: 'vps-external',
123
+ zone: 'external',
124
+ ipAddress: '167.99.123.45',
125
+ sshUser: 'root',
126
+ sshKey: 'test-key',
127
+ hardware: { cpu_cores: 1, memory_mb: 1024, disk_gb: 25 },
128
+ role: 'host',
129
+ interfaces: [],
130
+ assignedModuleIds: [],
131
+ });
132
+
133
+ expect(dmzMachine.zone).toBe('dmz');
134
+ expect(externalMachine.zone).toBe('external');
135
+ });
136
+ });
137
+
138
+ describe('getMachine', () => {
139
+ it('retrieves machine by ID', async () => {
140
+ const created = await addMachine({
141
+ hostname: 'test-machine',
142
+ zone: 'dmz',
143
+ ipAddress: '10.0.10.100',
144
+ sshUser: 'root',
145
+ sshKey: 'test-key',
146
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
147
+ role: 'host',
148
+ interfaces: [],
149
+ assignedModuleIds: [],
150
+ });
151
+
152
+ const retrieved = await getMachine(created.id);
153
+
154
+ expect(retrieved).toBeDefined();
155
+ expect(retrieved?.id).toBe(created.id);
156
+ expect(retrieved?.hostname).toBe('test-machine');
157
+ });
158
+
159
+ it('returns null for non-existent machine', async () => {
160
+ const result = await getMachine('non-existent-id');
161
+ expect(result).toBeNull();
162
+ });
163
+ });
164
+
165
+ describe('getMachineByHostname', () => {
166
+ it('retrieves machine by hostname', async () => {
167
+ await addMachine({
168
+ hostname: 'unique-hostname',
169
+ zone: 'internal',
170
+ ipAddress: '192.168.1.100',
171
+ sshUser: 'ubuntu',
172
+ sshKey: 'test-key',
173
+ hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 },
174
+ role: 'host',
175
+ interfaces: [],
176
+ assignedModuleIds: [],
177
+ });
178
+
179
+ const retrieved = await getMachineByHostname('unique-hostname');
180
+
181
+ expect(retrieved).toBeDefined();
182
+ expect(retrieved?.hostname).toBe('unique-hostname');
183
+ });
184
+
185
+ it('returns null for non-existent hostname', async () => {
186
+ const result = await getMachineByHostname('does-not-exist');
187
+ expect(result).toBeNull();
188
+ });
189
+ });
190
+
191
+ describe('listMachines', () => {
192
+ it('returns empty array when no machines', async () => {
193
+ const result = await listMachines();
194
+ expect(result).toEqual([]);
195
+ });
196
+
197
+ it('lists all machines without filters', async () => {
198
+ await addMachine({
199
+ hostname: 'machine-1',
200
+ zone: 'dmz',
201
+ ipAddress: '10.0.10.100',
202
+ sshUser: 'root',
203
+ sshKey: 'test-key',
204
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
205
+ role: 'host',
206
+ interfaces: [],
207
+ assignedModuleIds: [],
208
+ });
209
+
210
+ await addMachine({
211
+ hostname: 'machine-2',
212
+ zone: 'internal',
213
+ ipAddress: '192.168.1.100',
214
+ sshUser: 'ubuntu',
215
+ sshKey: 'test-key',
216
+ hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 },
217
+ role: 'host',
218
+ interfaces: [],
219
+ assignedModuleIds: [],
220
+ });
221
+
222
+ const result = await listMachines();
223
+ expect(result).toHaveLength(2);
224
+ });
225
+
226
+ it('filters machines by zone', async () => {
227
+ await addMachine({
228
+ hostname: 'dmz-machine',
229
+ zone: 'dmz',
230
+ ipAddress: '10.0.10.100',
231
+ sshUser: 'root',
232
+ sshKey: 'test-key',
233
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
234
+ role: 'host',
235
+ interfaces: [],
236
+ assignedModuleIds: [],
237
+ });
238
+
239
+ await addMachine({
240
+ hostname: 'external-machine',
241
+ zone: 'external',
242
+ ipAddress: '167.99.123.45',
243
+ sshUser: 'root',
244
+ sshKey: 'test-key',
245
+ hardware: { cpu_cores: 1, memory_mb: 1024, disk_gb: 25 },
246
+ role: 'host',
247
+ interfaces: [],
248
+ assignedModuleIds: [],
249
+ });
250
+
251
+ const dmzMachines = await listMachines({ zone: 'dmz' });
252
+ expect(dmzMachines).toHaveLength(1);
253
+ expect(dmzMachines[0].hostname).toBe('dmz-machine');
254
+
255
+ const externalMachines = await listMachines({ zone: 'external' });
256
+ expect(externalMachines).toHaveLength(1);
257
+ expect(externalMachines[0].hostname).toBe('external-machine');
258
+ });
259
+ });
260
+
261
+ describe('getMachineSshKey', () => {
262
+ it('decrypts and returns SSH key', async () => {
263
+ const machine = await addMachine({
264
+ hostname: 'test-machine',
265
+ zone: 'dmz',
266
+ ipAddress: '10.0.10.100',
267
+ sshUser: 'root',
268
+ sshKey: 'ssh-ed25519 AAAA_SECRET_KEY_12345',
269
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
270
+ role: 'host',
271
+ interfaces: [],
272
+ assignedModuleIds: [],
273
+ });
274
+
275
+ const decryptedKey = await getMachineSshKey(machine.id);
276
+ expect(decryptedKey).toBe('ssh-ed25519 AAAA_SECRET_KEY_12345');
277
+ });
278
+
279
+ it('throws error for non-existent machine', async () => {
280
+ await expect(getMachineSshKey('non-existent-id')).rejects.toThrow(/Machine not found/);
281
+ });
282
+ });
283
+
284
+ describe('assignModuleToMachine', () => {
285
+ it('assigns module to machine', async () => {
286
+ const machine = await addMachine({
287
+ hostname: 'test-machine',
288
+ zone: 'internal',
289
+ ipAddress: '192.168.1.100',
290
+ sshUser: 'ubuntu',
291
+ sshKey: 'test-key',
292
+ hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 },
293
+ role: 'host',
294
+ interfaces: [],
295
+ assignedModuleIds: [],
296
+ });
297
+
298
+ await assignModuleToMachine(machine.id, 'homebridge');
299
+
300
+ const updated = await getMachine(machine.id);
301
+ expect(updated?.assignedModuleIds).toEqual(['homebridge']);
302
+ });
303
+
304
+ it('supports multiple modules on same machine', async () => {
305
+ const machine = await addMachine({
306
+ hostname: 'multi-module-machine',
307
+ zone: 'internal',
308
+ ipAddress: '192.168.1.100',
309
+ sshUser: 'ubuntu',
310
+ sshKey: 'test-key',
311
+ hardware: { cpu_cores: 8, memory_mb: 16384, disk_gb: 256 },
312
+ role: 'host',
313
+ interfaces: [],
314
+ assignedModuleIds: [],
315
+ });
316
+
317
+ await assignModuleToMachine(machine.id, 'module-1');
318
+ await assignModuleToMachine(machine.id, 'module-2');
319
+ await assignModuleToMachine(machine.id, 'module-3');
320
+
321
+ const updated = await getMachine(machine.id);
322
+ expect(updated?.assignedModuleIds).toEqual(['module-1', 'module-2', 'module-3']);
323
+ });
324
+
325
+ it('throws error for non-existent machine', async () => {
326
+ await expect(assignModuleToMachine('non-existent-id', 'module-id')).rejects.toThrow(
327
+ /Machine not found/,
328
+ );
329
+ });
330
+ });
331
+
332
+ describe('unassignModuleFromMachine', () => {
333
+ it('removes module from machine', async () => {
334
+ const machine = await addMachine({
335
+ hostname: 'test-machine',
336
+ zone: 'internal',
337
+ ipAddress: '192.168.1.100',
338
+ sshUser: 'ubuntu',
339
+ sshKey: 'test-key',
340
+ hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 },
341
+ role: 'host',
342
+ interfaces: [],
343
+ assignedModuleIds: ['homebridge'],
344
+ });
345
+
346
+ await unassignModuleFromMachine(machine.id, 'homebridge');
347
+
348
+ const updated = await getMachine(machine.id);
349
+ expect(updated?.assignedModuleIds).toEqual([]);
350
+ });
351
+
352
+ it('removes specific module from multiple assignments', async () => {
353
+ const machine = await addMachine({
354
+ hostname: 'test-machine',
355
+ zone: 'internal',
356
+ ipAddress: '192.168.1.100',
357
+ sshUser: 'ubuntu',
358
+ sshKey: 'test-key',
359
+ hardware: { cpu_cores: 8, memory_mb: 16384, disk_gb: 256 },
360
+ role: 'host',
361
+ interfaces: [],
362
+ assignedModuleIds: ['module-1', 'module-2', 'module-3'],
363
+ });
364
+
365
+ await unassignModuleFromMachine(machine.id, 'module-2');
366
+
367
+ const updated = await getMachine(machine.id);
368
+ expect(updated?.assignedModuleIds).toEqual(['module-1', 'module-3']);
369
+ });
370
+
371
+ it('throws error for non-existent machine', async () => {
372
+ await expect(unassignModuleFromMachine('non-existent-id', 'module-id')).rejects.toThrow(
373
+ /Machine not found/,
374
+ );
375
+ });
376
+ });
377
+
378
+ describe('removeMachine', () => {
379
+ it('deletes machine from database', async () => {
380
+ const machine = await addMachine({
381
+ hostname: 'machine-to-delete',
382
+ zone: 'dmz',
383
+ ipAddress: '10.0.10.100',
384
+ sshUser: 'root',
385
+ sshKey: 'test-key',
386
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
387
+ role: 'host',
388
+ interfaces: [],
389
+ assignedModuleIds: [],
390
+ });
391
+
392
+ await removeMachine(machine.id);
393
+
394
+ const retrieved = await getMachine(machine.id);
395
+ expect(retrieved).toBeNull();
396
+ });
397
+
398
+ it('does not throw when removing non-existent machine', async () => {
399
+ // Should complete without error
400
+ await removeMachine('non-existent-id');
401
+ // If we get here, no error was thrown
402
+ expect(true).toBe(true);
403
+ });
404
+ });
405
+ });