@mindexec/cli 0.2.133 → 0.2.135
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 +3 -2
- package/scripts/remote-fast-mdm-browser-smoke.mjs +13 -17
- package/scripts/remote-frame-ws-smoke.mjs +11 -9
- package/scripts/remote-registry-follower-smoke.mjs +322 -0
- package/server.js +693 -10
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +50 -4
- package/wwwroot/service-worker-assets.js +2 -2
- package/wwwroot/service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.135",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node launch-bridge.cjs",
|
|
23
23
|
"dev": "node launch-bridge.cjs --watch",
|
|
24
|
-
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
24
|
+
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
25
25
|
"test:auth": "node scripts/auth-session-smoke.mjs",
|
|
26
26
|
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
27
27
|
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
|
|
31
31
|
"test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
|
|
32
32
|
"test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
|
|
33
|
+
"test:remote:registry": "node scripts/remote-registry-follower-smoke.mjs",
|
|
33
34
|
"test:remote:package": "node scripts/remote-agent-package-smoke.mjs",
|
|
34
35
|
"test:remote:live-rate": "node scripts/remote-fast-live-rate-smoke.mjs",
|
|
35
36
|
"test:remote:mdm-browser": "node scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
@@ -161,14 +161,14 @@ async function launchBrowser(chromium) {
|
|
|
161
161
|
throw new Error(`Unable to launch a Chromium browser: ${lastError?.message || 'unknown error'}`);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
async function paintRemoteFrameInBrowser(page, wsUrl, deviceId) {
|
|
164
|
+
async function paintRemoteFrameInBrowser(page, wsUrl, deviceId, fps = REQUESTED_FPS) {
|
|
165
165
|
await page.setContent(`
|
|
166
166
|
<!doctype html>
|
|
167
167
|
<meta charset="utf-8">
|
|
168
168
|
<canvas id="screen" width="320" height="180"></canvas>
|
|
169
169
|
`);
|
|
170
170
|
|
|
171
|
-
return await page.evaluate(async ({ wsUrl, deviceId }) => {
|
|
171
|
+
return await page.evaluate(async ({ wsUrl, deviceId, fps }) => {
|
|
172
172
|
const canvas = document.getElementById('screen');
|
|
173
173
|
const context = canvas.getContext('2d', { alpha: false, desynchronized: true }) || canvas.getContext('2d');
|
|
174
174
|
if (!context) {
|
|
@@ -203,7 +203,15 @@ async function paintRemoteFrameInBrowser(page, wsUrl, deviceId) {
|
|
|
203
203
|
const ws = new WebSocket(wsUrl);
|
|
204
204
|
ws.binaryType = 'arraybuffer';
|
|
205
205
|
ws.onopen = () => {
|
|
206
|
-
ws.send(JSON.stringify({
|
|
206
|
+
ws.send(JSON.stringify({
|
|
207
|
+
type: 'subscribe',
|
|
208
|
+
deviceIds: [deviceId],
|
|
209
|
+
autoStartLive: true,
|
|
210
|
+
fps,
|
|
211
|
+
maxWidth: 960,
|
|
212
|
+
maxHeight: 540,
|
|
213
|
+
quality: 60
|
|
214
|
+
}));
|
|
207
215
|
};
|
|
208
216
|
ws.onerror = () => {
|
|
209
217
|
clearTimeout(timeout);
|
|
@@ -262,7 +270,7 @@ async function paintRemoteFrameInBrowser(page, wsUrl, deviceId) {
|
|
|
262
270
|
}
|
|
263
271
|
};
|
|
264
272
|
});
|
|
265
|
-
}, { wsUrl, deviceId });
|
|
273
|
+
}, { wsUrl, deviceId, fps });
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
async function main() {
|
|
@@ -320,23 +328,11 @@ async function main() {
|
|
|
320
328
|
&& item.capabilities?.binaryFrames === true) || null;
|
|
321
329
|
}, 15000, `RemoteFast device registration\n${hostBridge.details()}\n${clientBridge.details()}`);
|
|
322
330
|
|
|
323
|
-
const live = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
|
|
324
|
-
method: 'POST',
|
|
325
|
-
body: JSON.stringify({
|
|
326
|
-
fps: REQUESTED_FPS,
|
|
327
|
-
maxWidth: 960,
|
|
328
|
-
maxHeight: 540,
|
|
329
|
-
quality: 60
|
|
330
|
-
})
|
|
331
|
-
});
|
|
332
|
-
assert.equal(live.ok, true, JSON.stringify(live.payload));
|
|
333
|
-
assert.equal(live.payload?.ok, true, JSON.stringify(live.payload));
|
|
334
|
-
|
|
335
331
|
const chromium = await loadPlaywrightChromium();
|
|
336
332
|
browser = await launchBrowser(chromium);
|
|
337
333
|
const page = await browser.newPage();
|
|
338
334
|
const wsUrl = `${hostBridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/frames/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`;
|
|
339
|
-
const painted = await paintRemoteFrameInBrowser(page, wsUrl, device.deviceId);
|
|
335
|
+
const painted = await paintRemoteFrameInBrowser(page, wsUrl, device.deviceId, REQUESTED_FPS);
|
|
340
336
|
assert.ok(painted.frameSeq > 0, JSON.stringify(painted));
|
|
341
337
|
assert.equal(painted.fps, REQUESTED_FPS, JSON.stringify(painted));
|
|
342
338
|
assert.ok(painted.width > 0 && painted.height > 0, JSON.stringify(painted));
|
|
@@ -151,7 +151,7 @@ async function main() {
|
|
|
151
151
|
|
|
152
152
|
const seed = await fetchJson(`${bridge.baseUrl}/api/remote/synthetic/seed`, {
|
|
153
153
|
method: 'POST',
|
|
154
|
-
body: JSON.stringify({ count: 4, connectedRatio: 1,
|
|
154
|
+
body: JSON.stringify({ count: 4, connectedRatio: 1, thumbnailRatio: 1, liveCount: 0 })
|
|
155
155
|
});
|
|
156
156
|
assert.equal(seed.ok, true);
|
|
157
157
|
|
|
@@ -165,20 +165,22 @@ async function main() {
|
|
|
165
165
|
ws.once('open', resolve);
|
|
166
166
|
ws.once('error', reject);
|
|
167
167
|
});
|
|
168
|
-
ws.send(JSON.stringify({
|
|
168
|
+
ws.send(JSON.stringify({
|
|
169
|
+
type: 'subscribe',
|
|
170
|
+
deviceIds: [device.deviceId],
|
|
171
|
+
autoStartLive: true,
|
|
172
|
+
fps: 12,
|
|
173
|
+
maxWidth: 960,
|
|
174
|
+
maxHeight: 540,
|
|
175
|
+
quality: 60
|
|
176
|
+
}));
|
|
169
177
|
|
|
170
178
|
const framePromise = waitForBinaryFrame(ws);
|
|
171
|
-
const live = await fetchJson(`${bridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
|
|
172
|
-
method: 'POST',
|
|
173
|
-
body: JSON.stringify({ fps: 20 })
|
|
174
|
-
});
|
|
175
|
-
assert.equal(live.ok, true);
|
|
176
|
-
|
|
177
179
|
const frame = await framePromise;
|
|
178
180
|
assert.equal(frame.metadata.type, 'remote.frame.binary');
|
|
179
181
|
assert.equal(frame.metadata.kind, 'live');
|
|
180
182
|
assert.equal(frame.metadata.deviceId, device.deviceId);
|
|
181
|
-
assert.equal(frame.metadata.fps,
|
|
183
|
+
assert.equal(frame.metadata.fps, 12);
|
|
182
184
|
assert.ok(frame.metadata.frameSeq > 0, 'frameSeq should be positive');
|
|
183
185
|
assert.equal(frame.metadata.mimeType, 'image/png');
|
|
184
186
|
assert.ok(frame.payload.length > 0, 'payload should be non-empty');
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { createServer } from 'node:http';
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const BRIDGE_TOKEN = 'remote-registry-follower-smoke-token';
|
|
13
|
+
const PAIR_TOKEN = 'remote-registry-follower-pair-token';
|
|
14
|
+
const USER_ID = '11111111-2222-4333-8444-555555555555';
|
|
15
|
+
const SUPABASE_KEY = 'remote-registry-follower-key';
|
|
16
|
+
const ACCESS_TOKEN = 'remote-registry-follower-access-token';
|
|
17
|
+
const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
|
|
19
|
+
function wait(ms) {
|
|
20
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function findFreePort() {
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
const server = net.createServer();
|
|
26
|
+
server.unref();
|
|
27
|
+
server.once('error', reject);
|
|
28
|
+
server.listen(0, '127.0.0.1', () => {
|
|
29
|
+
const address = server.address();
|
|
30
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
31
|
+
server.close(() => resolve(port));
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function fetchJson(url, options = {}) {
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
...options,
|
|
39
|
+
headers: {
|
|
40
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
41
|
+
'X-Bridge-Token': options.token || BRIDGE_TOKEN,
|
|
42
|
+
...(options.headers || {})
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
let payload = null;
|
|
46
|
+
try {
|
|
47
|
+
payload = await response.json();
|
|
48
|
+
} catch {
|
|
49
|
+
// Empty responses are allowed in failure diagnostics.
|
|
50
|
+
}
|
|
51
|
+
return { ok: response.ok, status: response.status, payload };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitFor(predicate, timeoutMs, label) {
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
let lastError = null;
|
|
57
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
58
|
+
try {
|
|
59
|
+
const value = await predicate();
|
|
60
|
+
if (value) {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
lastError = error;
|
|
65
|
+
}
|
|
66
|
+
await wait(100);
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Timed out waiting for ${label}${lastError ? `: ${lastError.message}` : ''}.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active = true }) {
|
|
72
|
+
return {
|
|
73
|
+
user_id: USER_ID,
|
|
74
|
+
active,
|
|
75
|
+
endpoint,
|
|
76
|
+
endpoint_candidates: endpointCandidates,
|
|
77
|
+
pair_token: PAIR_TOKEN,
|
|
78
|
+
lease_id: leaseId,
|
|
79
|
+
node_id: 'remote-registry-follower-node',
|
|
80
|
+
host_instance_id: `host-${leaseId}`,
|
|
81
|
+
expires_at: new Date(Date.now() + 60_000).toISOString()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function startFakeSupabase(getTarget) {
|
|
86
|
+
const requests = [];
|
|
87
|
+
const server = createServer((req, res) => {
|
|
88
|
+
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
89
|
+
requests.push({
|
|
90
|
+
method: req.method,
|
|
91
|
+
pathname: parsed.pathname,
|
|
92
|
+
authorization: req.headers.authorization || '',
|
|
93
|
+
apikey: req.headers.apikey || ''
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (req.method === 'GET' && parsed.pathname === '/rest/v1/remote_host_targets') {
|
|
97
|
+
assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
|
|
98
|
+
assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
|
|
99
|
+
res.writeHead(200, {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
'Cache-Control': 'no-store'
|
|
102
|
+
});
|
|
103
|
+
const target = getTarget();
|
|
104
|
+
res.end(JSON.stringify(target ? [target] : []));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
109
|
+
res.end(JSON.stringify({ error: 'not-found' }));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
server.once('error', reject);
|
|
114
|
+
server.listen(0, '127.0.0.1', () => {
|
|
115
|
+
const address = server.address();
|
|
116
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
117
|
+
resolve({
|
|
118
|
+
url: `http://127.0.0.1:${port}`,
|
|
119
|
+
requests,
|
|
120
|
+
stop: () => new Promise(done => server.close(done))
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false }) {
|
|
127
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
128
|
+
cwd: LOCAL_BRIDGE_DIR,
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
windowsHide: true,
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
BRIDGE_PORT: String(bridgePort),
|
|
134
|
+
BRIDGE_TOKEN,
|
|
135
|
+
BRIDGE_REQUIRE_TOKEN: '1',
|
|
136
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
137
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
138
|
+
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
139
|
+
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
140
|
+
WORKSPACE_PATH: workspacePath,
|
|
141
|
+
MINDEXEC_AUTH_DATA_ROOT: authRoot,
|
|
142
|
+
MINDEXEC_REMOTE_REGISTRY_FOLLOWER: follower ? '1' : '0',
|
|
143
|
+
MINDEXEC_REMOTE_REGISTRY_POLL_MS: '500',
|
|
144
|
+
MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
|
|
145
|
+
SUPABASE_URL: supabaseUrl,
|
|
146
|
+
SUPABASE_KEY,
|
|
147
|
+
NO_COLOR: '1'
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let stdout = '';
|
|
152
|
+
let stderr = '';
|
|
153
|
+
child.stdout.on('data', chunk => {
|
|
154
|
+
stdout += chunk.toString();
|
|
155
|
+
});
|
|
156
|
+
child.stderr.on('data', chunk => {
|
|
157
|
+
stderr += chunk.toString();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
161
|
+
const details = () => `${label} stdout=${stdout}\n${label} stderr=${stderr}`;
|
|
162
|
+
const stop = async () => {
|
|
163
|
+
if (child.exitCode === null && !child.killed) {
|
|
164
|
+
child.kill('SIGTERM');
|
|
165
|
+
await Promise.race([exitPromise, wait(5000)]);
|
|
166
|
+
if (child.exitCode === null && !child.killed) {
|
|
167
|
+
child.kill('SIGKILL');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
baseUrl: `http://127.0.0.1:${bridgePort}`,
|
|
174
|
+
child,
|
|
175
|
+
details,
|
|
176
|
+
stop
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function waitForBridge(bridge) {
|
|
181
|
+
return await waitFor(async () => {
|
|
182
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/status`);
|
|
183
|
+
return result.ok && result.payload?.status === 'ok' ? result.payload : null;
|
|
184
|
+
}, 30000, `bridge startup\n${bridge.details()}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function writeSession(authRoot) {
|
|
188
|
+
await mkdir(authRoot, { recursive: true });
|
|
189
|
+
await writeFile(path.join(authRoot, 'supabase-session.json'), JSON.stringify({
|
|
190
|
+
access_token: ACCESS_TOKEN,
|
|
191
|
+
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
|
192
|
+
user: {
|
|
193
|
+
id: USER_ID,
|
|
194
|
+
email: 'remote-registry-follower@example.test'
|
|
195
|
+
}
|
|
196
|
+
}), 'utf8');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function waitForManagedAgent(bridge, managerEndpoint, label) {
|
|
200
|
+
return await waitFor(async () => {
|
|
201
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/remote/agent/status`);
|
|
202
|
+
const agent = result.payload;
|
|
203
|
+
return agent?.running === true
|
|
204
|
+
&& agent?.ready === true
|
|
205
|
+
&& String(agent.manager || '') === managerEndpoint
|
|
206
|
+
? agent
|
|
207
|
+
: null;
|
|
208
|
+
}, 20000, `${label} managed agent\n${bridge.details()}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function waitForConnectedDevice(bridge, label) {
|
|
212
|
+
return await waitFor(async () => {
|
|
213
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
|
|
214
|
+
return result.payload?.devices?.find(device => device.connected === true) || null;
|
|
215
|
+
}, 12000, `${label} connected device\n${bridge.details()}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function main() {
|
|
219
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
|
|
220
|
+
let fakeSupabase = null;
|
|
221
|
+
let hostA = null;
|
|
222
|
+
let hostB = null;
|
|
223
|
+
let client = null;
|
|
224
|
+
|
|
225
|
+
const hostABridgePort = await findFreePort();
|
|
226
|
+
const hostARemotePort = await findFreePort();
|
|
227
|
+
const hostBBridgePort = await findFreePort();
|
|
228
|
+
const hostBRemotePort = await findFreePort();
|
|
229
|
+
const clientBridgePort = await findFreePort();
|
|
230
|
+
const clientRemotePort = await findFreePort();
|
|
231
|
+
const stalePort = await findFreePort();
|
|
232
|
+
const hostAEndpoint = `127.0.0.1:${hostARemotePort}`;
|
|
233
|
+
const hostBEndpoint = `127.0.0.1:${hostBRemotePort}`;
|
|
234
|
+
const staleEndpoint = `127.0.0.1:${stalePort}`;
|
|
235
|
+
let currentTarget = createRegistryTarget({
|
|
236
|
+
endpoint: hostAEndpoint,
|
|
237
|
+
endpointCandidates: [staleEndpoint, hostAEndpoint],
|
|
238
|
+
leaseId: 'lease-a'
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
fakeSupabase = await startFakeSupabase(() => currentTarget);
|
|
243
|
+
|
|
244
|
+
const hostAAuth = path.join(tempRoot, 'auth-host-a');
|
|
245
|
+
const hostBAuth = path.join(tempRoot, 'auth-host-b');
|
|
246
|
+
const clientAuth = path.join(tempRoot, 'auth-client');
|
|
247
|
+
await writeSession(clientAuth);
|
|
248
|
+
|
|
249
|
+
hostA = spawnBridge({
|
|
250
|
+
bridgePort: hostABridgePort,
|
|
251
|
+
remoteHubPort: hostARemotePort,
|
|
252
|
+
workspacePath: path.join(tempRoot, 'host-a'),
|
|
253
|
+
authRoot: hostAAuth,
|
|
254
|
+
label: 'host-a'
|
|
255
|
+
});
|
|
256
|
+
hostB = spawnBridge({
|
|
257
|
+
bridgePort: hostBBridgePort,
|
|
258
|
+
remoteHubPort: hostBRemotePort,
|
|
259
|
+
workspacePath: path.join(tempRoot, 'host-b'),
|
|
260
|
+
authRoot: hostBAuth,
|
|
261
|
+
label: 'host-b'
|
|
262
|
+
});
|
|
263
|
+
await waitForBridge(hostA);
|
|
264
|
+
await waitForBridge(hostB);
|
|
265
|
+
|
|
266
|
+
client = spawnBridge({
|
|
267
|
+
bridgePort: clientBridgePort,
|
|
268
|
+
remoteHubPort: clientRemotePort,
|
|
269
|
+
workspacePath: path.join(tempRoot, 'client'),
|
|
270
|
+
authRoot: clientAuth,
|
|
271
|
+
label: 'client',
|
|
272
|
+
supabaseUrl: fakeSupabase.url,
|
|
273
|
+
follower: true
|
|
274
|
+
});
|
|
275
|
+
await waitForBridge(client);
|
|
276
|
+
|
|
277
|
+
const firstAgent = await waitForManagedAgent(client, hostAEndpoint, 'host-a');
|
|
278
|
+
assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
|
|
279
|
+
assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
|
|
280
|
+
assert.equal(firstAgent.manager, hostAEndpoint);
|
|
281
|
+
assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
|
|
282
|
+
await waitForConnectedDevice(hostA, 'host-a');
|
|
283
|
+
|
|
284
|
+
currentTarget = createRegistryTarget({
|
|
285
|
+
endpoint: hostBEndpoint,
|
|
286
|
+
endpointCandidates: [hostBEndpoint],
|
|
287
|
+
leaseId: 'lease-b'
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const secondAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b');
|
|
291
|
+
assert.equal(secondAgent.usingNpx, false, JSON.stringify(secondAgent));
|
|
292
|
+
assert.match(String(secondAgent.launcher || ''), /mindexec-remote-fast/i);
|
|
293
|
+
assert.equal(secondAgent.manager, hostBEndpoint);
|
|
294
|
+
assert.equal(secondAgent.leaseId, 'lease-b');
|
|
295
|
+
await waitForConnectedDevice(hostB, 'host-b');
|
|
296
|
+
|
|
297
|
+
currentTarget = {
|
|
298
|
+
...currentTarget,
|
|
299
|
+
active: false,
|
|
300
|
+
expires_at: new Date(Date.now() - 1000).toISOString()
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await waitFor(async () => {
|
|
304
|
+
const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
|
|
305
|
+
return result.payload?.running === false
|
|
306
|
+
&& String(result.payload?.lastError || '') === 'registry-inactive'
|
|
307
|
+
? result.payload
|
|
308
|
+
: null;
|
|
309
|
+
}, 15000, `registry inactive stop\n${client.details()}`);
|
|
310
|
+
|
|
311
|
+
assert.ok(fakeSupabase.requests.length >= 2, JSON.stringify(fakeSupabase.requests));
|
|
312
|
+
console.log('Remote registry follower smoke OK');
|
|
313
|
+
} finally {
|
|
314
|
+
if (client) await client.stop();
|
|
315
|
+
if (hostB) await hostB.stop();
|
|
316
|
+
if (hostA) await hostA.stop();
|
|
317
|
+
if (fakeSupabase) await fakeSupabase.stop();
|
|
318
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await main();
|