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

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/cli",
3
- "version": "0.5.0-alpha.5",
3
+ "version": "0.5.0-alpha.6",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,9 +52,9 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@aws-sdk/client-s3": "^3.1024.0",
55
- "@celilo/capabilities": "^0.4.1",
55
+ "@celilo/capabilities": "^0.4.2",
56
56
  "@celilo/cli-display": "^0.1.9",
57
- "@celilo/event-bus": "^0.1.6",
57
+ "@celilo/event-bus": "^0.1.7",
58
58
  "@clack/prompts": "^1.1.0",
59
59
  "ajv": "^8.18.0",
60
60
  "drizzle-orm": "^0.36.4",
@@ -61,7 +61,7 @@ describe('generateHostsIni', () => {
61
61
  },
62
62
  {
63
63
  hostname: 'dns-ext',
64
- ansibleHost: '188.166.157.2',
64
+ ansibleHost: '192.0.2.20',
65
65
  ansibleUser: 'root',
66
66
  groups: ['dns_external'],
67
67
  },
@@ -72,7 +72,7 @@ describe('generateHostsIni', () => {
72
72
  expect(result).toContain('[homebridge]');
73
73
  expect(result).toContain('[dns_external]');
74
74
  expect(result).toContain('iot ansible_host=192.168.0.110 ansible_user=root');
75
- expect(result).toContain('dns-ext ansible_host=188.166.157.2 ansible_user=root');
75
+ expect(result).toContain('dns-ext ansible_host=192.0.2.20 ansible_user=root');
76
76
  });
77
77
 
78
78
  test('includes SSH private key file path when provided', () => {
@@ -136,8 +136,8 @@ describe('generateHostVarsYaml', () => {
136
136
  test('generates YAML for arrays', () => {
137
137
  const vars = {
138
138
  zone_records: [
139
- { name: 'ns1', type: 'A', value: '188.166.157.2' },
140
- { name: 'www', type: 'A', value: '71.36.99.96' },
139
+ { name: 'ns1', type: 'A', value: '192.0.2.20' },
140
+ { name: 'www', type: 'A', value: '203.0.113.10' },
141
141
  ],
142
142
  };
143
143
 
@@ -146,9 +146,9 @@ describe('generateHostVarsYaml', () => {
146
146
  expect(result).toContain('zone_records:');
147
147
  expect(result).toContain('- name: ns1');
148
148
  expect(result).toContain('type: A');
149
- expect(result).toContain('value: 188.166.157.2');
149
+ expect(result).toContain('value: 192.0.2.20');
150
150
  expect(result).toContain('- name: www');
151
- expect(result).toContain('value: 71.36.99.96');
151
+ expect(result).toContain('value: 203.0.113.10');
152
152
  });
153
153
 
154
154
  test('generates YAML for nested objects', () => {
@@ -359,13 +359,13 @@ describe('Database integration', () => {
359
359
  );
360
360
 
361
361
  upsertModuleConfig(db, 'dns', 'zone_records', [
362
- { name: 'ns1', type: 'A', value: '188.166.157.2' },
362
+ { name: 'ns1', type: 'A', value: '192.0.2.20' },
363
363
  ]);
364
364
 
365
365
  const vars = buildHostVars('dns', db);
366
366
 
367
367
  expect(Array.isArray(vars.zone_records)).toBe(true);
368
- expect(vars.zone_records).toEqual([{ name: 'ns1', type: 'A', value: '188.166.157.2' }]);
368
+ expect(vars.zone_records).toEqual([{ name: 'ns1', type: 'A', value: '192.0.2.20' }]);
369
369
  });
370
370
  });
371
371
 
@@ -416,13 +416,13 @@ describe('Database integration', () => {
416
416
  );
417
417
 
418
418
  upsertModuleConfig(db, 'dns-external', 'hostname', 'dns-ext');
419
- upsertModuleConfig(db, 'dns-external', 'vps_ip', '188.166.157.2');
419
+ upsertModuleConfig(db, 'dns-external', 'vps_ip', '192.0.2.20');
420
420
 
421
421
  const host = extractInventoryHost('dns-external', db);
422
422
 
423
423
  expect(host).not.toBeNull();
424
424
  expect(host?.hostname).toBe('dns-ext');
425
- expect(host?.ansibleHost).toBe('188.166.157.2'); // VPS IP used directly
425
+ expect(host?.ansibleHost).toBe('192.0.2.20'); // VPS IP used directly
426
426
  expect(host?.ansibleUser).toBe('root');
427
427
  expect(host?.groups).toEqual(['dns-external']);
428
428
  });
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
+ import { skipIntegration } from '../test-utils/integration-guard';
2
3
  import {
3
4
  isAnsibleInventoryAvailable,
4
5
  isAnsibleLintAvailable,
@@ -54,17 +55,23 @@ describe('validateWithAnsibleLint', () => {
54
55
  });
55
56
 
56
57
  describe('validatePlaybookSyntax', () => {
57
- test('returns error when playbook does not exist', async () => {
58
- const result = await validatePlaybookSyntax('/nonexistent/playbook.yml', '/tmp');
59
- expect(result.success).toBe(false);
60
- expect(result.error).toContain('Playbook not found');
61
- });
58
+ test.skipIf(skipIntegration({ tools: ['ansible'] }))(
59
+ 'returns error when playbook does not exist',
60
+ async () => {
61
+ const result = await validatePlaybookSyntax('/nonexistent/playbook.yml', '/tmp');
62
+ expect(result.success).toBe(false);
63
+ expect(result.error).toContain('Playbook not found');
64
+ },
65
+ );
62
66
 
63
- test('returns error when inventory does not exist', async () => {
64
- const result = await validatePlaybookSyntax('/tmp/playbook.yml', '/nonexistent/inventory');
65
- expect(result.success).toBe(false);
66
- expect(result.error).toContain('not found');
67
- });
67
+ test.skipIf(skipIntegration({ tools: ['ansible'] }))(
68
+ 'returns error when inventory does not exist',
69
+ async () => {
70
+ const result = await validatePlaybookSyntax('/tmp/playbook.yml', '/nonexistent/inventory');
71
+ expect(result.success).toBe(false);
72
+ expect(result.error).toContain('not found');
73
+ },
74
+ );
68
75
 
69
76
  test('returns error when ansible-playbook not installed', async () => {
70
77
  if (!isAnsiblePlaybookAvailable()) {
@@ -76,11 +83,14 @@ describe('validatePlaybookSyntax', () => {
76
83
  });
77
84
 
78
85
  describe('validateInventory', () => {
79
- test('returns error when inventory does not exist', async () => {
80
- const result = await validateInventory('/nonexistent/inventory');
81
- expect(result.success).toBe(false);
82
- expect(result.error).toContain('Inventory not found');
83
- });
86
+ test.skipIf(skipIntegration({ tools: ['ansible'] }))(
87
+ 'returns error when inventory does not exist',
88
+ async () => {
89
+ const result = await validateInventory('/nonexistent/inventory');
90
+ expect(result.success).toBe(false);
91
+ expect(result.error).toContain('Inventory not found');
92
+ },
93
+ );
84
94
 
85
95
  test('returns error when ansible-inventory not installed', async () => {
86
96
  if (!isAnsibleInventoryAvailable()) {
@@ -165,20 +165,20 @@ describe('celilo events command handlers', () => {
165
165
  });
166
166
  setupBus.close();
167
167
 
168
- const res = await handleEventsReply([String(query.id), '"lunacycle.net"'], {});
168
+ const res = await handleEventsReply([String(query.id), '"example.net"'], {});
169
169
  expect(res.success).toBe(true);
170
170
  if (!res.success) throw new Error('expected success');
171
171
  const data = res.data as { status: string; family: string; value: unknown };
172
172
  expect(data.status).toBe('replied');
173
173
  expect(data.family).toBe('config');
174
- expect(data.value).toBe('lunacycle.net');
174
+ expect(data.value).toBe('example.net');
175
175
 
176
176
  const checkBus = openBus({ dbPath, events: defineEvents({}) });
177
177
  const replies = checkBus.recentEvents({ type: 'config.required.lunacycle.domain.reply' });
178
178
  checkBus.close();
179
179
  expect(replies).toHaveLength(1);
180
180
  expect(replies[0].replyFor).toBe(query.id);
181
- expect(replies[0].payload).toEqual({ value: 'lunacycle.net' });
181
+ expect(replies[0].payload).toEqual({ value: 'example.net' });
182
182
  expect(replies[0].emittedBy).toBe('claude-config-responder');
183
183
  });
184
184
 
@@ -229,7 +229,7 @@ describe('celilo events command handlers', () => {
229
229
  setupBus.close();
230
230
 
231
231
  // A bare word isn't valid JSON — the operator must quote strings.
232
- const res = await handleEventsReply([String(query.id), 'lunacycle.net'], {});
232
+ const res = await handleEventsReply([String(query.id), 'example.net'], {});
233
233
  expect(res.success).toBe(false);
234
234
  if (res.success) throw new Error('expected failure');
235
235
  expect(res.error).toContain('Invalid JSON value');
@@ -439,7 +439,7 @@ export async function handleEventsReply(
439
439
  return {
440
440
  success: false,
441
441
  error: `Usage: celilo events reply <query-event-id> <value-json>
442
- e.g. celilo events reply 42 '"lunacycle.net"'`,
442
+ e.g. celilo events reply 42 '"example.net"'`,
443
443
  };
444
444
  }
445
445
  const queryId = Number(idArg);
@@ -454,7 +454,7 @@ export async function handleEventsReply(
454
454
  return {
455
455
  success: false,
456
456
  error: `Invalid JSON value: ${err instanceof Error ? err.message : String(err)}
457
- Encode the answer as JSON, e.g. '"lunacycle.net"', '8080', '["a","b"]'.`,
457
+ Encode the answer as JSON, e.g. '"example.net"', '8080', '["a","b"]'.`,
458
458
  };
459
459
  }
460
460
 
@@ -122,6 +122,14 @@ export async function handleServiceAddProxmox(
122
122
  validate: validateRequired('Storage'),
123
123
  });
124
124
 
125
+ // VM template name for `requires.system.type: vm` modules. Optional —
126
+ // skip if you only deploy LXC modules. See v2/PROXMOX_VM_TEMPLATE.md.
127
+ const vmTemplateInput = await promptText({
128
+ message: 'VM template name (optional — for VM-type modules):',
129
+ placeholder: 'e.g., ubuntu-2404-cloud-init-9000',
130
+ });
131
+ const vmTemplate = vmTemplateInput?.trim() || undefined;
132
+
125
133
  // Find storage that supports vztmpl content. We do this BEFORE prompting
126
134
  // for a template so the user only ever sees one storage in subsequent
127
135
  // messages, and so the saved volid uses the right storage.
@@ -178,6 +186,7 @@ export async function handleServiceAddProxmox(
178
186
  default_target_node: targetNode,
179
187
  lxc_template: lxcTemplate,
180
188
  storage,
189
+ ...(vmTemplate ? { vm_template: vmTemplate } : {}),
181
190
  },
182
191
  });
183
192
 
package/src/cli/index.ts CHANGED
@@ -287,7 +287,7 @@ Examples:
287
287
  celilo events tail --type deploy.completed.lunacycle # filter by type
288
288
  celilo events emit deploy.completed.lunacycle '{}' # operator-fired event
289
289
  celilo events tail --type 'config.required.*' # see pending deploy questions
290
- celilo events reply 42 '"lunacycle.net"' # answer query #42 (config)
290
+ celilo events reply 42 '"example.net"' # answer query #42 (config)
291
291
  `;
292
292
  return { success: true, message: helpText.trim() };
293
293
  }
@@ -1091,7 +1091,7 @@ export async function runCli(argv: string[]): Promise<CommandResult> {
1091
1091
  '',
1092
1092
  'Examples:',
1093
1093
  ' celilo hook run namecheap validate_config --debug',
1094
- ' celilo hook run namecheap container_created vps_ip=138.68.140.177',
1094
+ ' celilo hook run namecheap container_created vps_ip=192.0.2.30',
1095
1095
  ].join('\n'),
1096
1096
  };
1097
1097
  }
@@ -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
  });