mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-26 16:13:57 +00:00
(test): improve e2e CouchDB
This commit is contained in:
@@ -125,8 +125,9 @@ Initial discovery on Linux ARM64 found that:
|
||||
Current implementation status:
|
||||
|
||||
- Added `test/e2e-obsidian/runner` helpers for Obsidian discovery, CLI discovery, temporary vault creation, plug-in installation, process launch, CLI execution, and readiness polling.
|
||||
- Added `test:e2e:obsidian:discover`, `test:e2e:obsidian:cli-help`, `test:e2e:obsidian:smoke`, `test:e2e:obsidian:vault-reflection`, and `test:e2e:obsidian:install-appimage`.
|
||||
- Added `test:e2e:obsidian:discover`, `test:e2e:obsidian:cli-help`, `test:e2e:obsidian:smoke`, `test:e2e:obsidian:vault-reflection`, `test:e2e:obsidian:couchdb-upload`, and `test:e2e:obsidian:install-appimage`.
|
||||
- Added `startObsidianLiveSyncSession()` so future workflows can reuse the launch, vault open, community plug-in enablement, plug-in reload, and readiness sequence without duplicating smoke runner code.
|
||||
- Added CouchDB runner utilities that reuse `.test.env`/process environment values, create unique temporary databases, query uploaded documents directly, and clean up the database unless `E2E_OBSIDIAN_KEEP_COUCHDB=true` is set.
|
||||
- Added a manual AppImage installer that downloads Obsidian `1.12.7` for `arm64` or `x86_64`, stores it under `_testdata/obsidian`, and extracts it for FUSE-free execution.
|
||||
- Confirmed the smoke runner on Linux ARM64 with the extracted Obsidian `1.12.7` AppImage, `xvfb-run`, and the built Self-hosted LiveSync bundle.
|
||||
- Confirmed the runner can enable the Obsidian CLI through isolated `obsidian.json` state, open the temporary vault through `obsidian-cli`, enable community plug-ins through `app.plugins.setEnable(true)`, reload Self-hosted LiveSync, and verify readiness through `obsidian-cli eval`.
|
||||
@@ -139,6 +140,7 @@ Current verification:
|
||||
- `npm run test:e2e:obsidian:discover` finds `_testdata/obsidian/squashfs-root/obsidian` when the extracted AppImage is present.
|
||||
- `E2E_OBSIDIAN_SMOKE_TIMEOUT_MS=1000 npm run test:e2e:obsidian:smoke` passes locally.
|
||||
- `npm run test:e2e:obsidian:vault-reflection` creates a note through Obsidian's vault API, verifies the reflected file on disk, and reads it back through Obsidian.
|
||||
- `npm run test:e2e:obsidian:couchdb-upload` configures a unique CouchDB database, creates a note through Obsidian, commits it into the local database, runs one-shot synchronisation, and verifies that CouchDB contains the metadata document and all referenced chunk documents.
|
||||
- `npm run test:e2e:obsidian:install-appimage` reuses the existing AppImage and extracted binary when they are already present.
|
||||
|
||||
Known limits:
|
||||
@@ -161,6 +163,7 @@ This validates real boot-up, settings persistence, vault file access, database w
|
||||
Current implementation status:
|
||||
|
||||
- Added a pre-CouchDB workflow that creates a note through Obsidian's vault API, confirms the note is reflected as a real vault file, and reads the same note back through Obsidian. This covers the vault reflection part of the Phase 2 path before remote database setup is introduced.
|
||||
- Added a first CouchDB-backed upload workflow, modelled after the CLI Deno tests: reuse the standard CouchDB environment variables, create a unique remote database, apply CouchDB settings through the plug-in's setting service, commit the note through the real Obsidian vault path, run one-shot synchronisation, and assert that remote metadata and chunks exist.
|
||||
|
||||
### Phase 3: Two-Vault Synchronisation
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"test:e2e:obsidian:cli-help": "tsx test/e2e-obsidian/scripts/cli-help.ts",
|
||||
"test:e2e:obsidian:smoke": "tsx test/e2e-obsidian/scripts/smoke.ts",
|
||||
"test:e2e:obsidian:vault-reflection": "tsx test/e2e-obsidian/scripts/vault-reflection.ts",
|
||||
"test:e2e:obsidian:couchdb-upload": "tsx test/e2e-obsidian/scripts/couchdb-upload.ts",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:docker-couchdb:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-start.sh",
|
||||
"test:docker-couchdb:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-init.sh",
|
||||
|
||||
@@ -11,7 +11,8 @@ The current smoke runner verifies only the launch path:
|
||||
5. enable Obsidian community plug-ins for the temporary app profile,
|
||||
6. reload Self-hosted LiveSync through `obsidian-cli`,
|
||||
7. verify through `obsidian-cli eval` that the plug-in is loaded,
|
||||
8. terminate Obsidian and remove the temporary vault.
|
||||
8. optionally drive a real vault or CouchDB workflow through Obsidian's own API,
|
||||
9. terminate Obsidian and remove the temporary vault.
|
||||
|
||||
The runner does not require Self-hosted LiveSync to expose an E2E-only bridge. Readiness is checked from outside the plug-in through Obsidian's own CLI.
|
||||
|
||||
@@ -45,6 +46,15 @@ npm run test:e2e:obsidian:discover
|
||||
npm run test:e2e:obsidian:cli-help -- vaults verbose
|
||||
npm run test:e2e:obsidian:smoke
|
||||
npm run test:e2e:obsidian:vault-reflection
|
||||
npm run test:e2e:obsidian:couchdb-upload
|
||||
```
|
||||
|
||||
`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, configures Self-hosted LiveSync through `obsidian-cli eval`, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents.
|
||||
|
||||
Start the local CouchDB fixture first when one is not already running:
|
||||
|
||||
```bash
|
||||
npm run test:docker-couchdb:start
|
||||
```
|
||||
|
||||
Useful environment variables:
|
||||
@@ -61,6 +71,9 @@ Useful environment variables:
|
||||
- `E2E_OBSIDIAN_CLI_READY_TIMEOUT_MS`: timeout for waiting until the vault-side Obsidian CLI exposes the plug-in catalogue.
|
||||
- `E2E_OBSIDIAN_CLI_TIMEOUT_MS`: timeout for each `obsidian-cli` invocation.
|
||||
- `E2E_OBSIDIAN_FILE_TIMEOUT_MS`: timeout for waiting until a note created through Obsidian's vault API is reflected to disk.
|
||||
- `E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS`: timeout for waiting until Self-hosted LiveSync reports that its core lifecycle and local database are ready.
|
||||
- `E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS`: timeout for waiting until CouchDB contains uploaded E2E documents.
|
||||
- `E2E_OBSIDIAN_KEEP_COUCHDB=true`: keep the temporary CouchDB database for inspection.
|
||||
- `E2E_OBSIDIAN_STARTUP_GRACE_MS`: early process-exit detection window in milliseconds.
|
||||
- `E2E_OBSIDIAN_KEEP_VAULT=true`: keep the temporary vault for inspection.
|
||||
- `E2E_OBSIDIAN_USE_XVFB=false`: disable automatic `xvfb-run` on headless Linux.
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
export type CouchDbConfig = {
|
||||
uri: string;
|
||||
username: string;
|
||||
password: string;
|
||||
dbPrefix: string;
|
||||
};
|
||||
|
||||
export type CouchDbDocument = {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
type?: string;
|
||||
path?: string;
|
||||
children?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type CouchDbAllDocsResponse = {
|
||||
rows: Array<{
|
||||
id: string;
|
||||
key: string;
|
||||
value: { rev: string; deleted?: boolean };
|
||||
doc?: CouchDbDocument;
|
||||
}>;
|
||||
};
|
||||
|
||||
function parseEnvFile(content: string): Record<string, string> {
|
||||
const entries = content
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith("#"))
|
||||
.map((line) => {
|
||||
const equalsAt = line.indexOf("=");
|
||||
if (equalsAt < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const key = line.slice(0, equalsAt).trim();
|
||||
const rawValue = line.slice(equalsAt + 1).trim();
|
||||
const value = rawValue.replace(/^['"]|['"]$/gu, "");
|
||||
return [key, value] as const;
|
||||
})
|
||||
.filter((entry): entry is readonly [string, string] => entry !== undefined);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
function getEnvValue(values: Record<string, string | undefined>, ...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = values[key]?.trim();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw new Error(`Required CouchDB environment value is missing: ${keys.join(" or ")}`);
|
||||
}
|
||||
|
||||
function authHeader(config: Pick<CouchDbConfig, "username" | "password">): string {
|
||||
return `Basic ${Buffer.from(`${config.username}:${config.password}`).toString("base64")}`;
|
||||
}
|
||||
|
||||
function databaseUrl(config: Pick<CouchDbConfig, "uri">, dbName: string, suffix = ""): string {
|
||||
return `${config.uri.replace(/\/+$/u, "")}/${encodeURIComponent(dbName)}${suffix}`;
|
||||
}
|
||||
|
||||
async function couchDbRequest(
|
||||
config: Pick<CouchDbConfig, "uri" | "username" | "password">,
|
||||
path: string,
|
||||
init: RequestInit = {}
|
||||
): Promise<Response> {
|
||||
const response = await fetch(`${config.uri.replace(/\/+$/u, "")}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
authorization: authHeader(config),
|
||||
...init.headers,
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function loadCouchDbConfig(envFile = ".test.env"): Promise<CouchDbConfig> {
|
||||
let fileValues: Record<string, string> = {};
|
||||
try {
|
||||
fileValues = parseEnvFile(await readFile(resolve(envFile), "utf-8"));
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const values = { ...fileValues, ...process.env };
|
||||
return {
|
||||
uri: getEnvValue(values, "COUCHDB_URI", "hostname").replace(/\/+$/u, ""),
|
||||
username: getEnvValue(values, "COUCHDB_USER", "username"),
|
||||
password: getEnvValue(values, "COUCHDB_PASSWORD", "password"),
|
||||
dbPrefix: getEnvValue(values, "COUCHDB_DBNAME", "dbname"),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeUniqueDatabaseName(prefix: string, label: string): string {
|
||||
const safePrefix = prefix
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_$()+/-]+/gu, "-")
|
||||
.replace(/^-+/u, "")
|
||||
.slice(0, 80);
|
||||
const random = Math.random().toString(36).slice(2, 8);
|
||||
return `${safePrefix || "livesync-e2e"}-${label}-${Date.now()}-${random}`;
|
||||
}
|
||||
|
||||
export async function assertCouchDbReachable(config: CouchDbConfig): Promise<void> {
|
||||
const response = await couchDbRequest(config, "/_up");
|
||||
if (!response.ok) {
|
||||
throw new Error(`CouchDB is not reachable at ${config.uri}. HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCouchDbDatabase(config: CouchDbConfig, dbName: string): Promise<void> {
|
||||
const response = await fetch(databaseUrl(config, dbName), {
|
||||
method: "PUT",
|
||||
headers: { authorization: authHeader(config) },
|
||||
});
|
||||
if (!response.ok && response.status !== 412) {
|
||||
throw new Error(
|
||||
`Failed to create CouchDB database ${dbName}. HTTP ${response.status}: ${await response.text()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCouchDbDatabase(config: CouchDbConfig, dbName: string): Promise<void> {
|
||||
const response = await fetch(databaseUrl(config, dbName), {
|
||||
method: "DELETE",
|
||||
headers: { authorization: authHeader(config) },
|
||||
});
|
||||
if (!response.ok && response.status !== 404) {
|
||||
throw new Error(
|
||||
`Failed to delete CouchDB database ${dbName}. HTTP ${response.status}: ${await response.text()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllCouchDbDocs(config: CouchDbConfig, dbName: string): Promise<CouchDbAllDocsResponse> {
|
||||
const response = await fetch(databaseUrl(config, dbName, "/_all_docs?include_docs=true"), {
|
||||
headers: { authorization: authHeader(config) },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to read CouchDB documents from ${dbName}. HTTP ${response.status}: ${await response.text()}`
|
||||
);
|
||||
}
|
||||
return (await response.json()) as CouchDbAllDocsResponse;
|
||||
}
|
||||
|
||||
export async function waitForCouchDbDocs(
|
||||
config: CouchDbConfig,
|
||||
dbName: string,
|
||||
predicate: (docs: CouchDbDocument[]) => boolean,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ?? 15000)
|
||||
): Promise<CouchDbDocument[]> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastDocs: CouchDbDocument[] = [];
|
||||
while (Date.now() < deadline) {
|
||||
const response = await fetchAllCouchDbDocs(config, dbName);
|
||||
lastDocs = response.rows.flatMap((row) => (row.doc ? [row.doc] : []));
|
||||
if (predicate(lastDocs)) {
|
||||
return lastDocs;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(
|
||||
`Timed out waiting for CouchDB documents in ${dbName}. Last document IDs: ${lastDocs
|
||||
.map((doc) => doc._id)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import { evalObsidianJson } from "../runner/cli.ts";
|
||||
import {
|
||||
assertCouchDbReachable,
|
||||
createCouchDbDatabase,
|
||||
deleteCouchDbDatabase,
|
||||
loadCouchDbConfig,
|
||||
makeUniqueDatabaseName,
|
||||
waitForCouchDbDocs,
|
||||
} from "../runner/couchdb.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
type ConfiguredSettings = {
|
||||
isConfigured: boolean;
|
||||
liveSync: boolean;
|
||||
syncOnStart: boolean;
|
||||
syncOnSave: boolean;
|
||||
couchDB_URI: string;
|
||||
couchDB_DBNAME: string;
|
||||
};
|
||||
|
||||
type LocalDatabaseEntry = {
|
||||
id: string;
|
||||
path: string;
|
||||
type: string;
|
||||
children: string[];
|
||||
};
|
||||
|
||||
type CoreReadiness = {
|
||||
databaseReady: boolean;
|
||||
appReady: boolean;
|
||||
};
|
||||
|
||||
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "30000";
|
||||
|
||||
const notePath = "E2E/couchdb-upload.md";
|
||||
const noteContent = [
|
||||
"# CouchDB upload from real Obsidian",
|
||||
"",
|
||||
"This note is created through Obsidian and uploaded by Self-hosted LiveSync.",
|
||||
"The content is intentionally long enough to require chunk metadata in the local database.",
|
||||
"0123456789 abcdefghijklmnopqrstuvwxyz 0123456789 abcdefghijklmnopqrstuvwxyz",
|
||||
"0123456789 abcdefghijklmnopqrstuvwxyz 0123456789 abcdefghijklmnopqrstuvwxyz",
|
||||
`Created at: ${new Date().toISOString()}`,
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
function assertEqual(actual: unknown, expected: unknown, message: string): void {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function configureCouchDb(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
settings: {
|
||||
uri: string;
|
||||
username: string;
|
||||
password: string;
|
||||
dbName: string;
|
||||
}
|
||||
): Promise<ConfiguredSettings> {
|
||||
return await evalObsidianJson<ConfiguredSettings>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const plugin=app.plugins.plugins['obsidian-livesync'];",
|
||||
"const core=plugin.core;",
|
||||
"const nextSettings={",
|
||||
`couchDB_URI:${JSON.stringify(settings.uri)},`,
|
||||
`couchDB_USER:${JSON.stringify(settings.username)},`,
|
||||
`couchDB_PASSWORD:${JSON.stringify(settings.password)},`,
|
||||
`couchDB_DBNAME:${JSON.stringify(settings.dbName)},`,
|
||||
"remoteType:'',",
|
||||
"liveSync:false,",
|
||||
"syncOnStart:false,",
|
||||
"syncOnSave:false,",
|
||||
"usePluginSync:false,",
|
||||
"usePluginSyncV2:true,",
|
||||
"useEden:false,",
|
||||
"customChunkSize:1,",
|
||||
"sendChunksBulkMaxSize:1,",
|
||||
"chunkSplitterVersion:'v3-rabin-karp',",
|
||||
"readChunksOnline:false,",
|
||||
"disableCheckingConfigMismatch:true,",
|
||||
"isConfigured:true,",
|
||||
"};",
|
||||
"await core.services.setting.applyExternalSettings(nextSettings,true);",
|
||||
"await core.services.control.applySettings();",
|
||||
"const current=core.services.setting.currentSettings();",
|
||||
"return JSON.stringify({",
|
||||
"isConfigured:current.isConfigured,",
|
||||
"liveSync:current.liveSync,",
|
||||
"syncOnStart:current.syncOnStart,",
|
||||
"syncOnSave:current.syncOnSave,",
|
||||
"couchDB_URI:current.couchDB_URI,",
|
||||
"couchDB_DBNAME:current.couchDB_DBNAME,",
|
||||
"});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForLiveSyncCoreReady(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS ?? 20000)
|
||||
): Promise<CoreReadiness> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastReadiness: CoreReadiness | undefined;
|
||||
while (Date.now() < deadline) {
|
||||
lastReadiness = await evalObsidianJson<CoreReadiness>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"return JSON.stringify({",
|
||||
"databaseReady:core.services.database.isDatabaseReady(),",
|
||||
"appReady:core.services.appLifecycle.isReady(),",
|
||||
"});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
if (lastReadiness.databaseReady && lastReadiness.appReady) {
|
||||
return lastReadiness;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(`Timed out waiting for Self-hosted LiveSync core readiness: ${JSON.stringify(lastReadiness)}`);
|
||||
}
|
||||
|
||||
async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const settings=core.services.setting.currentSettings();",
|
||||
"const replicator=core.services.replicator.getActiveReplicator();",
|
||||
"await replicator.tryCreateRemoteDatabase(settings);",
|
||||
"await replicator.markRemoteResolved(settings);",
|
||||
"const status=await replicator.getRemoteStatus(settings);",
|
||||
"return JSON.stringify({status});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function createNoteAndWaitForLocalDb(cliBinary: string, env: NodeJS.ProcessEnv): Promise<LocalDatabaseEntry> {
|
||||
return await evalObsidianJson<LocalDatabaseEntry>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(notePath)};`,
|
||||
`const content=${JSON.stringify(noteContent)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"if(!(await app.vault.adapter.exists('E2E'))) await app.vault.createFolder('E2E');",
|
||||
"const existing=app.vault.getAbstractFileByPath(path);",
|
||||
"if(existing) await app.vault.delete(existing);",
|
||||
"await app.vault.create(path,content);",
|
||||
"const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));",
|
||||
"let entry=false;",
|
||||
"for(let i=0;i<40;i++){",
|
||||
"await core.services.fileProcessing.commitPendingFileEvents();",
|
||||
"entry=await core.localDatabase.getDBEntry(path,undefined,false,true).catch(()=>false);",
|
||||
"if(entry&&entry._id&&Array.isArray(entry.children)&&entry.children.length>0) break;",
|
||||
"await sleep(250);",
|
||||
"}",
|
||||
"if(!entry||!entry._id) throw new Error('Timed out waiting for local database entry');",
|
||||
"return JSON.stringify({id:entry._id,path:entry.path,type:entry.type,children:entry.children||[]});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function pushLocalChanges(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"await core.services.fileProcessing.commitPendingFileEvents();",
|
||||
"const result=await core.services.replication.replicate(true);",
|
||||
"return JSON.stringify({result:!!result});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const binary = requireObsidianBinary();
|
||||
const cli = discoverObsidianCli();
|
||||
if (!cli.binary) {
|
||||
throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`);
|
||||
}
|
||||
|
||||
const couchDb = await loadCouchDbConfig();
|
||||
const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "obsidian-upload");
|
||||
const vault = await createTemporaryVault();
|
||||
let session: ObsidianLiveSyncSession | undefined;
|
||||
|
||||
try {
|
||||
await assertCouchDbReachable(couchDb);
|
||||
await createCouchDbDatabase(couchDb, dbName);
|
||||
|
||||
console.log(`Using Obsidian executable: ${binary}`);
|
||||
console.log(`Temporary vault: ${vault.path}`);
|
||||
console.log(`Temporary CouchDB database: ${dbName}`);
|
||||
|
||||
session = await startObsidianLiveSyncSession({
|
||||
binary,
|
||||
cliBinary: cli.binary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
});
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
|
||||
const configured = await configureCouchDb(cli.binary, session.cliEnv, {
|
||||
uri: couchDb.uri,
|
||||
username: couchDb.username,
|
||||
password: couchDb.password,
|
||||
dbName,
|
||||
});
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
assertEqual(configured.isConfigured, true, "Self-hosted LiveSync was not marked as configured.");
|
||||
assertEqual(configured.couchDB_URI, couchDb.uri, "Configured CouchDB URI did not match.");
|
||||
assertEqual(configured.couchDB_DBNAME, dbName, "Configured CouchDB database name did not match.");
|
||||
assertEqual(configured.liveSync, false, "LiveSync should remain disabled during this one-shot workflow.");
|
||||
assertEqual(configured.syncOnStart, false, "Sync on start should remain disabled during this workflow.");
|
||||
assertEqual(configured.syncOnSave, false, "Sync on save should remain disabled during this workflow.");
|
||||
|
||||
await prepareRemote(cli.binary, session.cliEnv);
|
||||
const localEntry = await createNoteAndWaitForLocalDb(cli.binary, session.cliEnv);
|
||||
await pushLocalChanges(cli.binary, session.cliEnv);
|
||||
|
||||
const remoteDocs = await waitForCouchDbDocs(couchDb, dbName, (docs) => {
|
||||
const ids = new Set(docs.map((doc) => doc._id));
|
||||
return ids.has(localEntry.id) && localEntry.children.every((childId) => ids.has(childId));
|
||||
});
|
||||
const remoteMetadata = remoteDocs.find((doc) => doc._id === localEntry.id);
|
||||
assertEqual(
|
||||
remoteMetadata?.path,
|
||||
localEntry.path,
|
||||
"Remote metadata path did not match the local database entry."
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Uploaded metadata ${localEntry.id} and ${localEntry.children.length} chunk(s) to CouchDB database ${dbName}`
|
||||
);
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.app.stop();
|
||||
}
|
||||
await vault.dispose();
|
||||
if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") {
|
||||
await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user