mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-03 01:07:07 +00:00
Integrate E2EE rebuild preservation with current main
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
type SetupArtifact,
|
||||
} from "../runner/setupUri.ts";
|
||||
import { captureObsidianPage, openLiveSyncSettings, withObsidianPage } from "../runner/ui.ts";
|
||||
import { dismissConfigDoctorIfShown } from "../runner/upgradeWorkflow.ts";
|
||||
import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000";
|
||||
@@ -303,6 +304,7 @@ async function assertRemotePreferredE2EE(context: RunnerContext, expected: boole
|
||||
}
|
||||
|
||||
async function scheduleRemoteOverwrite(port: number): Promise<void> {
|
||||
await dismissConfigDoctorIfShown(port);
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
|
||||
const maintenance = await settingsNavigator.openPage("Maintenance");
|
||||
|
||||
@@ -12,8 +12,11 @@ import {
|
||||
} from "../runner/couchdb.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import {
|
||||
assertE2eCompatibilityMarker,
|
||||
assertEqual,
|
||||
configureCouchDb,
|
||||
createE2eCouchDbPluginData,
|
||||
createE2eObsidianDeviceLocalState,
|
||||
prepareRemote,
|
||||
pushLocalChanges,
|
||||
waitForLiveSyncCoreReady,
|
||||
@@ -149,31 +152,33 @@ async function startConfiguredSession(
|
||||
vault: TemporaryVault,
|
||||
deviceName: string
|
||||
): Promise<ObsidianLiveSyncSession> {
|
||||
const couchDbSettings = {
|
||||
uri: context.couchDb.uri,
|
||||
username: context.couchDb.username,
|
||||
password: context.couchDb.password,
|
||||
dbName: context.dbName,
|
||||
};
|
||||
const customisationSettings = {
|
||||
deviceAndVaultName: deviceName,
|
||||
usePluginSync: true,
|
||||
usePluginSyncV2: true,
|
||||
autoSweepPlugins: false,
|
||||
autoSweepPluginsPeriodic: false,
|
||||
syncInternalFiles: false,
|
||||
};
|
||||
const session = await startObsidianLiveSyncSession({
|
||||
binary: context.binary,
|
||||
cliBinary: context.cliBinary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
// This scenario exercises Customisation Sync, not onboarding. Seed a
|
||||
// configured Vault and its device-local compatibility acknowledgement.
|
||||
pluginData: createE2eCouchDbPluginData(couchDbSettings, customisationSettings),
|
||||
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
|
||||
});
|
||||
await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv);
|
||||
await configureCouchDb(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
{
|
||||
uri: context.couchDb.uri,
|
||||
username: context.couchDb.username,
|
||||
password: context.couchDb.password,
|
||||
dbName: context.dbName,
|
||||
},
|
||||
{
|
||||
deviceAndVaultName: deviceName,
|
||||
usePluginSync: true,
|
||||
usePluginSyncV2: true,
|
||||
autoSweepPlugins: false,
|
||||
autoSweepPluginsPeriodic: false,
|
||||
syncInternalFiles: false,
|
||||
}
|
||||
);
|
||||
await assertE2eCompatibilityMarker(context.cliBinary, session.cliEnv);
|
||||
await configureCouchDb(context.cliBinary, session.cliEnv, couchDbSettings, customisationSettings);
|
||||
await evalObsidianJson<unknown>(
|
||||
context.cliBinary,
|
||||
[
|
||||
|
||||
@@ -8,6 +8,7 @@ import { discoverObsidianCli, requireObsidianBinary } from "../runner/environmen
|
||||
import {
|
||||
assertEqual,
|
||||
pushLocalChanges,
|
||||
type ConfiguredSettings,
|
||||
waitForLiveSyncCoreReady,
|
||||
waitForLocalDatabaseEntry,
|
||||
} from "../runner/liveSyncWorkflow.ts";
|
||||
@@ -57,6 +58,10 @@ type RunnerContext = {
|
||||
activeSessions: Set<ObsidianLiveSyncSession>;
|
||||
};
|
||||
|
||||
type StartupSchedulingState = ConfiguredSettings & {
|
||||
periodicReplication: boolean;
|
||||
};
|
||||
|
||||
function sessionEnvironment(port: number): NodeJS.ProcessEnv {
|
||||
return { ...process.env, E2E_OBSIDIAN_REMOTE_DEBUGGING_PORT: String(port) };
|
||||
}
|
||||
@@ -187,6 +192,41 @@ async function waitForObjectStorageData(config: ObjectStorageConfig, prefix: str
|
||||
throw new Error(`Timed out waiting for Object Storage data under ${prefix}.`);
|
||||
}
|
||||
|
||||
async function configureMigratedStartupScheduling(
|
||||
cliBinary: string,
|
||||
environment: NodeJS.ProcessEnv
|
||||
): Promise<StartupSchedulingState> {
|
||||
return await evalObsidianJson<StartupSchedulingState>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const plugin=app.plugins.plugins['obsidian-livesync'];",
|
||||
"const core=plugin.core;",
|
||||
// Persist only the migration-shaped scheduling flags and leave the
|
||||
// generated URI unchanged. Device A stops before B creates the
|
||||
// return note, so save-triggered reconciliation cannot satisfy the
|
||||
// later start-up assertion.
|
||||
"await core.services.setting.applyExternalSettings({liveSync:true,syncOnStart:true,periodicReplication:false},true);",
|
||||
"const current=core.services.setting.currentSettings();",
|
||||
"return JSON.stringify({",
|
||||
"isConfigured:current.isConfigured,",
|
||||
"liveSync:current.liveSync,",
|
||||
"syncOnStart:current.syncOnStart,",
|
||||
"syncOnSave:current.syncOnSave,",
|
||||
"periodicReplication:current.periodicReplication,",
|
||||
"remoteType:current.remoteType,",
|
||||
"couchDB_URI:current.couchDB_URI,",
|
||||
"couchDB_DBNAME:current.couchDB_DBNAME,",
|
||||
"endpoint:current.endpoint,",
|
||||
"bucket:current.bucket,",
|
||||
"bucketPrefix:current.bucketPrefix,",
|
||||
"});",
|
||||
"})()",
|
||||
].join(""),
|
||||
environment
|
||||
);
|
||||
}
|
||||
|
||||
async function captureNote(port: number, path: string, text: string, filename: string): Promise<string> {
|
||||
await withObsidianPage(port, async (page) => {
|
||||
await page.evaluate((notePath) => {
|
||||
@@ -254,6 +294,24 @@ async function main(): Promise<void> {
|
||||
throw new Error("The first device returned the bootstrap Setup URI instead of generating a new one.");
|
||||
}
|
||||
screenshots.push(...generated.screenshots);
|
||||
const startupState = await configureMigratedStartupScheduling(context.cliBinary, sessionA.cliEnv);
|
||||
assertEqual(startupState.liveSync, true, "The first device did not persist its Continuous setting.");
|
||||
assertEqual(startupState.syncOnStart, true, "The first device did not persist syncOnStart.");
|
||||
assertEqual(
|
||||
startupState.periodicReplication,
|
||||
false,
|
||||
"Periodic replication could mask the syncOnStart return journey."
|
||||
);
|
||||
assertEqual(
|
||||
startupState.endpoint,
|
||||
objectStorage.endpoint,
|
||||
"Enabling syncOnStart changed the Object Storage endpoint."
|
||||
);
|
||||
assertEqual(
|
||||
startupState.bucketPrefix,
|
||||
bucketPrefix,
|
||||
"Enabling syncOnStart changed the Object Storage bucket prefix."
|
||||
);
|
||||
await stopSession(context, sessionA);
|
||||
|
||||
const sessionB = await startSession(context, vaultB, portB);
|
||||
@@ -290,7 +348,9 @@ async function main(): Promise<void> {
|
||||
const returningSessionA = await startSession(context, vaultA, portA);
|
||||
await waitForLiveSyncCoreReady(context.cliBinary, returningSessionA.cliEnv);
|
||||
await resumeCompatibilityReviewIfShown(portA);
|
||||
await pushLocalChanges(context.cliBinary, returningSessionA.cliEnv);
|
||||
// Deliberately omit manual replication here. Object Storage reports
|
||||
// Continuous as not applicable, so startup scheduling must honour the
|
||||
// retained syncOnStart setting by running an unattended OneShot.
|
||||
await waitForPathContent(vaultA, noteFromSecond, secondContent);
|
||||
screenshots.push(
|
||||
await captureNote(
|
||||
|
||||
@@ -179,14 +179,17 @@ async function openOnboardingFromSettings(): Promise<void> {
|
||||
async function dismissVisibleNotices(): Promise<void> {
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const notices = page.locator(".notice:visible");
|
||||
let noticeIndex = 0;
|
||||
while ((await notices.count()) > 0) {
|
||||
const noticeCount = await page.locator(".notice").count();
|
||||
await notices.first().click({ position: { x: 8, y: 8 }, timeout: uiTimeoutMs });
|
||||
await page.waitForFunction(
|
||||
(previousCount) => document.querySelectorAll(".notice").length < previousCount,
|
||||
noticeCount,
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
const marker = `livesync-e2e-notice-${noticeIndex++}`;
|
||||
await notices
|
||||
.first()
|
||||
.evaluate((element, value) => element.setAttribute("data-livesync-e2e-notice", value), marker);
|
||||
const markedNotice = page.locator(`[data-livesync-e2e-notice="${marker}"]`);
|
||||
await markedNotice.click({ position: { x: 8, y: 8 }, timeout: uiTimeoutMs });
|
||||
// Follow the notice which was clicked. Another concurrently added
|
||||
// notice must not make a total-count wait look permanently stuck.
|
||||
await markedNotice.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,7 +241,11 @@ async function runScenario(): Promise<void> {
|
||||
const consoleErrors: string[] = [];
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const browserExecutable = process.env.E2E_PLAYWRIGHT_CHROMIUM?.trim();
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(browserExecutable ? { executablePath: browserExecutable } : {}),
|
||||
});
|
||||
const firstVault = await createTemporaryVault("obsidian-livesync-p2p-check-first-e2e-");
|
||||
vaults.push(firstVault);
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
|
||||
|
||||
@@ -196,7 +196,7 @@ async function runAutomaticScenarios(): Promise<void> {
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const harness = page.locator('[data-testid="review-harness"]');
|
||||
await harness.locator('[data-testid="review-harness-run-automatic"]').click({ timeout: uiTimeoutMs });
|
||||
for (const id of ["settings-lifecycle", "p2p-composition"]) {
|
||||
for (const id of ["settings-lifecycle"]) {
|
||||
await harness
|
||||
.locator(`[data-testid="review-harness-result-${id}"]`)
|
||||
.getByText("Passed:", { exact: false })
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { evalObsidianJson } from "../runner/cli.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import { assertEqual, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
||||
import { assertEqual } from "../runner/liveSyncWorkflow.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
@@ -75,8 +75,8 @@ async function main(): Promise<void> {
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
});
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
|
||||
// The export is available while an unconfigured Vault remains outside
|
||||
// application readiness; the session helper has already loaded the plug-in.
|
||||
await configureSettingMarkdown(cli.binary, session.cliEnv);
|
||||
const content = await waitForFileContaining(vault.path, settingPath, [
|
||||
(value) => value.includes("````yaml:livesync-setting"),
|
||||
|
||||
@@ -536,7 +536,7 @@ async function captureHiddenFileGuideSettings(
|
||||
|
||||
const screenshots = [
|
||||
await captureObsidianElement(port, "guide-hidden-file-advanced-features.png", (page) =>
|
||||
liveSyncSettingPanelByTitle(page, "Setup", "Enable extra and advanced features")
|
||||
liveSyncSettingPanelByTitle(page, "General Settings", "Extra menus")
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user