@mindexec/cli 0.2.151 → 0.2.153
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 +6 -5
- package/scripts/remote-fleet-render-smoke.mjs +38 -14
- package/scripts/remote-input-ws-smoke.mjs +263 -0
- package/scripts/remote-registry-follower-smoke.mjs +115 -9
- package/server.js +642 -5
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +314 -45
- 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.153",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,14 +21,15 @@
|
|
|
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-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",
|
|
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-input-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",
|
|
28
28
|
"test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
|
|
29
|
-
"test:remote:http": "node scripts/remote-http-smoke.mjs",
|
|
30
|
-
"test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
|
|
31
|
-
"test:remote:
|
|
29
|
+
"test:remote:http": "node scripts/remote-http-smoke.mjs",
|
|
30
|
+
"test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
|
|
31
|
+
"test:remote:input-ws": "node scripts/remote-input-ws-smoke.mjs",
|
|
32
|
+
"test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
|
|
32
33
|
"test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
|
|
33
34
|
"test:remote:registry": "node scripts/remote-registry-follower-smoke.mjs",
|
|
34
35
|
"test:remote:package": "node scripts/remote-agent-package-smoke.mjs",
|
|
@@ -528,7 +528,8 @@ async function loadCss3DManager() {
|
|
|
528
528
|
status: 200,
|
|
529
529
|
json: async () => ({
|
|
530
530
|
bridgeToken: 'render-smoke-bridge-token',
|
|
531
|
-
remoteFrameWsPath: '/api/remote/frames/ws'
|
|
531
|
+
remoteFrameWsPath: '/api/remote/frames/ws',
|
|
532
|
+
remoteInputWsPath: '/api/remote/input/ws'
|
|
532
533
|
})
|
|
533
534
|
};
|
|
534
535
|
},
|
|
@@ -1180,21 +1181,44 @@ try {
|
|
|
1180
1181
|
code: 'KeyA',
|
|
1181
1182
|
repeat: false
|
|
1182
1183
|
});
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1184
|
+
controlShell.dispatchEvent({
|
|
1185
|
+
type: 'keyup',
|
|
1186
|
+
key: 'A',
|
|
1187
|
+
code: 'KeyA',
|
|
1188
|
+
repeat: false
|
|
1189
|
+
});
|
|
1190
|
+
await wait(50);
|
|
1191
|
+
const inputWsSends = diagnostics.webSocketSends
|
|
1192
|
+
.filter(send => send.url.includes('/api/remote/input/ws'))
|
|
1193
|
+
.map(send => {
|
|
1194
|
+
try {
|
|
1195
|
+
return JSON.parse(send.payload);
|
|
1196
|
+
} catch {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
})
|
|
1200
|
+
.filter(Boolean);
|
|
1201
|
+
assert.ok(inputWsSends.length >= 3, 'control popup should send pointer and keyboard input over the input WebSocket');
|
|
1202
|
+
const pointerDown = inputWsSends.find(message => message.input?.type === 'pointerDown');
|
|
1203
|
+
const keyDown = inputWsSends.find(message => message.input?.type === 'keyDown');
|
|
1204
|
+
const keyUp = inputWsSends.find(message => message.input?.type === 'keyUp');
|
|
1188
1205
|
assert.ok(pointerDown);
|
|
1189
1206
|
assert.ok(keyDown);
|
|
1190
|
-
assert.
|
|
1191
|
-
assert.equal(pointerDown.
|
|
1192
|
-
assert.equal(pointerDown.
|
|
1193
|
-
assert.
|
|
1194
|
-
assert.ok(pointerDown.
|
|
1195
|
-
assert.ok(pointerDown.
|
|
1196
|
-
assert.
|
|
1197
|
-
assert.equal(keyDown.
|
|
1207
|
+
assert.ok(keyUp);
|
|
1208
|
+
assert.equal(pointerDown.nodeId, 'remote-fleet-render-smoke');
|
|
1209
|
+
assert.equal(pointerDown.deviceId, patchTarget.DeviceId);
|
|
1210
|
+
assert.equal(pointerDown.input.button, 'left');
|
|
1211
|
+
assert.ok(pointerDown.input.controlLeaseId);
|
|
1212
|
+
assert.ok(pointerDown.input.normalizedX >= 0 && pointerDown.input.normalizedX <= 1);
|
|
1213
|
+
assert.ok(pointerDown.input.normalizedY >= 0 && pointerDown.input.normalizedY <= 1);
|
|
1214
|
+
assert.equal(keyDown.input.key, 'A');
|
|
1215
|
+
assert.equal(keyDown.input.code, 'KeyA');
|
|
1216
|
+
assert.equal(keyUp.input.key, 'A');
|
|
1217
|
+
assert.equal(keyUp.input.code, 'KeyA');
|
|
1218
|
+
assert.equal(
|
|
1219
|
+
dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs').length,
|
|
1220
|
+
0,
|
|
1221
|
+
'WebSocket input path should not use the slower DotNet input fallback in the happy path');
|
|
1198
1222
|
manager.closeRemoteFleetControlPopupForTest('render-smoke');
|
|
1199
1223
|
await wait(20);
|
|
1200
1224
|
assert.equal(document.body.querySelector('[data-remote-fleet-control-popup="true"]'), null);
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { WebSocket } from 'ws';
|
|
9
|
+
|
|
10
|
+
const BRIDGE_TOKEN = 'remote-input-ws-smoke-token';
|
|
11
|
+
const PAIR_TOKEN = 'remote-input-ws-pair-token';
|
|
12
|
+
const DEVICE_ID = 'remote-input-ws-device';
|
|
13
|
+
|
|
14
|
+
function wait(ms) {
|
|
15
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function waitFor(predicate, timeoutMs = 10000, label = 'condition') {
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
21
|
+
const value = await predicate();
|
|
22
|
+
if (value) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
await wait(50);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error(`Timed out waiting for ${label}.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function findFreePort() {
|
|
32
|
+
return await new Promise((resolve, reject) => {
|
|
33
|
+
const server = net.createServer();
|
|
34
|
+
server.unref();
|
|
35
|
+
server.once('error', reject);
|
|
36
|
+
server.listen(0, '127.0.0.1', () => {
|
|
37
|
+
const address = server.address();
|
|
38
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
39
|
+
server.close(() => resolve(port));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchJson(url, options = {}) {
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
...options,
|
|
47
|
+
headers: {
|
|
48
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
49
|
+
'X-Bridge-Token': BRIDGE_TOKEN,
|
|
50
|
+
...(options.headers || {})
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const payload = await response.json().catch(() => null);
|
|
54
|
+
return { ok: response.ok, status: response.status, payload };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function waitForBridge(baseUrl, details) {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
while (Date.now() - startedAt < 30000) {
|
|
60
|
+
try {
|
|
61
|
+
const result = await fetchJson(`${baseUrl}/api/remote/status`);
|
|
62
|
+
if (result.ok && result.payload?.started === true) {
|
|
63
|
+
return result.payload;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Server is still starting.
|
|
67
|
+
}
|
|
68
|
+
await wait(100);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(`Timed out waiting for LocalBridge.\n${details()}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spawnBridge({ bridgePort, remoteHubPort }) {
|
|
75
|
+
const bridgeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
76
|
+
const env = {
|
|
77
|
+
...process.env,
|
|
78
|
+
BRIDGE_PORT: String(bridgePort),
|
|
79
|
+
BRIDGE_TOKEN,
|
|
80
|
+
BRIDGE_REQUIRE_TOKEN: '1',
|
|
81
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
82
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
83
|
+
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
84
|
+
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
85
|
+
NO_COLOR: '1'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
89
|
+
cwd: bridgeRoot,
|
|
90
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
env
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let stdout = '';
|
|
96
|
+
let stderr = '';
|
|
97
|
+
child.stdout.on('data', chunk => {
|
|
98
|
+
stdout += chunk.toString();
|
|
99
|
+
});
|
|
100
|
+
child.stderr.on('data', chunk => {
|
|
101
|
+
stderr += chunk.toString();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
105
|
+
const stop = async () => {
|
|
106
|
+
if (child.exitCode === null && !child.killed) {
|
|
107
|
+
child.kill('SIGTERM');
|
|
108
|
+
await Promise.race([exitPromise, wait(5000)]);
|
|
109
|
+
if (child.exitCode === null && !child.killed) {
|
|
110
|
+
child.kill('SIGKILL');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
baseUrl: `http://127.0.0.1:${bridgePort}`,
|
|
117
|
+
details: () => `stdout=${stdout}\nstderr=${stderr}`,
|
|
118
|
+
stop
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function openWebSocket(url) {
|
|
123
|
+
const ws = new WebSocket(url);
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
ws.once('open', resolve);
|
|
126
|
+
ws.once('error', reject);
|
|
127
|
+
});
|
|
128
|
+
return ws;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function main() {
|
|
132
|
+
const bridgePort = await findFreePort();
|
|
133
|
+
const remoteHubPort = await findFreePort();
|
|
134
|
+
const bridge = spawnBridge({ bridgePort, remoteHubPort });
|
|
135
|
+
let agentWs = null;
|
|
136
|
+
let inputWs = null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await waitForBridge(bridge.baseUrl, bridge.details);
|
|
140
|
+
|
|
141
|
+
const receivedCommands = [];
|
|
142
|
+
agentWs = await openWebSocket(`ws://127.0.0.1:${remoteHubPort}/remote-agent`);
|
|
143
|
+
agentWs.on('message', data => {
|
|
144
|
+
const text = Buffer.isBuffer(data) ? data.toString('utf8') : String(data || '');
|
|
145
|
+
if (!text.trim().startsWith('{')) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const message = JSON.parse(text);
|
|
150
|
+
if (message.command === 'input.control') {
|
|
151
|
+
receivedCommands.push(message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
agentWs.send(JSON.stringify({
|
|
155
|
+
type: 'hello',
|
|
156
|
+
pairToken: PAIR_TOKEN,
|
|
157
|
+
deviceId: DEVICE_ID,
|
|
158
|
+
deviceName: 'Remote Input WS Smoke',
|
|
159
|
+
hostname: 'remote-input-ws-smoke',
|
|
160
|
+
platform: process.platform,
|
|
161
|
+
arch: process.arch,
|
|
162
|
+
pid: process.pid,
|
|
163
|
+
agentVersion: '0.0.0-input-ws-smoke',
|
|
164
|
+
runtime: 'node-smoke',
|
|
165
|
+
capabilities: {
|
|
166
|
+
status: true,
|
|
167
|
+
thumbnail: true,
|
|
168
|
+
liveStream: true,
|
|
169
|
+
binaryFrames: true,
|
|
170
|
+
control: true,
|
|
171
|
+
computerAgent: true,
|
|
172
|
+
taskDispatch: true,
|
|
173
|
+
aiAssist: false
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
agentWs.send(JSON.stringify({
|
|
177
|
+
type: 'status',
|
|
178
|
+
status: {
|
|
179
|
+
uptimeSec: 1,
|
|
180
|
+
totalMem: 2,
|
|
181
|
+
freeMem: 1,
|
|
182
|
+
timestamp: new Date().toISOString()
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
await waitFor(async () => {
|
|
187
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
|
|
188
|
+
return result.payload?.devices?.find(device => device.deviceId === DEVICE_ID && device.connected === true);
|
|
189
|
+
}, 10000, 'device registration');
|
|
190
|
+
|
|
191
|
+
inputWs = await openWebSocket(`${bridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/input/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
|
|
192
|
+
inputWs.send(JSON.stringify({
|
|
193
|
+
type: 'input',
|
|
194
|
+
deviceId: DEVICE_ID,
|
|
195
|
+
requestId: 'pointer-move',
|
|
196
|
+
input: {
|
|
197
|
+
type: 'pointerMove',
|
|
198
|
+
normalizedX: 0.42,
|
|
199
|
+
normalizedY: 0.54,
|
|
200
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
inputWs.send(JSON.stringify({
|
|
204
|
+
type: 'input',
|
|
205
|
+
deviceId: DEVICE_ID,
|
|
206
|
+
requestId: 'key-down',
|
|
207
|
+
input: {
|
|
208
|
+
type: 'keyDown',
|
|
209
|
+
key: 'A',
|
|
210
|
+
code: 'KeyA',
|
|
211
|
+
repeat: false,
|
|
212
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
inputWs.send(JSON.stringify({
|
|
216
|
+
type: 'input',
|
|
217
|
+
deviceId: DEVICE_ID,
|
|
218
|
+
requestId: 'key-up',
|
|
219
|
+
input: {
|
|
220
|
+
type: 'keyUp',
|
|
221
|
+
key: 'A',
|
|
222
|
+
code: 'KeyA',
|
|
223
|
+
repeat: false,
|
|
224
|
+
controlLeaseId: 'input-ws-smoke-lease'
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
await waitFor(() => receivedCommands.length >= 3, 10000, 'input.control commands');
|
|
229
|
+
const commandTypes = receivedCommands.map(command => command.payload?.type);
|
|
230
|
+
assert.deepEqual(commandTypes.slice(0, 3), ['pointerMove', 'keyDown', 'keyUp']);
|
|
231
|
+
assert.equal(receivedCommands[0].payload.normalizedX, 0.42);
|
|
232
|
+
assert.equal(receivedCommands[0].payload.normalizedY, 0.54);
|
|
233
|
+
assert.equal(receivedCommands[1].payload.key, 'A');
|
|
234
|
+
assert.equal(receivedCommands[1].payload.code, 'KeyA');
|
|
235
|
+
assert.equal(receivedCommands[2].payload.key, 'A');
|
|
236
|
+
assert.equal(receivedCommands[2].payload.code, 'KeyA');
|
|
237
|
+
|
|
238
|
+
const status = await fetchJson(`${bridge.baseUrl}/api/status?remoteInput=ws`);
|
|
239
|
+
assert.equal(status.ok, true);
|
|
240
|
+
assert.equal(status.payload?.remoteInputWsPath, '/api/remote/input/ws');
|
|
241
|
+
assert.ok(status.payload?.remoteInputWs?.clientCount >= 1, JSON.stringify(status.payload?.remoteInputWs));
|
|
242
|
+
assert.ok(status.payload?.remoteInputWs?.inputsQueued >= 3, JSON.stringify(status.payload?.remoteInputWs));
|
|
243
|
+
assert.ok(status.payload?.remoteInputWs?.pointerMovesQueued >= 1, JSON.stringify(status.payload?.remoteInputWs));
|
|
244
|
+
assert.ok(status.payload?.remoteInputWs?.keyEventsQueued >= 2, JSON.stringify(status.payload?.remoteInputWs));
|
|
245
|
+
assert.equal(status.payload?.remoteInputWs?.lastDeviceId, DEVICE_ID);
|
|
246
|
+
assert.equal(status.payload?.remoteInputWs?.lastInputType, 'keyUp');
|
|
247
|
+
|
|
248
|
+
console.log('Remote input WebSocket smoke OK');
|
|
249
|
+
} finally {
|
|
250
|
+
if (inputWs && inputWs.readyState === WebSocket.OPEN) {
|
|
251
|
+
inputWs.close();
|
|
252
|
+
}
|
|
253
|
+
if (agentWs && agentWs.readyState === WebSocket.OPEN) {
|
|
254
|
+
agentWs.close();
|
|
255
|
+
}
|
|
256
|
+
await bridge.stop();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
main().catch(error => {
|
|
261
|
+
console.error(error);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
});
|
|
@@ -83,10 +83,17 @@ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active =
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function startFakeSupabase(getTarget) {
|
|
86
|
+
function startFakeSupabase(getTarget, setTarget = null) {
|
|
87
87
|
const requests = [];
|
|
88
88
|
const realtimeClients = new Set();
|
|
89
|
-
const server = createServer((req, res) => {
|
|
89
|
+
const server = createServer(async (req, res) => {
|
|
90
|
+
const readBody = () => new Promise(resolve => {
|
|
91
|
+
let body = '';
|
|
92
|
+
req.on('data', chunk => {
|
|
93
|
+
body += chunk.toString();
|
|
94
|
+
});
|
|
95
|
+
req.on('end', () => resolve(body));
|
|
96
|
+
});
|
|
90
97
|
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
91
98
|
requests.push({
|
|
92
99
|
method: req.method,
|
|
@@ -107,6 +114,40 @@ function startFakeSupabase(getTarget) {
|
|
|
107
114
|
return;
|
|
108
115
|
}
|
|
109
116
|
|
|
117
|
+
if (req.method === 'POST' && parsed.pathname === '/rest/v1/rpc/set_remote_host_target') {
|
|
118
|
+
assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
|
|
119
|
+
assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
|
|
120
|
+
const body = JSON.parse(await readBody() || '{}');
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const existing = getTarget();
|
|
123
|
+
const sameLease = existing
|
|
124
|
+
&& String(existing.lease_id || '') === String(body.p_lease_id || '')
|
|
125
|
+
&& String(existing.host_instance_id || '') === String(body.p_host_instance_id || '');
|
|
126
|
+
const expired = !existing?.expires_at || Date.parse(existing.expires_at) <= now;
|
|
127
|
+
if (existing?.active === true && !sameLease && !expired && body.p_takeover !== true) {
|
|
128
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
129
|
+
res.end(JSON.stringify([{ ok: false, reason: 'host-target-taken' }]));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setTarget?.({
|
|
134
|
+
user_id: USER_ID,
|
|
135
|
+
active: true,
|
|
136
|
+
endpoint: String(body.p_endpoint || ''),
|
|
137
|
+
endpoint_candidates: Array.isArray(body.p_endpoint_candidates)
|
|
138
|
+
? body.p_endpoint_candidates
|
|
139
|
+
: [String(body.p_endpoint || '')].filter(Boolean),
|
|
140
|
+
pair_token: String(body.p_pair_token || ''),
|
|
141
|
+
lease_id: String(body.p_lease_id || ''),
|
|
142
|
+
node_id: String(body.p_node_id || ''),
|
|
143
|
+
host_instance_id: String(body.p_host_instance_id || ''),
|
|
144
|
+
expires_at: String(body.p_expires_at || new Date(Date.now() + 60_000).toISOString())
|
|
145
|
+
});
|
|
146
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
147
|
+
res.end(JSON.stringify([{ ok: true, reason: 'ok' }]));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
110
151
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
111
152
|
res.end(JSON.stringify({ error: 'not-found' }));
|
|
112
153
|
});
|
|
@@ -230,7 +271,7 @@ function startFakeSupabase(getTarget) {
|
|
|
230
271
|
});
|
|
231
272
|
}
|
|
232
273
|
|
|
233
|
-
function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false }) {
|
|
274
|
+
function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false, publicHost = '' }) {
|
|
234
275
|
const child = spawn(process.execPath, ['server.js'], {
|
|
235
276
|
cwd: LOCAL_BRIDGE_DIR,
|
|
236
277
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -243,6 +284,7 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
|
|
|
243
284
|
MINDEXEC_REMOTE_HUB: '1',
|
|
244
285
|
REMOTE_HUB_HOST: '127.0.0.1',
|
|
245
286
|
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
287
|
+
REMOTE_HUB_PUBLIC_HOST: publicHost,
|
|
246
288
|
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
247
289
|
WORKSPACE_PATH: workspacePath,
|
|
248
290
|
MINDEXEC_AUTH_DATA_ROOT: authRoot,
|
|
@@ -251,6 +293,7 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
|
|
|
251
293
|
MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
|
|
252
294
|
MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS: '250',
|
|
253
295
|
MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS: '100',
|
|
296
|
+
MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS: '5000',
|
|
254
297
|
SUPABASE_URL: supabaseUrl,
|
|
255
298
|
SUPABASE_KEY,
|
|
256
299
|
NO_COLOR: '1'
|
|
@@ -353,10 +396,13 @@ function killProcess(pid) {
|
|
|
353
396
|
async function main() {
|
|
354
397
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
|
|
355
398
|
let fakeSupabase = null;
|
|
399
|
+
let renewHost = null;
|
|
356
400
|
let hostA = null;
|
|
357
401
|
let hostB = null;
|
|
358
402
|
let client = null;
|
|
359
403
|
|
|
404
|
+
const renewHostBridgePort = await findFreePort();
|
|
405
|
+
const renewHostRemotePort = await findFreePort();
|
|
360
406
|
const hostABridgePort = await findFreePort();
|
|
361
407
|
const hostARemotePort = await findFreePort();
|
|
362
408
|
const hostBBridgePort = await findFreePort();
|
|
@@ -364,23 +410,82 @@ async function main() {
|
|
|
364
410
|
const clientBridgePort = await findFreePort();
|
|
365
411
|
const clientRemotePort = await findFreePort();
|
|
366
412
|
const stalePort = await findFreePort();
|
|
413
|
+
const renewPublicHost = '192.0.2.10';
|
|
414
|
+
const renewPublicEndpoint = `${renewPublicHost}:${renewHostRemotePort}`;
|
|
367
415
|
const hostAEndpoint = `127.0.0.1:${hostARemotePort}`;
|
|
368
416
|
const hostBEndpoint = `127.0.0.1:${hostBRemotePort}`;
|
|
369
417
|
const staleEndpoint = `127.0.0.1:${stalePort}`;
|
|
370
|
-
let currentTarget =
|
|
371
|
-
endpoint: hostAEndpoint,
|
|
372
|
-
endpointCandidates: [staleEndpoint, hostAEndpoint],
|
|
373
|
-
leaseId: 'lease-a'
|
|
374
|
-
});
|
|
418
|
+
let currentTarget = null;
|
|
375
419
|
|
|
376
420
|
try {
|
|
377
|
-
fakeSupabase = await startFakeSupabase(
|
|
421
|
+
fakeSupabase = await startFakeSupabase(
|
|
422
|
+
() => currentTarget,
|
|
423
|
+
target => {
|
|
424
|
+
currentTarget = target;
|
|
425
|
+
});
|
|
378
426
|
|
|
427
|
+
const renewHostAuth = path.join(tempRoot, 'auth-renew-host');
|
|
379
428
|
const hostAAuth = path.join(tempRoot, 'auth-host-a');
|
|
380
429
|
const hostBAuth = path.join(tempRoot, 'auth-host-b');
|
|
381
430
|
const clientAuth = path.join(tempRoot, 'auth-client');
|
|
431
|
+
await writeSession(renewHostAuth);
|
|
382
432
|
await writeSession(clientAuth);
|
|
383
433
|
|
|
434
|
+
renewHost = spawnBridge({
|
|
435
|
+
bridgePort: renewHostBridgePort,
|
|
436
|
+
remoteHubPort: renewHostRemotePort,
|
|
437
|
+
workspacePath: path.join(tempRoot, 'renew-host'),
|
|
438
|
+
authRoot: renewHostAuth,
|
|
439
|
+
label: 'renew-host',
|
|
440
|
+
supabaseUrl: fakeSupabase.url,
|
|
441
|
+
follower: true,
|
|
442
|
+
publicHost: renewPublicHost
|
|
443
|
+
});
|
|
444
|
+
await waitForBridge(renewHost);
|
|
445
|
+
|
|
446
|
+
const renewSetHost = await fetchJson(`${renewHost.baseUrl}/api/remote/host-target`, {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
token: BRIDGE_TOKEN,
|
|
449
|
+
body: JSON.stringify({
|
|
450
|
+
nodeId: 'remote-registry-renew-node',
|
|
451
|
+
enabled: true,
|
|
452
|
+
leaseMs: 60000
|
|
453
|
+
})
|
|
454
|
+
});
|
|
455
|
+
assert.equal(renewSetHost.ok, true, JSON.stringify(renewSetHost.payload));
|
|
456
|
+
assert.equal(renewSetHost.payload?.ok, true, JSON.stringify(renewSetHost.payload));
|
|
457
|
+
assert.equal(renewSetHost.payload?.active, true, JSON.stringify(renewSetHost.payload));
|
|
458
|
+
assert.equal(renewSetHost.payload?.hostTargetRenew?.status, 'active', JSON.stringify(renewSetHost.payload?.hostTargetRenew));
|
|
459
|
+
assert.equal(currentTarget?.endpoint, renewPublicEndpoint, JSON.stringify(currentTarget));
|
|
460
|
+
assert.equal(currentTarget?.node_id, 'remote-registry-renew-node', JSON.stringify(currentTarget));
|
|
461
|
+
await waitFor(() => {
|
|
462
|
+
const publishCount = fakeSupabase.requests.filter(request =>
|
|
463
|
+
request.method === 'POST'
|
|
464
|
+
&& request.pathname === '/rest/v1/rpc/set_remote_host_target').length;
|
|
465
|
+
return publishCount >= 2 ? publishCount : null;
|
|
466
|
+
}, 8000, `host-target auto renew publish\n${renewHost.details()}`);
|
|
467
|
+
const renewStatus = await fetchJson(`${renewHost.baseUrl}/api/status`);
|
|
468
|
+
assert.equal(renewStatus.payload?.remoteHostTargetRenew?.status, 'active', JSON.stringify(renewStatus.payload?.remoteHostTargetRenew));
|
|
469
|
+
assert.equal(renewStatus.payload?.remoteHostTargetRenew?.endpoint, renewPublicEndpoint, JSON.stringify(renewStatus.payload?.remoteHostTargetRenew));
|
|
470
|
+
|
|
471
|
+
const renewClear = await fetchJson(`${renewHost.baseUrl}/api/remote/host-target`, {
|
|
472
|
+
method: 'DELETE',
|
|
473
|
+
token: BRIDGE_TOKEN,
|
|
474
|
+
body: JSON.stringify({
|
|
475
|
+
nodeId: 'remote-registry-renew-node'
|
|
476
|
+
})
|
|
477
|
+
});
|
|
478
|
+
assert.equal(renewClear.ok, true, JSON.stringify(renewClear.payload));
|
|
479
|
+
assert.equal(renewClear.payload?.ok, true, JSON.stringify(renewClear.payload));
|
|
480
|
+
await renewHost.stop();
|
|
481
|
+
renewHost = null;
|
|
482
|
+
|
|
483
|
+
currentTarget = createRegistryTarget({
|
|
484
|
+
endpoint: hostAEndpoint,
|
|
485
|
+
endpointCandidates: [staleEndpoint, hostAEndpoint],
|
|
486
|
+
leaseId: 'lease-a'
|
|
487
|
+
});
|
|
488
|
+
|
|
384
489
|
hostA = spawnBridge({
|
|
385
490
|
bridgePort: hostABridgePort,
|
|
386
491
|
remoteHubPort: hostARemotePort,
|
|
@@ -465,6 +570,7 @@ async function main() {
|
|
|
465
570
|
if (client) await client.stop();
|
|
466
571
|
if (hostB) await hostB.stop();
|
|
467
572
|
if (hostA) await hostA.stop();
|
|
573
|
+
if (renewHost) await renewHost.stop();
|
|
468
574
|
if (fakeSupabase) await fakeSupabase.stop();
|
|
469
575
|
await rm(tempRoot, { recursive: true, force: true });
|
|
470
576
|
}
|