mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-27 00:23:56 +00:00
(test): hidden file sync roundtrip and misc
This commit is contained in:
@@ -74,7 +74,7 @@ export async function evalObsidianJson<T>(
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): Promise<T> {
|
||||
const result = await runObsidianCli(cliBinary, ["eval", `code=${code}`], env);
|
||||
if (result.code !== 0 || result.stdout.includes("Error:")) {
|
||||
if (result.code !== 0) {
|
||||
throw new Error(
|
||||
[
|
||||
`Failed to evaluate Obsidian JavaScript through CLI. code=${result.code}, signal=${result.signal}`,
|
||||
@@ -85,5 +85,18 @@ export async function evalObsidianJson<T>(
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
return parseEvalJson(result.stdout) as T;
|
||||
try {
|
||||
return parseEvalJson(result.stdout) as T;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
[
|
||||
`Failed to parse Obsidian CLI eval JSON. code=${result.code}, signal=${result.signal}`,
|
||||
error instanceof Error ? `parse error: ${error.message}` : undefined,
|
||||
result.stdout ? `stdout:\n${result.stdout}` : undefined,
|
||||
result.stderr ? `stderr:\n${result.stderr}` : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { evalObsidianJson } from "./cli.ts";
|
||||
import type { CouchDbConfig } from "./couchdb.ts";
|
||||
|
||||
export type ConfiguredSettings = {
|
||||
isConfigured: boolean;
|
||||
liveSync: boolean;
|
||||
syncOnStart: boolean;
|
||||
syncOnSave: boolean;
|
||||
couchDB_URI: string;
|
||||
couchDB_DBNAME: string;
|
||||
};
|
||||
|
||||
export type CoreReadiness = {
|
||||
databaseReady: boolean;
|
||||
appReady: boolean;
|
||||
};
|
||||
|
||||
export type LocalDatabaseEntry = {
|
||||
id: string;
|
||||
rev: string;
|
||||
path: string;
|
||||
type: string;
|
||||
children: string[];
|
||||
};
|
||||
|
||||
export function assertEqual(actual: unknown, expected: unknown, message: string): void {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureCouchDb(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
settings: Pick<CouchDbConfig, "uri" | "username" | "password"> & { dbName: string },
|
||||
overrides: Record<string, unknown> = {}
|
||||
): 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,",
|
||||
...Object.entries(overrides).map(([key, value]) => `${JSON.stringify(key)}:${JSON.stringify(value)},`),
|
||||
"};",
|
||||
"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
|
||||
);
|
||||
}
|
||||
|
||||
export 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)}`);
|
||||
}
|
||||
|
||||
export 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
|
||||
);
|
||||
}
|
||||
|
||||
export 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
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForLocalDatabaseEntry(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
path: string,
|
||||
options: { hidden?: boolean; timeoutMs?: number } = {}
|
||||
): Promise<LocalDatabaseEntry> {
|
||||
const timeoutMs = options.timeoutMs ?? Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000);
|
||||
return await evalObsidianJson<LocalDatabaseEntry>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const hidden=${JSON.stringify(options.hidden === true)};`,
|
||||
`const timeoutMs=${JSON.stringify(timeoutMs)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const deadline=Date.now()+timeoutMs;",
|
||||
"const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));",
|
||||
"let entry=false;",
|
||||
"while(Date.now()<deadline){",
|
||||
"await core.services.fileProcessing.commitPendingFileEvents();",
|
||||
"const dbPath=hidden?`i:${path}`:path;",
|
||||
"entry=await core.localDatabase.getDBEntry(dbPath,undefined,false,true).catch(()=>false);",
|
||||
"if(!entry||!entry._id){",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"entry=rows.map((row)=>row.doc).find((doc)=>doc&&(",
|
||||
"doc._id===dbPath||doc._id===path||doc.path===dbPath||doc.path===path||",
|
||||
"(typeof doc.path==='string'&&doc.path.endsWith(path))||",
|
||||
"(typeof doc._id==='string'&&doc._id.endsWith(path))",
|
||||
"))||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: ${path}`);",
|
||||
"return JSON.stringify({id:entry._id,rev:entry._rev,path:entry.path,type:entry.type,children:entry.children||[]});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
@@ -13,11 +13,12 @@ export type TemporaryVault = {
|
||||
|
||||
export async function createTemporaryVault(prefix = "obsidian-livesync-e2e-"): Promise<TemporaryVault> {
|
||||
const vaultPath = await mkdtemp(join(tmpdir(), prefix));
|
||||
const statePath = await mkdtemp(join(tmpdir(), `${prefix}state-`));
|
||||
const name = vaultPath.split(/[\\/]/).pop() ?? "obsidian-livesync-e2e";
|
||||
await mkdir(join(vaultPath, ".obsidian"), { recursive: true });
|
||||
const homePath = join(vaultPath, ".obsidian", "e2e-home");
|
||||
const xdgConfigPath = join(vaultPath, ".obsidian", "e2e-xdg-config");
|
||||
const userDataPath = join(vaultPath, ".obsidian", "e2e-user-data");
|
||||
const homePath = join(statePath, "home");
|
||||
const xdgConfigPath = join(statePath, "xdg-config");
|
||||
const userDataPath = join(statePath, "user-data");
|
||||
await mkdir(homePath, { recursive: true });
|
||||
await mkdir(xdgConfigPath, { recursive: true });
|
||||
await mkdir(userDataPath, { recursive: true });
|
||||
@@ -36,9 +37,13 @@ export async function createTemporaryVault(prefix = "obsidian-livesync-e2e-"): P
|
||||
dispose: async () => {
|
||||
if (process.env.E2E_OBSIDIAN_KEEP_VAULT === "true") {
|
||||
console.log(`Keeping temporary vault: ${vaultPath}`);
|
||||
console.log(`Keeping temporary Obsidian state: ${statePath}`);
|
||||
return;
|
||||
}
|
||||
await rm(vaultPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
||||
await Promise.all([
|
||||
rm(vaultPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }),
|
||||
rm(statePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }),
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user