mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 04:52:58 +00:00
Require explicit review for restored Vaults
This commit is contained in:
@@ -28,6 +28,7 @@ The remote `obsydian_livesync_version` document also carries the internal databa
|
||||
- Continue to use the internal database version `VER` for changes which require explicit compatibility review. Changing the plug-in SemVer alone does not increment `VER`.
|
||||
- Store the last acknowledged internal database version through Commonlib's device-local small-configuration contract under `database-compatibility-version`. Copy the legacy raw local-storage value into that contract once, then remove the legacy key after the copy has completed.
|
||||
- Initialise the marker to the current `VER` only when Commonlib identifies a genuinely new Vault with no pending review. An existing Vault with a missing or invalid marker requires review instead of being silently accepted.
|
||||
- Treat a missing marker on a configured Vault as an ambiguous device transition. Copying or restoring a Vault, or opening it with a new Obsidian profile, can preserve settings and database files without preserving device-local storage. Do not infer acknowledgement from an empty local database: a recovery operation, partial copy, or remote-first setup can also produce that state. Explain these cases and require an explicit decision in the compatibility dialogue.
|
||||
- Derive one structured pause from the acknowledged database version, Commonlib's settings-migration state, and any persisted legacy review message. Persist the generic `versionUpFlash` message without changing any automatic synchronisation setting, because Commonlib already treats that field as a replication gate.
|
||||
- Treat non-empty `versionUpFlash` as a runtime replication gate. Standard and one-shot replication must stop before remote work begins.
|
||||
- Present the reason in a dedicated dialogue after the Obsidian layout is ready. The details view is explanatory only and returns to the summary before any decision can be made. The safe default and closing either dialogue keep synchronisation paused. A persistent Notice and a command allow the dialogue to be reopened without using the settings pane.
|
||||
@@ -37,12 +38,20 @@ The remote `obsydian_livesync_version` document also carries the internal databa
|
||||
- Preserve the original legacy review message as a structured reason when no more specific database or settings-schema reason is available. Escape it before including it in Markdown UI.
|
||||
- Continue to reject a remote version document which is newer than the running implementation. That receiver-side check is independent of the local upgrade review.
|
||||
|
||||
### Flag-file recovery order
|
||||
|
||||
- Evaluate and persist the compatibility gate after settings load, before Obsidian layout-ready recovery begins. This blocks ordinary and one-shot replication even while the review dialogue has not yet opened.
|
||||
- Preserve the existing ordered flag-file recovery handlers: SCRAM at priority 5, fetch-all at priority 10, and rebuild-all at priority 20. These files express an explicit recovery instruction and may invoke their focused storage or rebuild service while ordinary replication remains gated.
|
||||
- Present the compatibility review at priority 30, after any selected recovery operation. A recovery handler which cancels start-up, keeps SCRAM active, or schedules a restart returns `false`, so the current process does not open a competing compatibility dialogue. If recovery completes and start-up continues, the dialogue opens before normal synchronisation is allowed to resume.
|
||||
- Never mark compatibility as acknowledged merely because fetch, rebuild, or local database reset completed. The person must still use the explicit resume action. This keeps destructive recovery intent separate from protocol and settings compatibility acknowledgement.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Ordinary releases no longer force the settings dialogue to show release notes. Important operational instructions must be clear in the published release notes and any explicit migration notice.
|
||||
- SemVer pre-releases such as `1.0.0-rc.0` no longer require a special numeric encoding inside plug-in settings.
|
||||
- An internal compatibility change remains fail-closed for replication, but it no longer destroys the person's synchronisation preferences.
|
||||
- A new installation has no previous internal-version marker and therefore does not show an upgrade review. Its initial settings and onboarding remain responsible for keeping replication disabled until configuration is complete.
|
||||
- A copied or restored configured Vault can show a one-time compatibility review on its new device or profile. This is intentional even when its local database appears empty, because emptiness does not prove how the Vault was produced.
|
||||
- An older installation cannot dismiss evidence that a newer implementation or settings schema has already been used on the device.
|
||||
- The Obsidian-specific dialogue depends only on a host-neutral compatibility result and the injected confirmation capability. Commonlib remains responsible for settings migration, device-local storage, and the replication gate.
|
||||
- A future incompatible database change must increment `VER`, provide an actionable review message, verify the remote version negotiation, and test both the pending and acknowledged states. A major SemVer increase without those changes has no database-compatibility effect.
|
||||
@@ -52,3 +61,5 @@ The remote `obsydian_livesync_version` document also carries the internal databa
|
||||
- Unit tests verify new-Vault initialisation, upgrades, missing and invalid markers, downgrades, future settings schemas, legacy marker migration, acknowledgement ordering, and save-failure recovery while retaining automatic synchronisation choices.
|
||||
- Unit tests verify that a pending review is honoured by the packaged Commonlib replication service before remote activity begins.
|
||||
- A real-Obsidian settings test verifies the dedicated summary and details dialogues, captures representative screenshots, confirms that the acknowledged internal version advances only after explicit resume, and confirms that the Change Log contains no acknowledgement control.
|
||||
- The real-Obsidian CouchDB workflow starts from configured plug-in data without a device-local marker, verifies the copied-or-restored Vault explanation, resumes through the actual dialogue, and then completes remote metadata, chunk, and activity checks. The two-Vault workflow performs the same review once per isolated Vault before reusing the acknowledged device state for later process launches.
|
||||
- Unit tests fix the layout-ready priority after the three flag-file recovery priorities, so a recovery which stops start-up cannot race the compatibility dialogue.
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ There are many settings in Self-hosted LiveSync. This document describes each se
|
||||
|
||||
This pane always shows the current release history. It does not track whether a particular plug-in version has been read and does not open automatically after an ordinary update.
|
||||
|
||||
Internal database or settings compatibility reviews use a separate safety dialogue, not this pane. The dialogue explains why remote synchronisation has been paused and preserves the automatic synchronisation choices which were configured before the update. Closing it keeps synchronisation paused. When the detected state can be handled by the running version, the explicit resume action records the current internal database version and restores the configured behaviour. A persistent Notice and the `Review why synchronisation is paused` command reopen the review. An older installation cannot dismiss a pause caused by a newer database or settings version.
|
||||
Internal database or settings compatibility reviews use a separate safety dialogue, not this pane. The dialogue explains why remote synchronisation has been paused and preserves the automatic synchronisation choices which were configured before the update. A configured Vault which was copied, restored, or opened in a new Obsidian profile can require this review because its device-local acknowledgement is not part of the Vault data. An empty local database is not accepted as evidence that it is safe to continue. Closing the dialogue keeps synchronisation paused. When the detected state can be handled by the running version, the explicit resume action records the current internal database version and restores the configured behaviour. A persistent Notice and the `Review why synchronisation is paused` command reopen the review. An older installation cannot dismiss a pause caused by a newer database or settings version.
|
||||
|
||||
## 1. Setup
|
||||
|
||||
|
||||
@@ -391,6 +391,8 @@ A flag file is a simple Markdown file located at the root of your vault. Its ver
|
||||
|
||||
When resetting synchronisation on this device or overwriting server data, restarting Obsidian is performed once for safety reasons. At that time, Self-hosted LiveSync uses these files to determine whether the process should be carried out. (This mechanism is especially useful on mobile devices to force cancellation if the database rebuilding fails). These files are not subject to synchronisation.
|
||||
|
||||
Flag-file recovery is handled before the compatibility-review dialogue. `redflag.md` keeps start-up stopped, while a cancelled recovery or one which schedules a restart also prevents a second dialogue from competing with it in that process. When fetch-all or rebuild-all completes and start-up continues, Self-hosted LiveSync can still ask for compatibility review before ordinary synchronisation resumes. Completing a recovery does not automatically acknowledge a database or settings version; use the explicit resume action after reviewing the explanation.
|
||||
|
||||
#### How to Resolve the Scram Loop
|
||||
|
||||
If you cannot disable Scram, please follow these steps:
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
export type CompatibilityReviewSummaryAction = "details" | "resume" | "keep-paused" | false;
|
||||
export type CompatibilityReviewDetailsAction = "back" | false;
|
||||
|
||||
// Explicit flag-file recovery runs at priorities 5, 10, and 20. Present the
|
||||
// compatibility review only after those operations have completed; a recovery
|
||||
// handler which stops start-up also prevents this dialogue from competing with it.
|
||||
export const COMPATIBILITY_REVIEW_LAYOUT_PRIORITY = 30;
|
||||
|
||||
export interface CompatibilityReviewUi {
|
||||
showSummary(pause: CompatibilityPause): Promise<CompatibilityReviewSummaryAction>;
|
||||
showDetails(pause: CompatibilityPause): Promise<CompatibilityReviewDetailsAction>;
|
||||
@@ -139,7 +144,7 @@ export function useCompatibilityReview(core: LiveSyncCore, ui: CompatibilityRevi
|
||||
core.services.appLifecycle.onLayoutReady.addHandler(() => {
|
||||
fireAndForget(() => controller.openReview());
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
}, COMPATIBILITY_REVIEW_LAYOUT_PRIORITY);
|
||||
core.services.appLifecycle.onUnload.addHandler(() => {
|
||||
controller.dispose();
|
||||
return Promise.resolve(true);
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
DATABASE_COMPATIBILITY_VERSION_KEY,
|
||||
legacyDatabaseCompatibilityVersionKey,
|
||||
} from "@/common/databaseCompatibility.ts";
|
||||
import { CompatibilityReviewController, type CompatibilityReviewUi } from "./compatibilityReview.ts";
|
||||
import {
|
||||
CompatibilityReviewController,
|
||||
type CompatibilityReviewUi,
|
||||
useCompatibilityReview,
|
||||
} from "./compatibilityReview.ts";
|
||||
|
||||
function migrationState(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
@@ -161,4 +165,26 @@ describe("compatibility review controller", () => {
|
||||
expect(fixture.ui.showSummary).not.toHaveBeenCalled();
|
||||
expect(fixture.ui.clearReminder).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("runs the review after the ordered red flag recovery handlers", () => {
|
||||
const onSettingLoaded = { addHandler: vi.fn() };
|
||||
const onLayoutReady = { addHandler: vi.fn() };
|
||||
const onUnload = { addHandler: vi.fn() };
|
||||
const core = {
|
||||
services: {
|
||||
appLifecycle: { onSettingLoaded, onLayoutReady, onUnload },
|
||||
API: { addCommand: vi.fn() },
|
||||
},
|
||||
} as never;
|
||||
const ui: CompatibilityReviewUi = {
|
||||
showSummary: vi.fn(),
|
||||
showDetails: vi.fn(),
|
||||
showReminder: vi.fn(),
|
||||
clearReminder: vi.fn(),
|
||||
};
|
||||
|
||||
useCompatibilityReview(core, ui);
|
||||
|
||||
expect(onLayoutReady.addHandler).toHaveBeenCalledWith(expect.any(Function), 30);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { CompatibilityPause, CompatibilityPauseReason } from "@/common/databaseCompatibility.ts";
|
||||
|
||||
export function compatibilityReviewSummaryMarkdown(pause: CompatibilityPause): string {
|
||||
const action = pause.resumable
|
||||
? "Before resuming, review the compatibility details and update Self-hosted LiveSync on every device which uses this remote database."
|
||||
: "This installation cannot safely acknowledge the detected state. Update Self-hosted LiveSync before attempting to synchronise again.";
|
||||
return `Remote synchronisation is paused on this device because its compatibility state requires attention.
|
||||
|
||||
${action}
|
||||
|
||||
Your automatic synchronisation preferences have not been changed. Closing this dialogue keeps synchronisation paused.`;
|
||||
}
|
||||
|
||||
function reasonMarkdown(reason: CompatibilityPauseReason): string {
|
||||
if (reason.source === "database-version") {
|
||||
if (reason.state === "upgrade") {
|
||||
return `- The last acknowledged internal database version was **${reason.acknowledgedVersion}** and this installation uses **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.state === "downgrade") {
|
||||
return `- This installation uses internal database version **${reason.currentVersion}**, but this device previously acknowledged newer version **${reason.acknowledgedVersion}**. An older installation must not resume synchronisation.`;
|
||||
}
|
||||
if (reason.state === "missing") {
|
||||
return `- No previously acknowledged internal database version was found for this existing Vault. This can happen when a Vault is copied or restored, or when it is opened with a new Obsidian profile. This installation uses version **${reason.currentVersion}**. An empty local database does not mean that it is safe to resume automatically.`;
|
||||
}
|
||||
return `- The saved internal database version marker is invalid. This installation uses version **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.source === "settings-schema") {
|
||||
if (reason.isFromFutureSchema) {
|
||||
return `- The saved settings use schema **${reason.sourceVersion}**, which is newer than schema **${reason.currentVersion}** supported by this installation.`;
|
||||
}
|
||||
return `- The settings were migrated from schema **${reason.sourceVersion}** to **${reason.currentVersion}** and require review before synchronisation resumes.`;
|
||||
}
|
||||
const escapedMessage = reason.message.replace(/[\\`*_{}[\]()<>#+.!|-]/gu, "\\$&");
|
||||
return `- An earlier compatibility review remains pending: ${escapedMessage}`;
|
||||
}
|
||||
|
||||
export function compatibilityReviewDetailsMarkdown(pause: CompatibilityPause): string {
|
||||
const resolution = pause.resumable
|
||||
? "After all devices have been updated, return to the compatibility review summary and explicitly resume synchronisation. The current internal version will only then be recorded as acknowledged."
|
||||
: "Install a compatible current version of Self-hosted LiveSync. This pause cannot be dismissed by the current installation.";
|
||||
return `## Why synchronisation is paused
|
||||
|
||||
${pause.reasons.map(reasonMarkdown).join("\n")}
|
||||
|
||||
## What the pause changes
|
||||
|
||||
- Remote replication is blocked before work begins.
|
||||
- Your saved automatic synchronisation preferences remain unchanged.
|
||||
- Closing either dialogue leaves the safety gate active.
|
||||
|
||||
## What to do next
|
||||
|
||||
${resolution}`;
|
||||
}
|
||||
@@ -1,70 +1,21 @@
|
||||
import { Notice } from "@/deps.ts";
|
||||
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
|
||||
import type { CompatibilityPause, CompatibilityPauseReason } from "@/common/databaseCompatibility.ts";
|
||||
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
|
||||
import type {
|
||||
CompatibilityReviewDetailsAction,
|
||||
CompatibilityReviewSummaryAction,
|
||||
CompatibilityReviewUi,
|
||||
} from "./compatibilityReview.ts";
|
||||
import {
|
||||
compatibilityReviewDetailsMarkdown,
|
||||
compatibilityReviewSummaryMarkdown,
|
||||
} from "./compatibilityReviewMarkdown.ts";
|
||||
|
||||
const REVIEW_DETAILS = "Review compatibility details";
|
||||
const KEEP_PAUSED = "Keep synchronisation paused";
|
||||
const RESUME = "Resume synchronisation";
|
||||
const BACK = "Back to compatibility review";
|
||||
|
||||
function summaryMarkdown(pause: CompatibilityPause): string {
|
||||
const action = pause.resumable
|
||||
? "Before resuming, review the compatibility details and update Self-hosted LiveSync on every device which uses this remote database."
|
||||
: "This installation cannot safely acknowledge the detected state. Update Self-hosted LiveSync before attempting to synchronise again.";
|
||||
return `Remote synchronisation is paused on this device because its compatibility state requires attention.
|
||||
|
||||
${action}
|
||||
|
||||
Your automatic synchronisation preferences have not been changed. Closing this dialogue keeps synchronisation paused.`;
|
||||
}
|
||||
|
||||
function reasonMarkdown(reason: CompatibilityPauseReason): string {
|
||||
if (reason.source === "database-version") {
|
||||
if (reason.state === "upgrade") {
|
||||
return `- The last acknowledged internal database version was **${reason.acknowledgedVersion}** and this installation uses **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.state === "downgrade") {
|
||||
return `- This installation uses internal database version **${reason.currentVersion}**, but this device previously acknowledged newer version **${reason.acknowledgedVersion}**. An older installation must not resume synchronisation.`;
|
||||
}
|
||||
if (reason.state === "missing") {
|
||||
return `- No previously acknowledged internal database version was found for this existing Vault. This installation uses version **${reason.currentVersion}**.`;
|
||||
}
|
||||
return `- The saved internal database version marker is invalid. This installation uses version **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.source === "settings-schema") {
|
||||
if (reason.isFromFutureSchema) {
|
||||
return `- The saved settings use schema **${reason.sourceVersion}**, which is newer than schema **${reason.currentVersion}** supported by this installation.`;
|
||||
}
|
||||
return `- The settings were migrated from schema **${reason.sourceVersion}** to **${reason.currentVersion}** and require review before synchronisation resumes.`;
|
||||
}
|
||||
const escapedMessage = reason.message.replace(/[\\`*_{}[\]()<>#+.!|-]/gu, "\\$&");
|
||||
return `- An earlier compatibility review remains pending: ${escapedMessage}`;
|
||||
}
|
||||
|
||||
function detailsMarkdown(pause: CompatibilityPause): string {
|
||||
const resolution = pause.resumable
|
||||
? "After all devices have been updated, return to the compatibility review summary and explicitly resume synchronisation. The current internal version will only then be recorded as acknowledged."
|
||||
: "Install a compatible current version of Self-hosted LiveSync. This pause cannot be dismissed by the current installation.";
|
||||
return `## Why synchronisation is paused
|
||||
|
||||
${pause.reasons.map(reasonMarkdown).join("\n")}
|
||||
|
||||
## What the pause changes
|
||||
|
||||
- Remote replication is blocked before work begins.
|
||||
- Your saved automatic synchronisation preferences remain unchanged.
|
||||
- Closing either dialogue leaves the safety gate active.
|
||||
|
||||
## What to do next
|
||||
|
||||
${resolution}`;
|
||||
}
|
||||
|
||||
export class ObsidianCompatibilityReviewUi implements CompatibilityReviewUi {
|
||||
private reminder: Notice | undefined;
|
||||
|
||||
@@ -76,7 +27,7 @@ export class ObsidianCompatibilityReviewUi implements CompatibilityReviewUi {
|
||||
: ([REVIEW_DETAILS, KEEP_PAUSED] as const);
|
||||
const result = await this.confirm.confirmWithMessage(
|
||||
"Synchronisation paused for compatibility review",
|
||||
summaryMarkdown(pause),
|
||||
compatibilityReviewSummaryMarkdown(pause),
|
||||
[...buttons],
|
||||
KEEP_PAUSED,
|
||||
undefined,
|
||||
@@ -91,7 +42,7 @@ export class ObsidianCompatibilityReviewUi implements CompatibilityReviewUi {
|
||||
async showDetails(pause: CompatibilityPause): Promise<CompatibilityReviewDetailsAction> {
|
||||
const result = await this.confirm.confirmWithMessage(
|
||||
"Compatibility review details",
|
||||
detailsMarkdown(pause),
|
||||
compatibilityReviewDetailsMarkdown(pause),
|
||||
[BACK],
|
||||
BACK,
|
||||
undefined,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
|
||||
import { compatibilityReviewDetailsMarkdown } from "./compatibilityReviewMarkdown.ts";
|
||||
|
||||
describe("Obsidian compatibility review", () => {
|
||||
it("explains why a configured Vault can be missing its device-local acknowledgement", async () => {
|
||||
const pause: CompatibilityPause = {
|
||||
resumable: true,
|
||||
reasons: [
|
||||
{
|
||||
source: "database-version",
|
||||
state: "missing",
|
||||
currentVersion: 12,
|
||||
resumable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const details = compatibilityReviewDetailsMarkdown(pause);
|
||||
expect(details).toContain("copied or restored");
|
||||
expect(details).toContain("new Obsidian profile");
|
||||
expect(details).toContain("does not mean that it is safe to resume automatically");
|
||||
});
|
||||
});
|
||||
@@ -82,12 +82,14 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe
|
||||
|
||||
`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, Svelte dialogue mounting, settings UI, vault reflection, CouchDB upload, CLI-to-Obsidian synchronisation, Object Storage upload, startup scan, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB and MinIO fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run.
|
||||
|
||||
`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, configures Self-hosted LiveSync through `obsidian-cli eval`, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents.
|
||||
`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, starts from configured plug-in data without the device-local compatibility marker, and verifies the copied-or-restored Vault explanation in the actual compatibility dialogue. It captures the summary and details, resumes explicitly, confirms that the marker was recorded, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents.
|
||||
|
||||
The same workflow checks the two remote-activity status boundaries. It first holds a real CouchDB request at the selected fetch implementation and confirms that `🌐N` is visible while `📲` is absent. It then holds the real one-shot replication immediately before its replicator call, confirms that `📲` is visible while no physical request is active, releases it, and requires the finite and bounded activity counts to return to zero, the request and response counts to balance, and both indicators to disappear. Finally, it creates a remote-only chunk, holds the real on-demand fetch immediately before its remote call, makes the same logical active and idle assertions, and verifies that the fetched chunk is written into the local database. These gates make the active states deterministic without replacing the remote request or operation.
|
||||
|
||||
If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, and the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory.
|
||||
|
||||
The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches seed the acknowledgement which the same real profile would retain, rather than repeatedly applying a first-device decision. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode.
|
||||
|
||||
`test:e2e:obsidian:cli-to-obsidian-sync` is the cross-runtime compatibility check for the official LiveSync CLI and the real Obsidian plug-in. Build the plug-in first, and build the local CLI too when no external CLI command is selected. The script uses E2EE, Path Obfuscation, and the current preferred chunk settings to create and synchronise a note through the CLI, starts real Obsidian with an isolated Vault and profile, synchronises the same CouchDB database, and verifies that the plug-in materialises identical note content. This covers the boundary that CLI-only and plug-in-only round trips do not exercise.
|
||||
|
||||
By default, the compatibility check runs `node src/apps/cli/dist/index.cjs`. Set `LIVESYNC_CLI_COMMAND` to test another CLI build or distribution. The value may be a quoted command line or a JSON array of executable and prefix arguments; the scenario arguments are appended without going through a shell.
|
||||
@@ -111,7 +113,7 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) --
|
||||
|
||||
`test:e2e:obsidian:two-vault-sync` runs a two-vault note synchronisation workflow. It verifies note creation, update, ordinary rename, a case-only file name change within the same directory, deletion, per-device target filters where one vault ignores a note that the other vault synchronises, and a separate encrypted round-trip with Path Obfuscation enabled. Directory case changes deliberately remain outside this scenario because they require directory-aware rename handling. The optional Markdown conflict automatic merge check can be enabled with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`, but it is not part of the default local suite.
|
||||
|
||||
`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises.
|
||||
`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one mobile-safe Notice with actionable touch targets; a manually dismissed group must not repeat its acknowledged rows when a later change arrives.
|
||||
|
||||
`test:e2e:obsidian:customisation-sync` runs a two-vault Customisation Sync workflow. It scans a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronises the entries through CouchDB, applies them on the second vault, verifies the resulting `.obsidian` files, propagates a snippet update, and verifies deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy.
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { evalObsidianJson } from "./cli.ts";
|
||||
import { SERVICE_CONTEXT_MEMBERS } from "../../contracts/serviceContext.ts";
|
||||
import { DATABASE_COMPATIBILITY_VERSION_KEY } from "../../../src/common/databaseCompatibility.ts";
|
||||
import { CURRENT_SETTING_VERSION } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { CouchDbConfig } from "./couchdb.ts";
|
||||
import type { ObjectStorageConfig } from "./objectStorage.ts";
|
||||
import { captureObsidianDialogue, withObsidianPage } from "./ui.ts";
|
||||
|
||||
export type ConfiguredSettings = {
|
||||
isConfigured: boolean;
|
||||
@@ -21,6 +25,28 @@ export type CoreReadiness = {
|
||||
appReady: boolean;
|
||||
};
|
||||
|
||||
export type ReplicationAttempt = CoreReadiness & {
|
||||
succeeded: boolean;
|
||||
isOnline: boolean;
|
||||
activeReplicator: string;
|
||||
versionUpFlash: string;
|
||||
unresolvedMessages: unknown[];
|
||||
};
|
||||
|
||||
export type CompatibilityMarkerState = {
|
||||
vaultName: string;
|
||||
additionalSuffix: string;
|
||||
expectedStorageKey: string;
|
||||
rawStorageValue: string | null;
|
||||
serviceValue: string;
|
||||
versionUpFlash: string;
|
||||
};
|
||||
|
||||
export type ResumeCompatibilityReviewOptions = {
|
||||
verifyMissingDeviceMarkerExplanation?: boolean;
|
||||
screenshotPrefix?: string;
|
||||
};
|
||||
|
||||
export type ObsidianServiceContextContractResult = {
|
||||
contextType: string;
|
||||
eventResult: string[];
|
||||
@@ -40,28 +66,170 @@ export type LocalDatabaseEntry = {
|
||||
children: string[];
|
||||
};
|
||||
|
||||
function e2ePreferredSettingsSource(): string[] {
|
||||
return [
|
||||
"liveSync:false,",
|
||||
"syncOnStart:false,",
|
||||
"syncOnSave:false,",
|
||||
"usePluginSync:false,",
|
||||
"usePluginSyncV2:true,",
|
||||
"useEden:false,",
|
||||
"customChunkSize:60,",
|
||||
"sendChunksBulk:false,",
|
||||
"sendChunksBulkMaxSize:1,",
|
||||
"chunkSplitterVersion:'v3-rabin-karp',",
|
||||
"readChunksOnline:true,",
|
||||
"disableCheckingConfigMismatch:false,",
|
||||
"enableCompression:false,",
|
||||
"hashAlg:'xxhash64',",
|
||||
"handleFilenameCaseSensitive:false,",
|
||||
"doNotUseFixedRevisionForChunks:true,",
|
||||
"E2EEAlgorithm:'v2',",
|
||||
"doctorProcessedVersion:'0.25.27',",
|
||||
"isConfigured:true,",
|
||||
];
|
||||
const E2E_PREFERRED_SETTINGS = {
|
||||
liveSync: false,
|
||||
syncOnStart: false,
|
||||
syncOnSave: false,
|
||||
usePluginSync: false,
|
||||
usePluginSyncV2: true,
|
||||
useEden: false,
|
||||
customChunkSize: 60,
|
||||
sendChunksBulk: false,
|
||||
sendChunksBulkMaxSize: 1,
|
||||
chunkSplitterVersion: "v3-rabin-karp",
|
||||
readChunksOnline: true,
|
||||
disableCheckingConfigMismatch: false,
|
||||
enableCompression: false,
|
||||
hashAlg: "xxhash64",
|
||||
handleFilenameCaseSensitive: false,
|
||||
doNotUseFixedRevisionForChunks: true,
|
||||
E2EEAlgorithm: "v2",
|
||||
doctorProcessedVersion: "0.25.27",
|
||||
settingVersion: CURRENT_SETTING_VERSION,
|
||||
isConfigured: true,
|
||||
} as const;
|
||||
|
||||
export function createE2eObsidianDeviceLocalState(
|
||||
vaultName: string,
|
||||
additionalSuffixOfDatabaseName = ""
|
||||
): Readonly<Record<string, string>> {
|
||||
return {
|
||||
[`${vaultName}-${additionalSuffixOfDatabaseName}-${DATABASE_COMPATIBILITY_VERSION_KEY}`]: `${VER}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function readE2eCompatibilityMarker(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv
|
||||
): Promise<CompatibilityMarkerState> {
|
||||
return await evalObsidianJson<CompatibilityMarkerState>(
|
||||
cliBinary,
|
||||
[
|
||||
"(()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const setting=core.services.setting;",
|
||||
"const settings=setting.currentSettings();",
|
||||
"const vaultName=core.services.API.getSystemVaultName();",
|
||||
`const markerKey=${JSON.stringify(DATABASE_COMPATIBILITY_VERSION_KEY)};`,
|
||||
"const additionalSuffix=`-${settings.additionalSuffixOfDatabaseName??''}`;",
|
||||
"const expectedStorageKey=`${vaultName}${additionalSuffix}-${markerKey}`;",
|
||||
"return JSON.stringify({",
|
||||
"vaultName,additionalSuffix,expectedStorageKey,",
|
||||
"rawStorageValue:localStorage.getItem(expectedStorageKey),",
|
||||
"serviceValue:setting.getSmallConfig(markerKey),",
|
||||
"versionUpFlash:settings.versionUpFlash,",
|
||||
"});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
export async function assertE2eCompatibilityMarker(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv
|
||||
): Promise<CompatibilityMarkerState> {
|
||||
const state = await readE2eCompatibilityMarker(cliBinary, env);
|
||||
if (state.serviceValue !== `${VER}`) {
|
||||
throw new Error(
|
||||
`The E2E compatibility marker was not available on first plug-in load: ${JSON.stringify(state)}`
|
||||
);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export async function assertE2eCompatibilityReviewPending(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv
|
||||
): Promise<CompatibilityMarkerState> {
|
||||
const state = await readE2eCompatibilityMarker(cliBinary, env);
|
||||
if (state.serviceValue !== "" || state.rawStorageValue !== null || state.versionUpFlash === "") {
|
||||
throw new Error(`The copied-Vault compatibility review was not pending: ${JSON.stringify(state)}`);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export async function resumeCompatibilityReview(
|
||||
port: number,
|
||||
options: ResumeCompatibilityReviewOptions = {}
|
||||
): Promise<void> {
|
||||
const timeoutMs = Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000);
|
||||
const title = "Synchronisation paused for compatibility review";
|
||||
const summaryLocator = (page: Parameters<Parameters<typeof withObsidianPage>[1]>[0]) =>
|
||||
page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: title }),
|
||||
});
|
||||
|
||||
if (options.screenshotPrefix) {
|
||||
const summaryScreenshot = await captureObsidianDialogue(
|
||||
port,
|
||||
`${options.screenshotPrefix}-summary.png`,
|
||||
async (page) => {
|
||||
await summaryLocator(page).waitFor({ state: "visible", timeout: timeoutMs });
|
||||
}
|
||||
);
|
||||
console.log(`Compatibility review summary screenshot: ${summaryScreenshot}`);
|
||||
}
|
||||
|
||||
if (options.verifyMissingDeviceMarkerExplanation === true) {
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const summary = summaryLocator(page);
|
||||
await summary.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
await summary.getByRole("button", { name: "Review compatibility details" }).click();
|
||||
});
|
||||
const detailsScreenshot = options.screenshotPrefix
|
||||
? await captureObsidianDialogue(port, `${options.screenshotPrefix}-details.png`, async (page) => {
|
||||
const details = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }),
|
||||
});
|
||||
await details.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
await details.getByText("copied or restored", { exact: false }).waitFor({
|
||||
state: "visible",
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
await details.getByText("new Obsidian profile", { exact: false }).waitFor({
|
||||
state: "visible",
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
await details
|
||||
.getByText("does not mean that it is safe to resume automatically", { exact: false })
|
||||
.waitFor({
|
||||
state: "visible",
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
})
|
||||
: undefined;
|
||||
if (detailsScreenshot) console.log(`Compatibility review details screenshot: ${detailsScreenshot}`);
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const details = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }),
|
||||
});
|
||||
await details.getByRole("button", { name: "Back to compatibility review" }).click();
|
||||
await summaryLocator(page).waitFor({ state: "visible", timeout: timeoutMs });
|
||||
});
|
||||
}
|
||||
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const summary = summaryLocator(page);
|
||||
await summary.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
await summary.getByRole("button", { name: "Resume synchronisation" }).click();
|
||||
await summary.waitFor({ state: "hidden", timeout: timeoutMs });
|
||||
});
|
||||
}
|
||||
|
||||
export function createE2eCouchDbPluginData(
|
||||
settings: Pick<CouchDbConfig, "uri" | "username" | "password"> & { dbName: string },
|
||||
overrides: Record<string, unknown> = {}
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
couchDB_URI: settings.uri,
|
||||
couchDB_USER: settings.username,
|
||||
couchDB_PASSWORD: settings.password,
|
||||
couchDB_DBNAME: settings.dbName,
|
||||
remoteType: "",
|
||||
...E2E_PREFERRED_SETTINGS,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function assertEqual(actual: unknown, expected: unknown, message: string): void {
|
||||
@@ -76,21 +244,14 @@ export async function configureCouchDb(
|
||||
settings: Pick<CouchDbConfig, "uri" | "username" | "password"> & { dbName: string },
|
||||
overrides: Record<string, unknown> = {}
|
||||
): Promise<ConfiguredSettings> {
|
||||
const nextSettings = createE2eCouchDbPluginData(settings, overrides);
|
||||
return await evalObsidianJson<ConfiguredSettings>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const plugin=app.plugins.plugins['obsidian-livesync'];",
|
||||
"const core=plugin.core;",
|
||||
"const nextSettings={",
|
||||
`couchDB_URI:${JSON.stringify(settings.uri)},`,
|
||||
`couchDB_USER:${JSON.stringify(settings.username)},`,
|
||||
`couchDB_PASSWORD:${JSON.stringify(settings.password)},`,
|
||||
`couchDB_DBNAME:${JSON.stringify(settings.dbName)},`,
|
||||
"remoteType:'',",
|
||||
...e2ePreferredSettingsSource(),
|
||||
...Object.entries(overrides).map(([key, value]) => `${JSON.stringify(key)}:${JSON.stringify(value)},`),
|
||||
"};",
|
||||
`const nextSettings=${JSON.stringify(nextSettings)};`,
|
||||
"await core.services.setting.applyExternalSettings(nextSettings,true);",
|
||||
"await core.services.control.applySettings();",
|
||||
"const current=core.services.setting.currentSettings();",
|
||||
@@ -115,25 +276,26 @@ export async function configureObjectStorage(
|
||||
settings: ObjectStorageConfig & { bucketPrefix: string },
|
||||
overrides: Record<string, unknown> = {}
|
||||
): Promise<ConfiguredSettings> {
|
||||
const nextSettings = {
|
||||
remoteType: "MINIO",
|
||||
endpoint: settings.endpoint,
|
||||
accessKey: settings.accessKey,
|
||||
secretKey: settings.secretKey,
|
||||
bucket: settings.bucket,
|
||||
region: settings.region,
|
||||
forcePathStyle: settings.forcePathStyle,
|
||||
bucketPrefix: settings.bucketPrefix,
|
||||
bucketCustomHeaders: "",
|
||||
...E2E_PREFERRED_SETTINGS,
|
||||
...overrides,
|
||||
};
|
||||
return await evalObsidianJson<ConfiguredSettings>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const plugin=app.plugins.plugins['obsidian-livesync'];",
|
||||
"const core=plugin.core;",
|
||||
"const nextSettings={",
|
||||
"remoteType:'MINIO',",
|
||||
`endpoint:${JSON.stringify(settings.endpoint)},`,
|
||||
`accessKey:${JSON.stringify(settings.accessKey)},`,
|
||||
`secretKey:${JSON.stringify(settings.secretKey)},`,
|
||||
`bucket:${JSON.stringify(settings.bucket)},`,
|
||||
`region:${JSON.stringify(settings.region)},`,
|
||||
`forcePathStyle:${JSON.stringify(settings.forcePathStyle)},`,
|
||||
`bucketPrefix:${JSON.stringify(settings.bucketPrefix)},`,
|
||||
"bucketCustomHeaders:'',",
|
||||
...e2ePreferredSettingsSource(),
|
||||
...Object.entries(overrides).map(([key, value]) => `${JSON.stringify(key)}:${JSON.stringify(value)},`),
|
||||
"};",
|
||||
`const nextSettings=${JSON.stringify(nextSettings)};`,
|
||||
"await core.services.setting.applyExternalSettings(nextSettings,true);",
|
||||
"await core.services.control.applySettings();",
|
||||
"const current=core.services.setting.currentSettings();",
|
||||
@@ -265,18 +427,31 @@ export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv):
|
||||
}
|
||||
|
||||
export async function pushLocalChanges(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
const attempt = await evalObsidianJson<ReplicationAttempt>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"await core.services.fileProcessing.commitPendingFileEvents();",
|
||||
"const result=await core.services.replication.replicate(true);",
|
||||
"return JSON.stringify({result:!!result});",
|
||||
"const settings=core.services.setting.currentSettings();",
|
||||
"const activeReplicator=core.services.replicator.getActiveReplicator();",
|
||||
"return JSON.stringify({",
|
||||
"succeeded:!!result,",
|
||||
"databaseReady:core.services.database.isDatabaseReady(),",
|
||||
"appReady:core.services.appLifecycle.isReady(),",
|
||||
"isOnline:core.services.API.isOnline,",
|
||||
"activeReplicator:activeReplicator?.constructor?.name??'(none)',",
|
||||
"versionUpFlash:settings.versionUpFlash,",
|
||||
"unresolvedMessages:(await core.services.appLifecycle.getUnresolvedMessages()).flat(),",
|
||||
"});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
if (!attempt.succeeded) {
|
||||
throw new Error(`Finite replication did not start or complete: ${JSON.stringify(attempt)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForLocalDatabaseEntry(
|
||||
|
||||
@@ -9,6 +9,7 @@ export type StartObsidianLiveSyncSessionOptions = {
|
||||
vault: TemporaryVault;
|
||||
startupGraceMs?: number;
|
||||
pluginData?: Record<string, unknown>;
|
||||
localStorageEntries?: Readonly<Record<string, string>>;
|
||||
};
|
||||
|
||||
export async function startObsidianLiveSyncSession(
|
||||
@@ -22,5 +23,6 @@ export async function startObsidianLiveSyncSession(
|
||||
artifactRoot: process.cwd(),
|
||||
startupGraceMs: options.startupGraceMs,
|
||||
pluginData: options.pluginData,
|
||||
localStorageEntries: options.localStorageEntries,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ import {
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import {
|
||||
assertEqual,
|
||||
assertE2eCompatibilityMarker,
|
||||
assertE2eCompatibilityReviewPending,
|
||||
configureCouchDb,
|
||||
createE2eCouchDbPluginData,
|
||||
prepareRemote,
|
||||
resumeCompatibilityReview,
|
||||
waitForLiveSyncCoreReady,
|
||||
type LocalDatabaseEntry,
|
||||
} from "../runner/liveSyncWorkflow.ts";
|
||||
@@ -101,8 +105,20 @@ async function main(): Promise<void> {
|
||||
cliBinary: cli.binary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
pluginData: createE2eCouchDbPluginData({
|
||||
uri: couchDb.uri,
|
||||
username: couchDb.username,
|
||||
password: couchDb.password,
|
||||
dbName,
|
||||
}),
|
||||
});
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
await assertE2eCompatibilityReviewPending(cli.binary, session.cliEnv);
|
||||
await resumeCompatibilityReview(session.remoteDebuggingPort, {
|
||||
verifyMissingDeviceMarkerExplanation: true,
|
||||
screenshotPrefix: "compatibility-review-copied-vault",
|
||||
});
|
||||
await assertE2eCompatibilityMarker(cli.binary, session.cliEnv);
|
||||
|
||||
const configured = await configureCouchDb(cli.binary, session.cliEnv, {
|
||||
uri: couchDb.uri,
|
||||
|
||||
@@ -13,9 +13,14 @@ import {
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import {
|
||||
assertEqual,
|
||||
assertE2eCompatibilityMarker,
|
||||
assertE2eCompatibilityReviewPending,
|
||||
configureCouchDb,
|
||||
createE2eCouchDbPluginData,
|
||||
createE2eObsidianDeviceLocalState,
|
||||
prepareRemote,
|
||||
pushLocalChanges,
|
||||
resumeCompatibilityReview,
|
||||
waitForLiveSyncCoreReady,
|
||||
waitForLocalDatabaseEntry,
|
||||
type LocalDatabaseEntry,
|
||||
@@ -42,6 +47,7 @@ type RunnerContext = {
|
||||
cliBinary: string;
|
||||
couchDb: CouchDbConfig;
|
||||
dbName: string;
|
||||
reviewedVaults: Set<string>;
|
||||
};
|
||||
|
||||
async function writeVaultFile(vaultPath: string, path: string, content: string): Promise<void> {
|
||||
@@ -163,24 +169,32 @@ async function startConfiguredSession(
|
||||
vault: TemporaryVault,
|
||||
overrides: Record<string, unknown> = {}
|
||||
): Promise<ObsidianLiveSyncSession> {
|
||||
const couchDbSettings = {
|
||||
uri: context.couchDb.uri,
|
||||
username: context.couchDb.username,
|
||||
password: context.couchDb.password,
|
||||
dbName: context.dbName,
|
||||
};
|
||||
const reviewAlreadyCompleted = context.reviewedVaults.has(vault.path);
|
||||
const session = await startObsidianLiveSyncSession({
|
||||
binary: context.binary,
|
||||
cliBinary: context.cliBinary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
pluginData: createE2eCouchDbPluginData(couchDbSettings, overrides),
|
||||
// The real profile retains this device-local acknowledgement. Seed it
|
||||
// on later process launches because the isolated Electron runner does
|
||||
// not currently preserve localStorage across those launches.
|
||||
localStorageEntries: reviewAlreadyCompleted ? createE2eObsidianDeviceLocalState(vault.name) : undefined,
|
||||
});
|
||||
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,
|
||||
},
|
||||
overrides
|
||||
);
|
||||
if (!reviewAlreadyCompleted) {
|
||||
await assertE2eCompatibilityReviewPending(context.cliBinary, session.cliEnv);
|
||||
await resumeCompatibilityReview(session.remoteDebuggingPort);
|
||||
}
|
||||
await assertE2eCompatibilityMarker(context.cliBinary, session.cliEnv);
|
||||
if (!reviewAlreadyCompleted) context.reviewedVaults.add(vault.path);
|
||||
await configureCouchDb(context.cliBinary, session.cliEnv, couchDbSettings, overrides);
|
||||
await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv);
|
||||
await prepareRemote(context.cliBinary, session.cliEnv);
|
||||
return session;
|
||||
@@ -517,8 +531,14 @@ async function main(): Promise<void> {
|
||||
const vaultB = await createTemporaryVault();
|
||||
const encryptedVaultA = await createTemporaryVault();
|
||||
const encryptedVaultB = await createTemporaryVault();
|
||||
const context: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName };
|
||||
const encryptedContext: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName: encryptedDbName };
|
||||
const context: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName, reviewedVaults: new Set() };
|
||||
const encryptedContext: RunnerContext = {
|
||||
binary,
|
||||
cliBinary: cli.binary,
|
||||
couchDb,
|
||||
dbName: encryptedDbName,
|
||||
reviewedVaults: new Set(),
|
||||
};
|
||||
|
||||
try {
|
||||
await assertCouchDbReachable(couchDb);
|
||||
|
||||
+2
-1
@@ -20,7 +20,8 @@ Earlier releases remain available in the [0.25 release history](docs/releases/0.
|
||||
|
||||
### Improved
|
||||
|
||||
- Stacked compatibility-review actions vertically and kept persistent review reminders clear of mobile close controls. Long setup dialogues now keep their action area inside the mobile safe area while their content remains scrollable.
|
||||
- Stacked compatibility-review actions vertically and kept persistent review reminders clear of mobile close controls. Long setup dialogues now keep their action area inside the mobile safe area while their content remains scrollable. A configured Vault with no device-local acknowledgement now explains that it may have been copied, restored, or opened in a new profile instead of trying to infer safety from an empty local database.
|
||||
- Combined Hidden File Sync plug-in reload and Obsidian restart notifications into one mobile-safe message with clearly labelled actions, avoiding the stacked notifications reported in [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555). A group dismissed by the user no longer reappears with its previously acknowledged rows when a later change arrives.
|
||||
- Replaced remote-size decision prompts shown during startup with long-lived, clickable notices. The detailed choices now open only when requested and no longer select an answer automatically after a timeout.
|
||||
- Removed the obsolete prompt and automatic bulk chunk pre-send from initial and rebuild uploads. These operations retain the standard two-pass replication used to converge follow-up writes and conflict resolution.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user