@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,142 @@
1
+ /**
2
+ * Filesystem Test Utilities
3
+ *
4
+ * Provides functions for creating and managing temporary directories and files
5
+ * for test isolation.
6
+ */
7
+
8
+ import { cp, mkdtemp, rm } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ /**
13
+ * Create temporary directory for test
14
+ *
15
+ * Directory is created in system temp dir with unique name.
16
+ * Caller is responsible for cleanup via removeTempDirectory().
17
+ *
18
+ * @returns Path to temporary directory
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const tempDir = await createTempDirectory();
23
+ * // Use tempDir...
24
+ * await removeTempDirectory(tempDir);
25
+ * ```
26
+ */
27
+ export async function createTempDirectory(): Promise<string> {
28
+ return await mkdtemp(join(tmpdir(), 'celilo-test-'));
29
+ }
30
+
31
+ /**
32
+ * Remove temporary directory (with retry for locked files)
33
+ *
34
+ * Handles file locking issues by retrying once after a short delay.
35
+ * Useful on Windows where files may be locked briefly.
36
+ *
37
+ * @param dir - Directory to remove
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const tempDir = await createTempDirectory();
42
+ * // Use tempDir...
43
+ * await removeTempDirectory(tempDir);
44
+ * ```
45
+ */
46
+ export async function removeTempDirectory(dir: string): Promise<void> {
47
+ try {
48
+ await rm(dir, { recursive: true, force: true });
49
+ } catch (_error) {
50
+ // Retry once after 100ms (helps with file locking issues)
51
+ await new Promise((resolve) => setTimeout(resolve, 100));
52
+ await rm(dir, { recursive: true, force: true });
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Copy test fixture to temporary directory
58
+ *
59
+ * Recursively copies source to destination.
60
+ * Useful for setting up test data from fixtures.
61
+ *
62
+ * @param fixturePath - Source path (relative to cwd)
63
+ * @param destPath - Destination path
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const tempDir = await createTempDirectory();
68
+ * await copyFixture('./test-fixtures/modules/artifact-test', tempDir);
69
+ * // tempDir now contains copy of artifact-test module
70
+ * await removeTempDirectory(tempDir);
71
+ * ```
72
+ */
73
+ export async function copyFixture(fixturePath: string, destPath: string): Promise<void> {
74
+ await cp(fixturePath, destPath, { recursive: true });
75
+ }
76
+
77
+ /**
78
+ * Create multiple temporary directories
79
+ *
80
+ * Useful for tests that need multiple isolated directories.
81
+ *
82
+ * @param count - Number of directories to create
83
+ * @returns Array of directory paths
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const [dir1, dir2] = await createMultipleTempDirectories(2);
88
+ * // Use dir1 and dir2...
89
+ * await removeMultipleTempDirectories([dir1, dir2]);
90
+ * ```
91
+ */
92
+ export async function createMultipleTempDirectories(count: number): Promise<string[]> {
93
+ const directories: string[] = [];
94
+ for (let i = 0; i < count; i++) {
95
+ directories.push(await createTempDirectory());
96
+ }
97
+ return directories;
98
+ }
99
+
100
+ /**
101
+ * Remove multiple temporary directories
102
+ *
103
+ * @param directories - Array of directory paths to remove
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const dirs = await createMultipleTempDirectories(3);
108
+ * // Use dirs...
109
+ * await removeMultipleTempDirectories(dirs);
110
+ * ```
111
+ */
112
+ export async function removeMultipleTempDirectories(directories: string[]): Promise<void> {
113
+ for (const dir of directories) {
114
+ await removeTempDirectory(dir);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Create temporary directory with cleanup function
120
+ *
121
+ * Returns both directory path and cleanup function.
122
+ * Convenient pattern for use in test setup/teardown.
123
+ *
124
+ * @returns Object with directory path and cleanup function
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const { path: tempDir, cleanup } = await createTempDirectoryWithCleanup();
129
+ * // Use tempDir...
130
+ * await cleanup();
131
+ * ```
132
+ */
133
+ export async function createTempDirectoryWithCleanup(): Promise<{
134
+ path: string;
135
+ cleanup: () => Promise<void>;
136
+ }> {
137
+ const path = await createTempDirectory();
138
+ const cleanup = async () => {
139
+ await removeTempDirectory(path);
140
+ };
141
+ return { path, cleanup };
142
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { existsSync } from 'node:fs';
3
+ import {
4
+ getFixturePath,
5
+ getModuleTestConfig,
6
+ getModuleTestSecrets,
7
+ getSystemTestConfig,
8
+ getSystemTestSecrets,
9
+ loadTestValues,
10
+ } from './fixtures';
11
+
12
+ describe('Fixture Test Utilities', () => {
13
+ describe('getFixturePath', () => {
14
+ test('returns absolute path to fixture', () => {
15
+ const path = getFixturePath('modules/artifact-test');
16
+ expect(path).toContain('test-fixtures/modules/artifact-test');
17
+ expect(path).toMatch(/^[/\\]/); // Starts with / or \ (absolute path)
18
+ });
19
+
20
+ test('handles nested paths', () => {
21
+ const path = getFixturePath('modules/artifact-test/manifest.yml');
22
+ expect(path).toContain('test-fixtures/modules/artifact-test/manifest.yml');
23
+ });
24
+
25
+ test('returns path that exists', () => {
26
+ const path = getFixturePath('modules/artifact-test');
27
+ expect(existsSync(path)).toBe(true);
28
+ });
29
+ });
30
+
31
+ describe('loadTestValues', () => {
32
+ test('loads test-values.yml', async () => {
33
+ const testValues = await loadTestValues();
34
+ expect(testValues).toBeDefined();
35
+ expect(testValues).toHaveProperty('system');
36
+ expect(testValues).toHaveProperty('modules');
37
+ });
38
+
39
+ test('contains expected system configuration', async () => {
40
+ const testValues = await loadTestValues();
41
+ expect(testValues.system).toBeDefined();
42
+ expect(testValues.system?.dns).toBeDefined();
43
+ });
44
+
45
+ test('contains expected module data', async () => {
46
+ const testValues = await loadTestValues();
47
+ expect(testValues.modules).toBeDefined();
48
+ expect(testValues.modules?.homebridge).toBeDefined();
49
+ });
50
+ });
51
+
52
+ describe('getModuleTestConfig', () => {
53
+ test('returns module configuration', async () => {
54
+ const config = await getModuleTestConfig('homebridge');
55
+ expect(config).toBeDefined();
56
+ expect(config.vmid).toBe(2110);
57
+ expect(config.hostname).toBe('iot');
58
+ });
59
+
60
+ test('excludes secrets from config', async () => {
61
+ const config = await getModuleTestConfig('dns-external');
62
+ expect(config).toBeDefined();
63
+ expect(config.vps_ip).toBe('188.166.157.2');
64
+ expect(config.secrets).toBeUndefined();
65
+ });
66
+
67
+ test('returns empty object for non-existent module', async () => {
68
+ const config = await getModuleTestConfig('non-existent-module');
69
+ expect(config).toEqual({});
70
+ });
71
+
72
+ test('handles module with no secrets', async () => {
73
+ // Homebridge module has config but no secrets in test-values.yml
74
+ const config = await getModuleTestConfig('homebridge');
75
+ expect(config).toBeDefined();
76
+ expect(config.hostname).toBeDefined();
77
+ });
78
+ });
79
+
80
+ describe('getModuleTestSecrets', () => {
81
+ test('returns module secrets', async () => {
82
+ const secrets = await getModuleTestSecrets('dns-external');
83
+ expect(secrets).toBeDefined();
84
+ expect(secrets.knot_ddns_tsig_secret).toBeTruthy();
85
+ expect(typeof secrets.knot_ddns_tsig_secret).toBe('string');
86
+ });
87
+
88
+ test('returns empty object for module with no secrets', async () => {
89
+ const secrets = await getModuleTestSecrets('test-module');
90
+ expect(secrets).toEqual({});
91
+ });
92
+
93
+ test('returns empty object for non-existent module', async () => {
94
+ const secrets = await getModuleTestSecrets('non-existent-module');
95
+ expect(secrets).toEqual({});
96
+ });
97
+ });
98
+
99
+ describe('getSystemTestConfig', () => {
100
+ test('returns system configuration', async () => {
101
+ const config = await getSystemTestConfig();
102
+ expect(config).toBeDefined();
103
+ expect(config.primary_domain).toBe('example.com');
104
+ expect(config.dns).toBeDefined();
105
+ });
106
+
107
+ test('contains nested configuration', async () => {
108
+ const config = await getSystemTestConfig();
109
+ expect(config.dns).toBeDefined();
110
+ const dns = config.dns as Record<string, unknown>;
111
+ expect(dns.primary).toBe('192.168.0.1');
112
+ });
113
+ });
114
+
115
+ describe('getSystemTestSecrets', () => {
116
+ test('returns system secrets (currently empty after container-services migration)', async () => {
117
+ const secrets = await getSystemTestSecrets();
118
+ expect(secrets).toBeDefined();
119
+ expect(typeof secrets).toBe('object');
120
+ // Note: Proxmox credentials are now per-service, not system-wide secrets
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Fixture Test Utilities
3
+ *
4
+ * Provides functions for loading test fixtures and test data.
5
+ * Centralizes fixture path resolution and test data loading.
6
+ */
7
+
8
+ import { readFile } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ import { parse as parseYaml } from 'yaml';
11
+
12
+ /**
13
+ * Get fixture path
14
+ *
15
+ * Resolves path to fixture directory or file.
16
+ * All fixtures are in ./test-fixtures/ directory.
17
+ *
18
+ * @param fixtureName - Fixture name (e.g., 'modules/artifact-test')
19
+ * @returns Absolute path to fixture
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const modulePath = getFixturePath('modules/artifact-test');
24
+ * // Returns: /absolute/path/to/celilo/backend/test-fixtures/modules/artifact-test
25
+ * ```
26
+ */
27
+ export function getFixturePath(fixtureName: string): string {
28
+ return join(process.cwd(), 'test-fixtures', fixtureName);
29
+ }
30
+
31
+ /**
32
+ * Test values structure
33
+ *
34
+ * Mirrors test-values.yml structure.
35
+ * Top-level keys: system, system_secrets, modules
36
+ * Module values can include config and secrets
37
+ */
38
+ export interface TestValues {
39
+ system?: Record<string, unknown>;
40
+ system_secrets?: Record<string, unknown>;
41
+ modules?: Record<string, Record<string, unknown>>;
42
+ }
43
+
44
+ /**
45
+ * Load test values from test-values.yml
46
+ *
47
+ * Loads test configuration and secrets from centralized test data file.
48
+ * Use for integration tests that need realistic test data.
49
+ *
50
+ * @returns Test values object
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const testValues = await loadTestValues();
55
+ * const caddy = testValues.modules?.caddy;
56
+ * expect(caddy.config?.hostname).toBe('www');
57
+ * ```
58
+ */
59
+ export async function loadTestValues(): Promise<TestValues> {
60
+ const testValuesPath = getFixturePath('test-values.yml');
61
+ const content = await readFile(testValuesPath, 'utf-8');
62
+ return parseYaml(content) as TestValues;
63
+ }
64
+
65
+ /**
66
+ * Get module test configuration
67
+ *
68
+ * Retrieves configuration for specific module from test-values.yml.
69
+ * Returns empty object if module not found.
70
+ * Excludes 'secrets' key from returned config.
71
+ *
72
+ * @param moduleId - Module ID
73
+ * @returns Module configuration
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const config = await getModuleTestConfig('homebridge');
78
+ * expect(config.vmid).toBe(2110);
79
+ * ```
80
+ */
81
+ export async function getModuleTestConfig(moduleId: string): Promise<Record<string, unknown>> {
82
+ const testValues = await loadTestValues();
83
+ const moduleData = testValues.modules?.[moduleId];
84
+ if (!moduleData) {
85
+ return {};
86
+ }
87
+
88
+ // Return all data except 'secrets'
89
+ const { secrets, ...config } = moduleData as Record<string, unknown> & {
90
+ secrets?: unknown;
91
+ };
92
+ return config;
93
+ }
94
+
95
+ /**
96
+ * Get module test secrets
97
+ *
98
+ * Retrieves secrets for specific module from test-values.yml.
99
+ * Returns empty object if module not found or has no secrets.
100
+ *
101
+ * @param moduleId - Module ID
102
+ * @returns Module secrets
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const secrets = await getModuleTestSecrets('dns-external');
107
+ * expect(secrets.knot_ddns_tsig_secret).toBeTruthy();
108
+ * ```
109
+ */
110
+ export async function getModuleTestSecrets(moduleId: string): Promise<Record<string, unknown>> {
111
+ const testValues = await loadTestValues();
112
+ const moduleData = testValues.modules?.[moduleId];
113
+ if (!moduleData) {
114
+ return {};
115
+ }
116
+
117
+ const secrets = (moduleData as Record<string, unknown>).secrets;
118
+ if (!secrets || typeof secrets !== 'object') {
119
+ return {};
120
+ }
121
+
122
+ return secrets as Record<string, unknown>;
123
+ }
124
+
125
+ /**
126
+ * Get system test configuration
127
+ *
128
+ * Retrieves system-wide configuration from test-values.yml.
129
+ *
130
+ * @returns System configuration
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const config = await getSystemTestConfig();
135
+ * expect(config.dns).toBeDefined();
136
+ * ```
137
+ */
138
+ export async function getSystemTestConfig(): Promise<Record<string, unknown>> {
139
+ const testValues = await loadTestValues();
140
+ return testValues.system ?? {};
141
+ }
142
+
143
+ /**
144
+ * Get system test secrets
145
+ *
146
+ * Retrieves system-wide secrets from test-values.yml.
147
+ *
148
+ * @returns System secrets
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const secrets = await getSystemTestSecrets();
153
+ * // Note: Provider credentials are now per-service, not system-wide
154
+ * expect(secrets).toBeDefined();
155
+ * ```
156
+ */
157
+ export async function getSystemTestSecrets(): Promise<Record<string, unknown>> {
158
+ const testValues = await loadTestValues();
159
+ return testValues.system_secrets ?? {};
160
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Golden File Comparison Utilities
3
+ *
4
+ * Compares generated output against golden reference files for regression testing.
5
+ */
6
+
7
+ import { existsSync } from 'node:fs';
8
+ import { readFile, readdir } from 'node:fs/promises';
9
+ import { join, relative } from 'node:path';
10
+
11
+ export interface FileDifference {
12
+ path: string;
13
+ type: 'missing-in-golden' | 'missing-in-generated' | 'content-mismatch';
14
+ goldenContent?: string;
15
+ generatedContent?: string;
16
+ }
17
+
18
+ export interface ComparisonResult {
19
+ pass: boolean;
20
+ differences: FileDifference[];
21
+ filesCompared: number;
22
+ filesSkipped: number;
23
+ }
24
+
25
+ export interface ComparisonOptions {
26
+ /**
27
+ * Files to exclude from comparison (glob patterns relative to base dir)
28
+ * Example: ['**\/*.json', 'secrets/**']
29
+ */
30
+ excludePatterns?: string[];
31
+ }
32
+
33
+ /**
34
+ * Compare two directories recursively
35
+ *
36
+ * @param goldenDir - Path to golden reference directory
37
+ * @param generatedDir - Path to generated output directory
38
+ * @param options - Comparison options
39
+ * @returns Comparison result with any differences found
40
+ */
41
+ export async function compareDirectories(
42
+ goldenDir: string,
43
+ generatedDir: string,
44
+ options: ComparisonOptions = {},
45
+ ): Promise<ComparisonResult> {
46
+ const differences: FileDifference[] = [];
47
+ let filesCompared = 0;
48
+ let filesSkipped = 0;
49
+
50
+ // Files to skip (contain encrypted secrets that change each run)
51
+ const skipPatterns = options.excludePatterns || [];
52
+ const shouldSkip = (path: string): boolean => {
53
+ return skipPatterns.some((pattern) => {
54
+ // Simple glob matching - just check if path includes pattern
55
+ return path.includes(pattern.replace('**/', '').replace('*', ''));
56
+ });
57
+ };
58
+
59
+ // Get all files from both directories
60
+ const goldenFiles = await getAllFiles(goldenDir);
61
+ const generatedFiles = await getAllFiles(generatedDir);
62
+
63
+ // Convert to relative paths for comparison
64
+ const goldenRelativePaths = new Set(goldenFiles.map((f) => relative(goldenDir, f)));
65
+ const generatedRelativePaths = new Set(generatedFiles.map((f) => relative(generatedDir, f)));
66
+
67
+ // Check for files in golden but not in generated
68
+ for (const relativePath of goldenRelativePaths) {
69
+ if (!generatedRelativePaths.has(relativePath)) {
70
+ // Skip if matches exclude pattern
71
+ if (shouldSkip(relativePath)) {
72
+ filesSkipped++;
73
+ continue;
74
+ }
75
+ differences.push({
76
+ path: relativePath,
77
+ type: 'missing-in-generated',
78
+ });
79
+ }
80
+ }
81
+
82
+ // Check for files in generated but not in golden
83
+ for (const relativePath of generatedRelativePaths) {
84
+ if (!goldenRelativePaths.has(relativePath)) {
85
+ // Skip if matches exclude pattern
86
+ if (shouldSkip(relativePath)) {
87
+ filesSkipped++;
88
+ continue;
89
+ }
90
+ differences.push({
91
+ path: relativePath,
92
+ type: 'missing-in-golden',
93
+ });
94
+ }
95
+ }
96
+
97
+ // Compare content of files that exist in both
98
+ for (const relativePath of goldenRelativePaths) {
99
+ if (generatedRelativePaths.has(relativePath)) {
100
+ // Skip files that match exclude patterns
101
+ if (shouldSkip(relativePath)) {
102
+ filesSkipped++;
103
+ continue;
104
+ }
105
+
106
+ const goldenPath = join(goldenDir, relativePath);
107
+ const generatedPath = join(generatedDir, relativePath);
108
+
109
+ const goldenContent = await readFile(goldenPath, 'utf-8');
110
+ const generatedContent = await readFile(generatedPath, 'utf-8');
111
+
112
+ if (goldenContent !== generatedContent) {
113
+ differences.push({
114
+ path: relativePath,
115
+ type: 'content-mismatch',
116
+ goldenContent,
117
+ generatedContent,
118
+ });
119
+ }
120
+
121
+ filesCompared++;
122
+ }
123
+ }
124
+
125
+ return {
126
+ pass: differences.length === 0,
127
+ differences,
128
+ filesCompared,
129
+ filesSkipped,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Get all files in a directory recursively
135
+ *
136
+ * @param dir - Directory path
137
+ * @returns Array of absolute file paths
138
+ */
139
+ async function getAllFiles(dir: string): Promise<string[]> {
140
+ const files: string[] = [];
141
+
142
+ if (!existsSync(dir)) {
143
+ return files;
144
+ }
145
+
146
+ const entries = await readdir(dir, { withFileTypes: true });
147
+
148
+ for (const entry of entries) {
149
+ const fullPath = join(dir, entry.name);
150
+
151
+ if (entry.isDirectory()) {
152
+ const subFiles = await getAllFiles(fullPath);
153
+ files.push(...subFiles);
154
+ } else {
155
+ files.push(fullPath);
156
+ }
157
+ }
158
+
159
+ return files;
160
+ }
161
+
162
+ /**
163
+ * Format differences for display
164
+ *
165
+ * @param differences - Array of differences
166
+ * @returns Formatted string
167
+ */
168
+ export function formatDifferences(differences: FileDifference[]): string {
169
+ const lines: string[] = [];
170
+
171
+ for (const diff of differences) {
172
+ switch (diff.type) {
173
+ case 'missing-in-golden':
174
+ lines.push(`❌ File exists in generated but not in golden: ${diff.path}`);
175
+ lines.push(' (Run with --update-golden to add this file)');
176
+ break;
177
+
178
+ case 'missing-in-generated':
179
+ lines.push(`❌ File exists in golden but not in generated: ${diff.path}`);
180
+ break;
181
+
182
+ case 'content-mismatch':
183
+ lines.push(`❌ Content mismatch: ${diff.path}`);
184
+ if (diff.goldenContent && diff.generatedContent) {
185
+ lines.push(' Golden (first 200 chars):');
186
+ lines.push(` ${diff.goldenContent.substring(0, 200).replace(/\n/g, '\\n')}`);
187
+ lines.push(' Generated (first 200 chars):');
188
+ lines.push(` ${diff.generatedContent.substring(0, 200).replace(/\n/g, '\\n')}`);
189
+ }
190
+ break;
191
+ }
192
+
193
+ lines.push('');
194
+ }
195
+
196
+ return lines.join('\n');
197
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Test Utilities - Public API
3
+ *
4
+ * Exports all test utilities for use in integration and unit tests.
5
+ */
6
+
7
+ // Persistent CLI testing (new)
8
+ export { CLIContext, ResponseBuilder, type RunOptions } from './cli-context';
9
+ export { type CLIResult, CLIResultImpl } from './cli-result';
10
+ export {
11
+ fixtures,
12
+ homebridgeModule,
13
+ caddyModule,
14
+ dnsExternalModule,
15
+ multiModule,
16
+ } from './module-fixtures';
17
+
18
+ // Legacy CLI testing (deprecated - use CLIContext instead)
19
+ export { runCli, runCliExpectingFailure } from './cli';
20
+
21
+ // Integration test setup
22
+ export { setupIntegrationTest, type IntegrationTestContext } from './integration';
23
+
24
+ // Database utilities
25
+ export {
26
+ setupTestDatabase,
27
+ setupTestDatabaseFile,
28
+ cleanupTestDatabase,
29
+ } from './database';
30
+
31
+ // Filesystem utilities
32
+ export {
33
+ createTempDirectory,
34
+ removeTempDirectory,
35
+ copyFixture,
36
+ createTempDirectoryWithCleanup,
37
+ } from './filesystem';
38
+
39
+ // Module utilities
40
+ export {
41
+ importTestModule,
42
+ cleanupTestModule,
43
+ importMultipleTestModules,
44
+ cleanupMultipleTestModules,
45
+ getModuleState,
46
+ type ImportTestModuleOptions,
47
+ } from './modules';
48
+
49
+ // Fixture utilities
50
+ export {
51
+ getFixturePath,
52
+ loadTestValues,
53
+ getModuleTestConfig,
54
+ getModuleTestSecrets,
55
+ getSystemTestConfig,
56
+ getSystemTestSecrets,
57
+ type TestValues,
58
+ } from './fixtures';
59
+
60
+ // Golden file testing
61
+ export {
62
+ compareDirectories,
63
+ formatDifferences,
64
+ type FileDifference,
65
+ type ComparisonResult,
66
+ type ComparisonOptions,
67
+ } from './golden-diff';
68
+
69
+ // Value extraction
70
+ export {
71
+ extractTerraformValues,
72
+ extractAnsibleValues,
73
+ compareExtractedValues,
74
+ formatValueMismatches,
75
+ type ExtractedValues,
76
+ type ValueExtractionResult,
77
+ } from './value-extractor';