@serfab/cadre-host 0.10.0 → 0.12.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.
- package/README.md +406 -296
- package/dist/bin/host.d.ts +11 -7
- package/dist/bin/host.js +40 -12
- package/dist/bin/host.js.map +1 -1
- package/dist/installer/config.d.ts +4 -4
- package/dist/installer/config.js +5 -39
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/identity.d.ts +8 -1
- package/dist/installer/identity.js +8 -1
- package/dist/installer/identity.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +2 -2
- package/dist/orchestrator/host-process-orchestrator.js +9 -4
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/server/settings-store.d.ts +1 -1
- package/dist/server/settings-store.js +5 -1
- package/dist/server/settings-store.js.map +1 -1
- package/dist/server/static.js +38 -38
- package/dist/ui/assets/index-C-iks41M.js.map +1 -1
- package/dist/ui/index.html +12 -12
- package/package.json +4 -4
- package/service/cadre-host.service.tmpl +16 -16
- package/service/com.serfab.cadre-host.plist.tmpl +35 -35
- package/service/install-service.ps1 +98 -98
- package/service/uninstall-service.ps1 +19 -19
- package/src/__tests__/build-targets.test.ts +26 -26
- package/src/__tests__/cli-invite.smoke.test.ts +192 -192
- package/src/__tests__/cli-nat.smoke.test.ts +212 -212
- package/src/__tests__/cli.smoke.test.ts +51 -46
- package/src/__tests__/global-setup.ts +56 -51
- package/src/__tests__/host-owner.smoke.test.ts +123 -123
- package/src/__tests__/orchestrator-env-scrub.test.ts +140 -140
- package/src/__tests__/orchestrator-node-identity.test.ts +5 -5
- package/src/__tests__/orchestrator-owner.test.ts +198 -198
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -153
- package/src/__tests__/orchestrator-push.test.ts +145 -145
- package/src/__tests__/orchestrator.test.ts +951 -951
- package/src/auth/__tests__/duration.test.ts +28 -28
- package/src/auth/__tests__/self-label.test.ts +85 -85
- package/src/auth/__tests__/trust-circle-store.test.ts +115 -115
- package/src/auth/__tests__/trust-circle.test.ts +445 -445
- package/src/auth/duration.ts +33 -33
- package/src/auth/index.ts +21 -21
- package/src/auth/self-label.ts +74 -74
- package/src/auth/trust-circle-store.ts +155 -155
- package/src/auth/types.ts +99 -99
- package/src/bin/__tests__/cli-reference.test.ts +167 -0
- package/src/bin/host.ts +1326 -1299
- package/src/donation/__tests__/donation-service.test.ts +1068 -1068
- package/src/donation/__tests__/donation-supervisor.test.ts +510 -510
- package/src/donation/__tests__/fake-orchestrator.test.ts +168 -168
- package/src/donation/__tests__/fake-orchestrator.ts +225 -225
- package/src/donation/__tests__/grant-service.test.ts +213 -213
- package/src/donation/__tests__/grant-store.test.ts +113 -113
- package/src/donation/donation-service.ts +962 -962
- package/src/donation/donation-store.ts +171 -171
- package/src/donation/donation-supervisor.ts +388 -388
- package/src/donation/grant-service.ts +200 -200
- package/src/donation/grant-store.ts +152 -152
- package/src/donation/index.ts +64 -64
- package/src/donation/types.ts +243 -243
- package/src/installer/__tests__/config.test.ts +134 -112
- package/src/installer/__tests__/identity.test.ts +34 -34
- package/src/installer/__tests__/paths.test.ts +47 -47
- package/src/installer/__tests__/service-host.test.ts +62 -62
- package/src/installer/__tests__/wizard.test.ts +64 -64
- package/src/installer/config.ts +5 -43
- package/src/installer/identity.ts +56 -49
- package/src/installer/paths.ts +109 -109
- package/src/installer/platform.ts +27 -27
- package/src/installer/service-host/index.ts +21 -21
- package/src/installer/service-host/template.ts +21 -21
- package/src/installer/wizard.ts +103 -103
- package/src/nat/__tests__/address-resolver.test.ts +82 -82
- package/src/nat/__tests__/ddns-duckdns.test.ts +83 -83
- package/src/nat/__tests__/ddns-updater.test.ts +253 -253
- package/src/nat/__tests__/external-ip.test.ts +124 -124
- package/src/nat/__tests__/nat-service.test.ts +18 -0
- package/src/nat/__tests__/nat-store.test.ts +93 -93
- package/src/nat/__tests__/port-mapper.test.ts +191 -191
- package/src/nat/__tests__/reachability.test.ts +24 -24
- package/src/nat/__tests__/secrets.test.ts +178 -178
- package/src/nat/address-resolver.ts +51 -51
- package/src/nat/ddns/duckdns.ts +76 -76
- package/src/nat/ddns/index.ts +55 -55
- package/src/nat/ddns/updater.ts +243 -243
- package/src/nat/external-ip.ts +154 -154
- package/src/nat/index.ts +71 -71
- package/src/nat/nat-service.ts +501 -501
- package/src/nat/nat-store.ts +154 -154
- package/src/nat/port-mapper.ts +329 -329
- package/src/nat/reachability.ts +34 -34
- package/src/nat/secrets/file-store.ts +130 -130
- package/src/nat/secrets/index.ts +112 -112
- package/src/nat/secrets/keytar-store.ts +67 -67
- package/src/nat/types.ts +159 -159
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +9 -4
- package/src/orchestrator/index.ts +10 -10
- package/src/orchestrator/log-rotator.ts +53 -53
- package/src/orchestrator/node-identity.ts +40 -40
- package/src/orchestrator/pid-liveness.ts +24 -24
- package/src/orchestrator/port-allocator.ts +91 -91
- package/src/orchestrator/state-store.ts +90 -90
- package/src/orchestrator/types.ts +141 -141
- package/src/owner/__tests__/owner-node-client.test.ts +194 -194
- package/src/owner/index.ts +12 -12
- package/src/owner/owner-node-client.ts +259 -259
- package/src/push/__tests__/push.test.ts +90 -90
- package/src/push/index.ts +146 -146
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +118 -118
- package/src/server/__tests__/grants-admin-route.test.ts +73 -73
- package/src/server/__tests__/grants-route.test.ts +263 -263
- package/src/server/__tests__/nodes-route.test.ts +259 -259
- package/src/server/__tests__/origin-guard.test.ts +72 -72
- package/src/server/__tests__/publishers.test.ts +216 -216
- package/src/server/__tests__/server.smoke.test.ts +253 -253
- package/src/server/__tests__/settings-route.test.ts +195 -194
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/static.test.ts +145 -145
- package/src/server/__tests__/status-route.test.ts +134 -134
- package/src/server/__tests__/strands-route.test.ts +233 -233
- package/src/server/error-handler.ts +178 -178
- package/src/server/events/bus.ts +42 -42
- package/src/server/events/sse-route.ts +81 -81
- package/src/server/events/types.ts +23 -23
- package/src/server/index.ts +267 -267
- package/src/server/origin-guard.ts +84 -84
- package/src/server/routes/grants-admin.ts +41 -41
- package/src/server/routes/grants.ts +210 -210
- package/src/server/routes/nat.ts +63 -63
- package/src/server/routes/nodes.ts +170 -170
- package/src/server/routes/settings.ts +123 -123
- package/src/server/routes/status.ts +118 -118
- package/src/server/routes/strands.ts +54 -54
- package/src/server/routes/trust-circle.ts +49 -49
- package/src/server/routes/update.ts +43 -43
- package/src/server/server.ts +122 -122
- package/src/server/settings-store.ts +5 -1
- package/src/server/static.ts +207 -207
- package/src/strands/__tests__/strand-service.test.ts +165 -165
- package/src/strands/index.ts +14 -14
- package/src/strands/strand-service.ts +135 -135
- package/src/strands/types.ts +102 -102
- package/src/update/__tests__/apply.test.ts +158 -158
- package/src/update/__tests__/manifest.test.ts +128 -128
- package/src/update/__tests__/release-key.test.ts +42 -42
- package/src/update/__tests__/sign.test.ts +78 -78
- package/src/update/__tests__/store.test.ts +81 -81
- package/src/update/__tests__/update-service.integration.test.ts +256 -256
- package/src/update/__tests__/version.test.ts +31 -31
- package/src/update/apply.ts +190 -190
- package/src/update/index.ts +341 -341
- package/src/update/release-key.ts +71 -71
- package/src/update/sign.ts +74 -74
- package/src/update/store.ts +101 -101
- package/src/update/types.ts +109 -109
- package/src/update/version.ts +80 -80
|
@@ -1,216 +1,216 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verifies the publisher wiring inside createLocalUiServer:
|
|
3
|
-
* - orchestrator.onStateChange → 'node-state-changed'
|
|
4
|
-
* - NatService.getStatus() at boot → 'connectivity-changed'
|
|
5
|
-
* - UpdateService.getState() polling → 'update-available'
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
13
|
-
|
|
14
|
-
import { createLocalUiServer } from '../index.js';
|
|
15
|
-
import { EventBus } from '../events/bus.js';
|
|
16
|
-
import type { LocalUiEvent } from '../events/types.js';
|
|
17
|
-
import type { HostProcessOrchestrator } from '../../orchestrator/index.js';
|
|
18
|
-
import type { ManagedNodeInfo, NodeStateListener } from '../../orchestrator/types.js';
|
|
19
|
-
import type { TrustCircleService } from '../../auth/index.js';
|
|
20
|
-
import type { NatService } from '../../nat/index.js';
|
|
21
|
-
import type { NatStatusSnapshot } from '../../nat/types.js';
|
|
22
|
-
import type { UpdateService } from '../../update/index.js';
|
|
23
|
-
import type { UpdateState } from '../../update/types.js';
|
|
24
|
-
|
|
25
|
-
const CONNECTIVITY: NatStatusSnapshot = {
|
|
26
|
-
portMode: 'auto-upnp',
|
|
27
|
-
externalPort: 4001,
|
|
28
|
-
internalPort: 4001,
|
|
29
|
-
routerExternalIp: '203.0.113.1',
|
|
30
|
-
mappingLeaseExpiresAt: null,
|
|
31
|
-
externalIp: '203.0.113.1',
|
|
32
|
-
externalIpDetectedAt: null,
|
|
33
|
-
cgnatDetected: false,
|
|
34
|
-
directReachability: 'reachable',
|
|
35
|
-
lastTestedAt: null,
|
|
36
|
-
ddns: {
|
|
37
|
-
providerId: null,
|
|
38
|
-
hostname: null,
|
|
39
|
-
externallyManaged: false,
|
|
40
|
-
lastUpdateAt: null,
|
|
41
|
-
lastUpdateOk: null,
|
|
42
|
-
lastError: null,
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function fakeTrust(): TrustCircleService {
|
|
47
|
-
return {
|
|
48
|
-
list: async () => ({ members: [], pending: [] }),
|
|
49
|
-
} as unknown as TrustCircleService;
|
|
50
|
-
}
|
|
51
|
-
function fakeNat(): NatService {
|
|
52
|
-
return { getStatus: () => CONNECTIVITY, putSettings: async () => CONNECTIVITY } as unknown as NatService;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface ManualOrchestrator extends HostProcessOrchestrator {
|
|
56
|
-
__emit: (info: ManagedNodeInfo) => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function manualOrchestrator(): ManualOrchestrator {
|
|
60
|
-
const listeners = new Set<NodeStateListener>();
|
|
61
|
-
const inst = {
|
|
62
|
-
listNodes: () => [],
|
|
63
|
-
getNode: () => undefined,
|
|
64
|
-
resolveDockerId: () => undefined,
|
|
65
|
-
onStateChange: (l: NodeStateListener) => { listeners.add(l); return () => { listeners.delete(l); }; },
|
|
66
|
-
__emit: (info: ManagedNodeInfo) => { for (const l of listeners) l(info); },
|
|
67
|
-
} as unknown as ManualOrchestrator;
|
|
68
|
-
return inst;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
class FakeUpdate {
|
|
72
|
-
private state: UpdateState;
|
|
73
|
-
constructor(initial: UpdateState) { this.state = initial; }
|
|
74
|
-
setState(next: UpdateState): void { this.state = next; }
|
|
75
|
-
async getState(): Promise<UpdateState> { return this.state; }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function writeConfig(dir: string): void {
|
|
79
|
-
const cfg = {
|
|
80
|
-
version: 2,
|
|
81
|
-
installId: 'inst-x',
|
|
82
|
-
uiPort: 8765,
|
|
83
|
-
libp2pPort: 4001,
|
|
84
|
-
dataDir: dir,
|
|
85
|
-
identityPath: join(dir, 'identity.key'),
|
|
86
|
-
upnpEnabled: true,
|
|
87
|
-
installedAt: '2025-01-01T00:00:00Z',
|
|
88
|
-
installerVersion: '0.6.0',
|
|
89
|
-
updates: { autoApply: false },
|
|
90
|
-
};
|
|
91
|
-
writeFileSync(join(dir, 'host.config.json'), JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
describe('publisher wiring', () => {
|
|
95
|
-
let dataDir: string;
|
|
96
|
-
let bus: EventBus;
|
|
97
|
-
let received: LocalUiEvent[];
|
|
98
|
-
let server: ReturnType<typeof createLocalUiServer>;
|
|
99
|
-
let orchestrator: ManualOrchestrator;
|
|
100
|
-
|
|
101
|
-
beforeEach(async () => {
|
|
102
|
-
dataDir = mkdtempSync(join(tmpdir(), 'cadre-host-pub-'));
|
|
103
|
-
writeConfig(dataDir);
|
|
104
|
-
bus = new EventBus();
|
|
105
|
-
received = [];
|
|
106
|
-
bus.subscribe((e) => received.push(e));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
afterEach(async () => {
|
|
110
|
-
if (server) await server.stop();
|
|
111
|
-
rmSync(dataDir, { recursive: true, force: true });
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('orchestrator.onStateChange → node-state-changed', async () => {
|
|
115
|
-
orchestrator = manualOrchestrator();
|
|
116
|
-
server = createLocalUiServer({
|
|
117
|
-
uiPort: 8765,
|
|
118
|
-
dataDir,
|
|
119
|
-
orchestrator,
|
|
120
|
-
trustCircle: fakeTrust(),
|
|
121
|
-
nat: fakeNat(),
|
|
122
|
-
events: bus,
|
|
123
|
-
forcePort: 0,
|
|
124
|
-
});
|
|
125
|
-
await server.start();
|
|
126
|
-
orchestrator.__emit({
|
|
127
|
-
id: 'alice',
|
|
128
|
-
dockerId: '111:abc',
|
|
129
|
-
partyId: 'p',
|
|
130
|
-
profile: 'storage',
|
|
131
|
-
status: 'stopped',
|
|
132
|
-
spawnedAt: 'x',
|
|
133
|
-
workdir: '/tmp',
|
|
134
|
-
ports: { health: 1, metrics: 2, p2p: 3, admin: 4 },
|
|
135
|
-
});
|
|
136
|
-
expect(received).toContainEqual({
|
|
137
|
-
type: 'node-state-changed',
|
|
138
|
-
nodeId: 'alice',
|
|
139
|
-
status: 'stopped',
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('emits an initial connectivity-changed at start', async () => {
|
|
144
|
-
orchestrator = manualOrchestrator();
|
|
145
|
-
server = createLocalUiServer({
|
|
146
|
-
uiPort: 8765,
|
|
147
|
-
dataDir,
|
|
148
|
-
orchestrator,
|
|
149
|
-
trustCircle: fakeTrust(),
|
|
150
|
-
nat: fakeNat(),
|
|
151
|
-
events: bus,
|
|
152
|
-
forcePort: 0,
|
|
153
|
-
});
|
|
154
|
-
await server.start();
|
|
155
|
-
const conn = received.filter((e) => e.type === 'connectivity-changed');
|
|
156
|
-
expect(conn).toHaveLength(1);
|
|
157
|
-
expect(conn[0]).toMatchObject({
|
|
158
|
-
type: 'connectivity-changed',
|
|
159
|
-
portMode: 'auto-upnp',
|
|
160
|
-
directReachability: 'reachable',
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('publishes update-available when UpdateService.getState() shows a new version', async () => {
|
|
165
|
-
orchestrator = manualOrchestrator();
|
|
166
|
-
const update = new FakeUpdate({ version: 1 });
|
|
167
|
-
server = createLocalUiServer({
|
|
168
|
-
uiPort: 8765,
|
|
169
|
-
dataDir,
|
|
170
|
-
orchestrator,
|
|
171
|
-
trustCircle: fakeTrust(),
|
|
172
|
-
nat: fakeNat(),
|
|
173
|
-
events: bus,
|
|
174
|
-
update: update as unknown as UpdateService,
|
|
175
|
-
forcePort: 0,
|
|
176
|
-
});
|
|
177
|
-
await server.start();
|
|
178
|
-
// The boot tick already happened — no available yet.
|
|
179
|
-
expect(received.filter((e) => e.type === 'update-available')).toHaveLength(0);
|
|
180
|
-
// Simulate the update store noticing a new version. Call start()'s
|
|
181
|
-
// internal tick by setting state and waiting briefly.
|
|
182
|
-
update.setState({
|
|
183
|
-
version: 1,
|
|
184
|
-
available: { version: '0.7.0', publishedAt: '2025-06-01T00:00:00Z', releaseNotesUrl: 'https://x' },
|
|
185
|
-
});
|
|
186
|
-
// The 60 s interval is too slow for a unit test — drive a second start
|
|
187
|
-
// cycle by re-invoking via the server's effective polling pattern: we
|
|
188
|
-
// expose nothing for tests, so wait briefly then simulate the boot tick
|
|
189
|
-
// by recreating the server with the same fake. Easier: assert the
|
|
190
|
-
// start-time tick (which we already covered above) and that putting an
|
|
191
|
-
// update in state before start() yields an event.
|
|
192
|
-
await server.stop();
|
|
193
|
-
server = createLocalUiServer({
|
|
194
|
-
uiPort: 8765,
|
|
195
|
-
dataDir,
|
|
196
|
-
orchestrator,
|
|
197
|
-
trustCircle: fakeTrust(),
|
|
198
|
-
nat: fakeNat(),
|
|
199
|
-
events: bus,
|
|
200
|
-
update: update as unknown as UpdateService,
|
|
201
|
-
forcePort: 0,
|
|
202
|
-
});
|
|
203
|
-
received.length = 0;
|
|
204
|
-
bus.subscribe((e) => received.push(e));
|
|
205
|
-
await server.start();
|
|
206
|
-
// Let the boot-time tick microtask flush.
|
|
207
|
-
await new Promise<void>((r) => setTimeout(r, 20));
|
|
208
|
-
const updateEvents = received.filter((e) => e.type === 'update-available');
|
|
209
|
-
expect(updateEvents.length).toBeGreaterThan(0);
|
|
210
|
-
expect(updateEvents[0]).toMatchObject({
|
|
211
|
-
type: 'update-available',
|
|
212
|
-
version: '0.7.0',
|
|
213
|
-
releaseNotesUrl: 'https://x',
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the publisher wiring inside createLocalUiServer:
|
|
3
|
+
* - orchestrator.onStateChange → 'node-state-changed'
|
|
4
|
+
* - NatService.getStatus() at boot → 'connectivity-changed'
|
|
5
|
+
* - UpdateService.getState() polling → 'update-available'
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
|
|
14
|
+
import { createLocalUiServer } from '../index.js';
|
|
15
|
+
import { EventBus } from '../events/bus.js';
|
|
16
|
+
import type { LocalUiEvent } from '../events/types.js';
|
|
17
|
+
import type { HostProcessOrchestrator } from '../../orchestrator/index.js';
|
|
18
|
+
import type { ManagedNodeInfo, NodeStateListener } from '../../orchestrator/types.js';
|
|
19
|
+
import type { TrustCircleService } from '../../auth/index.js';
|
|
20
|
+
import type { NatService } from '../../nat/index.js';
|
|
21
|
+
import type { NatStatusSnapshot } from '../../nat/types.js';
|
|
22
|
+
import type { UpdateService } from '../../update/index.js';
|
|
23
|
+
import type { UpdateState } from '../../update/types.js';
|
|
24
|
+
|
|
25
|
+
const CONNECTIVITY: NatStatusSnapshot = {
|
|
26
|
+
portMode: 'auto-upnp',
|
|
27
|
+
externalPort: 4001,
|
|
28
|
+
internalPort: 4001,
|
|
29
|
+
routerExternalIp: '203.0.113.1',
|
|
30
|
+
mappingLeaseExpiresAt: null,
|
|
31
|
+
externalIp: '203.0.113.1',
|
|
32
|
+
externalIpDetectedAt: null,
|
|
33
|
+
cgnatDetected: false,
|
|
34
|
+
directReachability: 'reachable',
|
|
35
|
+
lastTestedAt: null,
|
|
36
|
+
ddns: {
|
|
37
|
+
providerId: null,
|
|
38
|
+
hostname: null,
|
|
39
|
+
externallyManaged: false,
|
|
40
|
+
lastUpdateAt: null,
|
|
41
|
+
lastUpdateOk: null,
|
|
42
|
+
lastError: null,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function fakeTrust(): TrustCircleService {
|
|
47
|
+
return {
|
|
48
|
+
list: async () => ({ members: [], pending: [] }),
|
|
49
|
+
} as unknown as TrustCircleService;
|
|
50
|
+
}
|
|
51
|
+
function fakeNat(): NatService {
|
|
52
|
+
return { getStatus: () => CONNECTIVITY, putSettings: async () => CONNECTIVITY } as unknown as NatService;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ManualOrchestrator extends HostProcessOrchestrator {
|
|
56
|
+
__emit: (info: ManagedNodeInfo) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function manualOrchestrator(): ManualOrchestrator {
|
|
60
|
+
const listeners = new Set<NodeStateListener>();
|
|
61
|
+
const inst = {
|
|
62
|
+
listNodes: () => [],
|
|
63
|
+
getNode: () => undefined,
|
|
64
|
+
resolveDockerId: () => undefined,
|
|
65
|
+
onStateChange: (l: NodeStateListener) => { listeners.add(l); return () => { listeners.delete(l); }; },
|
|
66
|
+
__emit: (info: ManagedNodeInfo) => { for (const l of listeners) l(info); },
|
|
67
|
+
} as unknown as ManualOrchestrator;
|
|
68
|
+
return inst;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class FakeUpdate {
|
|
72
|
+
private state: UpdateState;
|
|
73
|
+
constructor(initial: UpdateState) { this.state = initial; }
|
|
74
|
+
setState(next: UpdateState): void { this.state = next; }
|
|
75
|
+
async getState(): Promise<UpdateState> { return this.state; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writeConfig(dir: string): void {
|
|
79
|
+
const cfg = {
|
|
80
|
+
version: 2,
|
|
81
|
+
installId: 'inst-x',
|
|
82
|
+
uiPort: 8765,
|
|
83
|
+
libp2pPort: 4001,
|
|
84
|
+
dataDir: dir,
|
|
85
|
+
identityPath: join(dir, 'identity.key'),
|
|
86
|
+
upnpEnabled: true,
|
|
87
|
+
installedAt: '2025-01-01T00:00:00Z',
|
|
88
|
+
installerVersion: '0.6.0',
|
|
89
|
+
updates: { autoApply: false },
|
|
90
|
+
};
|
|
91
|
+
writeFileSync(join(dir, 'host.config.json'), JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('publisher wiring', () => {
|
|
95
|
+
let dataDir: string;
|
|
96
|
+
let bus: EventBus;
|
|
97
|
+
let received: LocalUiEvent[];
|
|
98
|
+
let server: ReturnType<typeof createLocalUiServer>;
|
|
99
|
+
let orchestrator: ManualOrchestrator;
|
|
100
|
+
|
|
101
|
+
beforeEach(async () => {
|
|
102
|
+
dataDir = mkdtempSync(join(tmpdir(), 'cadre-host-pub-'));
|
|
103
|
+
writeConfig(dataDir);
|
|
104
|
+
bus = new EventBus();
|
|
105
|
+
received = [];
|
|
106
|
+
bus.subscribe((e) => received.push(e));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(async () => {
|
|
110
|
+
if (server) await server.stop();
|
|
111
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('orchestrator.onStateChange → node-state-changed', async () => {
|
|
115
|
+
orchestrator = manualOrchestrator();
|
|
116
|
+
server = createLocalUiServer({
|
|
117
|
+
uiPort: 8765,
|
|
118
|
+
dataDir,
|
|
119
|
+
orchestrator,
|
|
120
|
+
trustCircle: fakeTrust(),
|
|
121
|
+
nat: fakeNat(),
|
|
122
|
+
events: bus,
|
|
123
|
+
forcePort: 0,
|
|
124
|
+
});
|
|
125
|
+
await server.start();
|
|
126
|
+
orchestrator.__emit({
|
|
127
|
+
id: 'alice',
|
|
128
|
+
dockerId: '111:abc',
|
|
129
|
+
partyId: 'p',
|
|
130
|
+
profile: 'storage',
|
|
131
|
+
status: 'stopped',
|
|
132
|
+
spawnedAt: 'x',
|
|
133
|
+
workdir: '/tmp',
|
|
134
|
+
ports: { health: 1, metrics: 2, p2p: 3, admin: 4 },
|
|
135
|
+
});
|
|
136
|
+
expect(received).toContainEqual({
|
|
137
|
+
type: 'node-state-changed',
|
|
138
|
+
nodeId: 'alice',
|
|
139
|
+
status: 'stopped',
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('emits an initial connectivity-changed at start', async () => {
|
|
144
|
+
orchestrator = manualOrchestrator();
|
|
145
|
+
server = createLocalUiServer({
|
|
146
|
+
uiPort: 8765,
|
|
147
|
+
dataDir,
|
|
148
|
+
orchestrator,
|
|
149
|
+
trustCircle: fakeTrust(),
|
|
150
|
+
nat: fakeNat(),
|
|
151
|
+
events: bus,
|
|
152
|
+
forcePort: 0,
|
|
153
|
+
});
|
|
154
|
+
await server.start();
|
|
155
|
+
const conn = received.filter((e) => e.type === 'connectivity-changed');
|
|
156
|
+
expect(conn).toHaveLength(1);
|
|
157
|
+
expect(conn[0]).toMatchObject({
|
|
158
|
+
type: 'connectivity-changed',
|
|
159
|
+
portMode: 'auto-upnp',
|
|
160
|
+
directReachability: 'reachable',
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('publishes update-available when UpdateService.getState() shows a new version', async () => {
|
|
165
|
+
orchestrator = manualOrchestrator();
|
|
166
|
+
const update = new FakeUpdate({ version: 1 });
|
|
167
|
+
server = createLocalUiServer({
|
|
168
|
+
uiPort: 8765,
|
|
169
|
+
dataDir,
|
|
170
|
+
orchestrator,
|
|
171
|
+
trustCircle: fakeTrust(),
|
|
172
|
+
nat: fakeNat(),
|
|
173
|
+
events: bus,
|
|
174
|
+
update: update as unknown as UpdateService,
|
|
175
|
+
forcePort: 0,
|
|
176
|
+
});
|
|
177
|
+
await server.start();
|
|
178
|
+
// The boot tick already happened — no available yet.
|
|
179
|
+
expect(received.filter((e) => e.type === 'update-available')).toHaveLength(0);
|
|
180
|
+
// Simulate the update store noticing a new version. Call start()'s
|
|
181
|
+
// internal tick by setting state and waiting briefly.
|
|
182
|
+
update.setState({
|
|
183
|
+
version: 1,
|
|
184
|
+
available: { version: '0.7.0', publishedAt: '2025-06-01T00:00:00Z', releaseNotesUrl: 'https://x' },
|
|
185
|
+
});
|
|
186
|
+
// The 60 s interval is too slow for a unit test — drive a second start
|
|
187
|
+
// cycle by re-invoking via the server's effective polling pattern: we
|
|
188
|
+
// expose nothing for tests, so wait briefly then simulate the boot tick
|
|
189
|
+
// by recreating the server with the same fake. Easier: assert the
|
|
190
|
+
// start-time tick (which we already covered above) and that putting an
|
|
191
|
+
// update in state before start() yields an event.
|
|
192
|
+
await server.stop();
|
|
193
|
+
server = createLocalUiServer({
|
|
194
|
+
uiPort: 8765,
|
|
195
|
+
dataDir,
|
|
196
|
+
orchestrator,
|
|
197
|
+
trustCircle: fakeTrust(),
|
|
198
|
+
nat: fakeNat(),
|
|
199
|
+
events: bus,
|
|
200
|
+
update: update as unknown as UpdateService,
|
|
201
|
+
forcePort: 0,
|
|
202
|
+
});
|
|
203
|
+
received.length = 0;
|
|
204
|
+
bus.subscribe((e) => received.push(e));
|
|
205
|
+
await server.start();
|
|
206
|
+
// Let the boot-time tick microtask flush.
|
|
207
|
+
await new Promise<void>((r) => setTimeout(r, 20));
|
|
208
|
+
const updateEvents = received.filter((e) => e.type === 'update-available');
|
|
209
|
+
expect(updateEvents.length).toBeGreaterThan(0);
|
|
210
|
+
expect(updateEvents[0]).toMatchObject({
|
|
211
|
+
type: 'update-available',
|
|
212
|
+
version: '0.7.0',
|
|
213
|
+
releaseNotesUrl: 'https://x',
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|