Fix automatic P2P settings restoration in CLI host mode

This commit is contained in:
vorotamoroz committed 2026-10-14 08:02:13 +00:00
1 parent 0956663f7b
commit 3e2b4dcb3b
16 files changed
+944 -25

No files matched your search

+5 -1
View File
@@ -5,6 +5,7 @@ import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFu
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
import type { P2PPeerConnectionMetrics, P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import { fsPromises } from "@vrtmrz/livesync-commonlib/node";
import type { CLIP2PHostSettings } from "./types";
type CLIP2PPeer = {
peerId: string;
@@ -191,8 +192,11 @@ export async function syncWithPeer(
/** Connect the headless P2P host and transfer transport ownership to the caller. */
export async function openP2PHost(
core: LiveSyncBaseCore<ServiceContext, never>,
p2pService: CLIP2PService | undefined
p2pService: CLIP2PService | undefined,
originalP2PSettings: CLIP2PHostSettings
): Promise<CLIP2PService> {
// Restore only P2P policy after database readiness, before peer discovery.
await core.services.setting.applyPartial(originalP2PSettings);
const service = requireP2PService(core, p2pService);
await service.transportLifecycle.connect();
return service;
+84 -1
View File
@@ -1,5 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { collectPeers, createPeerConnectionStatsPayload, parseTimeoutSeconds, syncWithPeer } from "./p2p";
import { collectPeers, createPeerConnectionStatsPayload, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
import { AutoAccepting } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { CLIP2PHostSettings } from "./types";
function createCore() {
const settings = { P2P_Enabled: true, P2P_AppID: "app-id", P2P_IsHeadless: false };
@@ -37,6 +39,87 @@ function createP2PService() {
}
describe("p2p command helpers", () => {
const passiveHostSettings: CLIP2PHostSettings = {
P2P_Enabled: true,
P2P_AutoAccepting: AutoAccepting.NONE,
P2P_AutoStart: false,
P2P_AutoBroadcast: false,
P2P_AutoSyncPeers: "",
P2P_AutoWatchPeers: "",
};
function createHostCore() {
const settings = {
...passiveHostSettings,
P2P_Enabled: false,
P2P_IsHeadless: false,
P2P_AppID: "app-id",
liveSync: false,
suspendFileWatching: true,
};
const applyPartial = vi.fn(async (partial: CLIP2PHostSettings) => {
Object.assign(settings, partial);
});
const saveSettingData = vi.fn();
const applySettings = vi.fn();
return {
core: {
services: {
setting: { currentSettings: () => settings, applyPartial, saveSettingData },
control: { applySettings },
},
},
settings,
applyPartial,
saveSettingData,
applySettings,
};
}
it.each([
passiveHostSettings,
{ ...passiveHostSettings, P2P_AutoBroadcast: true, P2P_AutoWatchPeers: "Desktop" },
{ ...passiveHostSettings, P2P_AutoAccepting: AutoAccepting.ALL, P2P_AutoSyncPeers: "Desktop,android" },
])("connects with the configured host policy without saving or restarting the lifecycle (%j)", async (original) => {
const host = createHostCore();
const { service, connect, disconnect } = createP2PService();
connect.mockImplementation(async () => {
expect(host.settings).toMatchObject({ ...original, P2P_IsHeadless: true });
});
await expect(openP2PHost(host.core as never, service as never, original)).resolves.toBe(service);
expect(host.applyPartial).toHaveBeenCalledExactlyOnceWith(original);
expect(connect).toHaveBeenCalledOnce();
expect(disconnect).not.toHaveBeenCalled();
expect(host.settings).toMatchObject({ liveSync: false, suspendFileWatching: true });
expect(host.saveSettingData).not.toHaveBeenCalled();
expect(host.applySettings).not.toHaveBeenCalled();
});
it("keeps P2P disabled when the saved host policy disables it", async () => {
const host = createHostCore();
const { service, connect } = createP2PService();
await expect(
openP2PHost(host.core as never, service as never, { ...passiveHostSettings, P2P_Enabled: false })
).rejects.toThrow("P2P is disabled");
expect(connect).not.toHaveBeenCalled();
});
it("does not connect when restoring the host policy fails", async () => {
const host = createHostCore();
const { service, connect } = createP2PService();
host.applyPartial.mockRejectedValue(new Error("settings failed"));
await expect(openP2PHost(host.core as never, service as never, passiveHostSettings)).rejects.toThrow(
"settings failed"
);
expect(connect).not.toHaveBeenCalled();
});
it("accepts non-negative timeout", () => {
expect(parseTimeoutSeconds("0", "p2p-peers")).toBe(0);
expect(parseTimeoutSeconds("2.5", "p2p-sync")).toBe(2.5);
+1 -1
View File
@@ -214,7 +214,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
if (options.command === "p2p-host") {
writeStderrLine(standardIo, "[Command] p2p-host");
await openP2PHost(core, context.p2pReplicator);
await openP2PHost(core, context.p2pReplicator, context.originalP2PSettings);
writeStderrLine(standardIo, "[Ready] P2P host is running. Press Ctrl+C to stop.");
await new Promise(() => {});
return true;
+12
View File
@@ -49,6 +49,16 @@ export interface CLIOptions {
interval?: number;
}
export type CLIP2PHostSettings = Pick<
ObsidianLiveSyncSettings,
| "P2P_Enabled"
| "P2P_AutoAccepting"
| "P2P_AutoStart"
| "P2P_AutoBroadcast"
| "P2P_AutoSyncPeers"
| "P2P_AutoWatchPeers"
>;
export interface CLICommandContext {
databasePath: string;
vaultPath: string;
@@ -58,6 +68,8 @@ export interface CLICommandContext {
/** Current-result contract owned by the P2P service feature. */
p2pReplicator?: UseP2PReplicatorResult;
settingsPath: string;
/** Configured P2P policy before temporary CLI startup suspension. */
originalP2PSettings: CLIP2PHostSettings;
originalSyncSettings: Pick<
ObsidianLiveSyncSettings,
| "liveSync"