@serfab/cadre-host 0.12.0 → 1.0.0

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 (70) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/host.js +9 -1
  3. package/dist/bin/host.js.map +1 -1
  4. package/dist/donation/__tests__/fake-orchestrator.js +1 -1
  5. package/dist/donation/__tests__/fake-orchestrator.js.map +1 -1
  6. package/dist/donation/donation-service.d.ts +7 -2
  7. package/dist/donation/donation-service.js +16 -2
  8. package/dist/donation/donation-service.js.map +1 -1
  9. package/dist/donation/donation-supervisor.d.ts +12 -2
  10. package/dist/donation/donation-supervisor.js +25 -2
  11. package/dist/donation/donation-supervisor.js.map +1 -1
  12. package/dist/donation/types.d.ts +2 -1
  13. package/dist/donation/types.js.map +1 -1
  14. package/dist/installer/index.d.ts +9 -1
  15. package/dist/installer/index.js +5 -1
  16. package/dist/installer/index.js.map +1 -1
  17. package/dist/orchestrator/host-process-orchestrator.d.ts +71 -11
  18. package/dist/orchestrator/host-process-orchestrator.js +173 -75
  19. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  20. package/dist/orchestrator/port-allocator.d.ts +44 -2
  21. package/dist/orchestrator/port-allocator.js +63 -5
  22. package/dist/orchestrator/port-allocator.js.map +1 -1
  23. package/dist/orchestrator/port-probe.d.ts +15 -0
  24. package/dist/orchestrator/port-probe.js +22 -0
  25. package/dist/orchestrator/port-probe.js.map +1 -0
  26. package/dist/orchestrator/types.d.ts +13 -1
  27. package/dist/orchestrator/types.js.map +1 -1
  28. package/dist/server/routes/bootstrap-node-validation.d.ts +115 -0
  29. package/dist/server/routes/bootstrap-node-validation.js +197 -0
  30. package/dist/server/routes/bootstrap-node-validation.js.map +1 -0
  31. package/dist/server/routes/grants.js +8 -25
  32. package/dist/server/routes/grants.js.map +1 -1
  33. package/dist/server/routes/provision-request-validation.d.ts +45 -0
  34. package/dist/server/routes/provision-request-validation.js +97 -0
  35. package/dist/server/routes/provision-request-validation.js.map +1 -0
  36. package/dist/server/server.d.ts +4 -1
  37. package/dist/server/server.js +36 -2
  38. package/dist/server/server.js.map +1 -1
  39. package/dist/ui/assets/{index-C-iks41M.js → index-CEoNRwM_.js} +3 -3
  40. package/dist/ui/assets/{index-C-iks41M.js.map → index-CEoNRwM_.js.map} +1 -1
  41. package/dist/ui/index.html +1 -1
  42. package/package.json +5 -4
  43. package/src/__tests__/orchestrator-env-scrub.test.ts +22 -0
  44. package/src/__tests__/orchestrator-node-identity.test.ts +4 -4
  45. package/src/__tests__/orchestrator-owner.test.ts +45 -2
  46. package/src/__tests__/orchestrator-ports.test.ts +172 -0
  47. package/src/__tests__/orchestrator.test.ts +1062 -951
  48. package/src/bin/host.ts +9 -1
  49. package/src/donation/__tests__/donation-service.test.ts +94 -7
  50. package/src/donation/__tests__/donation-supervisor.test.ts +50 -12
  51. package/src/donation/__tests__/fake-orchestrator.test.ts +168 -168
  52. package/src/donation/__tests__/fake-orchestrator.ts +1 -1
  53. package/src/donation/donation-service.ts +22 -3
  54. package/src/donation/donation-supervisor.ts +25 -2
  55. package/src/donation/types.ts +2 -1
  56. package/src/installer/__tests__/installer.smoke.test.ts +263 -218
  57. package/src/installer/index.ts +327 -314
  58. package/src/orchestrator/host-process-orchestrator.ts +1250 -1138
  59. package/src/orchestrator/port-allocator.ts +148 -91
  60. package/src/orchestrator/port-probe.ts +31 -0
  61. package/src/orchestrator/types.ts +13 -1
  62. package/src/server/__tests__/bootstrap-node-validation.test.ts +138 -0
  63. package/src/server/__tests__/grants-route.test.ts +394 -263
  64. package/src/server/__tests__/nodes-route.test.ts +2 -2
  65. package/src/server/__tests__/publishers.test.ts +1 -1
  66. package/src/server/__tests__/status-route.test.ts +1 -1
  67. package/src/server/routes/bootstrap-node-validation.ts +206 -0
  68. package/src/server/routes/grants.ts +9 -26
  69. package/src/server/routes/provision-request-validation.ts +105 -0
  70. package/src/server/server.ts +157 -122
@@ -1,218 +1,263 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
- import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
-
6
- import { Installer } from '../index.js';
7
- import type { ServiceHost, ServiceHostContext, ServiceHostStatus } from '../service-host/types.js';
8
-
9
- class FakeServiceHost implements ServiceHost {
10
- readonly name = 'cadre-host';
11
- installCalls: ServiceHostContext[] = [];
12
- uninstallCalls: ServiceHostContext[] = [];
13
- installed = false;
14
- running = false;
15
-
16
- async install(ctx: ServiceHostContext): Promise<void> {
17
- this.installCalls.push(ctx);
18
- this.installed = true;
19
- this.running = true;
20
- }
21
- async uninstall(ctx: ServiceHostContext): Promise<void> {
22
- this.uninstallCalls.push(ctx);
23
- this.installed = false;
24
- this.running = false;
25
- }
26
- async restart(_ctx: ServiceHostContext): Promise<void> {
27
- this.running = true;
28
- }
29
- async status(_ctx: ServiceHostContext): Promise<ServiceHostStatus> {
30
- return { installed: this.installed, running: this.running };
31
- }
32
- renderUnit(_ctx: ServiceHostContext): string | null {
33
- return 'rendered-unit';
34
- }
35
- }
36
-
37
- describe('Installer smoke', () => {
38
- let tmp: string;
39
-
40
- beforeEach(() => {
41
- tmp = mkdtempSync(join(tmpdir(), 'cadre-host-installer-smoke-'));
42
- });
43
-
44
- afterEach(() => {
45
- rmSync(tmp, { recursive: true, force: true });
46
- });
47
-
48
- it('install creates config + identity + invokes service-host', async () => {
49
- const fake = new FakeServiceHost();
50
- const installer = new Installer({ platform: 'linux', installerVersion: 'test-1.0.0' });
51
- const result = await installer.install({
52
- nonInteractive: true,
53
- dataDir: tmp,
54
- uiPort: 19999,
55
- libp2pPort: 14001,
56
- openBrowser: false,
57
- noInvite: true,
58
- serviceHost: fake,
59
- });
60
-
61
- expect(result.dataDir).toBe(tmp);
62
- expect(result.uiUrl).toBe('http://127.0.0.1:19999/');
63
- expect(result.serviceName).toBe('cadre-host');
64
- expect(result.configPath).toBe(join(tmp, 'host.config.json'));
65
-
66
- expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
67
- expect(existsSync(join(tmp, 'identity.key'))).toBe(true);
68
- expect(existsSync(join(tmp, 'nat.json'))).toBe(true);
69
- expect(existsSync(join(tmp, 'logs'))).toBe(true);
70
-
71
- expect(fake.installCalls).toHaveLength(1);
72
- expect(fake.installCalls[0]!.dataDir).toBe(tmp);
73
- });
74
-
75
- it('uninstall removes data only when --remove-data is set', async () => {
76
- const fake = new FakeServiceHost();
77
- const installer = new Installer({ platform: 'linux' });
78
- await installer.install({
79
- nonInteractive: true,
80
- dataDir: tmp,
81
- uiPort: 19998,
82
- libp2pPort: 14002,
83
- openBrowser: false,
84
- noInvite: true,
85
- serviceHost: fake,
86
- });
87
- expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
88
-
89
- await installer.uninstall({ yes: true, removeData: false, dataDir: tmp, serviceHost: fake });
90
- expect(fake.uninstallCalls).toHaveLength(1);
91
- expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
92
-
93
- await installer.uninstall({ yes: true, removeData: true, dataDir: tmp, serviceHost: fake });
94
- expect(existsSync(tmp)).toBe(false);
95
- });
96
-
97
- it('rejects --system in v1', async () => {
98
- const fake = new FakeServiceHost();
99
- const installer = new Installer({ platform: 'linux' });
100
- await expect(installer.install({
101
- nonInteractive: true,
102
- dataDir: tmp,
103
- system: true,
104
- serviceHost: fake,
105
- openBrowser: false,
106
- noInvite: true,
107
- })).rejects.toThrow(/system.*not yet supported/);
108
- expect(fake.installCalls).toHaveLength(0);
109
- });
110
-
111
- it('passes CLI overrides to the wizard as suggested defaults', async () => {
112
- // Regression: the interactive wizard previously showed the platform
113
- // default (`~/.local/share/cadre-host`) in the prompt even when the
114
- // caller passed `--data-dir <path>`. Whatever the user typed was then
115
- // silently discarded because the CLI flag won post-hoc. The fix: CLI
116
- // overrides become the wizard's suggested defaults, and the wizard's
117
- // returned answers stand on their own.
118
- const fake = new FakeServiceHost();
119
- const installer = new Installer({ platform: 'linux' });
120
- let seenDefaults: { dataDir: string; uiPort: number; libp2pPort: number; upnpEnabled: boolean; ownCadre: boolean } | undefined;
121
- await installer.install({
122
- nonInteractive: false,
123
- dataDir: tmp,
124
- uiPort: 19997,
125
- libp2pPort: 14003,
126
- noUpnp: true,
127
- openBrowser: false,
128
- noInvite: true,
129
- serviceHost: fake,
130
- wizard: async (defaults) => {
131
- seenDefaults = { ...defaults };
132
- // Simulate the user pressing Enter at every prompt.
133
- return {
134
- dataDir: defaults.dataDir,
135
- uiPort: defaults.uiPort,
136
- libp2pPort: defaults.libp2pPort,
137
- upnpEnabled: defaults.upnpEnabled,
138
- configureDdns: false,
139
- ownCadre: defaults.ownCadre,
140
- };
141
- },
142
- });
143
-
144
- expect(seenDefaults).toEqual({
145
- dataDir: tmp,
146
- uiPort: 19997,
147
- libp2pPort: 14003,
148
- upnpEnabled: false,
149
- ownCadre: false,
150
- });
151
- expect(fake.installCalls).toHaveLength(1);
152
- expect(fake.installCalls[0]!.dataDir).toBe(tmp);
153
- });
154
-
155
- it('honors wizard-typed input when no CLI override is passed', async () => {
156
- // Mirror of the previous test: with no `--data-dir`, the wizard's
157
- // returned value is what the installer uses.
158
- const fake = new FakeServiceHost();
159
- const installer = new Installer({ platform: 'linux' });
160
- await installer.install({
161
- nonInteractive: false,
162
- openBrowser: false,
163
- noInvite: true,
164
- serviceHost: fake,
165
- wizard: async () => ({
166
- dataDir: tmp,
167
- uiPort: 19996,
168
- libp2pPort: 14004,
169
- upnpEnabled: true,
170
- configureDdns: false,
171
- ownCadre: false,
172
- }),
173
- });
174
- expect(fake.installCalls).toHaveLength(1);
175
- expect(fake.installCalls[0]!.dataDir).toBe(tmp);
176
- });
177
-
178
- it('writes ownCadre.enabled=false by default and true when opted in', async () => {
179
- const { readHostConfig } = await import('../config.js');
180
-
181
- // Default (donor-only): --own-cadre absent → ownCadre.enabled false.
182
- const fakeA = new FakeServiceHost();
183
- const installerA = new Installer({ platform: 'linux' });
184
- await installerA.install({
185
- nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fakeA,
186
- });
187
- const donorCfg = readHostConfig(join(tmp, 'host.config.json'));
188
- expect(donorCfg.ownCadre).toEqual({ enabled: false });
189
-
190
- // Opt-in: ownCadre:true (the --own-cadre flag) → ownCadre.enabled true.
191
- const tmp2 = mkdtempSync(join(tmpdir(), 'cadre-host-installer-owncadre-'));
192
- try {
193
- const fakeB = new FakeServiceHost();
194
- const installerB = new Installer({ platform: 'linux' });
195
- await installerB.install({
196
- nonInteractive: true, dataDir: tmp2, ownCadre: true, openBrowser: false, noInvite: true, serviceHost: fakeB,
197
- });
198
- const founderCfg = readHostConfig(join(tmp2, 'host.config.json'));
199
- expect(founderCfg.ownCadre).toEqual({ enabled: true });
200
- } finally {
201
- rmSync(tmp2, { recursive: true, force: true });
202
- }
203
- });
204
-
205
- it('re-running install preserves the existing identity', async () => {
206
- const fake = new FakeServiceHost();
207
- const installer = new Installer({ platform: 'linux' });
208
- await installer.install({
209
- nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fake,
210
- });
211
- const idBefore = (await import('node:fs')).readFileSync(join(tmp, 'identity.key'));
212
- await installer.install({
213
- nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fake,
214
- });
215
- const idAfter = (await import('node:fs')).readFileSync(join(tmp, 'identity.key'));
216
- expect(Buffer.from(idBefore).equals(Buffer.from(idAfter))).toBe(true);
217
- });
218
- });
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { Installer } from '../index.js';
7
+ import type { ServiceHost, ServiceHostContext, ServiceHostStatus } from '../service-host/types.js';
8
+
9
+ class FakeServiceHost implements ServiceHost {
10
+ readonly name = 'cadre-host';
11
+ installCalls: ServiceHostContext[] = [];
12
+ uninstallCalls: ServiceHostContext[] = [];
13
+ installed = false;
14
+ running = false;
15
+
16
+ async install(ctx: ServiceHostContext): Promise<void> {
17
+ this.installCalls.push(ctx);
18
+ this.installed = true;
19
+ this.running = true;
20
+ }
21
+ async uninstall(ctx: ServiceHostContext): Promise<void> {
22
+ this.uninstallCalls.push(ctx);
23
+ this.installed = false;
24
+ this.running = false;
25
+ }
26
+ async restart(_ctx: ServiceHostContext): Promise<void> {
27
+ this.running = true;
28
+ }
29
+ async status(_ctx: ServiceHostContext): Promise<ServiceHostStatus> {
30
+ return { installed: this.installed, running: this.running };
31
+ }
32
+ renderUnit(_ctx: ServiceHostContext): string | null {
33
+ return 'rendered-unit';
34
+ }
35
+ }
36
+
37
+ describe('Installer smoke', () => {
38
+ let tmp: string;
39
+
40
+ beforeEach(() => {
41
+ tmp = mkdtempSync(join(tmpdir(), 'cadre-host-installer-smoke-'));
42
+ });
43
+
44
+ afterEach(() => {
45
+ rmSync(tmp, { recursive: true, force: true });
46
+ });
47
+
48
+ it('install creates config + identity + invokes service-host', async () => {
49
+ const fake = new FakeServiceHost();
50
+ const installer = new Installer({ platform: 'linux', installerVersion: 'test-1.0.0' });
51
+ const result = await installer.install({
52
+ nonInteractive: true,
53
+ dataDir: tmp,
54
+ uiPort: 19999,
55
+ libp2pPort: 14001,
56
+ openBrowser: false,
57
+ noInvite: true,
58
+ serviceHost: fake,
59
+ });
60
+
61
+ expect(result.dataDir).toBe(tmp);
62
+ expect(result.uiUrl).toBe('http://127.0.0.1:19999/');
63
+ expect(result.serviceName).toBe('cadre-host');
64
+ expect(result.configPath).toBe(join(tmp, 'host.config.json'));
65
+
66
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
67
+ expect(existsSync(join(tmp, 'identity.key'))).toBe(true);
68
+ expect(existsSync(join(tmp, 'nat.json'))).toBe(true);
69
+ expect(existsSync(join(tmp, 'logs'))).toBe(true);
70
+
71
+ expect(fake.installCalls).toHaveLength(1);
72
+ expect(fake.installCalls[0]!.dataDir).toBe(tmp);
73
+ });
74
+
75
+ it('noService writes the data dir without registering a service', async () => {
76
+ const fake = new FakeServiceHost();
77
+ const installer = new Installer({ platform: 'linux', installerVersion: 'test-1.0.0' });
78
+ const result = await installer.install({
79
+ nonInteractive: true,
80
+ dataDir: tmp,
81
+ uiPort: 19995,
82
+ libp2pPort: 14005,
83
+ noService: true,
84
+ serviceHost: fake,
85
+ });
86
+
87
+ expect(fake.installCalls).toHaveLength(0);
88
+ expect(result.serviceName).toBeUndefined();
89
+ expect(result.uiUrl).toBe('http://127.0.0.1:19995/');
90
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
91
+ expect(existsSync(join(tmp, 'identity.key'))).toBe(true);
92
+ expect(existsSync(join(tmp, 'nat.json'))).toBe(true);
93
+ });
94
+
95
+ it('noService on an interactive install skips the enrollment-invite fetch', async () => {
96
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
97
+ try {
98
+ const installer = new Installer({ platform: 'linux', installerVersion: 'test-1.0.0' });
99
+ const result = await installer.install({
100
+ nonInteractive: false,
101
+ noService: true,
102
+ wizard: async () => ({
103
+ dataDir: tmp,
104
+ uiPort: 19994,
105
+ libp2pPort: 14006,
106
+ upnpEnabled: false,
107
+ configureDdns: false,
108
+ ownCadre: false,
109
+ }),
110
+ });
111
+
112
+ expect(fetchSpy).not.toHaveBeenCalled();
113
+ expect(result.enrollmentInvite).toBeUndefined();
114
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
115
+ } finally {
116
+ fetchSpy.mockRestore();
117
+ }
118
+ });
119
+
120
+ it('uninstall removes data only when --remove-data is set', async () => {
121
+ const fake = new FakeServiceHost();
122
+ const installer = new Installer({ platform: 'linux' });
123
+ await installer.install({
124
+ nonInteractive: true,
125
+ dataDir: tmp,
126
+ uiPort: 19998,
127
+ libp2pPort: 14002,
128
+ openBrowser: false,
129
+ noInvite: true,
130
+ serviceHost: fake,
131
+ });
132
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
133
+
134
+ await installer.uninstall({ yes: true, removeData: false, dataDir: tmp, serviceHost: fake });
135
+ expect(fake.uninstallCalls).toHaveLength(1);
136
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
137
+
138
+ await installer.uninstall({ yes: true, removeData: true, dataDir: tmp, serviceHost: fake });
139
+ expect(existsSync(tmp)).toBe(false);
140
+ });
141
+
142
+ it('rejects --system in v1', async () => {
143
+ const fake = new FakeServiceHost();
144
+ const installer = new Installer({ platform: 'linux' });
145
+ await expect(installer.install({
146
+ nonInteractive: true,
147
+ dataDir: tmp,
148
+ system: true,
149
+ serviceHost: fake,
150
+ openBrowser: false,
151
+ noInvite: true,
152
+ })).rejects.toThrow(/system.*not yet supported/);
153
+ expect(fake.installCalls).toHaveLength(0);
154
+ });
155
+
156
+ it('passes CLI overrides to the wizard as suggested defaults', async () => {
157
+ // Regression: the interactive wizard previously showed the platform
158
+ // default (`~/.local/share/cadre-host`) in the prompt even when the
159
+ // caller passed `--data-dir <path>`. Whatever the user typed was then
160
+ // silently discarded because the CLI flag won post-hoc. The fix: CLI
161
+ // overrides become the wizard's suggested defaults, and the wizard's
162
+ // returned answers stand on their own.
163
+ const fake = new FakeServiceHost();
164
+ const installer = new Installer({ platform: 'linux' });
165
+ let seenDefaults: { dataDir: string; uiPort: number; libp2pPort: number; upnpEnabled: boolean; ownCadre: boolean } | undefined;
166
+ await installer.install({
167
+ nonInteractive: false,
168
+ dataDir: tmp,
169
+ uiPort: 19997,
170
+ libp2pPort: 14003,
171
+ noUpnp: true,
172
+ openBrowser: false,
173
+ noInvite: true,
174
+ serviceHost: fake,
175
+ wizard: async (defaults) => {
176
+ seenDefaults = { ...defaults };
177
+ // Simulate the user pressing Enter at every prompt.
178
+ return {
179
+ dataDir: defaults.dataDir,
180
+ uiPort: defaults.uiPort,
181
+ libp2pPort: defaults.libp2pPort,
182
+ upnpEnabled: defaults.upnpEnabled,
183
+ configureDdns: false,
184
+ ownCadre: defaults.ownCadre,
185
+ };
186
+ },
187
+ });
188
+
189
+ expect(seenDefaults).toEqual({
190
+ dataDir: tmp,
191
+ uiPort: 19997,
192
+ libp2pPort: 14003,
193
+ upnpEnabled: false,
194
+ ownCadre: false,
195
+ });
196
+ expect(fake.installCalls).toHaveLength(1);
197
+ expect(fake.installCalls[0]!.dataDir).toBe(tmp);
198
+ });
199
+
200
+ it('honors wizard-typed input when no CLI override is passed', async () => {
201
+ // Mirror of the previous test: with no `--data-dir`, the wizard's
202
+ // returned value is what the installer uses.
203
+ const fake = new FakeServiceHost();
204
+ const installer = new Installer({ platform: 'linux' });
205
+ await installer.install({
206
+ nonInteractive: false,
207
+ openBrowser: false,
208
+ noInvite: true,
209
+ serviceHost: fake,
210
+ wizard: async () => ({
211
+ dataDir: tmp,
212
+ uiPort: 19996,
213
+ libp2pPort: 14004,
214
+ upnpEnabled: true,
215
+ configureDdns: false,
216
+ ownCadre: false,
217
+ }),
218
+ });
219
+ expect(fake.installCalls).toHaveLength(1);
220
+ expect(fake.installCalls[0]!.dataDir).toBe(tmp);
221
+ });
222
+
223
+ it('writes ownCadre.enabled=false by default and true when opted in', async () => {
224
+ const { readHostConfig } = await import('../config.js');
225
+
226
+ // Default (donor-only): --own-cadre absent → ownCadre.enabled false.
227
+ const fakeA = new FakeServiceHost();
228
+ const installerA = new Installer({ platform: 'linux' });
229
+ await installerA.install({
230
+ nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fakeA,
231
+ });
232
+ const donorCfg = readHostConfig(join(tmp, 'host.config.json'));
233
+ expect(donorCfg.ownCadre).toEqual({ enabled: false });
234
+
235
+ // Opt-in: ownCadre:true (the --own-cadre flag) → ownCadre.enabled true.
236
+ const tmp2 = mkdtempSync(join(tmpdir(), 'cadre-host-installer-owncadre-'));
237
+ try {
238
+ const fakeB = new FakeServiceHost();
239
+ const installerB = new Installer({ platform: 'linux' });
240
+ await installerB.install({
241
+ nonInteractive: true, dataDir: tmp2, ownCadre: true, openBrowser: false, noInvite: true, serviceHost: fakeB,
242
+ });
243
+ const founderCfg = readHostConfig(join(tmp2, 'host.config.json'));
244
+ expect(founderCfg.ownCadre).toEqual({ enabled: true });
245
+ } finally {
246
+ rmSync(tmp2, { recursive: true, force: true });
247
+ }
248
+ });
249
+
250
+ it('re-running install preserves the existing identity', async () => {
251
+ const fake = new FakeServiceHost();
252
+ const installer = new Installer({ platform: 'linux' });
253
+ await installer.install({
254
+ nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fake,
255
+ });
256
+ const idBefore = (await import('node:fs')).readFileSync(join(tmp, 'identity.key'));
257
+ await installer.install({
258
+ nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fake,
259
+ });
260
+ const idAfter = (await import('node:fs')).readFileSync(join(tmp, 'identity.key'));
261
+ expect(Buffer.from(idBefore).equals(Buffer.from(idAfter))).toBe(true);
262
+ });
263
+ });