Retire obsolete browser Harness

This commit is contained in:
vorotamoroz
2026-07-20 13:22:22 +00:00
parent 80d1416a81
commit 4cbe5679e1
38 changed files with 45 additions and 2689 deletions
-148
View File
@@ -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
-51
View File
@@ -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;
}
};
}
-165
View File
@@ -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>;
}
}
-70
View File
@@ -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;
}
-21
View File
@@ -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");
}
-2
View File
@@ -1,2 +0,0 @@
#!/bin/bash
echo "P2P Init - No additional initialization required."
-33
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
docker stop relay-test
docker rm relay-test
docker stop webpeer-test
docker rm webpeer-test
-129
View File
@@ -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();
}
-125
View File
@@ -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();
});
});
-275
View File
@@ -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);
});
});
}
-50
View File
@@ -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 });
});
});
-50
View File
@@ -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 });
});
});
-115
View File
@@ -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();
}
}
-39
View File
@@ -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;
-194
View File
@@ -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"
-175
View File
@@ -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();
}
-165
View File
@@ -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);
}
}
});
});
-161
View File
@@ -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();
});
});
-51
View File
@@ -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 });
});
});
-53
View File
@@ -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());
});
});
-94
View File
@@ -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);
});
});
-77
View File
@@ -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,
];
-3
View File
@@ -1,3 +0,0 @@
import { server } from "vitest/browser";
const { readFile, writeFile } = server.commands;
export { readFile, writeFile };
-17
View File
@@ -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 summers day?
Thou art more lovely and more temperate:
Rough winds do shake the darling buds of May,
And summers lease hath all too short a date:
📜🖋️ 🏺 🏛️ 春望𠮷‌ché🇷🇺Аa‮RTLO🏳️‍🌈👨‍👩‍👧‍👦lʼanatraアイウエオ