mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-26 16:13:57 +00:00
(test): customisation sync 2
This commit is contained in:
@@ -144,13 +144,13 @@ Current verification:
|
||||
- `npm run test:e2e:obsidian:startup-scan` verifies that a file written while Obsidian is stopped is picked up during the next real Obsidian boot and uploaded to CouchDB after one-shot synchronisation.
|
||||
- `npm run test:e2e:obsidian:two-vault-sync` verifies two-vault note synchronisation: creation, update, deletion, Markdown conflict automatic merging with the merged result propagated by a second synchronisation, and per-device target-filter differences.
|
||||
- `npm run test:e2e:obsidian:hidden-file-snippet-sync` verifies hidden file synchronisation as a two-vault round-trip: creation, deletion, automatic JSON conflict merging with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target-pattern differences.
|
||||
- `npm run test:e2e:obsidian:customisation-sync` verifies a two-vault Customisation Sync snippet workflow: scan a real snippet CSS file into per-file Customisation Sync data, synchronise it through CouchDB, apply it on the second vault, and assert the resulting `.obsidian/snippets/*.css` file.
|
||||
- `npm run test:e2e:obsidian:customisation-sync` verifies a two-vault Customisation Sync workflow: scan a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronise them through CouchDB, apply them on the second vault, assert the resulting `.obsidian` files, propagate a snippet update, and verify deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy.
|
||||
- `npm run test:e2e:obsidian:setting-markdown-export` verifies that setting Markdown export creates a vault file and omits credentials when credential export is disabled.
|
||||
- `npm run test:e2e:obsidian:install-appimage` reuses the existing AppImage and extracted binary when they are already present.
|
||||
|
||||
Known limits:
|
||||
|
||||
- The smoke runner currently proves only one-vault launch and plug-in load readiness. It does not yet exercise synchronisation, settings persistence, restart behaviour, or database writes.
|
||||
- The smoke runner currently proves only one-vault launch and plug-in load readiness. Broader workflows are covered by separate real Obsidian scripts, including CouchDB upload, startup scan, two-vault note synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export.
|
||||
- Cross-platform support is still discovery-level. The working path has been validated on Linux ARM64.
|
||||
- CI wiring is not yet implemented. CI should use `OBSIDIAN_BINARY` or a cached `_testdata/obsidian/squashfs-root` rather than downloading the AppImage on every run.
|
||||
|
||||
@@ -169,7 +169,7 @@ 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.
|
||||
- Added Obsidian-specific workflows for boot-time vault scanning, two-vault note synchronisation, hidden `.obsidian/snippets` file round-tripping, hidden JSON conflict resolution, Customisation Sync snippet application, per-device target-filter differences, and setting Markdown export. These scenarios assert against CouchDB documents, vault files, or real Obsidian UI outcomes instead of internal service state.
|
||||
- Added Obsidian-specific workflows for boot-time vault scanning, two-vault note synchronisation, hidden `.obsidian/snippets` file round-tripping, hidden JSON conflict resolution, Customisation Sync application for snippets, config JSON files, and plug-in fixtures, per-device target-filter differences, and setting Markdown export. These scenarios assert against CouchDB documents, vault files, or real Obsidian UI outcomes instead of internal service state.
|
||||
|
||||
### Phase 3: Two-Vault Synchronisation
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ npm run test:e2e:obsidian:setting-markdown-export
|
||||
|
||||
`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises.
|
||||
|
||||
`test:e2e:obsidian:customisation-sync` runs a two-vault Customisation Sync workflow. It scans a real snippet CSS file into per-file Customisation Sync data, synchronises the entry through CouchDB, applies it on the second vault, and verifies the resulting `.obsidian/snippets/*.css` file.
|
||||
`test:e2e:obsidian:customisation-sync` runs a two-vault Customisation Sync workflow. It scans a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronises the entries through CouchDB, applies them on the second vault, verifies the resulting `.obsidian` files, propagates a snippet update, and verifies deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy.
|
||||
|
||||
`test:e2e:obsidian:setting-markdown-export` enables setting Markdown export, waits for the generated Markdown file in the vault, and verifies that credentials are omitted when `writeCredentialsForSettingSync=false`.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { evalObsidianJson } from "../runner/cli.ts";
|
||||
import {
|
||||
@@ -35,6 +35,47 @@ const snippetContent = [
|
||||
"}",
|
||||
"",
|
||||
].join("\n");
|
||||
const snippetUpdatedContent = [
|
||||
"body {",
|
||||
" --livesync-customisation-e2e-colour: #73548f;",
|
||||
"}",
|
||||
"",
|
||||
".livesync-customisation-e2e {",
|
||||
" background-color: var(--livesync-customisation-e2e-colour);",
|
||||
"}",
|
||||
"",
|
||||
].join("\n");
|
||||
const configPath = ".obsidian/livesync-customisation-e2e.json";
|
||||
const configContent = JSON.stringify({ source: "customisation-sync", enabled: true }, null, 4) + "\n";
|
||||
const pluginDir = ".obsidian/plugins/livesync-e2e-sample";
|
||||
const pluginManifestPath = `${pluginDir}/manifest.json`;
|
||||
const pluginMainPath = `${pluginDir}/main.js`;
|
||||
const pluginStylesPath = `${pluginDir}/styles.css`;
|
||||
const pluginManifestContent =
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "livesync-e2e-sample",
|
||||
name: "LiveSync E2E Sample",
|
||||
version: "0.0.1",
|
||||
minAppVersion: "1.0.0",
|
||||
description: "A sample plug-in fixture for real Obsidian E2E.",
|
||||
author: "Self-hosted LiveSync",
|
||||
isDesktopOnly: false,
|
||||
},
|
||||
null,
|
||||
4
|
||||
) + "\n";
|
||||
const pluginMainContent = [
|
||||
"module.exports = class LiveSyncE2ESamplePlugin extends Plugin {",
|
||||
" async onload() {",
|
||||
" this.register(() => undefined);",
|
||||
" }",
|
||||
"};",
|
||||
"",
|
||||
].join("\n");
|
||||
const pluginStylesContent = ".livesync-e2e-sample { color: #73548f; }\n";
|
||||
const sourceDeviceName = "customisation-sync-a";
|
||||
const targetDeviceName = "customisation-sync-b";
|
||||
|
||||
type RunnerContext = {
|
||||
binary: string;
|
||||
@@ -63,6 +104,10 @@ async function writeVaultFile(vaultPath: string, path: string, content: string):
|
||||
await writeFile(fullPath, content, "utf-8");
|
||||
}
|
||||
|
||||
async function removeVaultFile(vaultPath: string, path: string): Promise<void> {
|
||||
await rm(join(vaultPath, path), { force: true });
|
||||
}
|
||||
|
||||
async function readVaultFile(vaultPath: string, path: string): Promise<string> {
|
||||
return await readFile(join(vaultPath, path), "utf-8");
|
||||
}
|
||||
@@ -169,7 +214,7 @@ async function scanCustomisations(cliBinary: string, env: NodeJS.ProcessEnv): Pr
|
||||
);
|
||||
}
|
||||
|
||||
async function storeCustomisationSnippet(cliBinary: string, env: NodeJS.ProcessEnv, path: string): Promise<void> {
|
||||
async function storeCustomisationFile(cliBinary: string, env: NodeJS.ProcessEnv, path: string): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
@@ -184,7 +229,7 @@ async function storeCustomisationSnippet(cliBinary: string, env: NodeJS.ProcessE
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entries=rows.map((row)=>row.doc).filter((doc)=>doc?.path?.startsWith('ix:')).map((doc)=>doc.path);",
|
||||
"if(!result){",
|
||||
" throw new Error(`Could not store Customisation Sync snippet: path=${path}; term=${term}; category=${category}; stat=${JSON.stringify(stat)}; result=${JSON.stringify(result)}; entries=${JSON.stringify(entries)}`);",
|
||||
" throw new Error(`Could not store Customisation Sync file: path=${path}; term=${term}; category=${category}; stat=${JSON.stringify(stat)}; result=${JSON.stringify(result)}; entries=${JSON.stringify(entries)}`);",
|
||||
"}",
|
||||
"return JSON.stringify({ok:true,path,term,category,entries});",
|
||||
"})()",
|
||||
@@ -193,17 +238,102 @@ async function storeCustomisationSnippet(cliBinary: string, env: NodeJS.ProcessE
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteCustomisationSyncEntry(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
name: string,
|
||||
term?: string
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const category=${JSON.stringify(category)};`,
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entry=rows.map((row)=>row.doc).find((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`))&&!doc.deleted&&!doc._deleted)||false;",
|
||||
"if(!entry) throw new Error(`Could not find customisation sync entry to delete: ${category}/${name}`);",
|
||||
"if(!(await addOn.deleteConfigOnDatabase(entry.path))){",
|
||||
" throw new Error(`Could not delete Customisation Sync entry: ${entry.path}`);",
|
||||
"}",
|
||||
"return JSON.stringify({ok:true,path:entry.path});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForCustomisationEntry(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
filename: string,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
name: string,
|
||||
term?: string,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000)
|
||||
): Promise<CustomisationEntry> {
|
||||
return await evalObsidianJson<CustomisationEntry>(
|
||||
const entries = await waitForCustomisationEntries(cliBinary, env, category, name, 1, term, timeoutMs);
|
||||
return entries[0];
|
||||
}
|
||||
|
||||
async function waitForCustomisationEntries(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
name: string,
|
||||
count: number,
|
||||
term?: string,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000)
|
||||
): Promise<CustomisationEntry[]> {
|
||||
return await evalObsidianJson<CustomisationEntry[]>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const filename=${JSON.stringify(filename)};`,
|
||||
`const category=${JSON.stringify(category)};`,
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const count=${JSON.stringify(count)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
`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 entries=[];",
|
||||
"while(Date.now()<deadline){",
|
||||
" const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
" entries=rows.map((row)=>row.doc).filter((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`))&&Array.isArray(doc.children)&&doc.children.length>0);",
|
||||
" if(entries.length>=count) break;",
|
||||
" await sleep(250);",
|
||||
"}",
|
||||
"if(entries.length<count){",
|
||||
" const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
" const found=rows.map((row)=>row.doc).filter((doc)=>doc?.path?.startsWith('ix:')).map((doc)=>({id:doc._id,path:doc.path,children:doc.children?.length??0}));",
|
||||
" throw new Error(`Timed out waiting for customisation sync entries: ${category}/${name}; expected=${count}; entries=${JSON.stringify(found)}`);",
|
||||
"}",
|
||||
"return JSON.stringify(entries.map((entry)=>({id:entry._id,path:entry.path,children:entry.children||[]})));",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForCustomisationEntryAbsent(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
name: string,
|
||||
term?: string,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000)
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const category=${JSON.stringify(category)};`,
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
`const timeoutMs=${JSON.stringify(timeoutMs)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const deadline=Date.now()+timeoutMs;",
|
||||
@@ -211,37 +341,36 @@ async function waitForCustomisationEntry(
|
||||
"let entry=false;",
|
||||
"while(Date.now()<deadline){",
|
||||
" const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
" entry=rows.map((row)=>row.doc).find((doc)=>doc?.path?.includes('/SNIPPET/')&&doc.path?.endsWith(`%${filename}`))||false;",
|
||||
" if(entry&&entry._id&&Array.isArray(entry.children)&&entry.children.length>0) break;",
|
||||
" entry=rows.map((row)=>row.doc).find((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`))&&!doc.deleted&&!doc._deleted)||false;",
|
||||
" if(!entry) return JSON.stringify({ok:true});",
|
||||
" await sleep(250);",
|
||||
"}",
|
||||
"if(!entry||!entry._id){",
|
||||
" const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
" const entries=rows.map((row)=>row.doc).filter((doc)=>doc?.path?.startsWith('ix:')).map((doc)=>({id:doc._id,path:doc.path,children:doc.children?.length??0}));",
|
||||
" throw new Error(`Timed out waiting for customisation sync entry: ${filename}; entries=${JSON.stringify(entries)}`);",
|
||||
"}",
|
||||
"return JSON.stringify({id:entry._id,path:entry.path,children:entry.children||[]});",
|
||||
"throw new Error(`Timed out waiting for customisation sync entry deletion: ${category}/${name}; entry=${JSON.stringify(entry)}`);",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function applyRemoteCustomisationSnippet(
|
||||
async function applyRemoteCustomisationEntry(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
filename: string
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
name: string,
|
||||
term?: string
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const filename=${JSON.stringify(filename)};`,
|
||||
`const category=${JSON.stringify(category)};`,
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entry=rows.map((row)=>row.doc).find((doc)=>doc?.path?.includes('/SNIPPET/')&&doc.path?.endsWith(`%${filename}`))||false;",
|
||||
"if(!entry) throw new Error(`Could not find remote customisation entry: ${filename}`);",
|
||||
"const entry=rows.map((row)=>row.doc).find((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`)))||false;",
|
||||
"if(!entry) throw new Error(`Could not find remote customisation entry: ${category}/${name}`);",
|
||||
"const display=addOn.createPluginDataFromV2(entry.path);",
|
||||
"if(!display) throw new Error(`Could not create Customisation Sync display entry: ${entry.path}`);",
|
||||
"const file=await addOn.createPluginDataExFileV2(entry.path);",
|
||||
@@ -257,6 +386,42 @@ async function applyRemoteCustomisationSnippet(
|
||||
);
|
||||
}
|
||||
|
||||
async function applyRemoteCustomisationGroup(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "PLUGIN_MAIN",
|
||||
name: string,
|
||||
term?: string
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const category=${JSON.stringify(category)};`,
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entries=rows.map((row)=>row.doc).filter((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`)));",
|
||||
"if(entries.length===0) throw new Error(`Could not find remote customisation entries: ${category}/${name}`);",
|
||||
"const display=addOn.createPluginDataFromV2(entries[0].path);",
|
||||
"if(!display) throw new Error(`Could not create Customisation Sync display entry: ${entries[0].path}`);",
|
||||
"for(const entry of entries){",
|
||||
" const file=await addOn.createPluginDataExFileV2(entry.path);",
|
||||
" if(!file) throw new Error(`Could not load Customisation Sync file entry: ${entry.path}`);",
|
||||
" await display.setFile(file);",
|
||||
"}",
|
||||
"if(!(await addOn.applyDataV2(display))){",
|
||||
" throw new Error(`Could not apply Customisation Sync group: ${category}/${name}`);",
|
||||
"}",
|
||||
"return JSON.stringify({ok:true,count:entries.length});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const binary = requireObsidianBinary();
|
||||
const cli = discoverObsidianCli();
|
||||
@@ -271,6 +436,8 @@ async function main(): Promise<void> {
|
||||
const context: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName };
|
||||
const snippetPathParts = snippetPath.split("/");
|
||||
const snippetName = snippetPathParts[snippetPathParts.length - 1] ?? snippetPath;
|
||||
const configName = configPath.split("/").pop() ?? configPath;
|
||||
const pluginName = pluginDir.split("/").pop() ?? pluginDir;
|
||||
|
||||
try {
|
||||
await assertCouchDbReachable(couchDb);
|
||||
@@ -282,33 +449,141 @@ async function main(): Promise<void> {
|
||||
console.log(`Temporary CouchDB database: ${dbName}`);
|
||||
|
||||
await writeVaultFile(vaultA.path, snippetPath, snippetContent);
|
||||
await writeVaultFile(vaultA.path, configPath, configContent);
|
||||
await writeVaultFile(vaultA.path, pluginManifestPath, pluginManifestContent);
|
||||
await writeVaultFile(vaultA.path, pluginMainPath, pluginMainContent);
|
||||
await writeVaultFile(vaultA.path, pluginStylesPath, pluginStylesContent);
|
||||
|
||||
let session = await startConfiguredSession(context, vaultA, "customisation-sync-a");
|
||||
let session = await startConfiguredSession(context, vaultA, sourceDeviceName);
|
||||
const scanResult = await scanCustomisations(context.cliBinary, session.cliEnv);
|
||||
console.log(`Customisation scan files: ${scanResult.files.join(", ") || "(none)"}`);
|
||||
await storeCustomisationSnippet(context.cliBinary, session.cliEnv, snippetPath);
|
||||
const entry = await waitForCustomisationEntry(context.cliBinary, session.cliEnv, snippetName);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, snippetPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, configPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginManifestPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginMainPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginStylesPath);
|
||||
const entry = await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "SNIPPET", snippetName);
|
||||
const configEntry = await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "CONFIG", configName);
|
||||
const pluginEntries = await waitForCustomisationEntries(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"PLUGIN_MAIN",
|
||||
pluginName,
|
||||
3
|
||||
);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => {
|
||||
const ids = new Set(docs.map((doc) => doc._id));
|
||||
return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId));
|
||||
const entries = [entry, configEntry, ...pluginEntries];
|
||||
return entries.every(
|
||||
(target) => ids.has(target.id) && target.children.every((childId) => ids.has(childId))
|
||||
);
|
||||
});
|
||||
await session.app.stop();
|
||||
|
||||
session = await startConfiguredSession(context, vaultB, "customisation-sync-b");
|
||||
session = await startConfiguredSession(context, vaultB, targetDeviceName);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForCustomisationEntry(context.cliBinary, session.cliEnv, snippetName);
|
||||
await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "SNIPPET", snippetName, sourceDeviceName);
|
||||
assertEqual(
|
||||
await pathExists(vaultB.path, snippetPath),
|
||||
false,
|
||||
"Customisation Sync snippet was reflected before explicit application."
|
||||
);
|
||||
await applyRemoteCustomisationSnippet(context.cliBinary, session.cliEnv, snippetName);
|
||||
await applyRemoteCustomisationEntry(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"SNIPPET",
|
||||
snippetName,
|
||||
sourceDeviceName
|
||||
);
|
||||
const applied = await waitForPathContent(vaultB.path, snippetPath, (content) => content === snippetContent);
|
||||
await applyRemoteCustomisationEntry(context.cliBinary, session.cliEnv, "CONFIG", configName, sourceDeviceName);
|
||||
const appliedConfig = await waitForPathContent(vaultB.path, configPath, (content) => content === configContent);
|
||||
await applyRemoteCustomisationGroup(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"PLUGIN_MAIN",
|
||||
pluginName,
|
||||
sourceDeviceName
|
||||
);
|
||||
const appliedPluginManifest = await waitForPathContent(
|
||||
vaultB.path,
|
||||
pluginManifestPath,
|
||||
(content) => content === pluginManifestContent
|
||||
);
|
||||
const appliedPluginMain = await waitForPathContent(
|
||||
vaultB.path,
|
||||
pluginMainPath,
|
||||
(content) => content === pluginMainContent
|
||||
);
|
||||
const appliedPluginStyles = await waitForPathContent(
|
||||
vaultB.path,
|
||||
pluginStylesPath,
|
||||
(content) => content === pluginStylesContent
|
||||
);
|
||||
await session.app.stop();
|
||||
|
||||
assertEqual(applied, snippetContent, "Customisation Sync snippet content did not match after application.");
|
||||
console.log(`Customisation Sync applied snippet ${snippetName} from the remote database.`);
|
||||
assertEqual(appliedConfig, configContent, "Customisation Sync config content did not match after application.");
|
||||
assertEqual(
|
||||
appliedPluginManifest,
|
||||
pluginManifestContent,
|
||||
"Customisation Sync plug-in manifest did not match after application."
|
||||
);
|
||||
assertEqual(appliedPluginMain, pluginMainContent, "Customisation Sync plug-in main file did not match.");
|
||||
assertEqual(appliedPluginStyles, pluginStylesContent, "Customisation Sync plug-in stylesheet did not match.");
|
||||
|
||||
await writeVaultFile(vaultA.path, snippetPath, snippetUpdatedContent);
|
||||
session = await startConfiguredSession(context, vaultA, sourceDeviceName);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, snippetPath);
|
||||
await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "SNIPPET", snippetName);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await session.app.stop();
|
||||
|
||||
session = await startConfiguredSession(context, vaultB, targetDeviceName);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await applyRemoteCustomisationEntry(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"SNIPPET",
|
||||
snippetName,
|
||||
sourceDeviceName
|
||||
);
|
||||
const updated = await waitForPathContent(
|
||||
vaultB.path,
|
||||
snippetPath,
|
||||
(content) => content === snippetUpdatedContent
|
||||
);
|
||||
await session.app.stop();
|
||||
assertEqual(updated, snippetUpdatedContent, "Updated Customisation Sync snippet did not apply.");
|
||||
|
||||
await removeVaultFile(vaultA.path, snippetPath);
|
||||
session = await startConfiguredSession(context, vaultA, sourceDeviceName);
|
||||
await deleteCustomisationSyncEntry(context.cliBinary, session.cliEnv, "SNIPPET", snippetName, sourceDeviceName);
|
||||
await waitForCustomisationEntryAbsent(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"SNIPPET",
|
||||
snippetName,
|
||||
sourceDeviceName
|
||||
);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await session.app.stop();
|
||||
|
||||
session = await startConfiguredSession(context, vaultB, targetDeviceName);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForCustomisationEntryAbsent(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"SNIPPET",
|
||||
snippetName,
|
||||
sourceDeviceName
|
||||
);
|
||||
await session.app.stop();
|
||||
|
||||
console.log(
|
||||
`Customisation Sync applied snippet, config, and plug-in fixtures, then propagated snippet update and sync-data deletion.`
|
||||
);
|
||||
} finally {
|
||||
await vaultA.dispose();
|
||||
await vaultB.dispose();
|
||||
|
||||
Reference in New Issue
Block a user