@celilo/cli 1.8.0 → 1.9.1

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 (66) hide show
  1. package/CELILO_CORE_MODULES.md +2 -0
  2. package/CELILO_SUBSYSTEMS.md +2 -0
  3. package/drizzle/0028_capability_bindings.sql +26 -0
  4. package/drizzle/0029_module_instances.sql +58 -0
  5. package/drizzle/meta/_journal.json +14 -0
  6. package/package.json +2 -2
  7. package/src/cli/commands/module-show.ts +1 -0
  8. package/src/db/foreign-keys.test.ts +101 -0
  9. package/src/db/schema.ts +161 -5
  10. package/src/hooks/broker.test.ts +153 -0
  11. package/src/hooks/broker.ts +307 -0
  12. package/src/hooks/capability-loader-bindings.test.ts +163 -0
  13. package/src/hooks/capability-loader-firewall.test.ts +108 -0
  14. package/src/hooks/capability-loader.test.ts +10 -2
  15. package/src/hooks/capability-loader.ts +59 -2
  16. package/src/hooks/define-hook.test.ts +1 -0
  17. package/src/hooks/executor.test.ts +7 -0
  18. package/src/hooks/executor.ts +245 -111
  19. package/src/hooks/hook-protocol.test.ts +192 -0
  20. package/src/hooks/hook-protocol.ts +275 -0
  21. package/src/hooks/hook-runner.ts +231 -0
  22. package/src/hooks/hook-state-dir.test.ts +109 -0
  23. package/src/hooks/hook-timeout.test.ts +104 -0
  24. package/src/hooks/hook-trespass.test.ts +202 -0
  25. package/src/hooks/injected-capabilities.test.ts +75 -0
  26. package/src/hooks/mount-set.test.ts +148 -0
  27. package/src/hooks/mount-set.ts +234 -0
  28. package/src/hooks/test-fixtures/capability-calling-hook.ts +79 -0
  29. package/src/hooks/test-fixtures/runaway-hook.ts +26 -0
  30. package/src/hooks/test-fixtures/sigterm-ignoring-hook.ts +22 -0
  31. package/src/manifest/contracts/v1.ts +21 -6
  32. package/src/manifest/validate-provider-views.test.ts +61 -0
  33. package/src/manifest/validate.ts +21 -14
  34. package/src/module/packaging/module-state-directory.test.ts +105 -0
  35. package/src/module/packaging/package-rules.ts +10 -2
  36. package/src/policy/capability-shape-baseline.ts +8 -0
  37. package/src/policy/capability-shape.ts +13 -1
  38. package/src/policy/module-business-baseline.ts +36 -0
  39. package/src/policy/module-dep-reachability.test.ts +167 -0
  40. package/src/services/alerting/ack.test.ts +2 -2
  41. package/src/services/alerting/deferral.test.ts +2 -2
  42. package/src/services/alerting/delivery-loop.test.ts +2 -2
  43. package/src/services/alerting/deploy-hooks.test.ts +2 -2
  44. package/src/services/alerting/inbound-poller.test.ts +2 -2
  45. package/src/services/alerting/inbound.test.ts +2 -2
  46. package/src/services/alerting/notification-responder.test.ts +2 -2
  47. package/src/services/alerting/run-monitor.test.ts +2 -2
  48. package/src/services/alerting/store.test.ts +2 -2
  49. package/src/services/alerting/sweep-runner.test.ts +2 -2
  50. package/src/services/alerting/tokens.test.ts +2 -2
  51. package/src/services/capability-bindings.test.ts +104 -0
  52. package/src/services/capability-bindings.ts +107 -0
  53. package/src/services/capability-table-rows.test.ts +2 -2
  54. package/src/services/consumer-cleanup.test.ts +40 -3
  55. package/src/services/dns-internal-records.test.ts +3 -3
  56. package/src/services/fleet-checks.test.ts +4 -4
  57. package/src/services/module-instances.test.ts +198 -0
  58. package/src/services/module-instances.ts +96 -0
  59. package/src/services/module-journal.test.ts +2 -2
  60. package/src/services/module-subscriptions.test.ts +1 -1
  61. package/src/services/port-forwards.test.ts +2 -2
  62. package/src/services/trusted-sources.test.ts +3 -3
  63. package/src/templates/ingress-ip.test.ts +31 -0
  64. package/src/test-utils/database.ts +31 -1
  65. package/src/test-utils/module-fixtures.ts +147 -35
  66. package/src/test-utils/setup-test-db.ts +0 -80
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { CLIContext } from './cli-context';
11
+ import { getModuleTestConfig } from './fixtures';
11
12
  import { type IntegrationTestContext, setupIntegrationTest } from './integration';
12
13
 
13
14
  /**
@@ -38,6 +39,52 @@ interface HomebridgeModuleOptions extends BaseFixtureOptions {
38
39
  withMockInfrastructure?: boolean;
39
40
  }
40
41
 
42
+ /**
43
+ * A verified container service covering every allocatable zone, so a fixture
44
+ * can reach `module generate`.
45
+ *
46
+ * Extracted rather than copied: generation refuses without infrastructure, so
47
+ * every fixture that generates needs this, and a second inline copy would drift
48
+ * from the first the moment `containerServices` gains a column.
49
+ */
50
+ async function insertMockContainerService(dbPath: string): Promise<void> {
51
+ const { getDb } = await import('../db/client');
52
+ const { containerServices } = await import('../db/schema');
53
+ const originalDbPath = process.env.CELILO_DB_PATH;
54
+ process.env.CELILO_DB_PATH = dbPath;
55
+ try {
56
+ const db = getDb();
57
+ await db.insert(containerServices).values({
58
+ id: 'test-proxmox',
59
+ serviceId: 'test-proxmox',
60
+ name: 'Test Proxmox',
61
+ providerName: 'proxmox',
62
+ zones: ['internal', 'dmz', 'app', 'secure'],
63
+ apiCredentialsEncrypted: JSON.stringify({
64
+ encryptedValue: 'dummy',
65
+ iv: 'dummy',
66
+ authTag: 'dummy',
67
+ }),
68
+ providerConfig: {
69
+ default_target_node: 'pve',
70
+ lxc_template: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
71
+ storage: 'local-lvm',
72
+ },
73
+ verified: true,
74
+ verifiedAt: new Date(),
75
+ verificationError: null,
76
+ createdAt: new Date(),
77
+ updatedAt: new Date(),
78
+ });
79
+ } finally {
80
+ if (originalDbPath) {
81
+ process.env.CELILO_DB_PATH = originalDbPath;
82
+ } else {
83
+ process.env.CELILO_DB_PATH = undefined;
84
+ }
85
+ }
86
+ }
87
+
41
88
  /**
42
89
  * Homebridge module fixture result
43
90
  */
@@ -83,41 +130,7 @@ export async function homebridgeModule(
83
130
 
84
131
  // Setup mock infrastructure if requested (needed for generation)
85
132
  if (options.withMockInfrastructure) {
86
- const { getDb } = await import('../db/client');
87
- const { containerServices } = await import('../db/schema');
88
- const originalDbPath = process.env.CELILO_DB_PATH;
89
- process.env.CELILO_DB_PATH = ctx.dbPath;
90
- try {
91
- const db = getDb();
92
- await db.insert(containerServices).values({
93
- id: 'test-proxmox',
94
- serviceId: 'test-proxmox',
95
- name: 'Test Proxmox',
96
- providerName: 'proxmox',
97
- zones: ['internal', 'dmz', 'app', 'secure'],
98
- apiCredentialsEncrypted: JSON.stringify({
99
- encryptedValue: 'dummy',
100
- iv: 'dummy',
101
- authTag: 'dummy',
102
- }),
103
- providerConfig: {
104
- default_target_node: 'pve',
105
- lxc_template: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
106
- storage: 'local-lvm',
107
- },
108
- verified: true,
109
- verifiedAt: new Date(),
110
- verificationError: null,
111
- createdAt: new Date(),
112
- updatedAt: new Date(),
113
- });
114
- } finally {
115
- if (originalDbPath) {
116
- process.env.CELILO_DB_PATH = originalDbPath;
117
- } else {
118
- process.env.CELILO_DB_PATH = undefined;
119
- }
120
- }
133
+ await insertMockContainerService(ctx.dbPath);
121
134
  }
122
135
 
123
136
  // Create CLI context with integration test env
@@ -195,6 +208,105 @@ export async function homebridgeModule(
195
208
  };
196
209
  }
197
210
 
211
+ /**
212
+ * `dnsmasq-dhcp` fixture, for the golden comparison.
213
+ *
214
+ * ⚠️ IMPORTS THE REAL MODULE, `../../modules/dnsmasq-dhcp`, and not a copy under
215
+ * `test-fixtures/modules/`. That is the whole point of it.
216
+ *
217
+ * `homebridgeModule` imports a copy, and the copy has drifted 29 lines in the
218
+ * manifest and 6 in the Terraform template. So the golden gate compares
219
+ * generated-from-a-copy against golden-from-the-same-copy: both sides move
220
+ * together, and the shipped module is on neither. Worse, the copy is frozen at
221
+ * `version: 1.0.0` with the container-infrastructure variables `required: true`
222
+ * — the pre-fix state of a real bug (marking them required made homebridge
223
+ * container-only). The gate still passes against that shape and cannot notice
224
+ * it returning. Tracked as celilo#1084.
225
+ *
226
+ * Pointing at the shipped module means this golden fails when the generator's
227
+ * output for the shipped module changes, which is the only version worth having.
228
+ */
229
+ export interface DnsmasqDhcpModuleFixture {
230
+ cli: CLIContext;
231
+ moduleId: string;
232
+ config: Record<string, unknown>;
233
+ context: IntegrationTestContext;
234
+ cleanup: () => Promise<void>;
235
+ }
236
+
237
+ export interface DnsmasqDhcpModuleOptions extends BaseFixtureOptions {
238
+ /** Seed a container service, without which generation has no infrastructure. */
239
+ withMockInfrastructure?: boolean;
240
+ /** Set the `network.internal.*` system config the templates resolve against. */
241
+ withSystemConfig?: boolean;
242
+ }
243
+
244
+ export async function dnsmasqDhcpModule(
245
+ options: DnsmasqDhcpModuleOptions = {},
246
+ ): Promise<DnsmasqDhcpModuleFixture> {
247
+ const ctx = await setupIntegrationTest();
248
+
249
+ if (options.withMockInfrastructure) {
250
+ await insertMockContainerService(ctx.dbPath);
251
+ }
252
+
253
+ const cli = await CLIContext.create('src/cli/index.ts', {
254
+ CELILO_DB_PATH: ctx.dbPath,
255
+ CELILO_DATA_DIR: ctx.dataDir,
256
+ });
257
+
258
+ const moduleId = 'dnsmasq-dhcp';
259
+
260
+ if (options.withSystemConfig) {
261
+ // `internal` only — it is the single zone this module declares, and adding
262
+ // others would put values in the golden that nothing generated from.
263
+ (await cli.run('system config set network.bridge vmbr0')).expectSuccess();
264
+ (await cli.run('system config set network.internal.vlan 1')).expectSuccess();
265
+ (await cli.run('system config set network.internal.gateway 10.99.1.1')).expectSuccess();
266
+ (await cli.run('system config set network.internal.subnet 10.99.1.0/24')).expectSuccess();
267
+ (
268
+ await cli.run(
269
+ 'system config set ssh.public_key ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... test@celilo',
270
+ )
271
+ ).expectSuccess();
272
+ }
273
+
274
+ // Read from `test-fixtures/test-values.yml` rather than written out here, so
275
+ // that file's entry for this module is load-bearing rather than decorative. A
276
+ // golden generated from values nothing else references would drift from the
277
+ // canonical set with nothing noticing.
278
+ //
279
+ // That entry carries EVERY `required: true` variable, including the ones with
280
+ // defaults: celilo asks for a required variable whether or not it has one,
281
+ // and an unanswered ask has no headless answer, so the command blocks until
282
+ // its timeout naming no variable.
283
+ const config = await getModuleTestConfig(moduleId);
284
+ if (Object.keys(config).length === 0) {
285
+ throw new Error(
286
+ `test-values.yml carries no entry for '${moduleId}', so this fixture would generate from an empty config and the golden would record that. Add one.`,
287
+ );
288
+ }
289
+
290
+ (await cli.run(`module import ../../modules/${moduleId}`)).expectSuccess();
291
+
292
+ for (const [key, value] of Object.entries(config)) {
293
+ // An array has to reach the CLI as JSON; a scalar is quoted as-is.
294
+ const encoded = Array.isArray(value) ? `'${JSON.stringify(value)}'` : `"${value}"`;
295
+ (await cli.run(`module config set ${moduleId} ${key} ${encoded}`)).expectSuccess();
296
+ }
297
+
298
+ if (options.generated) {
299
+ (await cli.run(`module generate ${moduleId}`)).expectSuccess();
300
+ }
301
+
302
+ const cleanup = async () => {
303
+ await cli.dispose();
304
+ await ctx.cleanup();
305
+ };
306
+
307
+ return { cli, moduleId, config, context: ctx, cleanup };
308
+ }
309
+
198
310
  /**
199
311
  * Caddy module fixture options
200
312
  */
@@ -1,80 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { unlink } from 'node:fs/promises';
3
- import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
4
- import { type DbClient, createDbClient, findMigrationsFolder } from '../db/client';
5
-
6
- // `findMigrationsFolder` is imported from db/client rather than redefined here.
7
- // This file used to carry its own copy, and the copy had drifted: it listed
8
- // only CWD-RELATIVE candidates (`./drizzle`, `process.cwd()/drizzle`, …) and was
9
- // missing the `join(currentDir, '../../drizzle')` fallback that resolves
10
- // relative to the source file.
11
- //
12
- // The effect was invisible from `apps/celilo` and total from the repo root —
13
- // which is where CLAUDE.md says to run the gate. Every suite using
14
- // setupTestDatabase threw here, left `db` unassigned, and then failed again in
15
- // its own afterEach on `db.$client`, so one missing path candidate presented as
16
- // hundreds of unrelated-looking assertion failures (celilo: 668 across the
17
- // repo, 341 in apps/celilo/src alone).
18
-
19
- /**
20
- * Setup test database with real migrations
21
- *
22
- * This helper ensures tests use the same schema as production by running
23
- * actual migrations instead of manual CREATE TABLE statements.
24
- *
25
- * Benefits:
26
- * - Single source of truth for schema
27
- * - Tests automatically use latest schema
28
- * - Validates migration correctness
29
- *
30
- * @param testDbPath - Path to test database file
31
- * @returns Database client instance
32
- */
33
- export async function setupTestDatabase(testDbPath: string) {
34
- // Create database client
35
- const db = createDbClient({ path: testDbPath });
36
-
37
- // Find and run migrations
38
- const migrationsFolder = findMigrationsFolder();
39
- await migrate(db, { migrationsFolder });
40
-
41
- return db;
42
- }
43
-
44
- /**
45
- * Cleanup test database
46
- *
47
- * @param db - Database client to close
48
- * @param testDbPath - Path to test database file to delete
49
- */
50
- export async function cleanupTestDatabase(db: DbClient, testDbPath: string) {
51
- // Close database connection
52
- db.$client.close();
53
-
54
- // Delete test database file
55
- if (existsSync(testDbPath)) {
56
- await unlink(testDbPath);
57
- }
58
-
59
- // Also delete WAL and SHM files if they exist
60
- const walPath = `${testDbPath}-wal`;
61
- const shmPath = `${testDbPath}-shm`;
62
-
63
- if (existsSync(walPath)) {
64
- await unlink(walPath);
65
- }
66
-
67
- if (existsSync(shmPath)) {
68
- await unlink(shmPath);
69
- }
70
- }
71
-
72
- /**
73
- * Generate unique test database path
74
- *
75
- * @param prefix - Optional prefix for the database file (defaults to 'test-celilo')
76
- * @returns Unique database file path
77
- */
78
- export function generateTestDbPath(prefix = 'test-celilo'): string {
79
- return `./${prefix}-${Date.now()}-${Math.random()}.db`;
80
- }