mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 04:52:58 +00:00
Retire obsolete browser Harness
This commit is contained in:
@@ -1,148 +0,0 @@
|
||||
import { App } from "@/deps.ts";
|
||||
import ObsidianLiveSyncPlugin from "@/main";
|
||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import { SettingCache } from "./obsidian-mock";
|
||||
import { delay, fireAndForget, promiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
|
||||
import { EVENT_LAYOUT_READY, eventHub } from "@/common/events";
|
||||
|
||||
import { env } from "../suite/variables";
|
||||
|
||||
export type LiveSyncHarness = {
|
||||
app: App;
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
dispose: () => Promise<void>;
|
||||
disposalPromise: Promise<void>;
|
||||
isDisposed: () => boolean;
|
||||
};
|
||||
const isLiveSyncLogEnabled = env?.PRINT_LIVESYNC_LOGS === "true";
|
||||
function overrideLogFunction(vaultName: string) {
|
||||
setGlobalLogFunction((msg, level, key) => {
|
||||
if (!isLiveSyncLogEnabled) {
|
||||
return;
|
||||
}
|
||||
if (level && level < LOG_LEVEL_VERBOSE) {
|
||||
return;
|
||||
}
|
||||
if (msg instanceof Error) {
|
||||
console.error(msg.stack);
|
||||
} else {
|
||||
console.log(
|
||||
`[${vaultName}] :: [${key ?? "Global"}][${level ?? 1}]: ${msg instanceof Error ? msg.stack : msg}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateHarness(
|
||||
paramVaultName?: string,
|
||||
settings?: Partial<ObsidianLiveSyncSettings>
|
||||
): Promise<LiveSyncHarness> {
|
||||
// return await serialized("harness-generation-lock", async () => {
|
||||
// Dispose previous harness to avoid multiple harness running at the same time
|
||||
// if (previousHarness && !previousHarness.isDisposed()) {
|
||||
// console.log(`Previous harness detected, waiting for disposal...`);
|
||||
// await previousHarness.disposalPromise;
|
||||
// previousHarness = null;
|
||||
// await delay(100);
|
||||
// }
|
||||
const vaultName = paramVaultName ?? "TestVault" + Date.now();
|
||||
const setting = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...settings,
|
||||
};
|
||||
overrideLogFunction(vaultName);
|
||||
//@ts-ignore Mocked in harness
|
||||
const app = new App(vaultName);
|
||||
// setting and vault name
|
||||
SettingCache.set(app, setting);
|
||||
SettingCache.set(app.vault, vaultName);
|
||||
|
||||
//@ts-ignore
|
||||
const manifest_version = `${MANIFEST_VERSION || "0.0.0-harness"}`;
|
||||
overrideLogFunction(vaultName);
|
||||
const manifest = {
|
||||
id: "obsidian-livesync",
|
||||
name: "Self-hosted LiveSync (Harnessed)",
|
||||
version: manifest_version,
|
||||
minAppVersion: "0.15.0",
|
||||
description: "Testing",
|
||||
author: "vrtmrz",
|
||||
authorUrl: "",
|
||||
isDesktopOnly: false,
|
||||
};
|
||||
|
||||
const plugin = new ObsidianLiveSyncPlugin(app, manifest);
|
||||
overrideLogFunction(vaultName);
|
||||
// Initial load
|
||||
await delay(100);
|
||||
await plugin.onload();
|
||||
let isDisposed = false;
|
||||
const waitPromise = promiseWithResolvers<void>();
|
||||
eventHub.once(EVENT_PLATFORM_UNLOADED, () => {
|
||||
fireAndForget(async () => {
|
||||
console.log(`Harness for vault '${vaultName}' disposed.`);
|
||||
await delay(100);
|
||||
eventHub.offAll();
|
||||
isDisposed = true;
|
||||
waitPromise.resolve();
|
||||
});
|
||||
});
|
||||
eventHub.once(EVENT_LAYOUT_READY, () => {
|
||||
plugin.app.vault.trigger("layout-ready");
|
||||
});
|
||||
const harness: LiveSyncHarness = {
|
||||
app,
|
||||
plugin,
|
||||
dispose: async () => {
|
||||
await plugin.onunload();
|
||||
return waitPromise.promise;
|
||||
},
|
||||
disposalPromise: waitPromise.promise,
|
||||
isDisposed: () => isDisposed,
|
||||
};
|
||||
await delay(100);
|
||||
console.log(`Harness for vault '${vaultName}' is ready.`);
|
||||
// previousHarness = harness;
|
||||
return harness;
|
||||
}
|
||||
export async function waitForReady(harness: LiveSyncHarness): Promise<void> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (harness.plugin.core.services.appLifecycle.isReady()) {
|
||||
console.log("App Lifecycle is ready");
|
||||
return;
|
||||
}
|
||||
await delay(100);
|
||||
}
|
||||
throw new Error(`Initialisation Timed out!`);
|
||||
}
|
||||
|
||||
export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await delay(25);
|
||||
const processing =
|
||||
harness.plugin.core.services.replication.databaseQueueCount.value +
|
||||
harness.plugin.core.services.fileProcessing.totalQueued.value +
|
||||
harness.plugin.core.services.fileProcessing.batched.value +
|
||||
harness.plugin.core.services.fileProcessing.processing.value +
|
||||
harness.plugin.core.services.replication.storageApplyingCount.value;
|
||||
|
||||
if (processing === 0) {
|
||||
if (i > 0) {
|
||||
console.log(`Idle after ${i} loops`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function waitForClosed(harness: LiveSyncHarness): Promise<void> {
|
||||
await delay(100);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (harness.plugin.core.services.control.hasUnloaded()) {
|
||||
console.log("App has unloaded");
|
||||
return;
|
||||
}
|
||||
await delay(100);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
||||
export function interceptFetchForLogging() {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (...params: any[]) => {
|
||||
const paramObj = params[0];
|
||||
const initObj = params[1];
|
||||
const url = typeof paramObj === "string" ? paramObj : paramObj.url;
|
||||
const method = initObj?.method || "GET";
|
||||
const headers = initObj?.headers || {};
|
||||
const body = initObj?.body || null;
|
||||
const headersObj: Record<string, string> = {};
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach((value, key) => {
|
||||
headersObj[key] = value;
|
||||
});
|
||||
}
|
||||
console.dir({
|
||||
mockedFetch: {
|
||||
url,
|
||||
method,
|
||||
headers: headersObj,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const res = await originalFetch.apply(globalThis, params as any);
|
||||
console.log(`[Obsidian Mock] Fetch response: ${res.status} ${res.statusText} for ${method} ${url}`);
|
||||
const resClone = res.clone();
|
||||
const contentType = resClone.headers.get("content-type") || "";
|
||||
const isJson = contentType.includes("application/json");
|
||||
if (isJson) {
|
||||
const data = await resClone.json();
|
||||
console.dir({ mockedFetchResponseJson: data });
|
||||
} else {
|
||||
const ab = await resClone.arrayBuffer();
|
||||
const text = new TextDecoder().decode(ab);
|
||||
const isText = /^text\//.test(contentType);
|
||||
if (isText) {
|
||||
console.dir({
|
||||
mockedFetchResponseText: ab.byteLength < 1000 ? text : text.slice(0, 1000) + "...(truncated)",
|
||||
});
|
||||
} else {
|
||||
console.log(`[Obsidian Mock] Fetch response is of content-type ${contentType}, not logging body.`);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
// console.error("[Obsidian Mock] Fetch error:", e);
|
||||
console.error(`[Obsidian Mock] Fetch failed for ${method} ${url}, error:`, e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { delay } from "octagonal-wheels/promises";
|
||||
import type { BrowserContext, Page } from "playwright";
|
||||
import type { Plugin } from "vitest/config";
|
||||
import type { BrowserCommand } from "vitest/node";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
export const grantClipboardPermissions: BrowserCommand = async (ctx) => {
|
||||
if (ctx.provider.name === "playwright") {
|
||||
await ctx.context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
console.log("Granted clipboard permissions");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let peerPage: Page | undefined;
|
||||
let peerPageContext: BrowserContext | undefined;
|
||||
let previousName = "";
|
||||
async function setValue(page: Page, selector: string, value: string) {
|
||||
const e = await page.waitForSelector(selector);
|
||||
await e.fill(value);
|
||||
}
|
||||
async function closePeerContexts() {
|
||||
const peerPageLocal = peerPage;
|
||||
const peerPageContextLocal = peerPageContext;
|
||||
if (peerPageLocal) {
|
||||
await peerPageLocal.close();
|
||||
}
|
||||
if (peerPageContextLocal) {
|
||||
await peerPageContextLocal.close();
|
||||
}
|
||||
}
|
||||
export const openWebPeer: BrowserCommand<[P2PSyncSetting, serverPeerName: string]> = async (
|
||||
ctx,
|
||||
setting: P2PSyncSetting,
|
||||
serverPeerName: string = "p2p-livesync-web-peer"
|
||||
) => {
|
||||
if (ctx.provider.name === "playwright") {
|
||||
const previousPage = ctx.page;
|
||||
if (peerPage !== undefined) {
|
||||
if (previousName === serverPeerName) {
|
||||
console.log(`WebPeer for ${serverPeerName} already opened`);
|
||||
return;
|
||||
}
|
||||
console.log(`Closing previous WebPeer for ${previousName}`);
|
||||
await closePeerContexts();
|
||||
}
|
||||
console.log(`Opening webPeer`);
|
||||
return serialized("webpeer", async () => {
|
||||
const browser = ctx.context.browser()!;
|
||||
const context = await browser.newContext();
|
||||
peerPageContext = context;
|
||||
peerPage = await context.newPage();
|
||||
previousName = serverPeerName;
|
||||
console.log(`Navigating...`);
|
||||
await peerPage.goto("http://localhost:8081");
|
||||
await peerPage.waitForLoadState();
|
||||
console.log(`Navigated!`);
|
||||
await setValue(peerPage, "#app > main [placeholder*=wss]", setting.P2P_relays);
|
||||
await setValue(peerPage, "#app > main [placeholder*=anything]", setting.P2P_roomID);
|
||||
await setValue(peerPage, "#app > main [placeholder*=password]", setting.P2P_passphrase);
|
||||
await setValue(peerPage, "#app > main [placeholder*=iphone]", serverPeerName);
|
||||
// await peerPage.getByTitle("Enable P2P Replicator").setChecked(true);
|
||||
await peerPage.getByRole("checkbox").first().setChecked(true);
|
||||
// (await peerPage.waitForSelector("Save and Apply")).click();
|
||||
await peerPage.getByText("Save and Apply").click();
|
||||
await delay(100);
|
||||
await peerPage.reload();
|
||||
await delay(500);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await delay(100);
|
||||
const btn = peerPage.getByRole("button").filter({ hasText: /^connect/i });
|
||||
if ((await peerPage.getByText(/disconnect/i).count()) > 0) {
|
||||
break;
|
||||
}
|
||||
await btn.click();
|
||||
}
|
||||
await previousPage.bringToFront();
|
||||
ctx.context.on("close", async () => {
|
||||
console.log("Browser context is closing, closing peer page if exists");
|
||||
await closePeerContexts();
|
||||
});
|
||||
console.log("Web peer page opened");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const closeWebPeer: BrowserCommand = async (ctx) => {
|
||||
if (ctx.provider.name === "playwright") {
|
||||
return serialized("webpeer", async () => {
|
||||
await closePeerContexts();
|
||||
peerPage = undefined;
|
||||
peerPageContext = undefined;
|
||||
previousName = "";
|
||||
console.log("Web peer page closed");
|
||||
});
|
||||
}
|
||||
};
|
||||
export const acceptWebPeer: BrowserCommand = async (ctx) => {
|
||||
if (peerPage) {
|
||||
// Detect dialogue
|
||||
const buttonsOnDialogs = await peerPage.$$("popup .buttons button");
|
||||
for (const b of buttonsOnDialogs) {
|
||||
const text = (await b.innerText()).toLowerCase();
|
||||
// console.log(`Dialog button found: ${text}`);
|
||||
if (text === "accept") {
|
||||
console.log("Accepting dialog");
|
||||
await b.click({ timeout: 300 });
|
||||
await delay(500);
|
||||
}
|
||||
}
|
||||
const buttons = peerPage.getByRole("button").filter({ hasText: /^accept$/i });
|
||||
const a = await buttons.all();
|
||||
for (const b of a) {
|
||||
await b.click({ timeout: 300 });
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Write arbitrary text to a file on the Node.js host (used for phase handoff). */
|
||||
export const writeHandoffFile: BrowserCommand<[filePath: string, content: string]> = async (
|
||||
_ctx,
|
||||
filePath: string,
|
||||
content: string
|
||||
) => {
|
||||
const fs = await import("node:fs/promises");
|
||||
await fs.writeFile(filePath, content, "utf-8");
|
||||
};
|
||||
|
||||
/** Read a file from the Node.js host (used for phase handoff). */
|
||||
export const readHandoffFile: BrowserCommand<[filePath: string]> = async (_ctx, filePath: string): Promise<string> => {
|
||||
const fs = await import("node:fs/promises");
|
||||
return fs.readFile(filePath, "utf-8");
|
||||
};
|
||||
|
||||
export default function BrowserCommands(): Plugin {
|
||||
return {
|
||||
name: "vitest:custom-commands",
|
||||
config() {
|
||||
return {
|
||||
test: {
|
||||
browser: {
|
||||
commands: {
|
||||
grantClipboardPermissions,
|
||||
openWebPeer,
|
||||
closeWebPeer,
|
||||
acceptWebPeer,
|
||||
writeHandoffFile,
|
||||
readHandoffFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
declare module "vitest/browser" {
|
||||
interface BrowserCommands {
|
||||
grantClipboardPermissions: () => Promise<void>;
|
||||
openWebPeer: (setting: P2PSyncSetting, serverPeerName: string) => Promise<void>;
|
||||
closeWebPeer: () => Promise<void>;
|
||||
acceptWebPeer: () => Promise<boolean>;
|
||||
writeHandoffFile: (filePath: string, content: string) => Promise<void>;
|
||||
readHandoffFile: (filePath: string) => Promise<string>;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { page } from "vitest/browser";
|
||||
import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
|
||||
export async function waitForDialogShown(dialogText: string, timeout = 500) {
|
||||
const ttl = Date.now() + timeout;
|
||||
while (Date.now() < ttl) {
|
||||
try {
|
||||
await delay(50);
|
||||
const dialog = page
|
||||
.getByText(dialogText)
|
||||
.elements()
|
||||
.filter((e) => e.classList.contains("modal-title"))
|
||||
.filter((e) => e.checkVisibility());
|
||||
if (dialog.length === 0) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export async function waitForDialogHidden(dialogText: string | RegExp, timeout = 500) {
|
||||
const ttl = Date.now() + timeout;
|
||||
while (Date.now() < ttl) {
|
||||
try {
|
||||
await delay(50);
|
||||
const dialog = page
|
||||
.getByText(dialogText)
|
||||
.elements()
|
||||
.filter((e) => e.classList.contains("modal-title"))
|
||||
.filter((e) => e.checkVisibility());
|
||||
if (dialog.length > 0) {
|
||||
// console.log(`Still exist ${dialogText.toString()}`);
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function waitForButtonClick(buttonText: string | RegExp, timeout = 500) {
|
||||
const ttl = Date.now() + timeout;
|
||||
while (Date.now() < ttl) {
|
||||
try {
|
||||
await delay(100);
|
||||
const buttons = page
|
||||
.getByText(buttonText)
|
||||
.elements()
|
||||
.filter((e) => e.checkVisibility() && e.tagName.toLowerCase() == "button");
|
||||
if (buttons.length == 0) {
|
||||
// console.log(`Could not found ${buttonText.toString()}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`Button detected: ${buttonText.toString()}`);
|
||||
// console.dir(buttons[0])
|
||||
await page.elementLocator(buttons[0]).click();
|
||||
await delay(100);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
|
||||
export async function waitTaskWithFollowups<T>(
|
||||
task: Promise<T>,
|
||||
followup: () => Promise<void>,
|
||||
timeout: number = 10000,
|
||||
interval: number = 1000
|
||||
): Promise<T> {
|
||||
const symbolNotCompleted = Symbol("notCompleted");
|
||||
const isCompleted = () => Promise.race([task, Promise.resolve(symbolNotCompleted)]);
|
||||
const ttl = Date.now() + timeout;
|
||||
do {
|
||||
const state = await isCompleted();
|
||||
if (state !== symbolNotCompleted) {
|
||||
return state;
|
||||
}
|
||||
await followup();
|
||||
await delay(interval);
|
||||
} while (Date.now() < ttl);
|
||||
throw new Error("Task did not complete in time");
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "P2P Init - No additional initialization required."
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
script_dir=$(dirname "$0")
|
||||
webpeer_dir=$script_dir/../../src/apps/webpeer
|
||||
|
||||
docker run -d --name relay-test -p 4000:7777 \
|
||||
--tmpfs /app/strfry-db:rw,size=256m \
|
||||
--entrypoint sh \
|
||||
ghcr.io/hoytech/strfry:latest \
|
||||
-lc 'cat > /tmp/strfry.conf <<"EOF"
|
||||
db = "./strfry-db/"
|
||||
|
||||
relay {
|
||||
bind = "0.0.0.0"
|
||||
port = 7777
|
||||
nofiles = 100000
|
||||
|
||||
info {
|
||||
name = "livesync test relay"
|
||||
description = "local relay for livesync p2p tests"
|
||||
}
|
||||
|
||||
maxWebsocketPayloadSize = 131072
|
||||
autoPingSeconds = 55
|
||||
|
||||
writePolicy {
|
||||
plugin = ""
|
||||
}
|
||||
}
|
||||
EOF
|
||||
exec /app/strfry --config /tmp/strfry.conf relay'
|
||||
npm run --prefix $webpeer_dir build
|
||||
docker run -d --name webpeer-test -p 8081:8043 -v $webpeer_dir/dist:/srv/http pierrezemb/gostatic
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
docker stop relay-test
|
||||
docker rm relay-test
|
||||
docker stop webpeer-test
|
||||
docker rm webpeer-test
|
||||
@@ -1,129 +0,0 @@
|
||||
import { compareMTime, EVEN } from "@/common/utils";
|
||||
import { TFile, type DataWriteOptions } from "@/deps";
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { isDocContentSame, readContent } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { waitForIdle, type LiveSyncHarness } from "../harness/harness";
|
||||
import { expect } from "vitest";
|
||||
|
||||
export const defaultFileOption = {
|
||||
mtime: new Date(2026, 0, 1, 0, 1, 2, 3).getTime(),
|
||||
} as const satisfies DataWriteOptions;
|
||||
export async function storeFile(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
content: string | Blob,
|
||||
deleteBeforeSend = false,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
if (deleteBeforeSend && harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
if (content instanceof Blob) {
|
||||
console.log(`Creating binary file ${path}`);
|
||||
await harness.app.vault.createBinary(path, await content.arrayBuffer(), fileOptions);
|
||||
} else {
|
||||
await harness.app.vault.create(path, content, fileOptions);
|
||||
}
|
||||
|
||||
// Ensure file is created
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
expect(compareMTime(file.stat.mtime, fileOptions?.mtime ?? defaultFileOption.mtime)).toBe(EVEN);
|
||||
if (content instanceof Blob) {
|
||||
const readContent = await harness.app.vault.readBinary(file);
|
||||
expect(await isDocContentSame(readContent, content)).toBe(true);
|
||||
} else {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
}
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
return file;
|
||||
}
|
||||
export async function readFromLocalDB(harness: LiveSyncHarness, path: string) {
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
return entry;
|
||||
}
|
||||
export async function readFromVault(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
isBinary: boolean = false,
|
||||
fileOptions = defaultFileOption
|
||||
): Promise<string | ArrayBuffer> {
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
// console.log(`MTime: ${file.stat.mtime}, Expected: ${fileOptions.mtime}`);
|
||||
if (fileOptions.mtime !== undefined) {
|
||||
expect(compareMTime(file.stat.mtime, fileOptions.mtime)).toBe(EVEN);
|
||||
}
|
||||
const content = isBinary ? await harness.app.vault.readBinary(file) : await harness.app.vault.read(file);
|
||||
return content;
|
||||
}
|
||||
|
||||
throw new Error("File not found in vault");
|
||||
}
|
||||
export async function checkStoredFileInDB(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
content: string | Blob,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
const entry = await readFromLocalDB(harness, path);
|
||||
if (entry === false) {
|
||||
throw new Error("DB Content not found");
|
||||
}
|
||||
const contentToCheck = content instanceof Blob ? await content.arrayBuffer() : content;
|
||||
const isDocSame = await isDocContentSame(readContent(entry), contentToCheck);
|
||||
if (fileOptions.mtime !== undefined) {
|
||||
expect(compareMTime(entry.mtime, fileOptions.mtime)).toBe(EVEN);
|
||||
}
|
||||
expect(isDocSame).toBe(true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
export async function testFileWrite(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
content: string | Blob,
|
||||
skipCheckToBeWritten = false,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
const file = await storeFile(harness, path, content, false, fileOptions);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
const vaultFile = await readFromVault(harness, path, content instanceof Blob, fileOptions);
|
||||
expect(await isDocContentSame(vaultFile, content)).toBe(true);
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
if (skipCheckToBeWritten) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
await checkStoredFileInDB(harness, path, content);
|
||||
return Promise.resolve();
|
||||
}
|
||||
export async function testFileRead(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
expectedContent: string | Blob,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
await waitForIdle(harness);
|
||||
const file = await readFromVault(harness, path, expectedContent instanceof Blob, fileOptions);
|
||||
const isDocSame = await isDocContentSame(file, expectedContent);
|
||||
expect(isDocSame).toBe(true);
|
||||
// Check local database entry
|
||||
const entry = await readFromLocalDB(harness, path);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry === false) {
|
||||
throw new Error("DB Content not found");
|
||||
}
|
||||
const isDBDocSame = await isDocContentSame(readContent(entry), expectedContent);
|
||||
expect(isDBDocSame).toBe(true);
|
||||
return await Promise.resolve();
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { beforeAll, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { TFile } from "@/deps.ts";
|
||||
import { DEFAULT_SETTINGS, type FilePath, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { isDocContentSame, readContent } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { DummyFileSourceInisialised, generateBinaryFile, generateFile, init } from "../utils/dummyfile";
|
||||
|
||||
const localdb_test_setting = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
describe.skip("Plugin Integration Test (Local Database)", async () => {
|
||||
let harness: LiveSyncHarness;
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
harness = await generateHarness(vaultName, localdb_test_setting);
|
||||
await waitForReady(harness);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", async () => {
|
||||
expect(harness.plugin).toBeDefined();
|
||||
expect(harness.plugin.app).toBe(harness.app);
|
||||
return await Promise.resolve();
|
||||
});
|
||||
|
||||
it("should have services initialized", async () => {
|
||||
expect(harness.plugin.core.services).toBeDefined();
|
||||
return await Promise.resolve();
|
||||
});
|
||||
it("should have local database initialized", async () => {
|
||||
expect(harness.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harness.plugin.core.localDatabase.isReady).toBe(true);
|
||||
return await Promise.resolve();
|
||||
});
|
||||
|
||||
it("should store the changes into the local database", async () => {
|
||||
const path = "test-store6.md";
|
||||
const content = "Hello, World!";
|
||||
if (harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
await harness.app.vault.create(path, content);
|
||||
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
|
||||
if (file instanceof TFile) {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
// await delay(100); // Wait a bit for the local database to process
|
||||
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
expect(readContent(entry)).toBe(content);
|
||||
}
|
||||
return await Promise.resolve();
|
||||
});
|
||||
test.each([10, 100, 1000, 10000, 50000, 100000])("should handle large file of size %i bytes", async (size) => {
|
||||
const path = `test-large-file-${size}.md`;
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
if (harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
await harness.app.vault.create(path, content);
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
expect(readContent(entry)).toBe(content);
|
||||
}
|
||||
return await Promise.resolve();
|
||||
});
|
||||
|
||||
const binaryMap = Array.from({ length: 7 }, (_, i) => Math.pow(2, i * 4));
|
||||
test.each(binaryMap)("should handle binary file of size %i bytes", async (size) => {
|
||||
const path = `test-binary-file-${size}.bin`;
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
if (harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
await harness.app.vault.createBinary(path, await content.arrayBuffer());
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
const readContent = await harness.app.vault.readBinary(file);
|
||||
expect(await isDocContentSame(readContent, content)).toBe(true);
|
||||
}
|
||||
|
||||
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
const entryContent = await readContent(entry);
|
||||
if (!(entryContent instanceof ArrayBuffer)) {
|
||||
throw new Error("Entry content is not an ArrayBuffer");
|
||||
}
|
||||
// const expectedContent = await content.arrayBuffer();
|
||||
expect(await isDocContentSame(entryContent, content)).toBe(true);
|
||||
}
|
||||
return await Promise.resolve();
|
||||
});
|
||||
});
|
||||
@@ -1,275 +0,0 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { afterAll, beforeAll, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { RemoteTypes, type FilePath, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
import {
|
||||
DummyFileSourceInisialised,
|
||||
FILE_SIZE_BINS,
|
||||
FILE_SIZE_MD,
|
||||
generateBinaryFile,
|
||||
generateFile,
|
||||
} from "../utils/dummyfile";
|
||||
import { checkStoredFileInDB, testFileRead, testFileWrite } from "./db_common";
|
||||
import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { commands } from "vitest/browser";
|
||||
import { closeReplication, performReplication, prepareRemote } from "./sync_common";
|
||||
import type { DataWriteOptions } from "@/deps.ts";
|
||||
|
||||
type MTimedDataWriteOptions = DataWriteOptions & { mtime: number };
|
||||
export type TestOptions = {
|
||||
setting: ObsidianLiveSyncSettings;
|
||||
fileOptions: MTimedDataWriteOptions;
|
||||
};
|
||||
function generateName(prefix: string, type: string, ext: string, size: number) {
|
||||
return `${prefix}-${type}-file-${size}.${ext}`;
|
||||
}
|
||||
export function syncBasicCase(label: string, { setting, fileOptions }: TestOptions) {
|
||||
describe("Replication Suite Tests - " + label, () => {
|
||||
const nameFile = (type: string, ext: string, size: number) => generateName("sync-test", type, ext, size);
|
||||
let serverPeerName = "";
|
||||
// TODO: Harness disposal may broke the event loop of P2P replication
|
||||
// so we keep the harnesses alive until all tests are done.
|
||||
// It may trystero's somethong, or not.
|
||||
let harnessUpload: LiveSyncHarness;
|
||||
let harnessDownload: LiveSyncHarness;
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// await commands.closeWebPeer();
|
||||
serverPeerName = "t-" + Date.now();
|
||||
setting.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
setting.P2P_AutoSyncPeers = serverPeerName;
|
||||
setting.P2P_DevicePeerName = "client-" + Date.now();
|
||||
await commands.openWebPeer(setting, serverPeerName);
|
||||
}
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
await commands.closeWebPeer();
|
||||
// await closeP2PReplicatorConnections(harnessUpload);
|
||||
}
|
||||
});
|
||||
|
||||
describe("Remote Database Initialization", () => {
|
||||
let harnessInit: LiveSyncHarness;
|
||||
const sync_test_setting_init = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Remote Database Initialization - Vault: ${vaultName}`);
|
||||
harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
expect(harnessInit.plugin).toBeDefined();
|
||||
expect(harnessInit.plugin.app).toBe(harnessInit.app);
|
||||
await waitForIdle(harnessInit);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harnessInit.plugin.core.services.replicator.getActiveReplicator()?.closeReplication();
|
||||
await harnessInit.dispose();
|
||||
await delay(1000);
|
||||
});
|
||||
|
||||
it("should reset remote database", async () => {
|
||||
// harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
await prepareRemote(harnessInit, sync_test_setting_init, true);
|
||||
});
|
||||
it("should be prepared for replication", async () => {
|
||||
await waitForReady(harnessInit);
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
const status = await harnessInit.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(sync_test_setting_init);
|
||||
console.log("Connected devices after reset:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Upload", () => {
|
||||
const sync_test_setting_upload = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Replication Upload - Vault: ${vaultName}`);
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
sync_test_setting_upload.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
sync_test_setting_upload.P2P_AutoSyncPeers = serverPeerName;
|
||||
sync_test_setting_upload.P2P_DevicePeerName = "up-" + Date.now();
|
||||
}
|
||||
harnessUpload = await generateHarness(vaultName, sync_test_setting_upload);
|
||||
await waitForReady(harnessUpload);
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
await waitForIdle(harnessUpload);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessUpload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessUpload.plugin.core.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessUpload.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should prepare remote database", async () => {
|
||||
await prepareRemote(harnessUpload, sync_test_setting_upload, false);
|
||||
});
|
||||
|
||||
// describe("File Creation", async () => {
|
||||
it("should a file has been created", async () => {
|
||||
const content = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
// Perform replication
|
||||
// await harness.plugin.core.services.replication.replicate(true);
|
||||
});
|
||||
it("should different content of several files have been created correctly", async () => {
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should large file of size %i bytes has been created", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should binary file of size %i bytes has been created", async (size) => {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
const path = nameFile("binary", "bin", size);
|
||||
await testFileWrite(harnessUpload, path, content, true, fileOptions);
|
||||
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await checkStoredFileInDB(harnessUpload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
it("Replication after uploads", async () => {
|
||||
await performReplication(harnessUpload);
|
||||
await performReplication(harnessUpload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Download", () => {
|
||||
// Download into a new vault
|
||||
const sync_test_setting_download = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Replication Download - Vault: ${vaultName}`);
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
sync_test_setting_download.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
sync_test_setting_download.P2P_AutoSyncPeers = serverPeerName;
|
||||
sync_test_setting_download.P2P_DevicePeerName = "down-" + Date.now();
|
||||
}
|
||||
harnessDownload = await generateHarness(vaultName, sync_test_setting_download);
|
||||
await waitForReady(harnessDownload);
|
||||
await prepareRemote(harnessDownload, sync_test_setting_download, false);
|
||||
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
await delay(1000);
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessDownload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessDownload.plugin).toBeDefined();
|
||||
expect(harnessDownload.plugin.app).toBe(harnessDownload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessDownload.plugin.core.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessDownload.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should a file has been synchronised", async () => {
|
||||
const expectedContent = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileRead(harnessDownload, path, expectedContent, fileOptions);
|
||||
});
|
||||
it("should different content of several files have been synchronised", async () => {
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should the file %i bytes had been synchronised", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should binary file of size %i bytes had been synchronised", async (size) => {
|
||||
const path = nameFile("binary", "bin", size);
|
||||
|
||||
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (harnessDownload) {
|
||||
await closeReplication(harnessDownload);
|
||||
await harnessDownload.dispose();
|
||||
await delay(1000);
|
||||
}
|
||||
if (harnessUpload) {
|
||||
await closeReplication(harnessUpload);
|
||||
await harnessUpload.dispose();
|
||||
await delay(1000);
|
||||
}
|
||||
});
|
||||
it("Wait for idle state", async () => {
|
||||
await delay(100);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
import { defaultFileOption } from "./db_common";
|
||||
import { syncBasicCase } from "./sync.senario.basic.ts";
|
||||
import { settingBase } from "./variables.ts";
|
||||
const sync_test_setting_base = settingBase;
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
const remoteTypes = [RemoteTypes.REMOTE_COUCHDB];
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
const e2eeOptions = [false];
|
||||
// const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe.skip("Replication Suite Tests (Single)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
import { defaultFileOption } from "./db_common";
|
||||
import { syncBasicCase } from "./sync.senario.basic.ts";
|
||||
import { settingBase } from "./variables.ts";
|
||||
const sync_test_setting_base = settingBase;
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO];
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
const e2eeOptions = [false, true];
|
||||
// const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Replication Suite Tests (Normal)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
import { expect } from "vitest";
|
||||
import { waitForIdle, type LiveSyncHarness } from "../harness/harness";
|
||||
import { RemoteTypes, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
import { delay, fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { commands } from "vitest/browser";
|
||||
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { waitTaskWithFollowups } from "../lib/util";
|
||||
async function waitForP2PPeers(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// Wait for peers to connect
|
||||
const maxRetries = 20;
|
||||
let retries = maxRetries;
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
while (retries-- > 0) {
|
||||
fireAndForget(() => commands.acceptWebPeer());
|
||||
await delay(1000);
|
||||
const peers = replicator.knownAdvertisements;
|
||||
|
||||
if (peers && peers.length > 0) {
|
||||
console.log("P2P peers connected:", peers);
|
||||
return;
|
||||
}
|
||||
fireAndForget(() => commands.acceptWebPeer());
|
||||
console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`);
|
||||
console.dir(peers);
|
||||
await delay(1000);
|
||||
}
|
||||
console.log("Failed to connect P2P peers after retries");
|
||||
throw new Error("P2P peers did not connect in time.");
|
||||
}
|
||||
}
|
||||
export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
replicator.closeReplication();
|
||||
await delay(30);
|
||||
replicator.closeReplication();
|
||||
await delay(1000);
|
||||
console.log("P2P replicator connections closed");
|
||||
// if (replicator instanceof LiveSyncTrysteroReplicator) {
|
||||
// replicator.closeReplication();
|
||||
// await delay(1000);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
export async function performReplication(harness: LiveSyncHarness) {
|
||||
await waitForP2PPeers(harness);
|
||||
await delay(500);
|
||||
const p = harness.plugin.core.services.replication.replicate(true);
|
||||
const task =
|
||||
harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P
|
||||
? waitTaskWithFollowups(
|
||||
p,
|
||||
() => {
|
||||
// Accept any peer dialogs during replication (fire and forget)
|
||||
fireAndForget(() => commands.acceptWebPeer());
|
||||
return Promise.resolve();
|
||||
},
|
||||
30000,
|
||||
500
|
||||
)
|
||||
: p;
|
||||
const result = await task;
|
||||
// await waitForIdle(harness);
|
||||
// if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// await closeP2PReplicatorConnections(harness);
|
||||
// }
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function closeReplication(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
return await closeP2PReplicatorConnections(harness);
|
||||
}
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
console.log("No active replicator to close");
|
||||
return;
|
||||
}
|
||||
await replicator.closeReplication();
|
||||
await waitForIdle(harness);
|
||||
console.log("Replication closed");
|
||||
}
|
||||
|
||||
export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) {
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
if (shouldReset) {
|
||||
await delay(1000);
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryResetRemoteDatabase(harness.plugin.core.settings);
|
||||
} else {
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryCreateRemoteDatabase(harness.plugin.core.settings);
|
||||
}
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.markRemoteResolved(harness.plugin.core.settings);
|
||||
// No exceptions should be thrown
|
||||
const status = await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(harness.plugin.core.settings);
|
||||
console.log("Remote status:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { DoctorRegulation } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
ChunkAlgorithms,
|
||||
AutoAccepting,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
export const env = (import.meta as any).env;
|
||||
export const settingBase = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
couchDB_URI: `${env.hostname}`,
|
||||
couchDB_DBNAME: `${env.dbname}`,
|
||||
couchDB_USER: `${env.username}`,
|
||||
couchDB_PASSWORD: `${env.password}`,
|
||||
bucket: `${env.bucketName}`,
|
||||
region: "us-east-1",
|
||||
endpoint: `${env.minioEndpoint}`,
|
||||
accessKey: `${env.accessKey}`,
|
||||
secretKey: `${env.secretKey}`,
|
||||
useCustomRequestHandler: true,
|
||||
forcePathStyle: true,
|
||||
bucketPrefix: "",
|
||||
usePluginSyncV2: true,
|
||||
chunkSplitterVersion: ChunkAlgorithms.RabinKarp,
|
||||
doctorProcessedVersion: DoctorRegulation.version,
|
||||
notifyThresholdOfRemoteStorageSize: 800,
|
||||
P2P_AutoAccepting: AutoAccepting.ALL,
|
||||
P2P_AutoBroadcast: true,
|
||||
P2P_AutoStart: true,
|
||||
P2P_Enabled: true,
|
||||
P2P_passphrase: "p2psync-test",
|
||||
P2P_roomID: "p2psync-test",
|
||||
P2P_DevicePeerName: "p2psync-test",
|
||||
P2P_relays: "ws://localhost:4000/",
|
||||
P2P_AutoAcceptingPeers: "p2p-livesync-web-peer",
|
||||
P2P_SyncOnReplication: "p2p-livesync-web-peer",
|
||||
} as ObsidianLiveSyncSettings;
|
||||
@@ -1,194 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||
CLI_DIR="$REPO_ROOT/src/apps/cli"
|
||||
CLI_TEST_HELPERS="$CLI_DIR/test/test-helpers.sh"
|
||||
|
||||
source "$CLI_TEST_HELPERS"
|
||||
|
||||
RUN_BUILD="${RUN_BUILD:-1}"
|
||||
KEEP_TEST_DATA="${KEEP_TEST_DATA:-1}"
|
||||
VERBOSE_TEST_LOGGING="${VERBOSE_TEST_LOGGING:-1}"
|
||||
|
||||
RELAY="${RELAY:-ws://localhost:4000/}"
|
||||
USE_INTERNAL_RELAY="${USE_INTERNAL_RELAY:-1}"
|
||||
APP_ID="${APP_ID:-self-hosted-livesync-vitest-p2p}"
|
||||
HOST_PEER_NAME="${HOST_PEER_NAME:-p2p-cli-host}"
|
||||
|
||||
ROOM_ID="p2p-room-$(date +%s)-$RANDOM-$RANDOM"
|
||||
PASSPHRASE="p2p-pass-$(date +%s)-$RANDOM-$RANDOM"
|
||||
UPLOAD_PEER_NAME="p2p-upload-$(date +%s)-$RANDOM"
|
||||
DOWNLOAD_PEER_NAME="p2p-download-$(date +%s)-$RANDOM"
|
||||
UPLOAD_VAULT_NAME="TestVaultUpload-$(date +%s)-$RANDOM"
|
||||
DOWNLOAD_VAULT_NAME="TestVaultDownload-$(date +%s)-$RANDOM"
|
||||
|
||||
# ---- Build CLI ----
|
||||
if [[ "$RUN_BUILD" == "1" ]]; then
|
||||
echo "[INFO] building CLI"
|
||||
(cd "$CLI_DIR" && npm run build)
|
||||
fi
|
||||
|
||||
# ---- Temp directory ----
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-vitest-p2p.XXXXXX")"
|
||||
VAULT_HOST="$WORK_DIR/vault-host"
|
||||
SETTINGS_HOST="$WORK_DIR/settings-host.json"
|
||||
HOST_LOG="$WORK_DIR/p2p-host.log"
|
||||
# Handoff file: upload phase writes this; download phase reads it.
|
||||
HANDOFF_FILE="$WORK_DIR/p2p-test-handoff.json"
|
||||
mkdir -p "$VAULT_HOST"
|
||||
|
||||
# ---- Setup CLI command (uses npm run cli from CLI_DIR) ----
|
||||
# Override run_cli to invoke the built binary directly from CLI_DIR
|
||||
run_cli() {
|
||||
(cd "$CLI_DIR" && node dist/index.cjs "$@")
|
||||
}
|
||||
|
||||
# ---- Create host settings ----
|
||||
echo "[INFO] relay=$RELAY room=$ROOM_ID app=$APP_ID host=$HOST_PEER_NAME"
|
||||
cli_test_init_settings_file "$SETTINGS_HOST"
|
||||
cli_test_apply_p2p_settings "$SETTINGS_HOST" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" "~.*"
|
||||
|
||||
# Set host peer name
|
||||
SETTINGS_HOST_FILE="$SETTINGS_HOST" HOST_PEER_NAME_VAL="$HOST_PEER_NAME" HOST_PASSPHRASE_VAL="$PASSPHRASE" node <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const data = JSON.parse(fs.readFileSync(process.env.SETTINGS_HOST_FILE, "utf-8"));
|
||||
|
||||
// Keep tweak values aligned with browser-side P2P test settings.
|
||||
data.remoteType = "ONLY_P2P";
|
||||
data.encrypt = true;
|
||||
data.passphrase = process.env.HOST_PASSPHRASE_VAL;
|
||||
data.usePathObfuscation = true;
|
||||
data.handleFilenameCaseSensitive = false;
|
||||
data.customChunkSize = 50;
|
||||
data.usePluginSyncV2 = true;
|
||||
data.doNotUseFixedRevisionForChunks = false;
|
||||
|
||||
data.P2P_DevicePeerName = process.env.HOST_PEER_NAME_VAL;
|
||||
fs.writeFileSync(process.env.SETTINGS_HOST_FILE, JSON.stringify(data, null, 2), "utf-8");
|
||||
NODE
|
||||
|
||||
# ---- Cleanup trap ----
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
if [[ -n "${HOST_PID:-}" ]] && kill -0 "$HOST_PID" >/dev/null 2>&1; then
|
||||
echo "[INFO] stopping CLI host (PID=$HOST_PID)"
|
||||
kill -TERM "$HOST_PID" >/dev/null 2>&1 || true
|
||||
wait "$HOST_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ "${P2P_RELAY_STARTED:-0}" == "1" ]]; then
|
||||
cli_test_stop_p2p_relay
|
||||
fi
|
||||
|
||||
if [[ "$KEEP_TEST_DATA" != "1" ]]; then
|
||||
rm -rf "$WORK_DIR"
|
||||
else
|
||||
echo "[INFO] KEEP_TEST_DATA=1, preserving artefacts at $WORK_DIR"
|
||||
fi
|
||||
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
start_host() {
|
||||
local attempt=0
|
||||
while [[ "$attempt" -lt 5 ]]; do
|
||||
attempt=$((attempt + 1))
|
||||
echo "[INFO] starting CLI p2p-host (attempt $attempt/5)"
|
||||
: >"$HOST_LOG"
|
||||
(cd "$CLI_DIR" && node dist/index.cjs "$VAULT_HOST" --settings "$SETTINGS_HOST" -d p2p-host) >"$HOST_LOG" 2>&1 &
|
||||
HOST_PID=$!
|
||||
|
||||
local host_ready=0
|
||||
local exited_early=0
|
||||
for i in $(seq 1 30); do
|
||||
if grep -qF "P2P host is running" "$HOST_LOG" 2>/dev/null; then
|
||||
host_ready=1
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "$HOST_PID" >/dev/null 2>&1; then
|
||||
exited_early=1
|
||||
break
|
||||
fi
|
||||
echo "[INFO] waiting for p2p-host to be ready... ($i/30)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "$host_ready" == "1" ]]; then
|
||||
echo "[INFO] p2p-host is ready (PID=$HOST_PID)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
wait "$HOST_PID" >/dev/null 2>&1 || true
|
||||
HOST_PID=
|
||||
|
||||
if grep -qF "Resource temporarily unavailable" "$HOST_LOG" 2>/dev/null; then
|
||||
echo "[INFO] p2p-host database lock is still being released, retrying..."
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$exited_early" == "1" ]]; then
|
||||
echo "[FAIL] CLI host process exited unexpectedly" >&2
|
||||
else
|
||||
echo "[FAIL] p2p-host did not become ready within 30 seconds" >&2
|
||||
fi
|
||||
cat "$HOST_LOG" >&2
|
||||
exit 1
|
||||
done
|
||||
|
||||
echo "[FAIL] p2p-host could not be restarted after multiple attempts" >&2
|
||||
cat "$HOST_LOG" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---- Start local relay if needed ----
|
||||
if [[ "$USE_INTERNAL_RELAY" == "1" ]]; then
|
||||
if cli_test_is_local_p2p_relay "$RELAY"; then
|
||||
cli_test_start_p2p_relay
|
||||
P2P_RELAY_STARTED=1
|
||||
else
|
||||
echo "[INFO] USE_INTERNAL_RELAY=1 but RELAY is not local ($RELAY), skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
start_host
|
||||
|
||||
# Common env vars passed to both vitest runs
|
||||
P2P_ENV=(
|
||||
P2P_TEST_ROOM_ID="$ROOM_ID"
|
||||
P2P_TEST_PASSPHRASE="$PASSPHRASE"
|
||||
P2P_TEST_HOST_PEER_NAME="$HOST_PEER_NAME"
|
||||
P2P_TEST_RELAY="$RELAY"
|
||||
P2P_TEST_APP_ID="$APP_ID"
|
||||
P2P_TEST_HANDOFF_FILE="$HANDOFF_FILE"
|
||||
P2P_TEST_UPLOAD_PEER_NAME="$UPLOAD_PEER_NAME"
|
||||
P2P_TEST_DOWNLOAD_PEER_NAME="$DOWNLOAD_PEER_NAME"
|
||||
P2P_TEST_UPLOAD_VAULT_NAME="$UPLOAD_VAULT_NAME"
|
||||
P2P_TEST_DOWNLOAD_VAULT_NAME="$DOWNLOAD_VAULT_NAME"
|
||||
)
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# ---- Phase 1: Upload ----
|
||||
# Each vitest run gets a fresh browser process, so Trystero's module-level
|
||||
# global state (occupiedRooms, didInit, etc.) is clean for every phase.
|
||||
echo "[INFO] running P2P vitest — upload phase"
|
||||
env "${P2P_ENV[@]}" \
|
||||
npx dotenv-cli -e .env -e .test.env -- \
|
||||
vitest run --config vitest.config.p2p.ts test/suitep2p/syncp2p.p2p-up.test.ts
|
||||
echo "[INFO] upload phase completed"
|
||||
|
||||
# ---- Phase 2: Download ----
|
||||
# Keep the same host process alive so its database handle and relay presence stay stable.
|
||||
echo "[INFO] waiting 5s before download phase..."
|
||||
sleep 5
|
||||
echo "[INFO] running P2P vitest — download phase"
|
||||
env "${P2P_ENV[@]}" \
|
||||
npx dotenv-cli -e .env -e .test.env -- \
|
||||
vitest run --config vitest.config.p2p.ts test/suitep2p/syncp2p.p2p-down.test.ts
|
||||
echo "[INFO] download phase completed"
|
||||
|
||||
echo "[INFO] P2P vitest suite completed"
|
||||
@@ -1,175 +0,0 @@
|
||||
/**
|
||||
* P2P-specific sync helpers.
|
||||
*
|
||||
* Derived from test/suite/sync_common.ts but with all acceptWebPeer() calls
|
||||
* removed. When using a CLI p2p-host with P2P_AutoAcceptingPeers="~.*", peer
|
||||
* acceptance is automatic and no Playwright dialog interaction is needed.
|
||||
*/
|
||||
import { expect } from "vitest";
|
||||
import { waitForIdle, type LiveSyncHarness } from "../harness/harness";
|
||||
import { RemoteTypes, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { waitTaskWithFollowups } from "../lib/util";
|
||||
|
||||
const P2P_REPLICATION_TIMEOUT_MS = 180000;
|
||||
|
||||
async function testWebSocketConnection(relayUrl: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`[P2P Debug] Testing WebSocket connection to ${relayUrl}`);
|
||||
try {
|
||||
const ws = new WebSocket(relayUrl);
|
||||
const timer = setTimeout(() => {
|
||||
ws.close();
|
||||
reject(new Error(`WebSocket connection to ${relayUrl} timed out`));
|
||||
}, 5000);
|
||||
ws.onopen = () => {
|
||||
clearTimeout(timer);
|
||||
console.log(`[P2P Debug] WebSocket connected to ${relayUrl} successfully`);
|
||||
ws.close();
|
||||
resolve();
|
||||
};
|
||||
ws.onerror = (e) => {
|
||||
clearTimeout(timer);
|
||||
console.error(`[P2P Debug] WebSocket error connecting to ${relayUrl}:`, e);
|
||||
reject(new Error(`WebSocket connection to ${relayUrl} failed`));
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`[P2P Debug] WebSocket constructor threw:`, e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForP2PPeers(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const maxRetries = 20;
|
||||
let retries = maxRetries;
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
console.log("[P2P Debug] replicator type:", replicator?.constructor?.name);
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
|
||||
// Ensure P2P is open (getActiveReplicator returns a fresh instance that may not be open yet)
|
||||
if (!replicator.server?.isServing) {
|
||||
console.log("[P2P Debug] P2P not yet serving, calling open()");
|
||||
// Test WebSocket connectivity first
|
||||
const relay = harness.plugin.core.settings.P2P_relays?.split(",")[0]?.trim();
|
||||
if (relay) {
|
||||
try {
|
||||
await testWebSocketConnection(relay);
|
||||
} catch (e) {
|
||||
console.error("[P2P Debug] WebSocket connectivity test failed:", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await replicator.open();
|
||||
console.log("[P2P Debug] open() completed, isServing:", replicator.server?.isServing);
|
||||
} catch (e) {
|
||||
console.error("[P2P Debug] open() threw:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for P2P server to actually start (room joined)
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const serving = replicator.server?.isServing;
|
||||
console.log(`[P2P Debug] isServing: ${serving} (${i}/30)`);
|
||||
if (serving) break;
|
||||
await delay(500);
|
||||
if (i === 29) throw new Error("P2P server did not start in time.");
|
||||
}
|
||||
|
||||
while (retries-- > 0) {
|
||||
await delay(1000);
|
||||
const peers = replicator.knownAdvertisements;
|
||||
if (peers && peers.length > 0) {
|
||||
console.log("P2P peers connected:", peers);
|
||||
return;
|
||||
}
|
||||
console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`);
|
||||
console.dir(peers);
|
||||
await delay(1000);
|
||||
}
|
||||
console.log("Failed to connect P2P peers after retries");
|
||||
throw new Error("P2P peers did not connect in time.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
replicator.closeReplication();
|
||||
await delay(30);
|
||||
replicator.closeReplication();
|
||||
await delay(1000);
|
||||
console.log("P2P replicator connections closed");
|
||||
}
|
||||
}
|
||||
|
||||
export async function performReplication(harness: LiveSyncHarness) {
|
||||
await waitForP2PPeers(harness);
|
||||
await delay(500);
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
const knownPeers = replicator.knownAdvertisements;
|
||||
|
||||
const targetPeer = knownPeers.find((peer) => peer.name.startsWith("vault-host")) ?? knownPeers[0] ?? undefined;
|
||||
if (!targetPeer) {
|
||||
throw new Error("No connected P2P peer to synchronise with");
|
||||
}
|
||||
|
||||
const p = replicator.sync(targetPeer.peerId, true);
|
||||
const result = await waitTaskWithFollowups(p, () => Promise.resolve(), P2P_REPLICATION_TIMEOUT_MS, 500);
|
||||
if (result && typeof result === "object" && "error" in result && result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return await harness.plugin.core.services.replication.replicate(true);
|
||||
}
|
||||
|
||||
export async function closeReplication(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
return await closeP2PReplicatorConnections(harness);
|
||||
}
|
||||
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
console.log("No active replicator to close");
|
||||
return;
|
||||
}
|
||||
await replicator.closeReplication();
|
||||
await waitForIdle(harness);
|
||||
console.log("Replication closed");
|
||||
}
|
||||
|
||||
export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) {
|
||||
// P2P has no remote database to initialise — skip
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) return;
|
||||
|
||||
if (shouldReset) {
|
||||
await delay(1000);
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryResetRemoteDatabase(harness.plugin.core.settings);
|
||||
} else {
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryCreateRemoteDatabase(harness.plugin.core.settings);
|
||||
}
|
||||
await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.markRemoteResolved(harness.plugin.core.settings);
|
||||
const status = await harness.plugin.core.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(harness.plugin.core.settings);
|
||||
console.log("Remote status:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* P2P Replication Tests — Download phase (process 2 of 2)
|
||||
*
|
||||
* Executed by run-p2p-tests.sh as the second vitest process, after the
|
||||
* upload phase has completed and the CLI host holds all the data.
|
||||
*
|
||||
* Reads the handoff JSON written by the upload phase to know which files
|
||||
* to verify, then replicates from the CLI host and checks every file.
|
||||
*/
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import {
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type FilePath,
|
||||
type ObsidianLiveSyncSettings,
|
||||
AutoAccepting,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile";
|
||||
import { defaultFileOption, testFileRead } from "../suite/db_common";
|
||||
import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { closeReplication, performReplication } from "./sync_common_p2p";
|
||||
import { settingBase } from "../suite/variables";
|
||||
|
||||
const env = (import.meta as any).env;
|
||||
|
||||
const ROOM_ID: string = env.P2P_TEST_ROOM_ID ?? "p2p-test-room";
|
||||
const PASSPHRASE: string = env.P2P_TEST_PASSPHRASE ?? "p2p-test-pass";
|
||||
const HOST_PEER_NAME: string = env.P2P_TEST_HOST_PEER_NAME ?? "p2p-cli-host";
|
||||
const RELAY: string = env.P2P_TEST_RELAY ?? "ws://localhost:4000/";
|
||||
const APP_ID: string = env.P2P_TEST_APP_ID ?? "self-hosted-livesync-vitest-p2p";
|
||||
const DOWNLOAD_PEER_NAME: string = env.P2P_TEST_DOWNLOAD_PEER_NAME ?? `p2p-download-${Date.now()}`;
|
||||
const DOWNLOAD_VAULT_NAME: string = env.P2P_TEST_DOWNLOAD_VAULT_NAME ?? `TestVaultDownload-${Date.now()}`;
|
||||
const HANDOFF_FILE: string = env.P2P_TEST_HANDOFF_FILE ?? "/tmp/p2p-test-handoff.json";
|
||||
|
||||
console.log("[P2P Down] ROOM_ID:", ROOM_ID, "HOST:", HOST_PEER_NAME, "RELAY:", RELAY, "APP_ID:", APP_ID);
|
||||
console.log("[P2P Down] HANDOFF_FILE:", HANDOFF_FILE);
|
||||
|
||||
const p2pSetting: ObsidianLiveSyncSettings = {
|
||||
...settingBase,
|
||||
...PREFERRED_SETTING_SELF_HOSTED,
|
||||
showVerboseLog: true,
|
||||
remoteType: RemoteTypes.REMOTE_P2P,
|
||||
encrypt: true,
|
||||
passphrase: PASSPHRASE,
|
||||
usePathObfuscation: true,
|
||||
P2P_Enabled: true,
|
||||
P2P_AppID: APP_ID,
|
||||
handleFilenameCaseSensitive: false,
|
||||
P2P_AutoAccepting: AutoAccepting.ALL,
|
||||
P2P_AutoBroadcast: true,
|
||||
P2P_AutoStart: true,
|
||||
P2P_passphrase: PASSPHRASE,
|
||||
P2P_roomID: ROOM_ID,
|
||||
P2P_relays: RELAY,
|
||||
P2P_AutoAcceptingPeers: "~.*",
|
||||
P2P_SyncOnReplication: HOST_PEER_NAME,
|
||||
};
|
||||
|
||||
const fileOptions = defaultFileOption;
|
||||
const nameFile = (type: string, ext: string, size: number) => `p2p-cli-test-${type}-file-${size}.${ext}`;
|
||||
|
||||
/** Read the handoff JSON produced by the upload phase. */
|
||||
async function readHandoff(): Promise<{ fileSizeMd: number[]; fileSizeBins: number[] }> {
|
||||
const { commands } = await import("@vitest/browser/context");
|
||||
const raw = await commands.readHandoffFile(HANDOFF_FILE);
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
describe("P2P Replication — Download", () => {
|
||||
let harnessDownload: LiveSyncHarness;
|
||||
let fileSizeMd: number[] = [];
|
||||
let fileSizeBins: number[] = [];
|
||||
|
||||
const downloadSetting: ObsidianLiveSyncSettings = {
|
||||
...p2pSetting,
|
||||
P2P_DevicePeerName: DOWNLOAD_PEER_NAME,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
|
||||
const handoff = await readHandoff();
|
||||
fileSizeMd = handoff.fileSizeMd;
|
||||
fileSizeBins = handoff.fileSizeBins;
|
||||
console.log("[P2P Down] handoff loaded — md sizes:", fileSizeMd, "bin sizes:", fileSizeBins);
|
||||
|
||||
const vaultName = DOWNLOAD_VAULT_NAME;
|
||||
console.log(`[P2P Down] BeforeAll - Vault: ${vaultName}`);
|
||||
console.log(`[P2P Down] Peer name: ${DOWNLOAD_PEER_NAME}`);
|
||||
harnessDownload = await generateHarness(vaultName, downloadSetting);
|
||||
await waitForReady(harnessDownload);
|
||||
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
await delay(1000);
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
await delay(3000);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessDownload);
|
||||
await harnessDownload.dispose();
|
||||
await delay(1000);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessDownload.plugin).toBeDefined();
|
||||
expect(harnessDownload.plugin.app).toBe(harnessDownload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessDownload.plugin.core.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessDownload.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should have synchronised the stored file", async () => {
|
||||
await testFileRead(harnessDownload, nameFile("store", "md", 0), "Hello, World!", fileOptions);
|
||||
});
|
||||
|
||||
it("should have synchronised files with different content", async () => {
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions);
|
||||
});
|
||||
|
||||
// NOTE: test.each cannot use variables populated in beforeAll, so we use
|
||||
// a single it() that iterates over the sizes loaded from the handoff file.
|
||||
it("should have synchronised all large md files", async () => {
|
||||
for (const size of fileSizeMd) {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should have synchronised all binary files", async () => {
|
||||
for (const size of fileSizeBins) {
|
||||
const path = nameFile("binary", "bin", size);
|
||||
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* P2P Replication Tests — Upload phase (process 1 of 2)
|
||||
*
|
||||
* Executed by run-p2p-tests.sh as the first vitest process.
|
||||
* Writes files into the local DB, replicates them to the CLI host,
|
||||
* then writes a handoff JSON so the download process knows what to verify.
|
||||
*
|
||||
* Trystero has module-level global state (occupiedRooms, didInit, etc.)
|
||||
* that cannot be safely reused across upload→download within the same
|
||||
* browser process. Running upload and download as separate vitest
|
||||
* invocations gives each phase a fresh browser context.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import {
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
AutoAccepting,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
DummyFileSourceInisialised,
|
||||
FILE_SIZE_BINS,
|
||||
FILE_SIZE_MD,
|
||||
generateBinaryFile,
|
||||
generateFile,
|
||||
} from "../utils/dummyfile";
|
||||
import { checkStoredFileInDB, defaultFileOption, testFileWrite } from "../suite/db_common";
|
||||
import { delay } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { closeReplication, performReplication } from "./sync_common_p2p";
|
||||
import { settingBase } from "../suite/variables";
|
||||
|
||||
const env = (import.meta as any).env;
|
||||
|
||||
const ROOM_ID: string = env.P2P_TEST_ROOM_ID ?? "p2p-test-room";
|
||||
const PASSPHRASE: string = env.P2P_TEST_PASSPHRASE ?? "p2p-test-pass";
|
||||
const HOST_PEER_NAME: string = env.P2P_TEST_HOST_PEER_NAME ?? "p2p-cli-host";
|
||||
const RELAY: string = env.P2P_TEST_RELAY ?? "ws://localhost:4000/";
|
||||
const APP_ID: string = env.P2P_TEST_APP_ID ?? "self-hosted-livesync-vitest-p2p";
|
||||
const UPLOAD_PEER_NAME: string = env.P2P_TEST_UPLOAD_PEER_NAME ?? `p2p-upload-${Date.now()}`;
|
||||
const UPLOAD_VAULT_NAME: string = env.P2P_TEST_UPLOAD_VAULT_NAME ?? `TestVaultUpload-${Date.now()}`;
|
||||
// Path written by run-p2p-tests.sh; the download phase reads it back.
|
||||
const HANDOFF_FILE: string = env.P2P_TEST_HANDOFF_FILE ?? "/tmp/p2p-test-handoff.json";
|
||||
|
||||
console.log("[P2P Up] ROOM_ID:", ROOM_ID, "HOST:", HOST_PEER_NAME, "RELAY:", RELAY, "APP_ID:", APP_ID);
|
||||
console.log("[P2P Up] HANDOFF_FILE:", HANDOFF_FILE);
|
||||
|
||||
const p2pSetting: ObsidianLiveSyncSettings = {
|
||||
...settingBase,
|
||||
...PREFERRED_SETTING_SELF_HOSTED,
|
||||
showVerboseLog: true,
|
||||
remoteType: RemoteTypes.REMOTE_P2P,
|
||||
encrypt: true,
|
||||
passphrase: PASSPHRASE,
|
||||
usePathObfuscation: true,
|
||||
P2P_Enabled: true,
|
||||
P2P_AppID: APP_ID,
|
||||
handleFilenameCaseSensitive: false,
|
||||
P2P_AutoAccepting: AutoAccepting.ALL,
|
||||
P2P_AutoBroadcast: true,
|
||||
P2P_AutoStart: true,
|
||||
P2P_passphrase: PASSPHRASE,
|
||||
P2P_roomID: ROOM_ID,
|
||||
P2P_relays: RELAY,
|
||||
P2P_AutoAcceptingPeers: "~.*",
|
||||
P2P_SyncOnReplication: HOST_PEER_NAME,
|
||||
};
|
||||
|
||||
const fileOptions = defaultFileOption;
|
||||
const nameFile = (type: string, ext: string, size: number) => `p2p-cli-test-${type}-file-${size}.${ext}`;
|
||||
|
||||
/** Write the handoff JSON so the download phase knows which files to verify. */
|
||||
async function writeHandoff() {
|
||||
const handoff = {
|
||||
fileSizeMd: FILE_SIZE_MD,
|
||||
fileSizeBins: FILE_SIZE_BINS,
|
||||
};
|
||||
const { commands } = await import("@vitest/browser/context");
|
||||
await commands.writeHandoffFile(HANDOFF_FILE, JSON.stringify(handoff));
|
||||
console.log("[P2P Up] handoff written to", HANDOFF_FILE);
|
||||
}
|
||||
|
||||
describe("P2P Replication — Upload", () => {
|
||||
let harnessUpload: LiveSyncHarness;
|
||||
|
||||
const uploadSetting: ObsidianLiveSyncSettings = {
|
||||
...p2pSetting,
|
||||
P2P_DevicePeerName: UPLOAD_PEER_NAME,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
const vaultName = UPLOAD_VAULT_NAME;
|
||||
console.log(`[P2P Up] BeforeAll - Vault: ${vaultName}`);
|
||||
console.log(`[P2P Up] Peer name: ${UPLOAD_PEER_NAME}`);
|
||||
harnessUpload = await generateHarness(vaultName, uploadSetting);
|
||||
await waitForReady(harnessUpload);
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
await waitForIdle(harnessUpload);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessUpload);
|
||||
await harnessUpload.dispose();
|
||||
await delay(1000);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessUpload.plugin.core.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessUpload.plugin.core.localDatabase).toBeDefined();
|
||||
expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should create a file", async () => {
|
||||
await testFileWrite(harnessUpload, nameFile("store", "md", 0), "Hello, World!", false, fileOptions);
|
||||
});
|
||||
|
||||
it("should create several files with different content", async () => {
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should create large md file of size %i bytes", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should create binary file of size %i bytes", async (size) => {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
const path = nameFile("binary", "bin", size);
|
||||
await testFileWrite(harnessUpload, path, content, true, fileOptions);
|
||||
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
|
||||
if (!isTooLarge) {
|
||||
await checkStoredFileInDB(harnessUpload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
it("should replicate uploads to CLI host", async () => {
|
||||
await performReplication(harnessUpload);
|
||||
await performReplication(harnessUpload);
|
||||
});
|
||||
|
||||
it("should write handoff file for download phase", async () => {
|
||||
await writeHandoff();
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
import { settingBase } from "../suite/variables.ts";
|
||||
import { defaultFileOption } from "../suite/db_common";
|
||||
import { syncBasicCase } from "../suite/sync.senario.basic.ts";
|
||||
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const sync_test_setting_base = settingBase;
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO, RemoteTypes.REMOTE_P2P];
|
||||
const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
// const e2eeOptions = [false, true];
|
||||
const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Replication Suite Tests (P2P)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { writeFile } from "../utils/fileapi.vite";
|
||||
import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("Test File Teet", async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
|
||||
it("should generate binary file correctly", async () => {
|
||||
const size = 5000;
|
||||
let generatedSize = 0;
|
||||
const chunks: Uint8Array[] = [];
|
||||
const generator = generateBinaryFile(size);
|
||||
const blob = new Blob([...generator], { type: "application/octet-stream" });
|
||||
const buf = await blob.arrayBuffer();
|
||||
const hexDump = new Uint8Array(buf)
|
||||
//@ts-ignore
|
||||
.toHex()
|
||||
.match(/.{1,32}/g)
|
||||
?.join("\n");
|
||||
const secondDummy = generateBinaryFile(size);
|
||||
const secondBlob = new Blob([...secondDummy], { type: "application/octet-stream" });
|
||||
const secondBuf = await secondBlob.arrayBuffer();
|
||||
const secondHexDump = new Uint8Array(secondBuf)
|
||||
//@ts-ignore
|
||||
.toHex()
|
||||
.match(/.{1,32}/g)
|
||||
?.join("\n");
|
||||
if (hexDump !== secondHexDump) {
|
||||
throw new Error("Generated binary files do not match");
|
||||
}
|
||||
expect(hexDump).toBe(secondHexDump);
|
||||
// await writeFile("test/testtest/dummyfile.test.bin", buf);
|
||||
// await writeFile("test/testtest/dummyfile.test.bin.hexdump.txt", hexDump || "");
|
||||
});
|
||||
it("should generate text file correctly", async () => {
|
||||
const size = 25000;
|
||||
let generatedSize = 0;
|
||||
let content = "";
|
||||
const generator = generateFile(size);
|
||||
const out = [...generator];
|
||||
// const blob = new Blob(out, { type: "text/plain" });
|
||||
content = out.join("");
|
||||
|
||||
const secondDummy = generateFile(size);
|
||||
const secondOut = [...secondDummy];
|
||||
const secondContent = secondOut.join("");
|
||||
if (content !== secondContent) {
|
||||
throw new Error("Generated text files do not match");
|
||||
}
|
||||
expect(content).toBe(secondContent);
|
||||
// await writeFile("test/testtest/dummyfile.test.txt", await blob.text());
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
// Dialog Unit Tests
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { commands } from "vitest/browser";
|
||||
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { ChunkAlgorithms, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
import { DummyFileSourceInisialised } from "../utils/dummyfile";
|
||||
|
||||
import { page } from "vitest/browser";
|
||||
import { DoctorRegulation } from "@vrtmrz/livesync-commonlib/compat/common/configForDoc";
|
||||
import { waitForDialogHidden, waitForDialogShown } from "../lib/ui";
|
||||
const env = (import.meta as any).env;
|
||||
const dialog_setting_base = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
couchDB_URI: `${env.hostname}`,
|
||||
couchDB_DBNAME: `${env.dbname}`,
|
||||
couchDB_USER: `${env.username}`,
|
||||
couchDB_PASSWORD: `${env.password}`,
|
||||
bucket: `${env.bucketName}`,
|
||||
region: "us-east-1",
|
||||
endpoint: `${env.minioEndpoint}`,
|
||||
accessKey: `${env.accessKey}`,
|
||||
secretKey: `${env.secretKey}`,
|
||||
useCustomRequestHandler: true,
|
||||
forcePathStyle: true,
|
||||
bucketPrefix: "",
|
||||
usePluginSyncV2: true,
|
||||
chunkSplitterVersion: ChunkAlgorithms.RabinKarp,
|
||||
doctorProcessedVersion: DoctorRegulation.version,
|
||||
notifyThresholdOfRemoteStorageSize: 800,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
function checkDialogVisibility(dialogText: string, shouldBeVisible: boolean): void {
|
||||
const dialog = page.getByText(dialogText);
|
||||
expect(dialog).toHaveClass(/modal-title/);
|
||||
if (!shouldBeVisible) {
|
||||
expect(dialog).not.toBeVisible();
|
||||
} else {
|
||||
expect(dialog).toBeVisible();
|
||||
}
|
||||
return;
|
||||
}
|
||||
function checkDialogShown(dialogText: string) {
|
||||
checkDialogVisibility(dialogText, true);
|
||||
}
|
||||
function checkDialogHidden(dialogText: string) {
|
||||
checkDialogVisibility(dialogText, false);
|
||||
}
|
||||
|
||||
describe("Dialog Tests", async () => {
|
||||
// describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
const setting = dialog_setting_base;
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
await commands.grantClipboardPermissions();
|
||||
});
|
||||
let harness: LiveSyncHarness;
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
beforeAll(async () => {
|
||||
harness = await generateHarness(vaultName, setting);
|
||||
await waitForReady(harness);
|
||||
expect(harness.plugin).toBeDefined();
|
||||
expect(harness.plugin.app).toBe(harness.app);
|
||||
await waitForIdle(harness);
|
||||
});
|
||||
it("should show copy to clipboard dialog and confirm", async () => {
|
||||
const testString = "This is a test string to copy to clipboard.";
|
||||
const title = "Copy Test";
|
||||
const result = harness.plugin.core.services.UI.promptCopyToClipboard(title, testString);
|
||||
const isDialogShown = await waitForDialogShown(title, 500);
|
||||
expect(isDialogShown).toBe(true);
|
||||
const copyButton = page.getByText("📋");
|
||||
expect(copyButton).toBeDefined();
|
||||
expect(copyButton).toBeVisible();
|
||||
await copyButton.click();
|
||||
const copyResultButton = page.getByText("✔️");
|
||||
expect(copyResultButton).toBeDefined();
|
||||
expect(copyResultButton).toBeVisible();
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
expect(clipboardText).toBe(testString);
|
||||
const okButton = page.getByText("OK");
|
||||
expect(okButton).toBeDefined();
|
||||
expect(okButton).toBeVisible();
|
||||
await okButton.click();
|
||||
const resultValue = await result;
|
||||
expect(resultValue).toBe(true);
|
||||
// Check that the dialog is closed
|
||||
const isDialogHidden = await waitForDialogHidden(title, 500);
|
||||
expect(isDialogHidden).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { readFile } from "../utils/fileapi.vite.ts";
|
||||
let charset = "";
|
||||
export async function init() {
|
||||
console.log("Initializing dummyfile utils...");
|
||||
|
||||
charset = (await readFile("test/utils/testcharvariants.txt")).toString();
|
||||
console.log(`Loaded charset of length ${charset.length}`);
|
||||
console.log(charset);
|
||||
}
|
||||
export const DummyFileSourceInisialised = init();
|
||||
function* indexer(range: number = 1000, seed: number = 0): Generator<number, number, number> {
|
||||
let t = seed | 0;
|
||||
while (true) {
|
||||
t = (t + 0x6d2b79f5) | 0;
|
||||
let z = t;
|
||||
z = Math.imul(z ^ (z >>> 15), z | 1);
|
||||
z ^= z + Math.imul(z ^ (z >>> 7), z | 61);
|
||||
const float = ((z ^ (z >>> 14)) >>> 0) / 4294967296;
|
||||
yield Math.floor(float * range);
|
||||
}
|
||||
}
|
||||
|
||||
export function* generateFile(size: number): Generator<string> {
|
||||
const chunkSourceStr = charset;
|
||||
const chunkStore = [...chunkSourceStr]; // To support indexing avoiding multi-byte issues
|
||||
const bufSize = 1024;
|
||||
let buf = "";
|
||||
let generated = 0;
|
||||
const indexGen = indexer(chunkStore.length);
|
||||
while (generated < size) {
|
||||
const f = indexGen.next().value;
|
||||
buf += chunkStore[f];
|
||||
generated += 1;
|
||||
if (buf.length >= bufSize) {
|
||||
yield buf;
|
||||
buf = "";
|
||||
}
|
||||
}
|
||||
if (buf.length > 0) {
|
||||
yield buf;
|
||||
}
|
||||
}
|
||||
export function* generateBinaryFile(size: number): Generator<Uint8Array<ArrayBuffer>> {
|
||||
let generated = 0;
|
||||
const pattern = Array.from({ length: 256 }, (_, i) => i);
|
||||
const indexGen = indexer(pattern.length);
|
||||
const bufSize = 1024;
|
||||
const buf = new Uint8Array(bufSize);
|
||||
let bufIdx = 0;
|
||||
while (generated < size) {
|
||||
const f = indexGen.next().value;
|
||||
buf[bufIdx] = pattern[f];
|
||||
bufIdx += 1;
|
||||
generated += 1;
|
||||
if (bufIdx >= bufSize) {
|
||||
yield buf;
|
||||
bufIdx = 0;
|
||||
}
|
||||
}
|
||||
if (bufIdx > 0) {
|
||||
yield buf.subarray(0, bufIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// File size for markdown test files (10B to 1MB, roughly logarithmic scale)
|
||||
export const FILE_SIZE_MD = [10, 100, 1000, 10000, 100000, 1000000];
|
||||
// File size for test files (10B to 40MB, roughly logarithmic scale)
|
||||
export const FILE_SIZE_BINS = [
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
50000,
|
||||
100000,
|
||||
5000000,
|
||||
DEFAULT_SETTINGS.syncMaxSizeInMB * 1024 * 1024 + 1,
|
||||
];
|
||||
@@ -1,3 +0,0 @@
|
||||
import { server } from "vitest/browser";
|
||||
const { readFile, writeFile } = server.commands;
|
||||
export { readFile, writeFile };
|
||||
@@ -1,17 +0,0 @@
|
||||
國破山河在,城春草木深。
|
||||
感時花濺淚,恨別鳥驚心。
|
||||
烽火連三月,家書抵萬金。
|
||||
白頭搔更短,渾欲不勝簪。
|
||||
«Nel mezzo del cammin di nostra vita
|
||||
mi ritrovai per una selva oscura,
|
||||
ché la diritta via era smarrita.»
|
||||
Духовной жаждою томим,
|
||||
В пустыне мрачной я влачился, —
|
||||
И шестикрылый серафим
|
||||
На перепутье мне явился.
|
||||
Shall I compare thee to a summer’s day?
|
||||
Thou art more lovely and more temperate:
|
||||
Rough winds do shake the darling buds of May,
|
||||
And summer’s lease hath all too short a date:
|
||||
|
||||
📜🖋️ 🏺 🏛️ 春望𠮷ché🇷🇺АaRTLO🏳️🌈👨👩👧👦lʼanatraアイウエオ
|
||||
Reference in New Issue
Block a user