@celilo/cli 0.5.0-alpha.5 → 0.5.0-alpha.7

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 (33) hide show
  1. package/package.json +2 -2
  2. package/src/ansible/inventory.test.ts +10 -10
  3. package/src/ansible/validation.test.ts +25 -15
  4. package/src/api-clients/proxmox.test.ts +123 -1
  5. package/src/api-clients/proxmox.ts +292 -7
  6. package/src/cli/commands/events.test.ts +4 -4
  7. package/src/cli/commands/events.ts +2 -2
  8. package/src/cli/commands/proxmox-vm-template-build.ts +164 -0
  9. package/src/cli/commands/service-add-proxmox.ts +62 -3
  10. package/src/cli/commands/service-reconfigure.test.ts +115 -0
  11. package/src/cli/commands/service-reconfigure.ts +110 -5
  12. package/src/cli/index.ts +2 -2
  13. package/src/config/paths.test.ts +61 -48
  14. package/src/hooks/capability-loader-firewall.test.ts +3 -3
  15. package/src/infrastructure/property-extractor.test.ts +15 -0
  16. package/src/infrastructure/property-extractor.ts +12 -0
  17. package/src/manifest/schema.ts +7 -0
  18. package/src/manifest/validate.test.ts +53 -0
  19. package/src/services/bus-interview.test.ts +2 -2
  20. package/src/services/bus-secret-flow.test.ts +2 -2
  21. package/src/services/celilo-mgmt-hooks.test.ts +3 -2
  22. package/src/services/deploy-validation.test.ts +2 -2
  23. package/src/services/dns-provider-backfill.test.ts +2 -2
  24. package/src/services/dns-registrations.test.ts +10 -10
  25. package/src/services/module-build.test.ts +43 -38
  26. package/src/templates/generator.test.ts +62 -12
  27. package/src/templates/generator.ts +48 -50
  28. package/src/test-utils/fixtures.test.ts +1 -1
  29. package/src/test-utils/integration-guard.ts +33 -0
  30. package/src/types/infrastructure.ts +6 -0
  31. package/src/variables/computed/computed-integration.test.ts +3 -3
  32. package/src/variables/computed/computed.test.ts +5 -5
  33. package/src/variables/declarative-derivation.test.ts +6 -6
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
+ import { skipIntegration } from '../test-utils/integration-guard';
2
3
  import { getDataDir, getDbPath, getMasterKeyPath, getModuleStoragePath } from './paths';
3
4
 
4
5
  describe('paths configuration', () => {
@@ -34,18 +35,21 @@ describe('paths configuration', () => {
34
35
  expect(path).toMatch(/celilo-data\/modules$/);
35
36
  });
36
37
 
37
- it('returns platform-specific path by default', () => {
38
- process.env.CELILO_DATA_DIR = undefined;
39
- process.env.ENVIRONMENT = undefined;
38
+ it.skipIf(skipIntegration({ platform: 'darwin' }))(
39
+ 'returns platform-specific path by default',
40
+ () => {
41
+ process.env.CELILO_DATA_DIR = undefined;
42
+ process.env.ENVIRONMENT = undefined;
40
43
 
41
- const path = getModuleStoragePath();
44
+ const path = getModuleStoragePath();
42
45
 
43
- if (process.platform === 'darwin') {
44
- expect(path).toMatch(/Library\/Application Support\/celilo\/modules$/);
45
- } else {
46
- expect(path).toBe('/var/lib/celilo/modules');
47
- }
48
- });
46
+ if (process.platform === 'darwin') {
47
+ expect(path).toMatch(/Library\/Application Support\/celilo\/modules$/);
48
+ } else {
49
+ expect(path).toBe('/var/lib/celilo/modules');
50
+ }
51
+ },
52
+ );
49
53
 
50
54
  it('prioritizes CELILO_DATA_DIR over ENVIRONMENT=dev', () => {
51
55
  process.env.CELILO_DATA_DIR = '/custom/data';
@@ -73,18 +77,21 @@ describe('paths configuration', () => {
73
77
  expect(path).toMatch(/celilo-data$/);
74
78
  });
75
79
 
76
- it('returns platform-specific path by default', () => {
77
- process.env.CELILO_DATA_DIR = undefined;
78
- process.env.ENVIRONMENT = undefined;
79
-
80
- const path = getDataDir();
81
-
82
- if (process.platform === 'darwin') {
83
- expect(path).toMatch(/Library\/Application Support\/celilo$/);
84
- } else {
85
- expect(path).toBe('/var/lib/celilo');
86
- }
87
- });
80
+ it.skipIf(skipIntegration({ platform: 'darwin' }))(
81
+ 'returns platform-specific path by default',
82
+ () => {
83
+ process.env.CELILO_DATA_DIR = undefined;
84
+ process.env.ENVIRONMENT = undefined;
85
+
86
+ const path = getDataDir();
87
+
88
+ if (process.platform === 'darwin') {
89
+ expect(path).toMatch(/Library\/Application Support\/celilo$/);
90
+ } else {
91
+ expect(path).toBe('/var/lib/celilo');
92
+ }
93
+ },
94
+ );
88
95
  });
89
96
 
90
97
  describe('getMasterKeyPath', () => {
@@ -103,19 +110,22 @@ describe('paths configuration', () => {
103
110
  expect(path).toBe('/custom/data/master.key');
104
111
  });
105
112
 
106
- it('uses platform-specific data dir by default', () => {
107
- process.env.CELILO_MASTER_KEY_PATH = undefined;
108
- process.env.CELILO_DATA_DIR = undefined;
109
- process.env.ENVIRONMENT = undefined;
110
-
111
- const path = getMasterKeyPath();
112
-
113
- if (process.platform === 'darwin') {
114
- expect(path).toMatch(/Library\/Application Support\/celilo\/master\.key$/);
115
- } else {
116
- expect(path).toBe('/var/lib/celilo/master.key');
117
- }
118
- });
113
+ it.skipIf(skipIntegration({ platform: 'darwin' }))(
114
+ 'uses platform-specific data dir by default',
115
+ () => {
116
+ process.env.CELILO_MASTER_KEY_PATH = undefined;
117
+ process.env.CELILO_DATA_DIR = undefined;
118
+ process.env.ENVIRONMENT = undefined;
119
+
120
+ const path = getMasterKeyPath();
121
+
122
+ if (process.platform === 'darwin') {
123
+ expect(path).toMatch(/Library\/Application Support\/celilo\/master\.key$/);
124
+ } else {
125
+ expect(path).toBe('/var/lib/celilo/master.key');
126
+ }
127
+ },
128
+ );
119
129
  });
120
130
 
121
131
  describe('getDbPath', () => {
@@ -128,19 +138,22 @@ describe('paths configuration', () => {
128
138
  expect(path).toBe('/custom/path/celilo.db');
129
139
  });
130
140
 
131
- it('returns data dir + celilo.db on macOS in production', () => {
132
- process.env.CELILO_DB_PATH = undefined;
133
- process.env.CELILO_DATA_DIR = undefined;
134
- process.env.ENVIRONMENT = undefined;
135
-
136
- const path = getDbPath();
137
-
138
- if (process.platform === 'darwin') {
139
- expect(path).toMatch(/Library\/Application Support\/celilo\/celilo.db$/);
140
- } else {
141
- expect(path).toBe('/var/lib/celilo/celilo.db');
142
- }
143
- });
141
+ it.skipIf(skipIntegration({ platform: 'darwin' }))(
142
+ 'returns data dir + celilo.db on macOS in production',
143
+ () => {
144
+ process.env.CELILO_DB_PATH = undefined;
145
+ process.env.CELILO_DATA_DIR = undefined;
146
+ process.env.ENVIRONMENT = undefined;
147
+
148
+ const path = getDbPath();
149
+
150
+ if (process.platform === 'darwin') {
151
+ expect(path).toMatch(/Library\/Application Support\/celilo\/celilo.db$/);
152
+ } else {
153
+ expect(path).toBe('/var/lib/celilo/celilo.db');
154
+ }
155
+ },
156
+ );
144
157
 
145
158
  it('returns celilo-data/celilo.db in development mode', () => {
146
159
  process.env.CELILO_DB_PATH = undefined;
@@ -37,7 +37,7 @@ export default defineCapabilityFunction({
37
37
  capability: 'firewall',
38
38
  handler: ({ config, secrets }) => ({
39
39
  exposeService: async (opts) => ({
40
- externalIp: '71.36.99.96',
40
+ externalIp: '203.0.113.10',
41
41
  natIp: opts.internalIp,
42
42
  }),
43
43
  unexposeService: async () => {},
@@ -135,7 +135,7 @@ describe('Firewall Chain Building', () => {
135
135
  ports: [80],
136
136
  description: 'test',
137
137
  });
138
- expect(exposed.externalIp).toBe('71.36.99.96');
138
+ expect(exposed.externalIp).toBe('203.0.113.10');
139
139
  });
140
140
 
141
141
  test('builds chain with two providers (iptables → greenwave)', async () => {
@@ -206,7 +206,7 @@ describe('Firewall Chain Building', () => {
206
206
  });
207
207
 
208
208
  // External IP came from greenwave (leaf)
209
- expect(exposed.externalIp).toBe('71.36.99.96');
209
+ expect(exposed.externalIp).toBe('203.0.113.10');
210
210
  // NAT IP is iptables' NAT address
211
211
  expect(exposed.natIp).toBe('192.168.0.253');
212
212
  // iptables created local rules
@@ -78,6 +78,21 @@ describe('extractProxmoxProperties', () => {
78
78
  });
79
79
  });
80
80
 
81
+ test('omits vm_template when the service config has none (LXC services)', () => {
82
+ const properties = extractProxmoxProperties(100, '10.0.10.5', 'caddy', mockProxmoxConfig);
83
+ // Omitted, not empty — so a required-infrastructure `vm_template` var fails
84
+ // loudly rather than resolving to "".
85
+ expect('vm_template' in properties).toBe(false);
86
+ });
87
+
88
+ test('includes vm_template when the service config provides one (VM services)', () => {
89
+ const properties = extractProxmoxProperties(100, '10.0.10.5', 'builder', {
90
+ ...mockProxmoxConfig,
91
+ vm_template: 'ubuntu-2204-cloudinit',
92
+ });
93
+ expect(properties.vm_template).toBe('ubuntu-2204-cloudinit');
94
+ });
95
+
81
96
  test('converts vmid number to string', () => {
82
97
  const properties = extractProxmoxProperties(12345, '10.0.20.15', 'caddy', mockProxmoxConfig);
83
98
 
@@ -47,6 +47,13 @@ export interface ProxmoxProviderConfig {
47
47
  default_target_node: string;
48
48
  lxc_template: string;
49
49
  storage: string;
50
+ /**
51
+ * Cloud-init VM template to clone for `requires.system.type: vm` modules — the
52
+ * VM analogue of `lxc_template`. Optional: only Proxmox services that host VM
53
+ * modules configure it. A VM module declares `vm_template` as a *required*
54
+ * infrastructure var, so a service missing it fails loudly at resolution.
55
+ */
56
+ vm_template?: string;
50
57
  }
51
58
 
52
59
  /**
@@ -72,6 +79,11 @@ export function extractProxmoxProperties(
72
79
  target_node: providerConfig.default_target_node,
73
80
  lxc_template: providerConfig.lxc_template,
74
81
  storage: providerConfig.storage,
82
+ // VM clone source — present only when the service configures it. VM modules
83
+ // declare `vm_template` as a required infrastructure var (resolution errors
84
+ // if absent); LXC modules never reference it. Omitted (not empty) when unset
85
+ // so the resolver's required/optional handling stays correct.
86
+ ...(providerConfig.vm_template ? { vm_template: providerConfig.vm_template } : {}),
75
87
  };
76
88
  }
77
89
 
@@ -311,6 +311,13 @@ export const SystemResourceSchema = z.object({
311
311
  memory: z.number().int().positive().optional().describe('Recommended memory in MB'),
312
312
  disk: z.number().int().positive().optional().describe('Recommended disk size in GB'),
313
313
  storage: z.string().optional().describe('Proxmox storage backend (defaults to system config)'),
314
+ type: z
315
+ .enum(['lxc', 'vm'])
316
+ .default('lxc')
317
+ .describe(
318
+ 'Proxmox provisioning type: lxc (default) or vm (qemu, for Docker / kernel-module workloads). ' +
319
+ 'Modules declare this explicitly; celilo never infers it. Moot for machine-pool / external infra.',
320
+ ),
314
321
  zone: z
315
322
  .enum(['internal', 'dmz', 'app', 'secure', 'external'])
316
323
  .describe('Required security zone for this module'),
@@ -77,6 +77,59 @@ variables:
77
77
  }
78
78
  });
79
79
 
80
+ test('requires.system.type defaults to lxc when omitted', () => {
81
+ const yaml = `
82
+ ${CONTRACT_LINE}
83
+ id: homebridge
84
+ name: Homebridge
85
+ version: 1.0.0
86
+ requires:
87
+ system:
88
+ cpu: 1
89
+ zone: app
90
+ `;
91
+ const result = validateManifest(yaml);
92
+ expect(result.success).toBe(true);
93
+ if (result.success) {
94
+ expect(result.data.requires.system?.type).toBe('lxc');
95
+ }
96
+ });
97
+
98
+ test('requires.system.type accepts an explicit vm', () => {
99
+ const yaml = `
100
+ ${CONTRACT_LINE}
101
+ id: forgejo-runner
102
+ name: Forgejo Runner
103
+ version: 1.0.0
104
+ requires:
105
+ system:
106
+ cpu: 4
107
+ memory: 8192
108
+ type: vm
109
+ zone: dmz
110
+ `;
111
+ const result = validateManifest(yaml);
112
+ expect(result.success).toBe(true);
113
+ if (result.success) {
114
+ expect(result.data.requires.system?.type).toBe('vm');
115
+ }
116
+ });
117
+
118
+ test('requires.system.type rejects an unknown infra type', () => {
119
+ const yaml = `
120
+ ${CONTRACT_LINE}
121
+ id: homebridge
122
+ name: Homebridge
123
+ version: 1.0.0
124
+ requires:
125
+ system:
126
+ type: container
127
+ zone: app
128
+ `;
129
+ const result = validateManifest(yaml);
130
+ expect(result.success).toBe(false);
131
+ });
132
+
80
133
  test('should validate dns-external manifest with capability provider', () => {
81
134
  const yaml = `
82
135
  ${CONTRACT_LINE}
@@ -42,7 +42,7 @@ describe('busInterview', () => {
42
42
  const watch = responderBus.watch('config.required.lunacycle.domain', (event) => {
43
43
  responderBus.emitRaw(
44
44
  `${event.type}.reply`,
45
- { value: 'lunacycle.net' },
45
+ { value: 'example.net' },
46
46
  { replyFor: event.id, emittedBy: 'test-responder' },
47
47
  );
48
48
  });
@@ -58,7 +58,7 @@ describe('busInterview', () => {
58
58
  payload,
59
59
  );
60
60
 
61
- expect(reply.value).toBe('lunacycle.net');
61
+ expect(reply.value).toBe('example.net');
62
62
 
63
63
  watch.close();
64
64
  responderBus.close();
@@ -350,7 +350,7 @@ describe('bus-mediated interviewForMissingSecrets', () => {
350
350
  const { seen, close } = startTestResponder(
351
351
  bus,
352
352
  'secret.required.testmod.ddns_passwords',
353
- { value: JSON.stringify({ 'lunacycle.net': 'pw1', 'celilo.computer': 'pw2' }) },
353
+ { value: JSON.stringify({ 'example.net': 'pw1', 'celilo.computer': 'pw2' }) },
354
354
  testDb,
355
355
  );
356
356
 
@@ -380,7 +380,7 @@ describe('bus-mediated interviewForMissingSecrets', () => {
380
380
 
381
381
  const masterKey = await getOrCreateMasterKey();
382
382
  const stored = await readModuleSecretKey('testmod', 'ddns_passwords', testDb, masterKey);
383
- expect(stored).toBe(JSON.stringify({ 'lunacycle.net': 'pw1', 'celilo.computer': 'pw2' }));
383
+ expect(stored).toBe(JSON.stringify({ 'example.net': 'pw1', 'celilo.computer': 'pw2' }));
384
384
 
385
385
  close();
386
386
  });
@@ -18,6 +18,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
18
18
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
19
19
  import { tmpdir } from 'node:os';
20
20
  import { join } from 'node:path';
21
+ import { skipIntegration } from '../test-utils/integration-guard';
21
22
 
22
23
  import type { HookContext } from '@celilo/capabilities';
23
24
 
@@ -77,7 +78,7 @@ function buildContext(extras: Record<string, unknown>): HookContext {
77
78
  } as HookContext;
78
79
  }
79
80
 
80
- describe('celilo-mgmt on_backup', () => {
81
+ describe.skipIf(skipIntegration({ tools: ['wg'] }))('celilo-mgmt on_backup', () => {
81
82
  let dir: string;
82
83
  let backupDir: string;
83
84
  let crossModuleRoot: string;
@@ -214,7 +215,7 @@ describe('celilo-mgmt on_backup', () => {
214
215
  });
215
216
  });
216
217
 
217
- describe('celilo-mgmt on_restore', () => {
218
+ describe.skipIf(skipIntegration({ tools: ['wg'] }))('celilo-mgmt on_restore', () => {
218
219
  let dir: string;
219
220
  let restoreDir: string;
220
221
  let crossModuleWriteRoot: string;
@@ -265,8 +265,8 @@ describe('findMissingSecrets (shared)', () => {
265
265
  // The terminal-responder reads these off the bus payload to apply
266
266
  // input-time regex validation. Without manifest → MissingVariable
267
267
  // propagation, the responder never sees them and operators end
268
- // up entering invalid keys (e.g. 'www.lunacycle.net' instead of
269
- // the apex 'lunacycle.net').
268
+ // up entering invalid keys (e.g. 'www.example.net' instead of
269
+ // the apex 'example.net').
270
270
  const missing = await findMissingSecrets(
271
271
  'testmod',
272
272
  {
@@ -105,14 +105,14 @@ describe('backfillWebRouteDns (ISS-0029)', () => {
105
105
  seedFirewall('100.64.0.1');
106
106
  seedRoute('apt.celilo.computer', '/');
107
107
  seedRoute('apt.celilo.computer', '/-/publish'); // same host, different path → deduped
108
- seedRoute('registry.lunacycle.net', '/');
108
+ seedRoute('registry.example.net', '/');
109
109
 
110
110
  const calls: DnsRecordRequest[] = [];
111
111
  await backfillWebRouteDns('technitium', getDb(), silentLogger, stubLoader(calls));
112
112
 
113
113
  expect(calls.map((c) => c.host).sort()).toEqual([
114
114
  'apt.celilo.computer',
115
- 'registry.lunacycle.net',
115
+ 'registry.example.net',
116
116
  ]);
117
117
  expect(calls.every((c) => c.value === '100.64.0.1' && c.type === 'A')).toBe(true);
118
118
  });
@@ -50,19 +50,19 @@ describe('dns_registrations ledger', () => {
50
50
  recordDnsRegistration(db, {
51
51
  providerModuleId: 'namecheap',
52
52
  consumerModuleId: 'caddy',
53
- fqdn: 'www.lunacycle.net',
53
+ fqdn: 'www.example.net',
54
54
  ip: '198.51.100.7',
55
55
  });
56
56
  recordDnsRegistration(db, {
57
57
  providerModuleId: 'namecheap',
58
58
  consumerModuleId: 'caddy',
59
- fqdn: 'www.lunacycle.net',
59
+ fqdn: 'www.example.net',
60
60
  ip: '198.51.100.8',
61
61
  });
62
62
 
63
63
  const rows = listDnsRegistrations(db, { providerModuleId: 'namecheap' });
64
64
  expect(rows.length).toBe(1);
65
- expect(rows[0].fqdn).toBe('www.lunacycle.net');
65
+ expect(rows[0].fqdn).toBe('www.example.net');
66
66
  expect(rows[0].ip).toBe('198.51.100.8');
67
67
  expect(rows[0].consumerModuleId).toBe('caddy');
68
68
  expect(rows[0].refreshedAt).toBeNull();
@@ -72,7 +72,7 @@ describe('dns_registrations ledger', () => {
72
72
  recordDnsRegistration(db, {
73
73
  providerModuleId: 'namecheap',
74
74
  consumerModuleId: 'caddy',
75
- fqdn: 'git.lunacycle.net',
75
+ fqdn: 'git.example.net',
76
76
  ip: null,
77
77
  });
78
78
  stampDnsRegistrationsRefreshed(db, 'namecheap');
@@ -84,7 +84,7 @@ describe('dns_registrations ledger', () => {
84
84
  recordDnsRegistration(db, {
85
85
  providerModuleId: 'namecheap',
86
86
  consumerModuleId: 'caddy',
87
- fqdn: 'www.lunacycle.net',
87
+ fqdn: 'www.example.net',
88
88
  ip: '198.51.100.7',
89
89
  });
90
90
  db.delete(modules).where(eq(modules.id, 'caddy')).run();
@@ -106,15 +106,15 @@ describe('dns_registrations ledger', () => {
106
106
  consumerModuleId: 'caddy',
107
107
  });
108
108
 
109
- await wrapped.registerHost({ fqdn: 'www.lunacycle.net', ip: '198.51.100.7' });
110
- await wrapped.registerHost({ fqdn: 'fail.lunacycle.net', ip: '198.51.100.7' });
111
- await wrapped.registerHost({ fqdn: 'auto.lunacycle.net' });
109
+ await wrapped.registerHost({ fqdn: 'www.example.net', ip: '198.51.100.7' });
110
+ await wrapped.registerHost({ fqdn: 'fail.example.net', ip: '198.51.100.7' });
111
+ await wrapped.registerHost({ fqdn: 'auto.example.net' });
112
112
 
113
113
  expect(calls.length).toBe(3);
114
114
  const rows = listDnsRegistrations(db);
115
115
  const fqdns = rows.map((r) => r.fqdn).sort();
116
- expect(fqdns).toEqual(['auto.lunacycle.net', 'www.lunacycle.net']);
117
- const auto = rows.find((r) => r.fqdn === 'auto.lunacycle.net');
116
+ expect(fqdns).toEqual(['auto.example.net', 'www.example.net']);
117
+ const auto = rows.find((r) => r.fqdn === 'auto.example.net');
118
118
  expect(auto?.ip).toBeNull();
119
119
  });
120
120
  });
@@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm';
5
5
  import { log } from '../cli/prompts';
6
6
  import { type DbClient, createDbClient } from '../db/client';
7
7
  import { moduleBuilds, modules } from '../db/schema';
8
+ import { skipIntegration } from '../test-utils/integration-guard';
8
9
  import { buildModuleFromSource, getModuleBuildStatus, verifyArtifactsExist } from './module-build';
9
10
 
10
11
  const TEST_DB_PATH = './test-module-build.db';
@@ -156,12 +157,14 @@ describe('Module Build Service', () => {
156
157
  // We're just testing that detection happens (message logged)
157
158
  }, 10000); // 10 second timeout
158
159
 
159
- test('should record build metadata in database', async () => {
160
- await mkdir(TEST_MODULE_DIR, { recursive: true });
161
- await mkdir(`${TEST_MODULE_DIR}/build`, { recursive: true });
162
- await writeFile(
163
- `${TEST_MODULE_DIR}/build/playbook.yml`,
164
- `---
160
+ test.skipIf(skipIntegration({ tools: ['ansible'] }))(
161
+ 'should record build metadata in database',
162
+ async () => {
163
+ await mkdir(TEST_MODULE_DIR, { recursive: true });
164
+ await mkdir(`${TEST_MODULE_DIR}/build`, { recursive: true });
165
+ await writeFile(
166
+ `${TEST_MODULE_DIR}/build/playbook.yml`,
167
+ `---
165
168
  - name: Quick test build
166
169
  hosts: localhost
167
170
  gather_facts: false
@@ -170,43 +173,45 @@ describe('Module Build Service', () => {
170
173
  ansible.builtin.debug:
171
174
  msg: "Build test"
172
175
  `,
173
- );
176
+ );
174
177
 
175
- db.insert(modules)
176
- .values({
177
- id: 'record-test',
178
- name: 'Record Test',
179
- version: '1.0.0',
180
- sourcePath: TEST_MODULE_DIR,
181
- manifestData: {
178
+ db.insert(modules)
179
+ .values({
182
180
  id: 'record-test',
183
181
  name: 'Record Test',
184
182
  version: '1.0.0',
185
- build: {
186
- script: 'build/playbook.yml',
183
+ sourcePath: TEST_MODULE_DIR,
184
+ manifestData: {
185
+ id: 'record-test',
186
+ name: 'Record Test',
187
+ version: '1.0.0',
188
+ build: {
189
+ script: 'build/playbook.yml',
190
+ },
187
191
  },
188
- },
189
- })
190
- .run();
191
-
192
- const result = await buildModuleFromSource('record-test', db);
193
-
194
- // Build should succeed (simple debug task)
195
- expect(result.success).toBe(true);
196
-
197
- // Verify build metadata was recorded
198
- const buildRecord = await db
199
- .select()
200
- .from(moduleBuilds)
201
- .where(eq(moduleBuilds.moduleId, 'record-test'))
202
- .get();
203
-
204
- expect(buildRecord).toBeDefined();
205
- expect(buildRecord?.moduleId).toBe('record-test');
206
- expect(buildRecord?.version).toBe('1.0.0');
207
- expect(buildRecord?.status).toBe('success');
208
- expect(buildRecord?.environment).toBe('system'); // No flake.nix
209
- }, 10000); // 10 second timeout for ansible execution
192
+ })
193
+ .run();
194
+
195
+ const result = await buildModuleFromSource('record-test', db);
196
+
197
+ // Build should succeed (simple debug task)
198
+ expect(result.success).toBe(true);
199
+
200
+ // Verify build metadata was recorded
201
+ const buildRecord = await db
202
+ .select()
203
+ .from(moduleBuilds)
204
+ .where(eq(moduleBuilds.moduleId, 'record-test'))
205
+ .get();
206
+
207
+ expect(buildRecord).toBeDefined();
208
+ expect(buildRecord?.moduleId).toBe('record-test');
209
+ expect(buildRecord?.version).toBe('1.0.0');
210
+ expect(buildRecord?.status).toBe('success');
211
+ expect(buildRecord?.environment).toBe('system'); // No flake.nix
212
+ },
213
+ 10000,
214
+ ); // 10 second timeout for ansible execution
210
215
 
211
216
  // Regression: a build.command that references $CELILO_MODULE_SOURCE_DIR
212
217
  // (the variable celilo-registry's manifest uses to find sibling packages