* fix: avoid false offline results in Electron instance probes Use Chromium networking with one abortable deadline per direct-host attempt. Retry unreachable selector probes once while preserving identity, auth and compatibility checks, and clean up response bodies on every exit. Add regression coverage and document the native probe invariants. * fix: declare Electron probe's zod dependency Declare the existing workspace zod version as an Electron runtime dependency so probe imports do not rely on root hoisting. Update only the matching lockfile workspace entry, without changing resolved versions or parsing behavior.
193 lines
7.1 KiB
JavaScript
193 lines
7.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { probeElectronHostWithDeadline } from './electron-host-probe.mjs';
|
|
|
|
const response = (status, payload = null) => ({
|
|
status,
|
|
ok: status >= 200 && status < 300,
|
|
json: async () => payload,
|
|
});
|
|
|
|
const compatibleVersion = {
|
|
status: 'ok',
|
|
compatibility: {
|
|
capabilities: ['api.runtime-url.v1'],
|
|
apiVersion: 1,
|
|
minClientApiVersion: 1,
|
|
},
|
|
};
|
|
|
|
const baseProbe = (overrides = {}) => probeElectronHostWithDeadline({
|
|
url: 'https://instance.example',
|
|
timeoutMs: 2_000,
|
|
chromiumFetch: async (url) => url.endsWith('/api/version')
|
|
? response(200, compatibleVersion)
|
|
: response(200, {}),
|
|
isReady: () => true,
|
|
...overrides,
|
|
});
|
|
|
|
test('uses the Chromium transport as the authoritative ready-state transport', async () => {
|
|
const calls = [];
|
|
const result = await baseProbe({
|
|
chromiumFetch: async (url) => {
|
|
calls.push(url);
|
|
return url.endsWith('/api/version') ? response(200, compatibleVersion) : response(200, {});
|
|
},
|
|
});
|
|
|
|
assert.equal(result.status, 'ok');
|
|
assert.deepEqual(calls.map((url) => new URL(url).pathname), ['/api/version', '/auth/session']);
|
|
});
|
|
|
|
test('shares one absolute deadline across version and session requests', async () => {
|
|
let expire;
|
|
let scheduled = 0;
|
|
let clock = 0;
|
|
const signals = [];
|
|
const resultPromise = baseProbe({
|
|
now: () => clock,
|
|
scheduleTimeout: (callback) => {
|
|
scheduled += 1;
|
|
expire = callback;
|
|
return 1;
|
|
},
|
|
cancelTimeout: () => {},
|
|
chromiumFetch: async (url, options) => {
|
|
signals.push(options.signal);
|
|
if (url.endsWith('/api/version')) return response(200, compatibleVersion);
|
|
return new Promise((_, reject) => options.signal.addEventListener('abort', () => reject(new Error('aborted'))));
|
|
},
|
|
});
|
|
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
clock = 2_000;
|
|
expire();
|
|
const result = await resultPromise;
|
|
assert.equal(scheduled, 1);
|
|
assert.equal(new Set(signals).size, 1);
|
|
assert.deepEqual(result, { status: 'unreachable', latencyMs: 2_000 });
|
|
});
|
|
|
|
test('checks unauthenticated identity before sending sanitized bearer headers', async () => {
|
|
const calls = [];
|
|
const result = await baseProbe({
|
|
expectedServerId: 'server-a',
|
|
clientToken: 'secret-token',
|
|
requestHeaders: { 'X-Instance': 'remote', Authorization: 'attacker' },
|
|
chromiumFetch: async (url, options) => {
|
|
calls.push({ path: new URL(url).pathname, headers: options.headers });
|
|
if (url.endsWith('/health')) return response(200, { serverId: 'server-a' });
|
|
if (url.endsWith('/api/version')) return response(200, compatibleVersion);
|
|
return response(200, {});
|
|
},
|
|
});
|
|
|
|
assert.equal(result.status, 'ok');
|
|
assert.deepEqual(calls.map((call) => call.path), ['/health', '/api/version', '/auth/session']);
|
|
assert.equal(calls[0].headers.Authorization, undefined);
|
|
assert.equal(calls[1].headers.Authorization, 'Bearer secret-token');
|
|
assert.equal(calls[1].headers['X-Instance'], 'remote');
|
|
});
|
|
|
|
for (const [name, versionResponse, expected] of [
|
|
['401 auth', response(401), 'auth'],
|
|
['403 auth', response(403), 'auth'],
|
|
['wrong service', response(200, {}), 'wrong-service'],
|
|
['null payload', response(200, null), 'wrong-service'],
|
|
['string compatibility', response(200, { status: 'ok', compatibility: 'yes' }), 'wrong-service'],
|
|
['boolean compatibility', response(200, { status: 'ok', compatibility: true }), 'wrong-service'],
|
|
['numeric compatibility', response(200, { status: 'ok', compatibility: 1 }), 'wrong-service'],
|
|
['array compatibility', response(200, { status: 'ok', compatibility: [] }), 'incompatible'],
|
|
['missing capability', response(200, { ...compatibleVersion, compatibility: { ...compatibleVersion.compatibility, capabilities: [] } }), 'incompatible'],
|
|
['newer API', response(200, { ...compatibleVersion, compatibility: { ...compatibleVersion.compatibility, apiVersion: 2 } }), 'update-recommended'],
|
|
['newer minimum client', response(200, { ...compatibleVersion, compatibility: { ...compatibleVersion.compatibility, minClientApiVersion: 2 } }), 'update-recommended'],
|
|
]) {
|
|
test(`preserves authoritative ${name} classification`, async () => {
|
|
const result = await baseProbe({ chromiumFetch: async () => versionResponse });
|
|
assert.equal(result.status, expected);
|
|
});
|
|
}
|
|
|
|
test('rejects an explicit identity mismatch before bearer-bearing requests', async () => {
|
|
let calls = 0;
|
|
const result = await baseProbe({
|
|
expectedServerId: 'server-a',
|
|
clientToken: 'secret-token',
|
|
chromiumFetch: async () => {
|
|
calls += 1;
|
|
return response(200, { serverId: 'server-b' });
|
|
},
|
|
});
|
|
assert.equal(result.status, 'wrong-service');
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
test('rejects Electron manual-redirect errors before bearer-bearing requests', async () => {
|
|
let calls = 0;
|
|
const result = await baseProbe({
|
|
expectedServerId: 'expected',
|
|
clientToken: 'fixture-only',
|
|
chromiumFetch: async (_url, options) => {
|
|
calls++;
|
|
assert.equal(options.redirect, 'manual');
|
|
assert.equal(options.headers.Authorization, undefined);
|
|
throw new Error('Redirect was cancelled');
|
|
},
|
|
});
|
|
assert.equal(result.status, 'wrong-service');
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
for (const serverId of [undefined, null, 123, true, {}, [], '', ' ', ' server-a ']) {
|
|
test(`preserves optional health identity parsing for ${JSON.stringify(serverId)}`, async () => {
|
|
const result = await baseProbe({
|
|
expectedServerId: ' server-a ',
|
|
chromiumFetch: async (url) => url.endsWith('/health')
|
|
? response(200, { serverId })
|
|
: url.endsWith('/api/version') ? response(200, compatibleVersion) : response(200),
|
|
});
|
|
assert.equal(result.status, 'ok');
|
|
});
|
|
}
|
|
|
|
test('malformed version JSON remains wrong-service', async () => {
|
|
const result = await baseProbe({ chromiumFetch: async () => ({
|
|
...response(200), json: async () => { throw new SyntaxError('invalid fixture JSON'); },
|
|
}) });
|
|
assert.equal(result.status, 'wrong-service');
|
|
});
|
|
|
|
test('non-string expected identity and token retain upstream ignore semantics', async () => {
|
|
const calls = [];
|
|
const result = await baseProbe({
|
|
expectedServerId: 123,
|
|
clientToken: 123,
|
|
chromiumFetch: async (url, options) => {
|
|
calls.push(new URL(url).pathname);
|
|
assert.equal(options.headers.Authorization, undefined);
|
|
return url.endsWith('/api/version') ? response(200, compatibleVersion) : response(200);
|
|
},
|
|
});
|
|
assert.equal(result.status, 'ok');
|
|
assert.deepEqual(calls, ['/api/version', '/auth/session']);
|
|
});
|
|
|
|
test('aborts requests and cancels unused bodies before clearing the timer', async () => {
|
|
const events = [];
|
|
const result = await baseProbe({
|
|
scheduleTimeout: () => 1,
|
|
cancelTimeout: () => { events.push('timer-cleared'); },
|
|
chromiumFetch: async (_url, { signal }) => {
|
|
signal.addEventListener('abort', () => events.push('aborted'));
|
|
return {
|
|
...response(403),
|
|
body: { locked: false, cancel: async () => { events.push('body-cancelled'); } },
|
|
};
|
|
},
|
|
});
|
|
assert.equal(result.status, 'auth');
|
|
assert.deepEqual(events, ['aborted', 'body-cancelled', 'timer-cleared']);
|
|
});
|