Harden real-runtime workflow validation

This commit is contained in:
vorotamoroz
2026-08-31 00:35:29 +00:00
parent 3a23b7a6b0
commit 9a28d46287
8 changed files with 72 additions and 42 deletions
@@ -28,14 +28,16 @@ export async function startHeldOneShotReplication(cliBinary: string, env: NodeJS
"const core=app.plugins.plugins['obsidian-livesync'].core;",
"const replicator=core.services.replicator.getActiveReplicator();",
"if(!replicator) throw new Error('No active replicator is available.');",
"const original=replicator.openReplication;",
"const methodName=typeof replicator.openOneShotReplicationWithOutcome==='function'?'openOneShotReplicationWithOutcome':'openReplication';",
"const original=replicator[methodName];",
"if(typeof original!=='function') throw new Error('The active replicator has no one-shot entry point.');",
"let releaseGate;",
"const gate=new Promise((resolve)=>{releaseGate=resolve;});",
`const state={kind:${JSON.stringify(REMOTE_ACTIVITY_GATE_KIND.oneShot)},entered:false,done:false,released:false,error:undefined,result:undefined,promise:undefined,release:undefined,restore:undefined};`,
"state.release=()=>{if(!state.released){state.released=true;releaseGate();}};",
"state.restore=()=>{replicator.openReplication=original;};",
"state.restore=()=>{replicator[methodName]=original;};",
"host[stateKey]=state;",
"replicator.openReplication=async function(...args){",
"replicator[methodName]=async function(...args){",
"state.entered=true;",
"await gate;",
"return await original.apply(this,args);",
+12 -7
View File
@@ -38,7 +38,12 @@ export function modalByTitle(page: Page, title: string): Locator {
}
export async function captureGuideDialogue(port: number, filename: string, title: string): Promise<string> {
return await captureObsidianElement(port, filename, (page) => modalByTitle(page, title).locator(".modal").first());
return await captureObsidianElement(
port,
filename,
(page) => modalByTitle(page, title).locator(".modal").first(),
uiTimeoutMs
);
}
export async function assertVerticalActionLayout(port: number, title: string): Promise<void> {
@@ -182,16 +187,16 @@ export async function captureAndStartInitialisation(
? "Setup Complete: Preparing This P2P Device"
: p2pAdditionalDevice
? "Setup Complete: Preparing to Fetch from Another Device"
: mode === "new"
? "Setup Complete: Preparing to Initialise Server"
: "Setup Complete: Preparing to Fetch Synchronisation Data";
: mode === "new"
? "Setup Complete: Preparing to Initialise Server"
: "Setup Complete: Preparing to Fetch Synchronisation Data";
const button = p2pFirstDevice
? "Restart and Prepare This Device"
: p2pAdditionalDevice
? "Restart and Select Source Device"
: mode === "new"
? "Restart and Initialise Server"
: "Restart and Fetch Data";
: mode === "new"
? "Restart and Initialise Server"
: "Restart and Fetch Data";
if (p2pAdditionalDevice) {
await withObsidianPage(port, async (page) => {
const modal = modalByTitle(page, title);
+13 -3
View File
@@ -115,7 +115,6 @@ export async function openLiveSyncSettings(page: Page, timeoutMs = 10_000): Prom
const setting = host.app?.setting;
if (setting === undefined) throw new Error("Obsidian settings are unavailable");
setting.open();
setting.openTabById("obsidian-livesync");
});
const deadline = Date.now() + timeoutMs;
@@ -137,6 +136,16 @@ export async function openLiveSyncSettings(page: Page, timeoutMs = 10_000): Prom
}
if (settingsPage === undefined) throw new Error("Obsidian did not open its settings interface");
// Obsidian may discard a tab selection made before the settings modal has
// finished opening. Select the plug-in only after a settings renderer is
// visible so slower real-runtime sessions cannot remain on the About tab.
await hostPage.evaluate(() => {
const host = globalThis as ObsidianSettingsHost;
const setting = host.app?.setting;
if (setting === undefined) throw new Error("Obsidian settings are unavailable");
setting.openTabById("obsidian-livesync");
});
const dialogue = settingsPage.locator(".modal.mod-settings:visible").last();
const imperativeRoot = dialogue.locator(".sls-setting:visible").last();
const firstDeclarativeEntry = declarativePageEntry(dialogue, "Change Log");
@@ -270,7 +279,8 @@ export async function allowPendingObsidianTestVaultOpenAction(
export async function captureObsidianElement(
port: number,
filename: string,
resolveElement: (page: Page) => Locator | Promise<Locator>
resolveElement: (page: Page) => Locator | Promise<Locator>,
timeoutMs = 10_000
): Promise<string> {
const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e";
const screenshotPath = join(outputDirectory, filename);
@@ -279,7 +289,7 @@ export async function captureObsidianElement(
await withObsidianPage(port, async (page) => {
try {
const element = await resolveElement(page);
await element.waitFor({ state: "visible", timeout: 10000 });
await element.waitFor({ state: "visible", timeout: timeoutMs });
await element.screenshot({
path: screenshotPath,
animations: "disabled",