@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,82 @@
1
+ /**
2
+ * Completion Test Harness
3
+ *
4
+ * Provides utilities for testing CLI completion by invoking --get-completions directly.
5
+ * This bypasses shell complexity and tests the completion function itself.
6
+ */
7
+
8
+ import { expect } from 'bun:test';
9
+ import { runCli } from './cli';
10
+
11
+ /**
12
+ * Test harness for CLI completion
13
+ */
14
+ export class CompletionHarness {
15
+ constructor(private cli: string) {}
16
+
17
+ /**
18
+ * Get completions for a given set of words
19
+ *
20
+ * @param words - The command words (e.g., ['celilo', 'service'])
21
+ * @param partial - If true, complete the last word (for prefix matching). If false (default), complete after all words
22
+ * @returns Array of completion suggestions
23
+ */
24
+ async getCompletions(words: string[], partial = false): Promise<string[]> {
25
+ // If partial=true, we're completing the last word itself (prefix matching)
26
+ // If partial=false, we're completing the next word after all current words
27
+ const currentIndex = partial && words.length > 0 ? words.length - 1 : words.length;
28
+ try {
29
+ const result = runCli(this.cli, `--get-completions ${words.join(' ')} ${currentIndex}`);
30
+
31
+ // Parse completion output (one per line)
32
+ return result.split('\n').filter(Boolean);
33
+ } catch (_error) {
34
+ // Completion errors return empty array
35
+ return [];
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Assert that completions include all expected values
41
+ *
42
+ * @param words - The command words
43
+ * @param expected - Expected completion values
44
+ */
45
+ async expectCompletionsInclude(words: string[], expected: string[]): Promise<void> {
46
+ const actual = await this.getCompletions(words);
47
+ for (const exp of expected) {
48
+ expect(actual).toContain(exp);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Assert that completions exactly match expected values (order-independent)
54
+ *
55
+ * @param words - The command words
56
+ * @param expected - Expected completion values
57
+ */
58
+ async expectCompletionsExact(words: string[], expected: string[]): Promise<void> {
59
+ const actual = await this.getCompletions(words);
60
+ expect(actual.sort()).toEqual(expected.sort());
61
+ }
62
+
63
+ /**
64
+ * Assert that no completions are returned
65
+ *
66
+ * @param words - The command words
67
+ */
68
+ async expectNoCompletions(words: string[]): Promise<void> {
69
+ const actual = await this.getCompletions(words);
70
+ expect(actual).toEqual([]);
71
+ }
72
+
73
+ /**
74
+ * Assert that at least one completion is returned
75
+ *
76
+ * @param words - The command words
77
+ */
78
+ async expectSomeCompletions(words: string[]): Promise<void> {
79
+ const actual = await this.getCompletions(words);
80
+ expect(actual.length).toBeGreaterThan(0);
81
+ }
82
+ }
@@ -0,0 +1,182 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { existsSync } from 'node:fs';
3
+ import { modules } from '../db/schema';
4
+ import {
5
+ cleanupMultipleTestDatabases,
6
+ cleanupTestDatabase,
7
+ setupMultipleTestDatabases,
8
+ setupTestDatabase,
9
+ setupTestDatabaseFile,
10
+ } from './database';
11
+
12
+ describe('Database Test Utilities', () => {
13
+ describe('setupTestDatabase', () => {
14
+ test('creates in-memory database', async () => {
15
+ const db = await setupTestDatabase();
16
+ expect(db).toBeDefined();
17
+ await cleanupTestDatabase(db);
18
+ });
19
+
20
+ test('applies migrations', async () => {
21
+ const db = await setupTestDatabase();
22
+
23
+ // Should be able to query modules table
24
+ const result = await db.select().from(modules);
25
+ expect(result).toEqual([]);
26
+
27
+ await cleanupTestDatabase(db);
28
+ });
29
+
30
+ test('creates isolated databases', async () => {
31
+ const db1 = await setupTestDatabase();
32
+ const db2 = await setupTestDatabase();
33
+
34
+ // Insert into db1
35
+ await db1.insert(modules).values({
36
+ id: 'test-module',
37
+ name: 'Test Module',
38
+ version: '1.0.0',
39
+ state: 'IMPORTED',
40
+ manifestData: {},
41
+ sourcePath: '/tmp/test',
42
+ importedAt: new Date(),
43
+ updatedAt: new Date(),
44
+ });
45
+
46
+ // db2 should be empty
47
+ const result1 = await db1.select().from(modules);
48
+ const result2 = await db2.select().from(modules);
49
+
50
+ expect(result1).toHaveLength(1);
51
+ expect(result2).toHaveLength(0);
52
+
53
+ await cleanupTestDatabase(db1);
54
+ await cleanupTestDatabase(db2);
55
+ });
56
+ });
57
+
58
+ describe('setupTestDatabaseFile', () => {
59
+ test('creates file-based database', async () => {
60
+ const { db, path: dbPath, cleanup } = await setupTestDatabaseFile();
61
+
62
+ expect(db).toBeDefined();
63
+ expect(dbPath).toBeTruthy();
64
+ expect(existsSync(dbPath)).toBe(true);
65
+
66
+ await cleanup();
67
+ });
68
+
69
+ test('applies migrations', async () => {
70
+ const { db, cleanup } = await setupTestDatabaseFile();
71
+
72
+ // Should be able to query modules table
73
+ const result = await db.select().from(modules);
74
+ expect(result).toEqual([]);
75
+
76
+ await cleanup();
77
+ });
78
+
79
+ test('cleanup removes database file', async () => {
80
+ const { path: dbPath, cleanup } = await setupTestDatabaseFile();
81
+
82
+ expect(existsSync(dbPath)).toBe(true);
83
+
84
+ await cleanup();
85
+
86
+ // Database file should be gone
87
+ expect(existsSync(dbPath)).toBe(false);
88
+ });
89
+
90
+ test('database path can be used by CLI', async () => {
91
+ const { db, path: dbPath, cleanup } = await setupTestDatabaseFile();
92
+
93
+ // Path should be a valid file path
94
+ expect(dbPath).toMatch(/\/celilo-test-.*\/test\.db$/);
95
+
96
+ // Insert some data
97
+ await db.insert(modules).values({
98
+ id: 'cli-test',
99
+ name: 'CLI Test',
100
+ version: '1.0.0',
101
+ state: 'IMPORTED',
102
+ manifestData: {},
103
+ sourcePath: '/tmp/cli',
104
+ importedAt: new Date(),
105
+ updatedAt: new Date(),
106
+ });
107
+
108
+ // Data should be persisted (can be accessed by CLI)
109
+ const result = await db.select().from(modules);
110
+ expect(result).toHaveLength(1);
111
+
112
+ await cleanup();
113
+ });
114
+ });
115
+
116
+ describe('setupMultipleTestDatabases', () => {
117
+ test('creates multiple databases', async () => {
118
+ const databases = await setupMultipleTestDatabases(3);
119
+
120
+ expect(databases).toHaveLength(3);
121
+ expect(databases[0]).toBeDefined();
122
+ expect(databases[1]).toBeDefined();
123
+ expect(databases[2]).toBeDefined();
124
+
125
+ await cleanupMultipleTestDatabases(databases);
126
+ });
127
+
128
+ test('databases are isolated', async () => {
129
+ const [db1, db2, db3] = await setupMultipleTestDatabases(3);
130
+
131
+ // Insert different data in each
132
+ await db1.insert(modules).values({
133
+ id: 'module-1',
134
+ name: 'Module 1',
135
+ version: '1.0.0',
136
+ state: 'IMPORTED',
137
+ manifestData: {},
138
+ sourcePath: '/tmp/1',
139
+ importedAt: new Date(),
140
+ updatedAt: new Date(),
141
+ });
142
+
143
+ await db2.insert(modules).values({
144
+ id: 'module-2',
145
+ name: 'Module 2',
146
+ version: '1.0.0',
147
+ state: 'IMPORTED',
148
+ manifestData: {},
149
+ sourcePath: '/tmp/2',
150
+ importedAt: new Date(),
151
+ updatedAt: new Date(),
152
+ });
153
+
154
+ // Verify isolation
155
+ const result1 = await db1.select().from(modules);
156
+ const result2 = await db2.select().from(modules);
157
+ const result3 = await db3.select().from(modules);
158
+
159
+ expect(result1).toHaveLength(1);
160
+ expect(result1[0].id).toBe('module-1');
161
+
162
+ expect(result2).toHaveLength(1);
163
+ expect(result2[0].id).toBe('module-2');
164
+
165
+ expect(result3).toHaveLength(0);
166
+
167
+ await cleanupMultipleTestDatabases([db1, db2, db3]);
168
+ });
169
+ });
170
+
171
+ describe('cleanupTestDatabase', () => {
172
+ test('closes database', async () => {
173
+ const db = await setupTestDatabase();
174
+
175
+ await cleanupTestDatabase(db);
176
+
177
+ // Database should be closed (attempting to use it should fail)
178
+ // Note: Testing database closure is difficult with Drizzle's lazy query builders
179
+ expect(db).toBeDefined();
180
+ });
181
+ });
182
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Database Test Utilities
3
+ *
4
+ * Provides functions for creating and managing isolated test databases.
5
+ * Each test gets its own database to prevent state leakage.
6
+ */
7
+
8
+ import { Database } from 'bun:sqlite';
9
+ import { mkdtemp, rm } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { drizzle } from 'drizzle-orm/bun-sqlite';
13
+ import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
14
+ import { type DbClient, findMigrationsFolder } from '../db/client';
15
+ import * as schema from '../db/schema';
16
+
17
+ /**
18
+ * Create isolated test database (in-memory)
19
+ *
20
+ * Use for fast tests that don't need CLI interaction.
21
+ * Database is destroyed when closed.
22
+ *
23
+ * @returns In-memory SQLite database with migrations applied
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const db = await setupTestDatabase();
28
+ * // Use db...
29
+ * await cleanupTestDatabase(db);
30
+ * ```
31
+ */
32
+ export async function setupTestDatabase(): Promise<DbClient> {
33
+ const sqlite = new Database(':memory:');
34
+ const db = drizzle(sqlite, { schema });
35
+ const migrationsFolder = findMigrationsFolder();
36
+ await migrate(db, { migrationsFolder });
37
+ return db;
38
+ }
39
+
40
+ /**
41
+ * Create isolated test database (temporary file)
42
+ *
43
+ * Use for tests that need CLI to access database.
44
+ * Database is in a temporary directory that's cleaned up.
45
+ *
46
+ * @returns Object with database, path, and cleanup function
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const { db, path: dbPath, cleanup } = await setupTestDatabaseFile();
51
+ * // Use db and dbPath...
52
+ * await cleanup();
53
+ * ```
54
+ */
55
+ export async function setupTestDatabaseFile(): Promise<{
56
+ db: DbClient;
57
+ path: string;
58
+ cleanup: () => Promise<void>;
59
+ }> {
60
+ const tempDir = await mkdtemp(join(tmpdir(), 'celilo-test-'));
61
+ const dbPath = join(tempDir, 'test.db');
62
+ const sqlite = new Database(dbPath);
63
+ const db = drizzle(sqlite, { schema });
64
+ const migrationsFolder = findMigrationsFolder();
65
+ await migrate(db, { migrationsFolder });
66
+
67
+ const cleanup = async () => {
68
+ sqlite.close();
69
+ await rm(tempDir, { recursive: true, force: true });
70
+ };
71
+
72
+ return { db, path: dbPath, cleanup };
73
+ }
74
+
75
+ /**
76
+ * Clean up test database
77
+ *
78
+ * Closes the database connection.
79
+ * For in-memory databases, this destroys all data.
80
+ *
81
+ * @param db - Database to close
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const db = await setupTestDatabase();
86
+ * // ... tests ...
87
+ * await cleanupTestDatabase(db);
88
+ * ```
89
+ */
90
+ export async function cleanupTestDatabase(db: DbClient): Promise<void> {
91
+ db.$client.close();
92
+ }
93
+
94
+ /**
95
+ * Create multiple isolated databases
96
+ *
97
+ * Useful for tests that need multiple independent databases.
98
+ *
99
+ * @param count - Number of databases to create
100
+ * @returns Array of in-memory databases
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const [db1, db2] = await setupMultipleTestDatabases(2);
105
+ * // Use db1 and db2 independently...
106
+ * await cleanupMultipleTestDatabases([db1, db2]);
107
+ * ```
108
+ */
109
+ export async function setupMultipleTestDatabases(count: number): Promise<DbClient[]> {
110
+ const databases: DbClient[] = [];
111
+ for (let i = 0; i < count; i++) {
112
+ databases.push(await setupTestDatabase());
113
+ }
114
+ return databases;
115
+ }
116
+
117
+ /**
118
+ * Clean up multiple test databases
119
+ *
120
+ * @param databases - Array of databases to close
121
+ */
122
+ export async function cleanupMultipleTestDatabases(databases: DbClient[]): Promise<void> {
123
+ for (const db of databases) {
124
+ await cleanupTestDatabase(db);
125
+ }
126
+ }
@@ -0,0 +1,208 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import {
6
+ copyFixture,
7
+ createMultipleTempDirectories,
8
+ createTempDirectory,
9
+ createTempDirectoryWithCleanup,
10
+ removeMultipleTempDirectories,
11
+ removeTempDirectory,
12
+ } from './filesystem';
13
+
14
+ describe('Filesystem Test Utilities', () => {
15
+ const createdDirs: string[] = [];
16
+
17
+ afterEach(async () => {
18
+ // Cleanup any directories that weren't cleaned up in test
19
+ for (const dir of createdDirs) {
20
+ try {
21
+ if (existsSync(dir)) {
22
+ await removeTempDirectory(dir);
23
+ }
24
+ } catch {
25
+ // Ignore cleanup errors
26
+ }
27
+ }
28
+ createdDirs.length = 0;
29
+ });
30
+
31
+ describe('createTempDirectory', () => {
32
+ test('creates unique temporary directory', async () => {
33
+ const dir = await createTempDirectory();
34
+ createdDirs.push(dir);
35
+
36
+ expect(existsSync(dir)).toBe(true);
37
+ expect(dir).toMatch(/celilo-test-/);
38
+ });
39
+
40
+ test('creates different directories on multiple calls', async () => {
41
+ const dir1 = await createTempDirectory();
42
+ const dir2 = await createTempDirectory();
43
+ createdDirs.push(dir1, dir2);
44
+
45
+ expect(dir1).not.toBe(dir2);
46
+ expect(existsSync(dir1)).toBe(true);
47
+ expect(existsSync(dir2)).toBe(true);
48
+ });
49
+
50
+ test('directory is writable', async () => {
51
+ const dir = await createTempDirectory();
52
+ createdDirs.push(dir);
53
+
54
+ const testFile = join(dir, 'test.txt');
55
+ await writeFile(testFile, 'test content');
56
+
57
+ expect(existsSync(testFile)).toBe(true);
58
+ const content = await readFile(testFile, 'utf-8');
59
+ expect(content).toBe('test content');
60
+ });
61
+ });
62
+
63
+ describe('removeTempDirectory', () => {
64
+ test('removes empty directory', async () => {
65
+ const dir = await createTempDirectory();
66
+ expect(existsSync(dir)).toBe(true);
67
+
68
+ await removeTempDirectory(dir);
69
+ expect(existsSync(dir)).toBe(false);
70
+ });
71
+
72
+ test('removes directory with files', async () => {
73
+ const dir = await createTempDirectory();
74
+ await writeFile(join(dir, 'test.txt'), 'content');
75
+
76
+ expect(existsSync(dir)).toBe(true);
77
+
78
+ await removeTempDirectory(dir);
79
+ expect(existsSync(dir)).toBe(false);
80
+ });
81
+
82
+ test('removes directory with subdirectories', async () => {
83
+ const dir = await createTempDirectory();
84
+ const subdir = join(dir, 'subdir');
85
+ await writeFile(join(dir, 'test.txt'), 'content');
86
+
87
+ // Create subdirectory with file
88
+ const { mkdir } = await import('node:fs/promises');
89
+ await mkdir(subdir);
90
+ await writeFile(join(subdir, 'nested.txt'), 'nested');
91
+
92
+ expect(existsSync(dir)).toBe(true);
93
+ expect(existsSync(subdir)).toBe(true);
94
+
95
+ await removeTempDirectory(dir);
96
+ expect(existsSync(dir)).toBe(false);
97
+ });
98
+
99
+ test('handles non-existent directory gracefully', async () => {
100
+ // Should not throw
101
+ await expect(removeTempDirectory('/tmp/does-not-exist-12345')).resolves.toBeUndefined();
102
+ });
103
+ });
104
+
105
+ describe('copyFixture', () => {
106
+ test('copies fixture to destination', async () => {
107
+ const tempDir = await createTempDirectory();
108
+ createdDirs.push(tempDir);
109
+
110
+ const destPath = join(tempDir, 'artifact-test');
111
+ await copyFixture('./test-fixtures/modules/artifact-test', destPath);
112
+
113
+ expect(existsSync(destPath)).toBe(true);
114
+ expect(existsSync(join(destPath, 'manifest.yml'))).toBe(true);
115
+ expect(existsSync(join(destPath, 'terraform'))).toBe(true);
116
+ });
117
+
118
+ test('preserves directory structure', async () => {
119
+ const tempDir = await createTempDirectory();
120
+ createdDirs.push(tempDir);
121
+
122
+ const destPath = join(tempDir, 'artifact-test');
123
+ await copyFixture('./test-fixtures/modules/artifact-test', destPath);
124
+
125
+ expect(existsSync(join(destPath, 'terraform', 'main.tf'))).toBe(true);
126
+ expect(existsSync(join(destPath, 'build', 'build.sh'))).toBe(true);
127
+ });
128
+
129
+ test('preserves file contents', async () => {
130
+ const tempDir = await createTempDirectory();
131
+ createdDirs.push(tempDir);
132
+
133
+ const destPath = join(tempDir, 'artifact-test');
134
+ await copyFixture('./test-fixtures/modules/artifact-test', destPath);
135
+
136
+ const manifest = await readFile(join(destPath, 'manifest.yml'), 'utf-8');
137
+ expect(manifest).toContain('id: artifact-test');
138
+ });
139
+ });
140
+
141
+ describe('createMultipleTempDirectories', () => {
142
+ test('creates multiple directories', async () => {
143
+ const dirs = await createMultipleTempDirectories(3);
144
+ createdDirs.push(...dirs);
145
+
146
+ expect(dirs).toHaveLength(3);
147
+ expect(existsSync(dirs[0])).toBe(true);
148
+ expect(existsSync(dirs[1])).toBe(true);
149
+ expect(existsSync(dirs[2])).toBe(true);
150
+ });
151
+
152
+ test('directories are unique', async () => {
153
+ const dirs = await createMultipleTempDirectories(3);
154
+ createdDirs.push(...dirs);
155
+
156
+ expect(dirs[0]).not.toBe(dirs[1]);
157
+ expect(dirs[1]).not.toBe(dirs[2]);
158
+ expect(dirs[0]).not.toBe(dirs[2]);
159
+ });
160
+
161
+ test('creates zero directories', async () => {
162
+ const dirs = await createMultipleTempDirectories(0);
163
+ expect(dirs).toHaveLength(0);
164
+ });
165
+ });
166
+
167
+ describe('removeMultipleTempDirectories', () => {
168
+ test('removes multiple directories', async () => {
169
+ const dirs = await createMultipleTempDirectories(3);
170
+
171
+ expect(existsSync(dirs[0])).toBe(true);
172
+ expect(existsSync(dirs[1])).toBe(true);
173
+ expect(existsSync(dirs[2])).toBe(true);
174
+
175
+ await removeMultipleTempDirectories(dirs);
176
+
177
+ expect(existsSync(dirs[0])).toBe(false);
178
+ expect(existsSync(dirs[1])).toBe(false);
179
+ expect(existsSync(dirs[2])).toBe(false);
180
+ });
181
+
182
+ test('handles empty array', async () => {
183
+ await expect(removeMultipleTempDirectories([])).resolves.toBeUndefined();
184
+ });
185
+ });
186
+
187
+ describe('createTempDirectoryWithCleanup', () => {
188
+ test('creates directory and cleanup function', async () => {
189
+ const { path: dir, cleanup } = await createTempDirectoryWithCleanup();
190
+
191
+ expect(existsSync(dir)).toBe(true);
192
+ expect(typeof cleanup).toBe('function');
193
+
194
+ await cleanup();
195
+ expect(existsSync(dir)).toBe(false);
196
+ });
197
+
198
+ test('cleanup removes directory', async () => {
199
+ const { path: dir, cleanup } = await createTempDirectoryWithCleanup();
200
+ await writeFile(join(dir, 'test.txt'), 'content');
201
+
202
+ expect(existsSync(dir)).toBe(true);
203
+
204
+ await cleanup();
205
+ expect(existsSync(dir)).toBe(false);
206
+ });
207
+ });
208
+ });