@celilo/cli 0.5.0-alpha.4 → 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.
Files changed (46) hide show
  1. package/drizzle/0010_dns_internal_records.sql +12 -0
  2. package/drizzle/0011_backups_name.sql +1 -0
  3. package/drizzle/meta/_journal.json +14 -0
  4. package/package.json +2 -2
  5. package/src/ansible/inventory.test.ts +10 -10
  6. package/src/ansible/validation.test.ts +25 -15
  7. package/src/cli/command-registry.ts +13 -2
  8. package/src/cli/commands/events.test.ts +4 -4
  9. package/src/cli/commands/events.ts +2 -2
  10. package/src/cli/commands/service-add-proxmox.ts +9 -0
  11. package/src/cli/commands/system-doctor.ts +135 -40
  12. package/src/cli/commands/system-migrate.test.ts +40 -0
  13. package/src/cli/commands/system-migrate.ts +65 -0
  14. package/src/cli/completion.ts +1 -0
  15. package/src/cli/index.ts +7 -2
  16. package/src/config/paths.test.ts +61 -48
  17. package/src/db/client.ts +15 -146
  18. package/src/db/migrate.ts +14 -6
  19. package/src/db/schema-introspection.ts +88 -0
  20. package/src/db/schema.ts +38 -0
  21. package/src/hooks/capability-loader-firewall.test.ts +3 -3
  22. package/src/hooks/capability-loader.ts +24 -15
  23. package/src/infrastructure/property-extractor.test.ts +15 -0
  24. package/src/infrastructure/property-extractor.ts +12 -0
  25. package/src/manifest/schema.ts +7 -0
  26. package/src/manifest/validate.test.ts +53 -0
  27. package/src/services/bus-interview.test.ts +2 -2
  28. package/src/services/bus-secret-flow.test.ts +2 -2
  29. package/src/services/celilo-mgmt-hooks.test.ts +3 -2
  30. package/src/services/deploy-preflight.ts +25 -0
  31. package/src/services/deploy-validation.test.ts +2 -2
  32. package/src/services/dns-internal-records.test.ts +126 -0
  33. package/src/services/dns-internal-records.ts +119 -0
  34. package/src/services/dns-provider-backfill.test.ts +2 -2
  35. package/src/services/dns-registrations.test.ts +10 -10
  36. package/src/services/fleet-checks.test.ts +495 -0
  37. package/src/services/fleet-checks.ts +663 -0
  38. package/src/services/module-build.test.ts +43 -38
  39. package/src/templates/generator.test.ts +62 -12
  40. package/src/templates/generator.ts +69 -50
  41. package/src/test-utils/fixtures.test.ts +1 -1
  42. package/src/test-utils/integration-guard.ts +33 -0
  43. package/src/types/infrastructure.ts +6 -0
  44. package/src/variables/computed/computed-integration.test.ts +3 -3
  45. package/src/variables/computed/computed.test.ts +5 -5
  46. package/src/variables/declarative-derivation.test.ts +6 -6
@@ -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;
@@ -29,6 +29,7 @@ import { capabilities, moduleConfigs, modules, secrets } from '../db/schema';
29
29
  import { type ModuleManifest, getSingularSystemSpec } from '../manifest/schema';
30
30
  import { buildResolutionContext } from '../variables/context';
31
31
  import { E2E_CONFLICT_FIX, runningE2eContainers } from './e2e-guard';
32
+ import { describeCapabilityProblem, findBrokenCapabilityDerivations } from './fleet-checks';
32
33
 
33
34
  export interface PreflightResult {
34
35
  success: boolean;
@@ -232,6 +233,30 @@ export async function runPreflight(
232
233
  });
233
234
  }
234
235
  }
236
+
237
+ // 4b. Capability-derived variables resolve to a concrete value. The
238
+ // scan above only catches values that ARE present-but-templated; a
239
+ // required `source: capability` var whose chain is broken upstream is
240
+ // silently DROPPED during derivation (the hasUnresolved guard), so it's
241
+ // absent from selfConfig entirely and a template `$self:<x>` later dies
242
+ // with a cryptic "not found". Assert it here against the RESOLVED
243
+ // capabilities map so the operator gets the named missing link up front
244
+ // instead of at generate time (ISS-0095 / ISS-0115).
245
+ for (const problem of findBrokenCapabilityDerivations(
246
+ moduleId,
247
+ manifest,
248
+ context.capabilities,
249
+ )) {
250
+ errors.push({
251
+ category: problem.reason === 'no-provider' ? 'missing-capability' : 'unresolved-template',
252
+ message: describeCapabilityProblem(problem),
253
+ variable: problem.variable,
254
+ suggestion:
255
+ problem.reason === 'no-provider'
256
+ ? `Deploy a module that provides '${problem.capability}', then redeploy '${moduleId}'`
257
+ : `Redeploy '${problem.capability}'s provider so it populates ${problem.capability}.${problem.path}, then redeploy '${moduleId}'`,
258
+ });
259
+ }
235
260
  } catch (error) {
236
261
  // Resolution context may fail — that's informative too
237
262
  warnings.push({
@@ -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
  {
@@ -0,0 +1,126 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import type { DnsRecordRequest } from '@celilo/capabilities';
6
+ import type { DbClient } from '../db/client';
7
+ import { modules } from '../db/schema';
8
+ import { setupTestDatabase } from '../test-utils/setup-test-db';
9
+ import {
10
+ listDnsInternalRecords,
11
+ recordDnsInternalRecord,
12
+ removeDnsInternalRecord,
13
+ withDnsInternalLedger,
14
+ } from './dns-internal-records';
15
+
16
+ describe('dns-internal-records ledger', () => {
17
+ let dir: string;
18
+ let dbPath: string;
19
+ let db: DbClient;
20
+
21
+ beforeEach(async () => {
22
+ dir = mkdtempSync(join(tmpdir(), 'dns-int-'));
23
+ dbPath = join(dir, 'celilo.db');
24
+ process.env.CELILO_DB_PATH = dbPath;
25
+ db = await setupTestDatabase(dbPath);
26
+ // FK targets: provider + consumer modules.
27
+ for (const id of ['technitium', 'forgejo']) {
28
+ db.insert(modules)
29
+ .values({
30
+ id,
31
+ name: id,
32
+ version: '1.0.0',
33
+ state: 'VERIFIED',
34
+ manifestData: {},
35
+ sourcePath: `/src/${id}`,
36
+ })
37
+ .run();
38
+ }
39
+ });
40
+ afterEach(() => {
41
+ db.$client.close();
42
+ process.env.CELILO_DB_PATH = undefined;
43
+ try {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ } catch {
46
+ /* ignore */
47
+ }
48
+ });
49
+
50
+ const ctx = () => ({ db, providerModuleId: 'technitium', consumerModuleId: 'forgejo' });
51
+
52
+ it('records, lists, and upserts by (provider, host)', () => {
53
+ recordDnsInternalRecord(db, { ...ctx(), host: 'git-ssh.x', ip: '10.0.20.14' });
54
+ let rows = listDnsInternalRecords(db);
55
+ expect(rows).toHaveLength(1);
56
+ expect(rows[0].ip).toBe('10.0.20.14');
57
+
58
+ // Same (provider, host) updates in place — the natIp fix re-registering.
59
+ recordDnsInternalRecord(db, { ...ctx(), host: 'git-ssh.x', ip: '192.168.0.253' });
60
+ rows = listDnsInternalRecords(db);
61
+ expect(rows).toHaveLength(1);
62
+ expect(rows[0].ip).toBe('192.168.0.253');
63
+ });
64
+
65
+ it('removes a record by (provider, host)', () => {
66
+ recordDnsInternalRecord(db, { ...ctx(), host: 'a.x', ip: '1.1.1.1' });
67
+ recordDnsInternalRecord(db, { ...ctx(), host: 'b.x', ip: '2.2.2.2' });
68
+ removeDnsInternalRecord(db, { providerModuleId: 'technitium', host: 'a.x' });
69
+ const rows = listDnsInternalRecords(db);
70
+ expect(rows.map((r) => r.host)).toEqual(['b.x']);
71
+ });
72
+
73
+ describe('withDnsInternalLedger', () => {
74
+ function fakeProvider() {
75
+ const calls: Array<['register' | 'delete', DnsRecordRequest]> = [];
76
+ const iface = {
77
+ async registerRecord(req: DnsRecordRequest) {
78
+ calls.push(['register', req]);
79
+ },
80
+ async deleteRecord(req: DnsRecordRequest) {
81
+ calls.push(['delete', req]);
82
+ },
83
+ };
84
+ return { iface, calls };
85
+ }
86
+
87
+ it('records A-record registrations and passes the call through', async () => {
88
+ const { iface, calls } = fakeProvider();
89
+ const wrapped = withDnsInternalLedger(iface, ctx());
90
+ await wrapped.registerRecord({ host: 'git-ssh.x', type: 'A', value: '192.168.0.253' });
91
+ expect(calls).toHaveLength(1); // underlying provider still called
92
+ const rows = listDnsInternalRecords(db);
93
+ expect(rows).toHaveLength(1);
94
+ expect(rows[0]).toMatchObject({ host: 'git-ssh.x', ip: '192.168.0.253' });
95
+ });
96
+
97
+ it('does NOT ledger non-A records', async () => {
98
+ const { iface } = fakeProvider();
99
+ const wrapped = withDnsInternalLedger(iface, ctx());
100
+ await wrapped.registerRecord({ host: 'mail.x', type: 'MX', value: 'mx.x' });
101
+ expect(listDnsInternalRecords(db)).toHaveLength(0);
102
+ });
103
+
104
+ it('removes the ledger row on A-record delete', async () => {
105
+ const { iface } = fakeProvider();
106
+ const wrapped = withDnsInternalLedger(iface, ctx());
107
+ await wrapped.registerRecord({ host: 'git-ssh.x', type: 'A', value: '192.168.0.253' });
108
+ await wrapped.deleteRecord({ host: 'git-ssh.x', type: 'A', value: '192.168.0.253' });
109
+ expect(listDnsInternalRecords(db)).toHaveLength(0);
110
+ });
111
+
112
+ it('does not write the ledger if the underlying register throws', async () => {
113
+ const iface = {
114
+ async registerRecord(): Promise<void> {
115
+ throw new Error('resolver down');
116
+ },
117
+ async deleteRecord(): Promise<void> {},
118
+ };
119
+ const wrapped = withDnsInternalLedger(iface, ctx());
120
+ await expect(
121
+ wrapped.registerRecord({ host: 'git-ssh.x', type: 'A', value: '1.2.3.4' }),
122
+ ).rejects.toThrow('resolver down');
123
+ expect(listDnsInternalRecords(db)).toHaveLength(0);
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Internal split-horizon DNS A-record ledger (designs/CELILO_DOCTOR_FLEET_DRIFT.md
3
+ * Phase 4, ISS-0094 / ISS-0111).
4
+ *
5
+ * The dns_internal capability (technitium/knot) registers records straight
6
+ * into the resolver's own DB, leaving celilo no offline record of what it
7
+ * asked to be served. This ledger — the internal-DNS sibling of
8
+ * `dns_registrations` — records every successful `registerRecord({type:'A'})`
9
+ * so `celilo system doctor` can assert, without a live resolver probe, that
10
+ * service hostnames resolve to the firewall natIp (LAN-reachable) rather than
11
+ * a zone-side container IP a LAN device can't route to.
12
+ *
13
+ * Row lifecycle is FK cascade — records die with their provider or consumer.
14
+ */
15
+
16
+ import type { DnsInternalCapability, DnsRecordRequest } from '@celilo/capabilities';
17
+ import { and, eq } from 'drizzle-orm';
18
+ import type { DbClient } from '../db/client';
19
+ import { dnsInternalRecords } from '../db/schema';
20
+
21
+ export interface DnsInternalRecordRow {
22
+ host: string;
23
+ ip: string;
24
+ providerModuleId: string;
25
+ consumerModuleId: string;
26
+ registeredAt: Date;
27
+ }
28
+
29
+ export function listDnsInternalRecords(
30
+ db: DbClient,
31
+ options: { providerModuleId?: string } = {},
32
+ ): DnsInternalRecordRow[] {
33
+ const query = db
34
+ .select({
35
+ host: dnsInternalRecords.host,
36
+ ip: dnsInternalRecords.ip,
37
+ providerModuleId: dnsInternalRecords.providerModuleId,
38
+ consumerModuleId: dnsInternalRecords.consumerModuleId,
39
+ registeredAt: dnsInternalRecords.registeredAt,
40
+ })
41
+ .from(dnsInternalRecords);
42
+ return options.providerModuleId
43
+ ? query.where(eq(dnsInternalRecords.providerModuleId, options.providerModuleId)).all()
44
+ : query.all();
45
+ }
46
+
47
+ export function recordDnsInternalRecord(
48
+ db: DbClient,
49
+ record: { providerModuleId: string; consumerModuleId: string; host: string; ip: string },
50
+ ): void {
51
+ db.insert(dnsInternalRecords)
52
+ .values({
53
+ providerModuleId: record.providerModuleId,
54
+ consumerModuleId: record.consumerModuleId,
55
+ host: record.host,
56
+ ip: record.ip,
57
+ registeredAt: new Date(),
58
+ })
59
+ .onConflictDoUpdate({
60
+ target: [dnsInternalRecords.providerModuleId, dnsInternalRecords.host],
61
+ set: {
62
+ consumerModuleId: record.consumerModuleId,
63
+ ip: record.ip,
64
+ registeredAt: new Date(),
65
+ },
66
+ })
67
+ .run();
68
+ }
69
+
70
+ export function removeDnsInternalRecord(
71
+ db: DbClient,
72
+ record: { providerModuleId: string; host: string },
73
+ ): void {
74
+ db.delete(dnsInternalRecords)
75
+ .where(
76
+ and(
77
+ eq(dnsInternalRecords.providerModuleId, record.providerModuleId),
78
+ eq(dnsInternalRecords.host, record.host),
79
+ ),
80
+ )
81
+ .run();
82
+ }
83
+
84
+ /**
85
+ * Wrap a dns_internal interface so A-record register/delete calls are
86
+ * mirrored into the ledger. Only A records are tracked — they're the
87
+ * host→IP mapping the natIp drift check reasons about; CNAMEs/others pass
88
+ * through unrecorded. A throw from the underlying call propagates before
89
+ * any ledger write, so only a confirmed register/delete earns a row change.
90
+ */
91
+ export function withDnsInternalLedger(
92
+ iface: DnsInternalCapability,
93
+ ctx: { db: DbClient; providerModuleId: string; consumerModuleId: string },
94
+ ): DnsInternalCapability {
95
+ const isA = (request: DnsRecordRequest) => request.type.toUpperCase() === 'A';
96
+ return {
97
+ ...iface,
98
+ async registerRecord(request: DnsRecordRequest): Promise<void> {
99
+ await iface.registerRecord(request);
100
+ if (isA(request)) {
101
+ recordDnsInternalRecord(ctx.db, {
102
+ providerModuleId: ctx.providerModuleId,
103
+ consumerModuleId: ctx.consumerModuleId,
104
+ host: request.host,
105
+ ip: request.value,
106
+ });
107
+ }
108
+ },
109
+ async deleteRecord(request: DnsRecordRequest): Promise<void> {
110
+ await iface.deleteRecord(request);
111
+ if (isA(request)) {
112
+ removeDnsInternalRecord(ctx.db, {
113
+ providerModuleId: ctx.providerModuleId,
114
+ host: request.host,
115
+ });
116
+ }
117
+ },
118
+ };
119
+ }
@@ -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
  });