@celilo/cli 2.2.0 → 2.2.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 (115) hide show
  1. package/package.json +2 -2
  2. package/schemas/system_config.json +5 -0
  3. package/src/ansible/inventory.test.ts +2 -1
  4. package/src/api/sessions.test.ts +2 -1
  5. package/src/cli/backup-rename.test.ts +2 -1
  6. package/src/cli/cli.test.ts +2 -1
  7. package/src/cli/commands/console-get-chain.test.ts +2 -1
  8. package/src/cli/commands/firewall-interface-list.test.ts +2 -1
  9. package/src/cli/commands/module-config.test.ts +2 -1
  10. package/src/cli/commands/module-deploy.ts +8 -2
  11. package/src/cli/commands/module-generate.test.ts +53 -0
  12. package/src/cli/commands/module-generate.ts +31 -26
  13. package/src/cli/commands/module-import-aspect.test.ts +2 -1
  14. package/src/cli/commands/module-import-registry.test.ts +2 -1
  15. package/src/cli/commands/module-operations.test.ts +2 -1
  16. package/src/cli/commands/module-publish.test.ts +5 -12
  17. package/src/cli/commands/module-update.test.ts +6 -5
  18. package/src/cli/commands/module-upgrade.test.ts +15 -0
  19. package/src/cli/commands/module-upgrade.ts +54 -2
  20. package/src/cli/commands/module-verify.test.ts +2 -3
  21. package/src/cli/commands/module-verify.ts +0 -1
  22. package/src/cli/commands/notify-config.test.ts +5 -3
  23. package/src/cli/commands/registry-owner.test.ts +2 -1
  24. package/src/cli/commands/registry-token.test.ts +2 -1
  25. package/src/cli/commands/system-apply-config-equivalence.test.ts +5 -7
  26. package/src/cli/commands/system-config.test.ts +148 -0
  27. package/src/cli/commands/system-config.ts +26 -1
  28. package/src/cli/commands/system-doctor.test.ts +71 -0
  29. package/src/cli/commands/system-doctor.ts +110 -24
  30. package/src/cli/commands/system-init-deprecation.test.ts +6 -3
  31. package/src/cli/commands/system-migrate.test.ts +2 -1
  32. package/src/cli/index.ts +2 -1
  33. package/src/cli/restore-command.test.ts +2 -1
  34. package/src/cli/restore-migration-failure.test.ts +2 -1
  35. package/src/config/paths.test.ts +3 -3
  36. package/src/db/migrate.test.ts +2 -1
  37. package/src/db/migrate.ts +16 -16
  38. package/src/hooks/capability-loader.test.ts +78 -0
  39. package/src/hooks/capability-loader.ts +37 -2
  40. package/src/hooks/executor.test.ts +87 -1
  41. package/src/hooks/executor.ts +57 -21
  42. package/src/hooks/hook-store.test.ts +6 -0
  43. package/src/hooks/hook-trespass.test.ts +17 -7
  44. package/src/hooks/jail.test.ts +61 -2
  45. package/src/hooks/jail.ts +53 -1
  46. package/src/module/packaging/audit.ts +9 -26
  47. package/src/module/packaging/build-paths.test.ts +127 -0
  48. package/src/module/packaging/build-paths.ts +175 -0
  49. package/src/module/packaging/build.test.ts +71 -1
  50. package/src/module/packaging/build.ts +60 -0
  51. package/src/module/packaging/extract.ts +1 -5
  52. package/src/policy/module-script-scan.ts +0 -40
  53. package/src/policy/no-swallowed-refusal.test.ts +9 -10
  54. package/src/services/alerting/ack.test.ts +2 -1
  55. package/src/services/alerting/cadence-migration.test.ts +3 -2
  56. package/src/services/alerting/coverage-source.test.ts +2 -1
  57. package/src/services/alerting/deferral.test.ts +2 -1
  58. package/src/services/alerting/delivery-loop.test.ts +2 -1
  59. package/src/services/alerting/deploy-hooks.test.ts +2 -1
  60. package/src/services/alerting/inbound-poller.test.ts +2 -1
  61. package/src/services/alerting/inbound.test.ts +2 -1
  62. package/src/services/alerting/notification-responder.test.ts +2 -1
  63. package/src/services/alerting/run-monitor.test.ts +2 -1
  64. package/src/services/alerting/store.test.ts +2 -1
  65. package/src/services/alerting/sweep-runner.test.ts +2 -1
  66. package/src/services/alerting/tokens.test.ts +2 -1
  67. package/src/services/aspect-approvals.test.ts +2 -1
  68. package/src/services/aspect-reconcile.test.ts +4 -3
  69. package/src/services/aspect-runner.test.ts +2 -1
  70. package/src/services/audit/module-integrity.test.ts +0 -21
  71. package/src/services/audit/module-integrity.ts +0 -14
  72. package/src/services/backup-age-agreement.test.ts +2 -1
  73. package/src/services/backup-envelope-roundtrip.test.ts +2 -1
  74. package/src/services/backup-in-flight-refusal.test.ts +2 -1
  75. package/src/services/bus-ensure-flow.test.ts +2 -1
  76. package/src/services/bus-interview-park.test.ts +2 -1
  77. package/src/services/bus-secret-flow.test.ts +2 -1
  78. package/src/services/capability-table-rows.test.ts +2 -1
  79. package/src/services/consumer-cleanup.test.ts +3 -2
  80. package/src/services/container-service.test.ts +2 -1
  81. package/src/services/control-plane-bootstrap.test.ts +2 -1
  82. package/src/services/cross-module-read.test.ts +2 -1
  83. package/src/services/deploy-validation.test.ts +3 -2
  84. package/src/services/dns-internal-records.test.ts +3 -2
  85. package/src/services/dns-provider-backfill.test.ts +2 -1
  86. package/src/services/dns-registrations.test.ts +2 -1
  87. package/src/services/ensure-interview.test.ts +3 -2
  88. package/src/services/fleet-checks.test.ts +3 -2
  89. package/src/services/fleet-key.test.ts +3 -2
  90. package/src/services/infrastructure-selector.test.ts +2 -1
  91. package/src/services/infrastructure-variable-resolver.test.ts +2 -1
  92. package/src/services/machine-pool.test.ts +2 -1
  93. package/src/services/module-config.test.ts +2 -1
  94. package/src/services/module-deploy.dns-repoint.test.ts +2 -1
  95. package/src/services/module-deploy.ts +36 -20
  96. package/src/services/module-operations.test.ts +2 -1
  97. package/src/services/module-subscriptions.test.ts +2 -1
  98. package/src/services/port-forwards.test.ts +2 -1
  99. package/src/services/programmatic-responder.aspect.test.ts +2 -1
  100. package/src/services/proxmox-reconcile.test.ts +2 -1
  101. package/src/services/restore-from-file.test.ts +3 -2
  102. package/src/services/restore-preflight.test.ts +2 -1
  103. package/src/services/secret-schema-loader.test.ts +2 -1
  104. package/src/services/ssh-key-manager.test.ts +2 -1
  105. package/src/services/static-content-converge.test.ts +5 -4
  106. package/src/services/static-content-converge.ts +33 -10
  107. package/src/services/system-state-stage.test.ts +2 -1
  108. package/src/services/trusted-sources.test.ts +3 -2
  109. package/src/services/zone-detector.test.ts +2 -1
  110. package/src/test-utils/bus-responder.ts +5 -3
  111. package/src/test-utils/db-path.ts +25 -0
  112. package/src/test-utils/integration.ts +7 -0
  113. package/src/test-utils/module-fixtures.ts +5 -6
  114. package/src/module/packaging/generated-plane.test.ts +0 -79
  115. package/src/module/packaging/generated-plane.ts +0 -134
@@ -6,6 +6,7 @@ import type { DnsRecordRequest, HookLogger } from '@celilo/capabilities';
6
6
  import { closeDb, getDb } from '../db/client';
7
7
  import { runMigrations } from '../db/migrate';
8
8
  import { capabilities, moduleConfigs, modules, webRoutes } from '../db/schema';
9
+ import { resetTestDbPath } from '../test-utils/db-path';
9
10
  import { backfillWebRouteDns } from './dns-provider-backfill';
10
11
 
11
12
  const silentLogger: HookLogger = {
@@ -26,7 +27,7 @@ describe('backfillWebRouteDns (ISS-0029)', () => {
26
27
 
27
28
  afterEach(() => {
28
29
  closeDb();
29
- process.env.CELILO_DB_PATH = undefined;
30
+ resetTestDbPath();
30
31
  try {
31
32
  rmSync(dir, { recursive: true, force: true });
32
33
  } catch {
@@ -14,6 +14,7 @@ import type { DnsRegistrarCapability } from '@celilo/capabilities';
14
14
  import { eq } from 'drizzle-orm';
15
15
  import { type DbClient, getDb } from '../db/client';
16
16
  import { modules } from '../db/schema';
17
+ import { resetTestDbPath } from '../test-utils/db-path';
17
18
  import {
18
19
  listDnsRegistrations,
19
20
  recordDnsRegistration,
@@ -44,7 +45,7 @@ describe('dns_registrations ledger', () => {
44
45
 
45
46
  afterEach(() => {
46
47
  rmSync(tempDir, { recursive: true, force: true });
47
- process.env.CELILO_DB_PATH = undefined;
48
+ resetTestDbPath();
48
49
  });
49
50
 
50
51
  test('record + list roundtrip; a re-assert is an upsert, not a second row', () => {
@@ -8,6 +8,7 @@ import { moduleConfigs, modules, secrets } from '../db/schema';
8
8
  import type { Ensure } from '../manifest/schema';
9
9
  import { decryptSecret } from '../secrets/encryption';
10
10
  import { getOrCreateMasterKey } from '../secrets/master-key';
11
+ import { resetTestDbPath } from '../test-utils/db-path';
11
12
  import {
12
13
  findEnsureOnProvider,
13
14
  interviewForEnsureInputs,
@@ -56,7 +57,7 @@ describe('interviewForEnsureInputs', () => {
56
57
 
57
58
  afterEach(() => {
58
59
  rmSync(tempDir, { recursive: true, force: true });
59
- process.env.CELILO_DB_PATH = undefined;
60
+ resetTestDbPath();
60
61
  });
61
62
 
62
63
  test('appends to array config and sets secret object key', async () => {
@@ -185,7 +186,7 @@ describe('findEnsureOnProvider', () => {
185
186
 
186
187
  afterEach(() => {
187
188
  rmSync(tempDir, { recursive: true, force: true });
188
- process.env.CELILO_DB_PATH = undefined;
189
+ resetTestDbPath();
189
190
  });
190
191
 
191
192
  test('finds ensure by id on a registered provider', () => {
@@ -14,6 +14,7 @@ import {
14
14
  } from '../db/schema';
15
15
  import type { ModuleManifest } from '../manifest/schema';
16
16
  import { setupTestDatabaseAt } from '../test-utils/database';
17
+ import { resetTestDbPath } from '../test-utils/db-path';
17
18
  import { ensureInboundSubscriber, ensureSweepSubscriber } from './alerting/monitors';
18
19
  import { ensureBackupSweepSubscriber } from './backup-sweep';
19
20
  import { getDaemonUnitPath } from './events-daemon';
@@ -269,7 +270,7 @@ describe('checkSubscribers + checkCapabilityProviders', () => {
269
270
  afterEach(() => {
270
271
  bus.close();
271
272
  db.$client.close();
272
- process.env.CELILO_DB_PATH = undefined;
273
+ resetTestDbPath();
273
274
  process.env.EVENT_BUS_DB = undefined;
274
275
  try {
275
276
  rmSync(dir, { recursive: true, force: true });
@@ -978,7 +979,7 @@ describe('runFleetChecks includes host liveness (#728)', () => {
978
979
  afterEach(() => {
979
980
  bus.close();
980
981
  db.$client.close();
981
- process.env.CELILO_DB_PATH = undefined;
982
+ resetTestDbPath();
982
983
  process.env.EVENT_BUS_DB = undefined;
983
984
  try {
984
985
  rmSync(dir, { recursive: true, force: true });
@@ -9,6 +9,7 @@ import { execFileSync } from 'node:child_process';
9
9
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
+ import { resetTestDbPath } from '../test-utils/db-path';
12
13
  import { ensureFleetKey, findFleetPrivateKey, getFleetSshDir } from './fleet-key';
13
14
 
14
15
  describe('ensureFleetKey', () => {
@@ -20,7 +21,7 @@ describe('ensureFleetKey', () => {
20
21
  });
21
22
 
22
23
  afterEach(() => {
23
- process.env.CELILO_DB_PATH = undefined;
24
+ resetTestDbPath();
24
25
  rmSync(dataDir, { recursive: true, force: true });
25
26
  });
26
27
 
@@ -61,7 +62,7 @@ describe('findFleetPrivateKey', () => {
61
62
  });
62
63
 
63
64
  afterEach(() => {
64
- process.env.CELILO_DB_PATH = undefined;
65
+ resetTestDbPath();
65
66
  process.env.HOME = realHome;
66
67
  rmSync(dataDir, { recursive: true, force: true });
67
68
  rmSync(homeDir, { recursive: true, force: true });
@@ -10,6 +10,7 @@ import { closeDb, createDbClient } from '../db/client';
10
10
  import { runMigrations } from '../db/migrate';
11
11
  import { modules } from '../db/schema';
12
12
  import type { Module } from '../db/schema';
13
+ import { resetTestDbPath } from '../test-utils/db-path';
13
14
  import { addContainerService } from './container-service';
14
15
  import { InfrastructureError, selectInfrastructure } from './infrastructure-selector';
15
16
  import { addMachine } from './machine-pool';
@@ -46,7 +47,7 @@ describe('infrastructure-selector', () => {
46
47
  }
47
48
 
48
49
  // Clear environment variables
49
- process.env.CELILO_DB_PATH = undefined;
50
+ resetTestDbPath();
50
51
  process.env.CELILO_MASTER_KEY_PATH = undefined;
51
52
  });
52
53
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '@/db/schema';
12
12
  import type { ModuleManifest } from '@/manifest/schema';
13
13
  import { and, eq } from 'drizzle-orm';
14
+ import { resetTestDbPath } from '../test-utils/db-path';
14
15
  import { resolveInfrastructureVariables } from './infrastructure-variable-resolver';
15
16
  import { upsertModuleConfig } from './module-config';
16
17
 
@@ -38,7 +39,7 @@ beforeEach(() => {
38
39
 
39
40
  afterEach(async () => {
40
41
  db.$client.close();
41
- process.env.CELILO_DB_PATH = undefined;
42
+ resetTestDbPath();
42
43
 
43
44
  // Clean up database files
44
45
  if (existsSync(TEST_DB_PATH)) {
@@ -10,6 +10,7 @@ import { eq } from 'drizzle-orm';
10
10
  import { closeDb, createDbClient, getDb } from '../db/client';
11
11
  import { runMigrations } from '../db/migrate';
12
12
  import { machines, moduleInfrastructure, modules } from '../db/schema';
13
+ import { resetTestDbPath } from '../test-utils/db-path';
13
14
  import {
14
15
  addMachine,
15
16
  getMachine,
@@ -52,7 +53,7 @@ describe('machine-pool', () => {
52
53
  }
53
54
 
54
55
  // Clear environment variables
55
- process.env.CELILO_DB_PATH = undefined;
56
+ resetTestDbPath();
56
57
  process.env.CELILO_MASTER_KEY_PATH = undefined;
57
58
  });
58
59
 
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { closeDb, createDbClient } from '../db/client';
10
10
  import { runMigrations } from '../db/migrate';
11
11
  import { modules } from '../db/schema';
12
+ import { resetTestDbPath } from '../test-utils/db-path';
12
13
  import {
13
14
  formatConfigValue,
14
15
  getAllModuleConfigValues,
@@ -59,7 +60,7 @@ describe('module-config service', () => {
59
60
  afterEach(() => {
60
61
  closeDb();
61
62
  // Clean up environment variable
62
- process.env.CELILO_DB_PATH = undefined;
63
+ resetTestDbPath();
63
64
  // Clean up test database
64
65
  rmSync(testDir, { recursive: true, force: true });
65
66
  });
@@ -9,6 +9,7 @@ import { eq } from 'drizzle-orm';
9
9
  import { type DbClient, closeDb, getDb } from '../db/client';
10
10
  import { runMigrations } from '../db/migrate';
11
11
  import { capabilities, moduleSystems, modules, systemConfig } from '../db/schema';
12
+ import { resetTestDbPath } from '../test-utils/db-path';
12
13
  import { InterviewUnansweredError } from './interview-errors';
13
14
  import { planDnsRepoint, repointDnsPrimaryForProvider } from './module-deploy';
14
15
 
@@ -49,7 +50,7 @@ describe('repointDnsPrimaryForProvider (module-orchestrator-primitives slice 3)'
49
50
 
50
51
  afterEach(() => {
51
52
  closeDb();
52
- process.env.CELILO_DB_PATH = undefined;
53
+ resetTestDbPath();
53
54
  try {
54
55
  rmSync(dir, { recursive: true, force: true });
55
56
  } catch {
@@ -4,6 +4,7 @@
4
4
  * Coordinates full module deployment workflow across all phases
5
5
  */
6
6
 
7
+ import { rmSync } from 'node:fs';
7
8
  import { join } from 'node:path';
8
9
  import { ProgressDisplay } from '@celilo/cli-display';
9
10
  import { and, eq } from 'drizzle-orm';
@@ -104,6 +105,17 @@ export interface DeployOptions {
104
105
  * this mode — that requires a real hook run.
105
106
  */
106
107
  stopAfterInterview?: boolean;
108
+ /**
109
+ * D4 (control-plane-stops-building-modules): retain the generated project
110
+ * after a successful deploy instead of deleting it. The tree is ephemeral
111
+ * by design — rendered for this deploy from the signed payload plus live
112
+ * config, used, then removed — because a persisted generated tree cannot be
113
+ * validated (only its verbatim role assets have a meaningful digest, the
114
+ * rest is rendered and SUPPOSED to differ), it holds rendered secrets, and
115
+ * it is what the next deploy would ship. This flag keeps it for deliberate
116
+ * inspection. It is always kept when the deploy fails.
117
+ */
118
+ keepGeneratedProject?: boolean;
107
119
  }
108
120
 
109
121
  /**
@@ -368,6 +380,26 @@ export async function deployModule(
368
380
  closeDeployWindows(db, moduleId, new Date());
369
381
 
370
382
  if (result.success) {
383
+ // D4 (control-plane-stops-building-modules): the generated project is
384
+ // ephemeral. It was rendered for THIS deploy from the signed payload plus
385
+ // live config; a persisted copy is a tree nothing can validate, it holds
386
+ // rendered secrets, and it is what a later deploy would ship. Now that
387
+ // the deploy that needed it is done — ansible, hooks, DNS backfill and
388
+ // aspects all ran inside the impl — it goes. Kept on failure (the impl's
389
+ // early returns) and under --keep.
390
+ if (!options.keepGeneratedProject) {
391
+ const deployed = db.select().from(modules).where(eq(modules.id, moduleId)).get();
392
+ if (deployed) {
393
+ const generatedPath = `${deployed.sourcePath}/generated`;
394
+ try {
395
+ rmSync(generatedPath, { recursive: true, force: true });
396
+ log.info(`Removed generated/ for '${moduleId}' (ephemeral by design; --keep retains it)`);
397
+ } catch (cleanupError) {
398
+ const msg = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
399
+ log.warn(`Failed to remove generated/ for '${moduleId}': ${msg}`);
400
+ }
401
+ }
402
+ }
371
403
  // A module that declares a suggested interval starts being watched on its
372
404
  // first successful deploy — watching is the default, not a thing to
373
405
  // remember.
@@ -792,26 +824,10 @@ async function deployModuleImpl(
792
824
 
793
825
  log.success('Templates generated');
794
826
 
795
- // Pre-flight: refuse before contacting any system if the generated project
796
- // does not match the module tree it was generated from (D6).
797
- //
798
- // This is the check that would have turned celilo#925 from a silent success
799
- // into a refusal at the point of the fault. A verbatim role asset that
800
- // failed to refresh is invisible downstream: `cp` reports no error, Ansible
801
- // copies the stale bytes and reports `ok`, unchanged, and the module's
802
- // version advances past code that never shipped. Local, milliseconds, no
803
- // SSH. It deliberately does NOT verify after the deploy — Ansible's
804
- // `changed` is truthful about what Ansible did, and the lie is upstream of
805
- // Ansible.
806
- const { refuseIfGeneratedIsStale } = await import('../module/packaging/generated-plane');
807
- const staleGenerated = await refuseIfGeneratedIsStale(
808
- moduleId,
809
- module.sourcePath,
810
- generatedPath,
811
- );
812
- if (staleGenerated) {
813
- return { success: false, error: staleGenerated, phases };
814
- }
827
+ // The generated-tree staleness pre-flight (D6 of module-integrity-rigor)
828
+ // retired here (D5 of control-plane-stops-building-modules): the tree is
829
+ // rendered for THIS deploy and deleted when it succeeds, so nothing
830
+ // persists that could be stale relative to what ships.
815
831
 
816
832
  // Run validate_config hook if defined (e.g., credential validation via Playwright)
817
833
  if (manifest.hooks?.validate_config) {
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { closeDb } from '../db/client';
7
7
  import { runMigrations } from '../db/migrate';
8
+ import { resetTestDbPath } from '../test-utils/db-path';
8
9
  import {
9
10
  InFlightError,
10
11
  OPERATIONS_SWEEP_PATTERN,
@@ -30,7 +31,7 @@ describe('module-operations', () => {
30
31
 
31
32
  afterEach(() => {
32
33
  closeDb();
33
- process.env.CELILO_DB_PATH = undefined;
34
+ resetTestDbPath();
34
35
  try {
35
36
  rmSync(dir, { recursive: true, force: true });
36
37
  } catch {
@@ -10,6 +10,7 @@ import { ModuleManifestSchema } from '../manifest/schema';
10
10
  import { ModuleSubscriptionSchema } from '../manifest/schema';
11
11
  import type { ModuleManifest } from '../manifest/schema';
12
12
  import { setupTestDatabaseAt as migrateDbFile } from '../test-utils/database';
13
+ import { resetTestDbPath } from '../test-utils/db-path';
13
14
  import {
14
15
  registerModuleSubscriptions,
15
16
  resolveSubscription,
@@ -341,7 +342,7 @@ describe('resyncAllSubscriptions (ISS-0088)', () => {
341
342
  });
342
343
  afterEach(() => {
343
344
  closeDb();
344
- process.env.CELILO_DB_PATH = undefined;
345
+ resetTestDbPath();
345
346
  process.env.EVENT_BUS_DB = undefined;
346
347
  try {
347
348
  rmSync(dir, { recursive: true, force: true });
@@ -7,6 +7,7 @@ import { eq } from 'drizzle-orm';
7
7
  import type { DbClient } from '../db/client';
8
8
  import { portForwards } from '../db/schema';
9
9
  import { setupTestDatabaseAt } from '../test-utils/database';
10
+ import { resetTestDbPath } from '../test-utils/db-path';
10
11
  import { deleteClaimedRows } from './capability-table-rows';
11
12
  import { buildPortForwardStore } from './port-forwards';
12
13
 
@@ -27,7 +28,7 @@ describe('port-forward store', () => {
27
28
  });
28
29
  afterEach(() => {
29
30
  db.$client.close();
30
- process.env.CELILO_DB_PATH = undefined;
31
+ resetTestDbPath();
31
32
  try {
32
33
  rmSync(dir, { recursive: true, force: true });
33
34
  } catch {
@@ -17,6 +17,7 @@ import { join } from 'node:path';
17
17
  import { defineEvents, openBus } from '@celilo/event-bus';
18
18
  import { closeDb, getDb } from '../db/client';
19
19
  import { runMigrations } from '../db/migrate';
20
+ import { resetTestDbPath } from '../test-utils/db-path';
20
21
  import { type AspectRequiredPayload, EVENT_TYPES } from './bus-interview';
21
22
  import { startProgrammaticResponder } from './programmatic-responder';
22
23
 
@@ -72,7 +73,7 @@ describe('programmatic responder — aspect.required consent (#262)', () => {
72
73
  afterEach(() => {
73
74
  closeDb();
74
75
  rmSync(dir, { recursive: true, force: true });
75
- process.env.CELILO_DB_PATH = undefined;
76
+ resetTestDbPath();
76
77
  });
77
78
 
78
79
  it('approves (consented=true) when the policy approves the module', async () => {
@@ -27,6 +27,7 @@ import {
27
27
  modules,
28
28
  } from '../db/schema';
29
29
  import type { BaseModuleAspect } from '../manifest/schema';
30
+ import { resetTestDbPath } from '../test-utils/db-path';
30
31
  import { upsertModuleConfig } from './module-config';
31
32
  import { executeProxmoxReconcile, planProxmoxReconcile } from './proxmox-reconcile';
32
33
 
@@ -153,7 +154,7 @@ describe('proxmox-reconcile', () => {
153
154
 
154
155
  afterEach(() => {
155
156
  closeDb();
156
- process.env.CELILO_DB_PATH = undefined;
157
+ resetTestDbPath();
157
158
  try {
158
159
  rmSync(dir, { recursive: true, force: true });
159
160
  } catch {
@@ -23,6 +23,7 @@ import { tmpdir } from 'node:os';
23
23
  import { join } from 'node:path';
24
24
  import { closeDb } from '../db/client';
25
25
  import { runMigrations } from '../db/migrate';
26
+ import { resetTestDbPath } from '../test-utils/db-path';
26
27
  import { applyStagedSystemFiles, restoreFromArtifactFile } from './restore-from-file';
27
28
 
28
29
  describe('applyStagedSystemFiles', () => {
@@ -44,7 +45,7 @@ describe('applyStagedSystemFiles', () => {
44
45
 
45
46
  afterEach(() => {
46
47
  closeDb();
47
- process.env.CELILO_DB_PATH = undefined;
48
+ resetTestDbPath();
48
49
  process.env.CELILO_MASTER_KEY_PATH = undefined;
49
50
  process.env.CELILO_DATA_DIR = undefined;
50
51
  try {
@@ -205,7 +206,7 @@ describe('restoreFromArtifactFile error paths', () => {
205
206
 
206
207
  afterEach(() => {
207
208
  closeDb();
208
- process.env.CELILO_DB_PATH = undefined;
209
+ resetTestDbPath();
209
210
  process.env.CELILO_DATA_DIR = undefined;
210
211
  process.env.CELILO_MASTER_KEY_PATH = undefined;
211
212
  try {
@@ -5,6 +5,7 @@ import { join } from 'node:path';
5
5
  import { closeDb, getDb } from '../db/client';
6
6
  import { runMigrations } from '../db/migrate';
7
7
  import { modules } from '../db/schema';
8
+ import { resetTestDbPath } from '../test-utils/db-path';
8
9
  import {
9
10
  NonEmptyRestoreTargetError,
10
11
  assertRestoreTargetEmpty,
@@ -23,7 +24,7 @@ describe('restore pre-flight', () => {
23
24
 
24
25
  afterEach(() => {
25
26
  closeDb();
26
- process.env.CELILO_DB_PATH = undefined;
27
+ resetTestDbPath();
27
28
  process.env.CELILO_DATA_DIR = undefined;
28
29
  try {
29
30
  rmSync(dir, { recursive: true, force: true });
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { type DbClient, getDb } from '../db/client';
6
6
  import { modules } from '../db/schema';
7
+ import { resetTestDbPath } from '../test-utils/db-path';
7
8
  import {
8
9
  type SecretsSchema,
9
10
  getAllSecretMetadata,
@@ -40,7 +41,7 @@ describe('secret-schema-loader', () => {
40
41
  afterEach(() => {
41
42
  // Clean up temp directory
42
43
  rmSync(tempDir, { recursive: true, force: true });
43
- process.env.CELILO_DB_PATH = undefined;
44
+ resetTestDbPath();
44
45
  });
45
46
 
46
47
  describe('loadSecretsSchema', () => {
@@ -8,6 +8,7 @@ import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
9
  import { closeDb } from '../db/client';
10
10
  import { runMigrations } from '../db/migrate';
11
+ import { resetTestDbPath } from '../test-utils/db-path';
11
12
  import { addMachine } from './machine-pool';
12
13
  import {
13
14
  LOCAL_MACHINE_IP,
@@ -97,7 +98,7 @@ describe('ssh-key-manager', () => {
97
98
  }
98
99
 
99
100
  // Clear environment variables
100
- process.env.CELILO_DB_PATH = undefined;
101
+ resetTestDbPath();
101
102
  process.env.CELILO_DATA_DIR = undefined;
102
103
  process.env.CELILO_MASTER_KEY_PATH = undefined;
103
104
  });
@@ -14,6 +14,7 @@ import { eq } from 'drizzle-orm';
14
14
  import type { DbClient } from '../db/client';
15
15
  import { moduleConfigs, modules, webRoutes } from '../db/schema';
16
16
  import { setupTestDatabaseAt } from '../test-utils/database';
17
+ import { resetTestDbPath } from '../test-utils/db-path';
17
18
  import {
18
19
  ansibleTreeMentions,
19
20
  buildStaticContentPlan,
@@ -64,7 +65,7 @@ describe('planStaticContent — the declared release set (D10 task 4.1)', () =>
64
65
 
65
66
  afterEach(() => {
66
67
  db.$client.close();
67
- process.env.CELILO_DB_PATH = undefined;
68
+ resetTestDbPath();
68
69
  try {
69
70
  rmSync(dir, { recursive: true, force: true });
70
71
  } catch {
@@ -177,7 +178,7 @@ describe('resolveStaticContentRetention — operator config with a default', ()
177
178
 
178
179
  afterEach(() => {
179
180
  db.$client.close();
180
- process.env.CELILO_DB_PATH = undefined;
181
+ resetTestDbPath();
181
182
  try {
182
183
  rmSync(dir, { recursive: true, force: true });
183
184
  } catch {
@@ -279,7 +280,7 @@ describe('the state web overlay (celilo#1265)', () => {
279
280
 
280
281
  afterEach(() => {
281
282
  db.$client.close();
282
- process.env.CELILO_DB_PATH = undefined;
283
+ resetTestDbPath();
283
284
  try {
284
285
  rmSync(dir, { recursive: true, force: true });
285
286
  } catch {
@@ -359,7 +360,7 @@ describe('writeStaticContentVars — lands in the generated inventory', () => {
359
360
 
360
361
  afterEach(() => {
361
362
  db.$client.close();
362
- process.env.CELILO_DB_PATH = undefined;
363
+ resetTestDbPath();
363
364
  try {
364
365
  rmSync(dir, { recursive: true, force: true });
365
366
  } catch {
@@ -23,7 +23,7 @@
23
23
  * `module verify` (celilo#1018, deliberately left to that thread).
24
24
  */
25
25
 
26
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
26
+ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
27
27
  import { mkdir, writeFile } from 'node:fs/promises';
28
28
  import { join } from 'node:path';
29
29
  import { and, eq, isNotNull } from 'drizzle-orm';
@@ -237,9 +237,10 @@ export function staticContentVarsYaml(plan: StaticContentPlan): string {
237
237
  *
238
238
  * The shared half of both converge triggers: the capability's callback adds
239
239
  * `executeAnsible` after this, and the provider's own deploy already runs it.
240
- * Writing happens AFTER the deploy's staleness check (`refuseIfGeneratedIsStale`
241
- * only inspects verbatim role assets), so an injected vars file never reads as
242
- * a stale generation.
240
+ * Nothing inspects verbatim role assets any more — the staleness pre-flight
241
+ * retired with the ephemeral generated tree (D5 of
242
+ * control-plane-stops-building-modules), so an injected vars file cannot read
243
+ * as a stale generation because nothing reads staleness.
243
244
  */
244
245
  export async function writeStaticContentVars(
245
246
  db: DbClient,
@@ -308,13 +309,31 @@ export async function convergeStaticContent(
308
309
  return { success: false, error: `Provider module '${providerModuleId}' is not installed` };
309
310
  }
310
311
  const generatedPath = join(module.sourcePath, 'generated');
312
+ // D4 (control-plane-stops-building-modules): the generated project is
313
+ // ephemeral, so a successful provider deploy deletes it and nothing is
314
+ // persisted for this converge to read. Render it here, on demand, from the
315
+ // signed payload plus resolved config — generation is fast and local. A
316
+ // render failure fails loudly with the deploy-once remedy, same as the old
317
+ // missing-project refusal did; a tree that predates the converge support is
318
+ // still refused by the static_releases check below.
311
319
  if (!existsSync(generatedPath)) {
312
- return {
313
- success: false,
314
- error:
315
- `public_web provider '${providerModuleId}' has no generated deploy project at ${generatedPath}. ` +
316
- `Run \`celilo module deploy ${providerModuleId}\` once, then retry the publish.`,
317
- };
320
+ const { generateTemplates } = await import('../templates/generator');
321
+ const rendered = await generateTemplates({
322
+ moduleId: providerModuleId,
323
+ modulePath: module.sourcePath,
324
+ outputPath: generatedPath,
325
+ db,
326
+ skipVariableValidation: false,
327
+ });
328
+ if (!rendered.success) {
329
+ return {
330
+ success: false,
331
+ error:
332
+ `public_web provider '${providerModuleId}' has no generated deploy project at ${generatedPath} ` +
333
+ `and rendering one failed: ${rendered.error ?? 'unknown error'}. ` +
334
+ `Run \`celilo module deploy ${providerModuleId}\` once, then retry the publish.`,
335
+ };
336
+ }
318
337
  }
319
338
 
320
339
  const written = await writeStaticContentVars(db, providerModuleId, generatedPath);
@@ -342,5 +361,9 @@ export async function convergeStaticContent(
342
361
  varsPath: written.varsPath,
343
362
  };
344
363
  }
364
+ // D4: the converge rendered the tree, used it, and is done with it. The same
365
+ // no-secrets-at-rest rule the deploy follows applies here; on the failure
366
+ // paths above the tree stays for inspection.
367
+ rmSync(generatedPath, { recursive: true, force: true });
345
368
  return { success: true, varsPath: written.varsPath };
346
369
  }
@@ -13,6 +13,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
13
13
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
+ import { resetTestDbPath } from '../test-utils/db-path';
16
17
  import { snapshotDatabase, stageSystemState } from './system-state-stage';
17
18
 
18
19
  describe('stageSystemState', () => {
@@ -38,7 +39,7 @@ describe('stageSystemState', () => {
38
39
 
39
40
  afterEach(() => {
40
41
  process.env.CELILO_DATA_DIR = undefined;
41
- process.env.CELILO_DB_PATH = undefined;
42
+ resetTestDbPath();
42
43
  process.env.CELILO_MASTER_KEY_PATH = undefined;
43
44
  rmSync(dataDir, { recursive: true, force: true });
44
45
  });
@@ -6,6 +6,7 @@ import type { DbClient } from '../db/client';
6
6
  import { systemConfig } from '../db/schema';
7
7
  import { loadTrustedSubnets } from '../hooks/capability-loader';
8
8
  import { setupTestDatabaseAt } from '../test-utils/database';
9
+ import { resetTestDbPath } from '../test-utils/db-path';
9
10
  import {
10
11
  buildTrustedSourceStore,
11
12
  composeTrustedSubnets,
@@ -30,7 +31,7 @@ describe('trusted-source store', () => {
30
31
  });
31
32
  afterEach(() => {
32
33
  db.$client.close();
33
- process.env.CELILO_DB_PATH = undefined;
34
+ resetTestDbPath();
34
35
  try {
35
36
  rmSync(dir, { recursive: true, force: true });
36
37
  } catch {
@@ -147,7 +148,7 @@ describe('the render input excludes registrations; the reporting view includes t
147
148
  });
148
149
  afterEach(() => {
149
150
  db.$client.close();
150
- process.env.CELILO_DB_PATH = undefined;
151
+ resetTestDbPath();
151
152
  try {
152
153
  rmSync(dir, { recursive: true, force: true });
153
154
  } catch {
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { closeDb, createDbClient } from '../db/client';
10
10
  import { runMigrations } from '../db/migrate';
11
11
  import { systemConfig } from '../db/schema';
12
+ import { resetTestDbPath } from '../test-utils/db-path';
12
13
  import { detectZoneFromIp, isValidIp } from './zone-detector';
13
14
 
14
15
  describe('zone-detector', () => {
@@ -46,7 +47,7 @@ describe('zone-detector', () => {
46
47
  }
47
48
 
48
49
  // Clear environment variables
49
- process.env.CELILO_DB_PATH = undefined;
50
+ resetTestDbPath();
50
51
  });
51
52
 
52
53
  describe('detectZoneFromIp', () => {