mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-30 21:08:18 +00:00
Test P2P Fetch restart confirmation
This commit is contained in:
1 parent
8422c19881
commit
c4fc8f5118
2 files changed
+189
-2
No files matched your search
@@ -330,7 +330,8 @@ async function fetchFromFirstPeer(
|
||||
sessionA: ObsidianLiveSyncSession,
|
||||
portA: number,
|
||||
portB: number,
|
||||
screenshots: string[]
|
||||
screenshots: string[],
|
||||
selectionScreenshot = "guide-p2p-setup-select-first-device.png"
|
||||
): Promise<void> {
|
||||
try {
|
||||
await withObsidianPage(portB, async (page) => {
|
||||
@@ -363,7 +364,7 @@ async function fetchFromFirstPeer(
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
screenshots.push(await captureGuideDialogue(portB, "guide-p2p-setup-select-first-device.png", "P2P Rebuild"));
|
||||
screenshots.push(await captureGuideDialogue(portB, selectionScreenshot, "P2P Rebuild"));
|
||||
|
||||
let finished = false;
|
||||
const acceptor = acceptConnectionRequests([portA, portB], () => finished, screenshots);
|
||||
@@ -390,6 +391,186 @@ async function fetchFromFirstPeer(
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyInterruptedFetchRestart(
|
||||
context: RunnerContext,
|
||||
sessionA: ObsidianLiveSyncSession,
|
||||
sessionB: ObsidianLiveSyncSession,
|
||||
vaultA: TemporaryVault,
|
||||
vaultB: TemporaryVault,
|
||||
screenshots: string[]
|
||||
): Promise<void> {
|
||||
const portA = sessionA.remoteDebuggingPort;
|
||||
const portB = sessionB.remoteDebuggingPort;
|
||||
const remotePath = "E2E/p2p/fetched-after-restart.md";
|
||||
const remoteContent = "# P2P recovery source\n\nFetch these exact bytes after confirming the saved choices.\n";
|
||||
const localPath = "E2E/p2p/local-only-during-recovery.md";
|
||||
const localContent = "# Local recovery note\n\nKeep these exact bytes while restarting the P2P Fetch.\n";
|
||||
await writeNote(context.cliBinary, sessionA.cliEnv, remotePath, remoteContent);
|
||||
|
||||
await withObsidianPage(portB, async (page) => {
|
||||
const before = await page.evaluate(async (path) => {
|
||||
const app = (globalThis as any).app;
|
||||
const core = app.plugins.plugins["obsidian-livesync"].core;
|
||||
return {
|
||||
inVault: await app.vault.adapter.exists(path),
|
||||
inDatabase: !!(await core.localDatabase.getDBEntry(path, undefined, false, true)),
|
||||
};
|
||||
}, remotePath);
|
||||
assertEqual(before.inVault, false, "The recovery source note already existed in the receiving Vault.");
|
||||
assertEqual(before.inDatabase, false, "The recovery source note arrived before the scheduled P2P Fetch.");
|
||||
await page.evaluate(async () => {
|
||||
const core = (globalThis as any).app.plugins.plugins["obsidian-livesync"].core;
|
||||
if (!(await core.rebuilder.scheduleFetch())) throw new Error("Could not schedule the P2P Fetch.");
|
||||
});
|
||||
});
|
||||
|
||||
await withObsidianPage(portB, async (page) => {
|
||||
await modalByTitle(page, "Data retrieval scheduled").waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await page.evaluate(() => {
|
||||
const core = (globalThis as any).app.plugins.plugins["obsidian-livesync"].core;
|
||||
const fetch = core.rebuilder.$fetchLocalDBFast.bind(core.rebuilder);
|
||||
// Keep the actual P2P transport and interrupt before the subsequent Vault scan.
|
||||
core.rebuilder.$fetchLocalDBFast = async (...args: unknown[]) => {
|
||||
await fetch(...args);
|
||||
(globalThis as any).__p2pFetchInterrupted = true;
|
||||
throw new Error("E2E: interrupted after P2P receive and before Vault reflection");
|
||||
};
|
||||
});
|
||||
});
|
||||
screenshots.push(...(await confirmFastFetch(portB, { scenario: "p2p-fetch-restart", guide: "p2p-fetch-restart" })));
|
||||
await fetchFromFirstPeer(sessionA, portA, portB, screenshots, "p2p-fetch-interrupted-select-source.png");
|
||||
|
||||
const pending = await withObsidianPage(portB, async (page) => {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const core = (globalThis as any).app.plugins.plugins["obsidian-livesync"].core;
|
||||
const settings = core.services.setting.currentSettings();
|
||||
return (
|
||||
(globalThis as any).__p2pFetchInterrupted === true &&
|
||||
settings.suspendFileWatching &&
|
||||
settings.suspendParseReplicationResult
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
return await page.evaluate(
|
||||
async ({ remotePath, localPath, localContent }) => {
|
||||
const app = (globalThis as any).app;
|
||||
const core = app.plugins.plugins["obsidian-livesync"].core;
|
||||
const remoteEntry = await core.localDatabase.getDBEntry(remotePath, undefined, false, true);
|
||||
const remoteInVault = await app.vault.adapter.exists(remotePath);
|
||||
await app.vault.create(localPath, localContent);
|
||||
return {
|
||||
received: !!remoteEntry,
|
||||
remoteInVault,
|
||||
markdown: await app.vault.adapter.read("flag_fetch.md"),
|
||||
room: core.services.setting.currentSettings().P2P_roomID,
|
||||
};
|
||||
},
|
||||
{ remotePath, localPath, localContent }
|
||||
);
|
||||
});
|
||||
assertEqual(pending.received, true, "The interruption occurred before the real P2P receive completed.");
|
||||
assertEqual(pending.remoteInVault, false, "The interrupted P2P Fetch reflected the source note too early.");
|
||||
const block = /```json\s*\n([\s\S]*?)\n```/.exec(pending.markdown);
|
||||
if (!block) throw new Error("The interrupted P2P Fetch did not retain its choices in the flag.");
|
||||
const selection = JSON.parse(block[1]);
|
||||
assertEqual(selection.stage1, "Overwrite all with remote files", "The P2P overwrite choice was not retained.");
|
||||
assertEqual(
|
||||
selection.stage2,
|
||||
"Keep local files even if not on remote",
|
||||
"The P2P preservation choice was not retained."
|
||||
);
|
||||
console.log("P2P workflow: received the recovery note, interrupted before reflection, and retained the choices.");
|
||||
|
||||
await withObsidianPage(portB, async (page) => {
|
||||
await page.evaluate(() =>
|
||||
(globalThis as any).app.plugins.plugins["obsidian-livesync"].core.services.appLifecycle.performRestart()
|
||||
);
|
||||
});
|
||||
await withObsidianPage(portB, async (page) => {
|
||||
const modal = modalByTitle(page, "Resume scheduled fetch?");
|
||||
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const message = await modal.innerText();
|
||||
for (const expected of ["Current remote:", "P2P", pending.room, selection.stage1, selection.stage2]) {
|
||||
if (!message.includes(expected))
|
||||
throw new Error("The P2P resume dialogue omitted the remote or saved choices.");
|
||||
}
|
||||
const beforeConfirmation = await page.evaluate(async (remotePath) => {
|
||||
const app = (globalThis as any).app;
|
||||
const core = app.plugins.plugins["obsidian-livesync"].core;
|
||||
const settings = core.services.setting.currentSettings();
|
||||
return {
|
||||
reloaded: (globalThis as any).__p2pFetchInterrupted === undefined,
|
||||
remoteInVault: await app.vault.adapter.exists(remotePath),
|
||||
markdown: await app.vault.adapter.read("flag_fetch.md"),
|
||||
suspendFileWatching: settings.suspendFileWatching,
|
||||
suspendParseReplicationResult: settings.suspendParseReplicationResult,
|
||||
};
|
||||
}, remotePath);
|
||||
assertEqual(beforeConfirmation.reloaded, true, "Obsidian did not reload before the recovery confirmation.");
|
||||
assertEqual(beforeConfirmation.remoteInVault, false, "The source note was reflected before confirmation.");
|
||||
assertEqual(beforeConfirmation.markdown, pending.markdown, "Restart changed the retained P2P choices.");
|
||||
assertEqual(beforeConfirmation.suspendFileWatching, true, "File watching resumed before confirmation.");
|
||||
assertEqual(
|
||||
beforeConfirmation.suspendParseReplicationResult,
|
||||
true,
|
||||
"Database reflection resumed before confirmation."
|
||||
);
|
||||
});
|
||||
screenshots.push(await captureGuideDialogue(portB, "p2p-fetch-resume-confirmation.png", "Resume scheduled fetch?"));
|
||||
await withObsidianPage(portB, async (page) => {
|
||||
await modalByTitle(page, "Resume scheduled fetch?")
|
||||
.getByRole("button", { name: "Continue with these choices", exact: true })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
});
|
||||
await fetchFromFirstPeer(sessionA, portA, portB, screenshots, "p2p-fetch-resumed-select-source.png");
|
||||
await finishInitialisation(portB, context.cliBinary, sessionB.cliEnv);
|
||||
await waitForPathContent(vaultB, remotePath, remoteContent);
|
||||
await waitForPathContent(vaultB, localPath, localContent);
|
||||
await waitForPathContent(vaultA, remotePath, remoteContent);
|
||||
const completed = await withObsidianPage(
|
||||
portB,
|
||||
async (page) =>
|
||||
await page.evaluate(async () => {
|
||||
const app = (globalThis as any).app;
|
||||
const settings = app.plugins.plugins["obsidian-livesync"].core.services.setting.currentSettings();
|
||||
return {
|
||||
flags: await Promise.all(
|
||||
["flag_fetch.md", "redflag3.md"].map((path) => app.vault.adapter.exists(path))
|
||||
),
|
||||
suspendFileWatching: settings.suspendFileWatching,
|
||||
suspendParseReplicationResult: settings.suspendParseReplicationResult,
|
||||
};
|
||||
})
|
||||
);
|
||||
assertEqual(completed.flags.some(Boolean), false, "The resumed P2P Fetch retained a fetch flag.");
|
||||
assertEqual(completed.suspendFileWatching, false, "File watching remained suspended after P2P recovery.");
|
||||
assertEqual(
|
||||
completed.suspendParseReplicationResult,
|
||||
false,
|
||||
"Database reflection remained suspended after P2P recovery."
|
||||
);
|
||||
const sourceHasLocalNote = await withObsidianPage(
|
||||
portA,
|
||||
async (page) =>
|
||||
await page.evaluate(async (path) => (globalThis as any).app.vault.adapter.exists(path), localPath)
|
||||
);
|
||||
assertEqual(sourceHasLocalNote, false, "The recovery note was not local-only during the Fetch.");
|
||||
console.log(
|
||||
"P2P_FETCH_RESTART " +
|
||||
JSON.stringify({
|
||||
realReceiveBeforeInterruption: true,
|
||||
promptedAfterReload: true,
|
||||
remoteContentPreserved: true,
|
||||
localOnlyContentPreserved: true,
|
||||
flagsRemoved: true,
|
||||
reflectionResumed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function replicateFromStatusPane(port: number): Promise<void> {
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const heading = page.getByRole("heading", { name: "Detected Peers" }).last();
|
||||
@@ -539,6 +720,8 @@ async function main(): Promise<void> {
|
||||
);
|
||||
console.log("P2P workflow: initial Fetch from the first device completed.");
|
||||
|
||||
await verifyInterruptedFetchRestart(context, sessionA, sessionB, vaultA, vaultB, screenshots);
|
||||
|
||||
await writeNote(context.cliBinary, sessionB.cliEnv, noteFromSecond, secondContent);
|
||||
await reconnectP2PStatus(portA);
|
||||
await reconnectP2PStatus(portB);
|
||||
|
||||
Reference in new issue
Block a user