Add browser P2P connection check

This commit is contained in:
vorotamoroz
2026-07-31 14:13:34 +00:00
parent e32e3f545e
commit 48c398948a
20 changed files with 2710 additions and 8 deletions
@@ -0,0 +1,131 @@
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
import { DEFAULT_SETTINGS, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
import { describe, expect, it } from "vitest";
import {
P2P_CHECK_APP_ID,
generateP2PCheckSetup,
resolveLocalP2PCheckRelayOverride,
} from "@/apps/webpeer/src/P2PCheckSetup";
describe("P2P connection-check setup", () => {
it.each(["desktop", "mobile"] as const)(
"creates an isolated, disposable %s Setup URI and diagnostic browser peer",
async (target) => {
const generated = await generateP2PCheckSetup(target);
const decoded = await decodeSettingsFromSetupURI(generated.setupURI, generated.setupPassphrase);
expect(decoded).not.toBe(false);
if (decoded === false) {
throw new Error("The generated Setup URI could not be decoded");
}
const effective = { ...DEFAULT_SETTINGS, ...decoded };
expect(generated.target).toBe(target);
expect(generated.setupPassphrase).toMatch(/^[a-z2-9]{4}(?:-[a-z2-9]{4}){3}$/);
expect(generated.setupURI).toMatch(/^obsidian:\/\/setuplivesync\?settings=/);
expect(effective).toEqual(
expect.objectContaining({
remoteType: REMOTE_P2P,
isConfigured: true,
encrypt: true,
usePathObfuscation: true,
P2P_Enabled: true,
P2P_AppID: P2P_CHECK_APP_ID,
P2P_roomID: generated.groupId,
P2P_AutoStart: true,
P2P_AutoBroadcast: false,
})
);
expect(decoded.P2P_DevicePeerName).toBeUndefined();
expect(decoded.P2P_useDiagRTC).toBeUndefined();
expect(effective.passphrase).toHaveLength(32);
expect(effective.P2P_passphrase).toHaveLength(32);
expect(effective.passphrase).not.toBe(effective.P2P_passphrase);
expect(effective.P2P_AutoAccepting).toBe(DEFAULT_SETTINGS.P2P_AutoAccepting);
expect(effective.P2P_AutoSyncPeers).toBe("");
expect(effective.P2P_AutoWatchPeers).toBe("");
expect(effective.P2P_SyncOnReplication).toBe("");
const remoteConfigurations = Object.values(decoded.remoteConfigurations ?? {});
expect(remoteConfigurations).toHaveLength(1);
expect(decoded.activeConfigurationId).toBe(remoteConfigurations[0].id);
expect(decoded.P2P_ActiveRemoteConfigurationId).toBe(remoteConfigurations[0].id);
const deviceRemote = ConnectionStringParser.parse(remoteConfigurations[0].uri);
expect(deviceRemote).toEqual(
expect.objectContaining({
type: "p2p",
settings: expect.objectContaining({
P2P_AutoStart: true,
P2P_AutoBroadcast: false,
}),
})
);
expect("P2P_useDiagRTC" in deviceRemote.settings).toBe(false);
expect(generated.browserSettings).toEqual(
expect.objectContaining({
remoteType: REMOTE_P2P,
P2P_Enabled: true,
P2P_AppID: P2P_CHECK_APP_ID,
P2P_roomID: effective.P2P_roomID,
P2P_passphrase: effective.P2P_passphrase,
passphrase: effective.passphrase,
P2P_AutoStart: false,
P2P_AutoBroadcast: false,
P2P_useDiagRTC: true,
})
);
expect(generated.browserDeviceName).toMatch(/^p2p-check-browser-(?:desktop|mobile)-/);
const browserConfigurations = Object.values(generated.browserSettings.remoteConfigurations ?? {});
expect(browserConfigurations).toHaveLength(1);
const browserRemote = ConnectionStringParser.parse(browserConfigurations[0].uri);
expect(browserRemote).toEqual(
expect.objectContaining({
type: "p2p",
settings: expect.objectContaining({
P2P_AutoStart: false,
P2P_AutoBroadcast: false,
}),
})
);
expect("P2P_useDiagRTC" in browserRemote.settings).toBe(false);
}
);
it("creates independent rooms and secrets for separate checks", async () => {
const first = await generateP2PCheckSetup("desktop");
const second = await generateP2PCheckSetup("desktop");
expect(second.groupId).not.toBe(first.groupId);
expect(second.setupPassphrase).not.toBe(first.setupPassphrase);
expect(second.browserSettings.P2P_passphrase).not.toBe(first.browserSettings.P2P_passphrase);
expect(second.browserSettings.passphrase).not.toBe(first.browserSettings.passphrase);
});
it("uses the same explicitly selected relay for the Setup URI and browser peer", async () => {
const relay = "ws://127.0.0.1:4010/";
const generated = await generateP2PCheckSetup("desktop", { relay });
const decoded = await decodeSettingsFromSetupURI(generated.setupURI, generated.setupPassphrase);
expect(decoded).not.toBe(false);
if (decoded === false) {
throw new Error("The generated Setup URI could not be decoded");
}
expect(generated.relay).toBe(relay);
expect(decoded.P2P_relays).toBe(relay);
expect(generated.browserSettings.P2P_relays).toBe(relay);
});
it("accepts a relay query override only from a loopback-served check page", () => {
const search = "?relay=ws%3A%2F%2F127.0.0.1%3A4010%2F";
expect(resolveLocalP2PCheckRelayOverride({ hostname: "127.0.0.1", search })).toBe("ws://127.0.0.1:4010/");
expect(resolveLocalP2PCheckRelayOverride({ hostname: "localhost", search })).toBe("ws://127.0.0.1:4010/");
expect(resolveLocalP2PCheckRelayOverride({ hostname: "example.com", search })).toBeUndefined();
expect(
resolveLocalP2PCheckRelayOverride({ hostname: "127.0.0.1", search: "?relay=https%3A%2F%2Fexample.com" })
).toBeUndefined();
});
});
@@ -0,0 +1,179 @@
import { describe, expect, it } from "vitest";
import {
EMPTY_P2P_CHECK_DIAGNOSTICS,
P2P_CHECK_OBSERVATION_MILLISECONDS,
captureP2PAdditionalCheckBaseline,
deriveP2PAdditionalCheckProgress,
deriveP2PCheckOutcome,
} from "@/apps/webpeer/src/P2PCheckState";
describe("P2P connection-check outcome", () => {
it("waits for a target after the browser monitor joins the room", () => {
expect(deriveP2PCheckOutcome(EMPTY_P2P_CHECK_DIAGNOSTICS, true, 5_000)).toBe("waiting");
});
it("reports an active negotiation before any connection succeeds", () => {
expect(
deriveP2PCheckOutcome(
{
...EMPTY_P2P_CHECK_DIAGNOSTICS,
totalNewConnections: 1,
details: {
"rtc-1": {
connectionState: "connecting",
iceConnectionState: "checking",
},
},
},
true,
10_000
)
).toBe("connecting");
});
it("keeps a failed attempt retryable until the observation period expires", () => {
const diagnostics = {
...EMPTY_P2P_CHECK_DIAGNOSTICS,
totalNewConnections: 1,
totalFailedConnections: 1,
};
expect(deriveP2PCheckOutcome(diagnostics, true, 15_000)).toBe("retrying");
expect(deriveP2PCheckOutcome(diagnostics, true, P2P_CHECK_OBSERVATION_MILLISECONDS)).toBe("inconclusive");
});
it("lets a later success take precedence over failures and closure", () => {
expect(
deriveP2PCheckOutcome(
{
...EMPTY_P2P_CHECK_DIAGNOSTICS,
totalNewConnections: 2,
totalFailedConnections: 1,
totalSuccessfulConnections: 1,
totalClosedConnections: 1,
},
true,
P2P_CHECK_OBSERVATION_MILLISECONDS * 2
)
).toBe("connected");
});
it("distinguishes an idle page and a monitor-start error", () => {
expect(deriveP2PCheckOutcome(EMPTY_P2P_CHECK_DIAGNOSTICS, false, 0)).toBe("idle");
expect(deriveP2PCheckOutcome(EMPTY_P2P_CHECK_DIAGNOSTICS, false, 0, true)).toBe("error");
});
});
describe("same-room additional-device progress", () => {
const firstDeviceDiagnostics = {
...EMPTY_P2P_CHECK_DIAGNOSTICS,
totalNewConnections: 1,
totalSuccessfulConnections: 1,
details: {
"rtc-a": {
connectionState: "connected" as const,
iceConnectionState: "connected" as const,
},
},
};
it("captures the counter and currently active connection baseline", () => {
expect(captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics)).toEqual({
activeConnectionIds: ["rtc-a"],
totalClosedConnections: 0,
totalFailedConnections: 0,
totalNewConnections: 1,
totalSuccessfulConnections: 1,
});
});
it("does not mistake a reconnect from the first peer for an additional device", () => {
const baseline = captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics);
const progress = deriveP2PAdditionalCheckProgress(
{
...firstDeviceDiagnostics,
totalNewConnections: 2,
totalSuccessfulConnections: 2,
details: {
"rtc-a": {
connectionState: "closed",
iceConnectionState: "closed",
},
"rtc-b": {
connectionState: "connected",
iceConnectionState: "connected",
},
},
},
baseline,
10_000
);
expect(progress).toEqual({
activeConnections: 0,
closedConnections: 0,
failedConnections: 0,
newConnections: 1,
newActiveConnectionIds: ["rtc-b"],
outcome: "negotiating",
successfulConnections: 1,
});
});
it("requires both a new successful state and another simultaneous active connection", () => {
const baseline = captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics);
expect(
deriveP2PAdditionalCheckProgress(
{
...firstDeviceDiagnostics,
details: {
...firstDeviceDiagnostics.details,
"rtc-b": {
connectionState: "connected",
iceConnectionState: "connected",
},
},
},
baseline,
10_000
).outcome
).toBe("negotiating");
expect(
deriveP2PAdditionalCheckProgress(
{
...firstDeviceDiagnostics,
totalNewConnections: 2,
totalSuccessfulConnections: 2,
details: {
...firstDeviceDiagnostics.details,
"rtc-b": {
connectionState: "connected",
iceConnectionState: "connected",
},
},
},
baseline,
10_000
)
).toEqual({
activeConnections: 1,
closedConnections: 0,
failedConnections: 0,
newConnections: 1,
newActiveConnectionIds: ["rtc-b"],
outcome: "connected",
successfulConnections: 1,
});
});
it("marks an additional-device attempt inconclusive after its own observation period", () => {
const baseline = captureP2PAdditionalCheckBaseline(firstDeviceDiagnostics);
expect(
deriveP2PAdditionalCheckProgress(firstDeviceDiagnostics, baseline, P2P_CHECK_OBSERVATION_MILLISECONDS)
.outcome
).toBe("inconclusive");
});
});
+25 -1
View File
@@ -1,5 +1,5 @@
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { DEFAULT_SETTINGS, SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import { describe, expect, it, vi } from "vitest";
@@ -73,4 +73,28 @@ describe("WebPeer runtime composition", () => {
await expect(runtime.start()).rejects.toThrow("WebPeer local database could not be opened");
});
it("isolates a specialised runtime and applies its device name before opening the database", async () => {
const runtime = new WebPeerRuntime({
store: createMemoryStore(),
deviceName: " p2p-check-browser-desktop-abc ",
systemVaultName: "p2p-check-vault",
});
vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
vi.spyOn(runtime.services.setting, "currentSettings").mockReturnValue({
...DEFAULT_SETTINGS,
P2P_AutoStart: false,
P2P_Enabled: false,
});
const setSmallConfig = vi.spyOn(runtime.services.config, "setSmallConfig").mockImplementation(() => undefined);
const openDatabase = vi.spyOn(runtime.services.database, "openDatabase").mockImplementation(async () => {
expect(setSmallConfig).toHaveBeenCalledWith(SETTING_KEY_P2P_DEVICE_NAME, "p2p-check-browser-desktop-abc");
return true;
});
await runtime.start();
expect(runtime.services.API.getSystemVaultName()).toBe("p2p-check-vault");
expect(openDatabase).toHaveBeenCalledOnce();
});
});