Adopt Commonlib 0.2.0 with injected device storage

This commit is contained in:
vorotamoroz committed 2026-10-23 13:56:54 +00:00
1 parent e56098eb57
commit 96ad2737a7
60 files changed
+1499 -541

No files matched your search

+9 -8
View File
@@ -1,3 +1,4 @@
import { MemoryValueStore } from "@vrtmrz/value-store";
import { LOG_LEVEL_INFO, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { LiveSyncBrowserServiceHubOptions } from "@/apps/browser/createLiveSyncBrowserServiceHub";
@@ -168,7 +169,7 @@ describe("WebAppRuntime lifecycle", () => {
it("owns core start, directory scan, status reporting, and shutdown", async () => {
const reportStatus = vi.fn();
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus });
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus, deviceValues: new MemoryValueStore() });
await runtime.start();
@@ -193,7 +194,7 @@ describe("WebAppRuntime lifecycle", () => {
});
it("finalises application readiness after importing local files for optional P2P", async () => {
const runtime = new WebAppRuntime(createRootHandle());
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
await runtime.start();
vi.clearAllMocks();
runtimeMocks.currentSettings.mockReturnValue(unconfiguredSettings);
@@ -239,7 +240,7 @@ describe("WebAppRuntime lifecycle", () => {
});
it("does not publish readiness after a partial local-file import", async () => {
const runtime = new WebAppRuntime(createRootHandle());
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
await runtime.start();
vi.clearAllMocks();
runtimeMocks.collectFilesOnStorage.mockResolvedValue({
@@ -269,7 +270,7 @@ describe("WebAppRuntime lifecycle", () => {
});
it("does not publish readiness when database initialisation rejects manual preparation", async () => {
const runtime = new WebAppRuntime(createRootHandle());
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
await runtime.start();
vi.clearAllMocks();
runtimeMocks.onDatabaseInitialised.mockResolvedValue(false);
@@ -282,7 +283,7 @@ describe("WebAppRuntime lifecycle", () => {
});
it("does not publish readiness when pending file events cannot be committed", async () => {
const runtime = new WebAppRuntime(createRootHandle());
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
await runtime.start();
vi.clearAllMocks();
runtimeMocks.commitPendingFileEvents.mockResolvedValue(false);
@@ -295,7 +296,7 @@ describe("WebAppRuntime lifecycle", () => {
});
it("does not repeat readiness finalisation after a successful manual scan", async () => {
const runtime = new WebAppRuntime(createRootHandle());
const runtime = new WebAppRuntime(createRootHandle(), { deviceValues: new MemoryValueStore() });
await runtime.start();
vi.clearAllMocks();
@@ -311,7 +312,7 @@ describe("WebAppRuntime lifecycle", () => {
it("rejects a failed core start after cleaning up the partial runtime", async () => {
runtimeMocks.onLoad.mockResolvedValue(false);
const reportStatus = vi.fn();
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus });
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus, deviceValues: new MemoryValueStore() });
await expect(runtime.start()).rejects.toThrow("Failed to initialise LiveSync");
@@ -325,7 +326,7 @@ describe("WebAppRuntime lifecycle", () => {
it("shuts down once before scheduling a host reload", async () => {
const scheduleReload = vi.fn();
const runtime = new WebAppRuntime(createRootHandle(), { scheduleReload });
const runtime = new WebAppRuntime(createRootHandle(), { scheduleReload, deviceValues: new MemoryValueStore() });
await runtime.start();
runtimeMocks.options?.restart?.schedule();
+35 -6
View File
@@ -1,3 +1,4 @@
import { MemoryValueStore } from "@vrtmrz/value-store";
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { DEFAULT_SETTINGS, SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
@@ -27,6 +28,7 @@ describe("WebPeer runtime composition", () => {
translate: (key) => `webpeer:${key}`,
});
const runtime = new WebPeerRuntime({
deviceValues: new MemoryValueStore(),
context,
store: createMemoryStore(),
});
@@ -43,6 +45,7 @@ describe("WebPeer runtime composition", () => {
it("loads settings and opens the local database before announcing layout readiness", async () => {
const runtime = new WebPeerRuntime({
deviceValues: new MemoryValueStore(),
store: createMemoryStore(),
});
const loadSettings = vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
@@ -68,6 +71,7 @@ describe("WebPeer runtime composition", () => {
vi.useFakeTimers();
try {
const runtime = new WebPeerRuntime({
deviceValues: new MemoryValueStore(),
store: createMemoryStore(),
});
vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
@@ -77,9 +81,7 @@ describe("WebPeer runtime composition", () => {
P2P_Enabled: true,
});
vi.spyOn(runtime.services.database, "openDatabase").mockResolvedValue(true);
const onResumed = vi
.spyOn(runtime.services.appLifecycle, "onResumed")
.mockResolvedValue(true);
const onResumed = vi.spyOn(runtime.services.appLifecycle, "onResumed").mockResolvedValue(true);
const open = vi.spyOn(runtime.p2p.transportLifecycle, "connect").mockResolvedValue(undefined);
await runtime.start();
@@ -94,9 +96,11 @@ describe("WebPeer runtime composition", () => {
it("rejects start when the browser-local database cannot be opened", async () => {
const runtime = new WebPeerRuntime({
deviceValues: new MemoryValueStore(),
store: createMemoryStore(),
});
vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
vi.spyOn(runtime.services.setting, "currentSettings").mockReturnValue({ ...DEFAULT_SETTINGS });
vi.spyOn(runtime.services.database, "openDatabase").mockResolvedValue(false);
await expect(runtime.start()).rejects.toThrow("WebPeer local database could not be opened");
@@ -104,6 +108,7 @@ describe("WebPeer runtime composition", () => {
it("isolates a specialised runtime and applies its device name before opening the database", async () => {
const runtime = new WebPeerRuntime({
deviceValues: new MemoryValueStore(),
store: createMemoryStore(),
deviceName: " p2p-check-browser-desktop-abc ",
systemVaultName: "p2p-check-vault",
@@ -114,15 +119,39 @@ describe("WebPeer runtime composition", () => {
P2P_AutoStart: false,
P2P_Enabled: false,
});
const setSmallConfig = vi.spyOn(runtime.services.config, "setSmallConfig").mockImplementation(() => undefined);
let finishWrite!: () => void;
const setValue = vi.spyOn(runtime.services.config, "setValue").mockImplementation(
() =>
new Promise<void>((resolve) => {
finishWrite = resolve;
})
);
const openDatabase = vi.spyOn(runtime.services.database, "openDatabase").mockImplementation(async () => {
expect(setSmallConfig).toHaveBeenCalledWith(SETTING_KEY_P2P_DEVICE_NAME, "p2p-check-browser-desktop-abc");
expect(setValue).toHaveBeenCalledWith(SETTING_KEY_P2P_DEVICE_NAME, "p2p-check-browser-desktop-abc");
return true;
});
await runtime.start();
const starting = runtime.start();
await vi.waitFor(() => expect(setValue).toHaveBeenCalledOnce());
expect(openDatabase).not.toHaveBeenCalled();
finishWrite();
await starting;
expect(runtime.services.API.getSystemVaultName()).toBe("p2p-check-vault");
expect(openDatabase).toHaveBeenCalledOnce();
});
it("reads the current device name after the pane updates the store", async () => {
const runtime = new WebPeerRuntime({
deviceValues: new MemoryValueStore(),
store: createMemoryStore(),
systemVaultName: "webpeer-vault",
});
vi.spyOn(runtime.services.setting, "currentSettings").mockReturnValue({ ...DEFAULT_SETTINGS });
expect(await runtime.getDeviceName()).toBe("webpeer-vault");
await runtime.services.config.setValue(SETTING_KEY_P2P_DEVICE_NAME, "first-name");
expect(await runtime.getDeviceName()).toBe("first-name");
await runtime.services.config.setValue(SETTING_KEY_P2P_DEVICE_NAME, "renamed-device");
expect(await runtime.getDeviceName()).toBe("renamed-device");
});
});
+2 -3
View File
@@ -17,13 +17,12 @@ export const SERVICE_CONTEXT_MEMBERS = [
"vault",
"test",
"UI",
"config",
"keyValueDB",
"control",
] as const satisfies readonly Exclude<keyof ServiceHub, "context">[];
] as const satisfies readonly Exclude<keyof ServiceHub, "context" | "config">[];
export type ServiceContextMember = (typeof SERVICE_CONTEXT_MEMBERS)[number];
type MissingServiceContextMember = Exclude<Exclude<keyof ServiceHub, "context">, ServiceContextMember>;
type MissingServiceContextMember = Exclude<Exclude<keyof ServiceHub, "context" | "config">, ServiceContextMember>;
const serviceContextMembersAreExhaustive: [MissingServiceContextMember] extends [never] ? true : never = true;
void serviceContextMembersAreExhaustive;
+2 -2
View File
@@ -111,7 +111,7 @@ export async function readE2eCompatibilityMarker(
return await evalObsidianJson<CompatibilityMarkerState>(
cliBinary,
[
"(()=>{",
"(async()=>{",
"const core=app.plugins.plugins['obsidian-livesync'].core;",
"const setting=core.services.setting;",
"const settings=setting.currentSettings();",
@@ -122,7 +122,7 @@ export async function readE2eCompatibilityMarker(
"return JSON.stringify({",
"vaultName,additionalSuffix,expectedStorageKey,",
"rawStorageValue:localStorage.getItem(expectedStorageKey),",
"serviceValue:setting.getSmallConfig(markerKey),",
"serviceValue:await setting.config.getValue(markerKey,''),",
"versionUpFlash:settings.versionUpFlash,",
"});",
"})()",
+2 -2
View File
@@ -288,7 +288,7 @@ export async function readRuntimeUpgradeState(
"localDatabaseDocumentCount:databaseInfo.doc_count,",
"nodeId:nodeInfo?.nodeid??replicator?.nodeid??'',",
"legacyCompatibilityMarker:localStorage.getItem(`obsidian-live-sync-ver${vaultName}`),",
"compatibilityMarker:setting.getSmallConfig('database-compatibility-version')??'',",
"compatibilityMarker:await (setting.config?.getValue?.('database-compatibility-version','')??Promise.resolve(setting.getSmallConfig?.('database-compatibility-version')??'')),",
"compatibilityStorageEntries:Object.fromEntries(Array.from({length:localStorage.length},(_,index)=>localStorage.key(index))",
".filter((key)=>key!==null)",
".filter(key=>key.startsWith('obsidian-live-sync-ver')||key.endsWith('-database-compatibility-version'))",
@@ -335,7 +335,7 @@ export async function readRuntimeSettingsUpgradeState(
"const migrationState=setting.getSettingsMigrationState?.();",
"return JSON.stringify({",
"pluginVersion:app.plugins.manifests['obsidian-livesync']?.version??'unknown',",
"compatibilityMarker:setting.getSmallConfig?.('database-compatibility-version')??'',",
"compatibilityMarker:await (setting.config?.getValue?.('database-compatibility-version','')??Promise.resolve(setting.getSmallConfig?.('database-compatibility-version')??'')),",
"migrationState,",
"settings:{",
"isConfigured:settings.isConfigured,settingVersion:settings.settingVersion,",
+122 -60
View File
@@ -1,7 +1,9 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import type { Page } from "playwright";
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/setup-uri";
import { decryptString } from "@vrtmrz/livesync-commonlib/compat/encryption/stringEncryption";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { $msg } from "../../../src/common/translation.ts";
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
@@ -13,18 +15,34 @@ import {
captureObsidianPage,
obsidianRemoteDebuggingPort,
openLiveSyncSettings,
withObsidianPage,
withObsidianPage as withObsidianSessionPage,
} from "../runner/ui.ts";
import { createTemporaryVault } from "../runner/vault.ts";
const dialogRunStateKey = "__livesyncE2EDialogMount";
const savedP2PDeviceName = "e2e-p2p-saved-device";
const repairRunStateKey = "__livesyncE2ETroubleshootingRepair";
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_DIALOG_TIMEOUT_MS ?? 10000);
async function withObsidianPage<T>(port: number, operation: (page: Page) => Promise<T>): Promise<T> {
return await withObsidianSessionPage(port, async (page) => {
for (const candidate of page.context().pages()) {
const hasPlugin = await candidate
.evaluate(
() => (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"] !== undefined
)
.catch(() => false);
if (hasPlugin) return await operation(candidate);
}
throw new Error("The LiveSync Vault renderer is unavailable.");
});
}
type DialogueMode = "desktop" | "mobile";
type DialogueRunState = {
done: boolean;
deviceNameRead?: boolean;
error?: string;
expected?: unknown;
kind: string;
@@ -46,6 +64,14 @@ type LiveSyncTestPlugin = {
createAllChunks(force: boolean): Promise<unknown>;
};
modules: SetupManagerHandle[];
services: {
setting: {
config: {
getValue(key: string, defaultValue?: string): Promise<string>;
setValue(key: string, value: string): Promise<void>;
};
};
};
settings: Record<string, unknown>;
};
};
@@ -91,39 +117,63 @@ async function openRemoteSelectionDialogue(): Promise<void> {
async function openP2PRemoteSelectionDialogueForInspection(): Promise<void> {
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
await page.evaluate((stateKey) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
const manager = plugin.core.modules.find((module) => module.constructor.name === "SetupManager");
if (
typeof manager?.onSelectServer !== "function" ||
typeof manager.onConfirmApplySettingsFromWizard !== "function"
) {
throw new Error("Could not find the P2P setup workflow");
await page.evaluate(
async ({ stateKey, deviceNameKey, deviceName }) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded");
const initialPeerName = plugin.core.settings.P2P_DevicePeerName;
if (typeof initialPeerName === "string" && initialPeerName.trim() !== "") {
throw new Error("The P2P setup fixture unexpectedly has an initial device name.");
}
const manager = plugin.core.modules.find((module) => module.constructor.name === "SetupManager");
if (
typeof manager?.onSelectServer !== "function" ||
typeof manager.onConfirmApplySettingsFromWizard !== "function"
) {
throw new Error("Could not find the P2P setup workflow");
}
const config = plugin.core.services.setting.config;
await config.setValue(deviceNameKey, deviceName);
const originalGetValue = config.getValue;
const originalConfirm = manager.onConfirmApplySettingsFromWizard;
const state: DialogueRunState = {
kind: "p2p-compatibility-settings",
done: false,
deviceNameRead: false,
};
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
config.getValue = async (key, defaultValue) => {
const value = await originalGetValue.call(config, key, defaultValue);
if (key === deviceNameKey && value === deviceName) state.deviceNameRead = true;
return value;
};
manager.onConfirmApplySettingsFromWizard = async (settings: unknown) => {
state.expected = settings;
return true;
};
void manager
.onSelectServer(plugin.core.settings, "unknown")
.then(
(result) => {
state.result = result;
state.done = true;
},
(error: unknown) => {
state.error = error instanceof Error ? error.message : String(error);
state.done = true;
}
)
.finally(() => {
manager.onConfirmApplySettingsFromWizard = originalConfirm;
config.getValue = originalGetValue;
});
},
{
stateKey: dialogRunStateKey,
deviceNameKey: SETTING_KEY_P2P_DEVICE_NAME,
deviceName: savedP2PDeviceName,
}
const originalConfirm = manager.onConfirmApplySettingsFromWizard;
const state: DialogueRunState = { kind: "p2p-compatibility-settings", done: false };
(globalThis as unknown as Record<string, DialogueRunState>)[stateKey] = state;
manager.onConfirmApplySettingsFromWizard = async (settings: unknown) => {
state.expected = settings;
return true;
};
void manager
.onSelectServer(plugin.core.settings, "unknown")
.then(
(result) => {
state.result = result;
state.done = true;
},
(error: unknown) => {
state.error = error instanceof Error ? error.message : String(error);
state.done = true;
}
)
.finally(() => {
manager.onConfirmApplySettingsFromWizard = originalConfirm;
});
}, dialogRunStateKey);
);
});
}
@@ -388,7 +438,7 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string
if (previousDialogue === null) {
throw new Error("The remote selection dialogue did not expose a close control.");
}
await modal.locator(".modal-close-button").click({ timeout: uiTimeoutMs });
await modal.locator(".modal-close-button, .modal-header-button").last().click({ timeout: uiTimeoutMs });
await page.waitForFunction((element) => !element.isConnected, previousDialogue, { timeout: uiTimeoutMs });
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
}
@@ -495,9 +545,21 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
has: page.locator(".modal-title").filter({ hasText: "P2P Configuration" }),
});
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
const peerName = modal.locator('input[name="p2p-device-peer-id"]');
await peerName.waitFor({ state: "visible", timeout: uiTimeoutMs });
await page.waitForFunction(
(expectedName) => {
const input = document.querySelector('input[name="p2p-device-peer-id"]');
return input instanceof HTMLInputElement && !input.disabled && input.value === expectedName;
},
savedP2PDeviceName,
{ timeout: uiTimeoutMs }
);
if ((await peerName.inputValue()) !== savedP2PDeviceName) {
throw new Error("The saved P2P device name was not loaded before submitting the setup dialogue.");
}
await modal.locator('input[name="p2p-room-id"]').fill("e2e-p2p-compatibility");
await modal.locator('input[name="p2p-password"]').fill("e2e-passphrase");
await modal.locator('input[name="p2p-device-peer-id"]').fill("e2e-device");
const compatibility = modal.locator("details").filter({
has: page.locator("summary").filter({ hasText: "Connection compatibility" }),
@@ -565,8 +627,7 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
{ timeout: uiTimeoutMs }
);
const resetNotice = compatibility.locator(".sls-info-note-notice").filter({
hasText:
"TURN relay only requires TURN configuration. Connection path has been restored to Automatic.",
hasText: "TURN relay only requires TURN configuration. Connection path has been restored to Automatic.",
});
await resetNotice.waitFor({ state: "visible", timeout: uiTimeoutMs });
await resetNotice
@@ -594,10 +655,16 @@ async function verifyP2PCompatibilitySettingsDialogue(): Promise<string> {
});
const state = await assertDialogueRunCompleted();
if (state.deviceNameRead !== true) {
throw new Error("The saved P2P device name was not read through setting.config.");
}
if (state.result !== true || typeof state.expected !== "object" || state.expected === null) {
throw new Error("The P2P compatibility settings were not passed to the setup workflow.");
}
const settings = state.expected as Record<string, unknown>;
if (settings.P2P_DevicePeerName !== savedP2PDeviceName) {
throw new Error("The saved P2P device name was not passed through the setup workflow.");
}
if (settings.P2P_maxWirePayloadBytes !== 800 || settings.P2P_connectionPath !== "relay") {
throw new Error(
`The P2P compatibility choices did not round-trip through the setup dialogue: ${JSON.stringify({
@@ -672,15 +739,14 @@ async function verifyGenerateSetupUriDialogue(mode: DialogueMode): Promise<strin
const app = (globalThis as ObsidianTestGlobal).app;
return Boolean(
app?.plugins?.plugins["obsidian-livesync"]?.core?.settings.isConfigured &&
app.commands?.commands?.[commandId]
app.commands?.commands?.[commandId]
);
},
"obsidian-livesync:livesync-copysetupuri",
{ timeout: uiTimeoutMs }
);
return await page.evaluate(
(commandId) =>
(globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true,
(commandId) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true,
"obsidian-livesync:livesync-copysetupuri"
);
});
@@ -714,7 +780,8 @@ async function verifyGenerateSetupUriDialogue(mode: DialogueMode): Promise<strin
timeout: uiTimeoutMs,
});
await choice.getByRole("button", { name: "Time-bound", exact: true }).waitFor({
state: "visible", timeout: uiTimeoutMs,
state: "visible",
timeout: uiTimeoutMs,
});
if (mode === "mobile") await assertMobileDialogueLayout(page, choice, "Setup URI availability dialogue");
await choice.getByRole("button", { name: selected, exact: true }).click({ timeout: uiTimeoutMs });
@@ -866,6 +933,7 @@ async function verifyCompatibleAlignmentSettingDefault(): Promise<void> {
if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) {
throw new Error("The automatic compatible-setting policy was displayed as disabled while still undefined.");
}
await settingsNavigator.close();
});
}
@@ -1094,9 +1162,10 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
);
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
const liveSyncSettings = page.locator(".sls-setting");
const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
const liveSyncSettings = await settingsNavigator.openPage("Hatch");
const logSetting = liveSyncSettings.locator(".setting-item").filter({
has: page.getByText("Write logs into the file", { exact: true }),
has: settingsNavigator.page.getByText("Write logs into the file", { exact: true }),
});
await logSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs });
await page.waitForFunction(
@@ -1130,10 +1199,10 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
// Saving a toggle refreshes the settings pane. Resolve the visible control again so the
// second action does not target the detached pre-save element.
const refreshedLogToggle = page
.locator(".sls-setting:visible .setting-item:visible")
const refreshedLogToggle = liveSyncSettings
.locator(".setting-item:visible")
.filter({
has: page.getByText("Write logs into the file", { exact: true }),
has: settingsNavigator.page.getByText("Write logs into the file", { exact: true }),
})
.locator(".checkbox-container:visible")
.last();
@@ -1179,13 +1248,9 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
}
};
}, repairRunStateKey);
await page
.locator(".sls-setting:visible")
.last()
.getByRole("button", { name: "Recreate current chunks", exact: true })
.click({
timeout: uiTimeoutMs,
});
await liveSyncSettings.getByRole("button", { name: "Recreate current chunks", exact: true }).click({
timeout: uiTimeoutMs,
});
await page.waitForFunction(
(stateKey) =>
(globalThis as unknown as Record<string, DialogueRunState | undefined>)[stateKey]?.done === true,
@@ -1200,13 +1265,9 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
throw new Error(`Recreate missing chunks failed: ${repairState.error}`);
}
await page
.locator(".sls-setting:visible")
.last()
.getByRole("button", { name: "Begin inspection", exact: true })
.click({
timeout: uiTimeoutMs,
});
await liveSyncSettings.getByRole("button", { name: "Begin inspection", exact: true }).click({
timeout: uiTimeoutMs,
});
await page
.locator(".notice")
.filter({ hasText: /^done$/u })
@@ -1214,6 +1275,7 @@ async function verifyHatchSurfacesAndSafeActions(): Promise<string> {
state: "visible",
timeout: uiTimeoutMs,
});
await settingsNavigator.close();
});
return screenshotPath;
+11 -3
View File
@@ -372,13 +372,21 @@ async function restartAndResumeHarness(): Promise<string> {
await harness
.locator('[data-testid="review-harness-resumed"]')
.waitFor({ state: "visible", timeout: uiTimeoutMs });
const continuationRemoved = await page.evaluate((stateKey) => {
const continuationRemoved = await page.evaluate(async (stateKey) => {
const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) {
throw new Error("Self-hosted LiveSync is unavailable after restart.");
}
const core = (plugin as { core: { services: { setting: { getSmallConfig(key: string): string } } } }).core;
return core.services.setting.getSmallConfig(stateKey) === "";
const core = (
plugin as {
core: {
services: {
setting: { config: { getValue(key: string, defaultValue: string): Promise<string> } };
};
};
}
).core;
return (await core.services.setting.config.getValue(stateKey, "")) === "";
}, REVIEW_HARNESS_STATE_KEY);
if (!continuationRemoved) throw new Error("The one-shot continuation was not removed before use.");
await assertNoHorizontalOverflow(page, harness, { label: "resumed Review Harness" });
+17 -9
View File
@@ -58,8 +58,9 @@ type LiveSyncTestPlugin = {
usePowerUserMode: boolean;
useEdgeCaseMode: boolean;
hashCacheMaxCount: number;
customChunkSize: number;
};
getSmallConfig(key: string): string | null;
config: { getValue(key: string, defaultValue: string): Promise<string> };
};
};
};
@@ -484,12 +485,12 @@ async function verifyCompatibilityReview(): Promise<void> {
});
await withObsidianPage(port, async (page) => {
const markerBeforeAcknowledgement = await page.evaluate(() => {
const markerBeforeAcknowledgement = await page.evaluate(async () => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable");
return plugin.core.services.setting.getSmallConfig("database-compatibility-version");
return await plugin.core.services.setting.config.getValue("database-compatibility-version", "");
});
if (markerBeforeAcknowledgement !== null && markerBeforeAcknowledgement !== "") {
if (markerBeforeAcknowledgement !== "") {
throw new Error(
`The database version was marked as acknowledged before review: ${markerBeforeAcknowledgement}`
);
@@ -579,12 +580,12 @@ async function verifyCompatibilityReview(): Promise<void> {
.click();
await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs });
await page.waitForFunction(
(expectedVersion) => {
async (expectedVersion) => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
if (plugin === undefined) return false;
const setting = plugin.core.services.setting;
return (
setting.getSmallConfig("database-compatibility-version") === expectedVersion &&
(await setting.config.getValue("database-compatibility-version", "")) === expectedVersion &&
setting.currentSettings().versionUpFlash === ""
);
},
@@ -611,10 +612,16 @@ async function verifyConfigDoctorFollowsCompatibilityReview(): Promise<string> {
state: "visible",
timeout: uiTimeoutMs,
});
await doctor.getByText("Enhance chunk size", { exact: true }).waitFor({
state: "visible",
timeout: uiTimeoutMs,
const chunkSize = await page.evaluate(() => {
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
return plugin?.core.services.setting.currentSettings().customChunkSize;
});
if (chunkSize !== 0) {
throw new Error("Compatibility review must retain the fixture's zero chunk-size value.");
}
if ((await doctor.getByText("Enhance chunk size", { exact: true }).count()) !== 0) {
throw new Error("Config Doctor must accept the recommended zero chunk-size value.");
}
if ((await doctor.getByText("Data Compression", { exact: true }).count()) !== 0) {
throw new Error("Config Doctor still treats supported Data Compression as a problem.");
}
@@ -978,6 +985,7 @@ function createSettingsPluginData(settingsOnlyRun: boolean): Record<string, unkn
liveSync: false,
versionUpFlash: settingsOnlyRun ? "" : compatibilityReviewMessage,
notifyThresholdOfRemoteStorageSize: 0,
customChunkSize: 0,
syncOnStart: false,
syncOnSave: false,
syncOnEditorSave: false,