Two mitigations for local multi-instance contention over the shared relay identity: - A standby instance now waits a 2-minute grace period after the host claim frees before taking over, so a cleanly restarting host (app update or relaunch) — which reclaims at boot with no wait — always wins the restart window instead of stranding paired devices on another process. - Dev instances never host the relay passively: dev scripts set OPENCHAMBER_RELAY_HOST=off and the Electron dev shell is detected via OPENCHAMBER_ELECTRON_DEV. Explicit enable/pairing on such an instance still force-claims; OPENCHAMBER_RELAY_HOST=on overrides.
70 lines
2.5 KiB
JavaScript
70 lines
2.5 KiB
JavaScript
import { describe, it, expect, vi } from 'vitest';
|
|
import crypto from 'node:crypto';
|
|
|
|
import { createRelayService } from './service.js';
|
|
|
|
const makeService = (options = {}) => {
|
|
// In-memory settings store with a pre-seeded relay identity so the service
|
|
// never regenerates a signing key during the test.
|
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
let settings = {
|
|
relaySigningKey: {
|
|
privateJwk: privateKey.export({ format: 'jwk' }),
|
|
publicJwk: publicKey.export({ format: 'jwk' }),
|
|
},
|
|
privateRelay: { enabled: true, relayUrl: 'wss://relay.example.test/ws' },
|
|
...options.settings,
|
|
};
|
|
const hostLock = {
|
|
tryClaim: vi.fn(() => true),
|
|
forceClaim: vi.fn(() => true),
|
|
holdsClaim: vi.fn(() => true),
|
|
liveClaimantPid: vi.fn(() => null),
|
|
release: vi.fn(),
|
|
};
|
|
const service = createRelayService({
|
|
crypto,
|
|
readSettingsFromDiskMigrated: async () => settings,
|
|
writeSettingsToDisk: async (next) => { settings = next; },
|
|
readSettingsStrict: async () => settings,
|
|
getLocalPort: () => 0,
|
|
hasRelayDemand: options.hasRelayDemand ?? (async () => true),
|
|
hostLock,
|
|
allowPassiveHost: options.allowPassiveHost,
|
|
logger: { warn: () => {} },
|
|
});
|
|
return { service, hostLock, getSettings: () => settings };
|
|
};
|
|
|
|
describe('relay service passive hosting', () => {
|
|
it('never claims or starts the host passively when passive hosting is disabled', async () => {
|
|
const { service, hostLock } = makeService({ allowPassiveHost: false });
|
|
try {
|
|
await service.startIfEnabled();
|
|
let status = await service.getStatus();
|
|
expect(status.state).toBe('standby');
|
|
expect(hostLock.tryClaim).not.toHaveBeenCalled();
|
|
expect(hostLock.forceClaim).not.toHaveBeenCalled();
|
|
|
|
await service.reconcile();
|
|
status = await service.getStatus();
|
|
expect(status.state).toBe('standby');
|
|
expect(status.lastError).toContain('passive relay hosting is disabled');
|
|
expect(hostLock.tryClaim).not.toHaveBeenCalled();
|
|
} finally {
|
|
service.stop();
|
|
}
|
|
});
|
|
|
|
it('force-claims for an explicit pairing even when passive hosting is disabled', async () => {
|
|
const { service, hostLock } = makeService({ allowPassiveHost: false });
|
|
try {
|
|
const candidate = await service.ensureEnabledForPairing();
|
|
expect(candidate?.type).toBe('relay');
|
|
expect(hostLock.forceClaim).toHaveBeenCalled();
|
|
} finally {
|
|
service.stop();
|
|
}
|
|
});
|
|
});
|