fix(cli): share Vault-relative path exclusions

Apply watcher and logical-target exclusions within the selected Vault so
hidden or database-like parent directories do not suppress monitoring.
Protect primary, previous, and view databases, and provide a runtime opt-in
for ordinary database-like directories without changing database ownership.

Cover hidden roots, symlink restarts, exclusion parity, and encrypted
CouchDB create, modify, and delete operations with regression tests.

Refs #1340
This commit is contained in:
vorotamoroz committed 2026-11-12 08:51:11 +00:00
1 parent 78ebc2d298
commit 116c2dfbfb
20 files changed
+1703 -107

No files matched your search

+35
View File
@@ -77,6 +77,41 @@ npm run test:e2e:docker:mirror --workspace self-hosted-livesync-cli
The expected result is `PASS=7 FAIL=0`. These mirror suites use temporary local databases and need no CouchDB service. The complete `test:e2e:docker:all` command also runs the other Docker CLI suites and manages a disposable CouchDB fixture; use it to check the full Docker CI gate. Keep the ordinary-edit and unknown-provenance scenarios aligned between the Node.js and Bash suites.
## CLI daemon watcher path regression
Build the CLI, then run the focused task with Node.js 26 or later and Docker
available:
```bash
NODE_OPTIONS=--max-old-space-size=3072 npm run build --workspace self-hosted-livesync-cli
node test/e2e-cli/run-task.ts test:daemon-watch-paths
```
The task starts the disposable `couchdb-test` container, creates a unique
database, and removes the container during teardown. Docker, the `couchdb-test`
container name, and host port `5989` must be available. The CLI `.test.env` file supplies
the same CouchDB connection variables used by the other CLI CouchDB tests;
local test defaults apply when they are absent.
The test seeds two revisions for a baseline, runs `fetch` into a Vault under a
hidden `.obsidian` parent through a visible symlink, and starts the actual CLI
daemon. An independent CLI client confirms exact remote file bytes and deletion
after create, modify, and delete events. The current baseline Metadata revision
and bytes are checked on the fetched client and observer; its original revision
remains readable on the source and is checked in CouchDB's revision history. The
test also checks the default hidden-entry and `.livesync/ignore` exclusions,
then restarts the daemon with the physical Vault path and verifies the saved
database suffix, runtime scope, and further monitoring.
All three CLI clients use the same encrypted CouchDB profile and explicit test
passphrase. Chunk retrieval is configured to permit remote reads.
The test also creates an ordinary nested `archive-livesync-v2` directory. It
checks that the default policy keeps its file out of the independently
replicated database, then restarts both daemons with `--include-db-like-dirs`
and verifies its initial bytes, later modifications, and deletion. Local
database inspection occurs only after the corresponding daemon has stopped.
## CLI Fetch with on-demand chunks
Build the CLI, then run `node test/e2e-cli/run-task.ts test:fetch-on-demand-chunks`
+1
View File
@@ -6,6 +6,7 @@
"test:mirror",
"test:daemon",
"test:daemon-startup",
"test:daemon-watch-paths",
"test:push-pull",
"test:decoupled-vault",
"test:sync-two-local",
+3
View File
@@ -20,6 +20,9 @@
"test:daemon-startup": {
"test": ["test-daemon-startup.ts"]
},
"test:daemon-watch-paths": {
"test": ["test-daemon-watch-paths.ts"]
},
"test:decoupled-vault": {
"test": ["test-decoupled-vault.ts"]
},
+616
View File
@@ -0,0 +1,616 @@
import assert from "node:assert/strict";
import { mkdir, readFile, readdir, realpath, stat, symlink, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { test } from "node:test";
import { startCliInBackground, type BackgroundCliProcess } from "./helpers/backgroundCli.ts";
import { runCliOrFail, runCliWithInputOrFail, sanitiseCatStdout } from "./helpers/cli.ts";
import { startCouchdb, stopCouchdb } from "./helpers/docker.ts";
import { applyRemoteSyncSettings, initSettingsFile } from "./helpers/settings.ts";
import { TempDir } from "./helpers/temp.ts";
const INITIAL_REPLICATION_COMPLETE = "[Daemon] Initial replication complete";
const LIVESYNC_ACTIVE = "LiveSync active";
const TEST_PASSPHRASE = "issue-1340-couchdb-test-passphrase";
interface MetadataInfo {
id: string;
revision: string;
}
function envOrDefault(keys: string[], fallback: string): string {
for (const key of keys) {
const value = process.env[key]?.trim();
if (value) return value;
}
return fallback;
}
function waitForTick(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 100));
}
async function setChunkRetrievalSettings(settingsPath: string): Promise<void> {
const settings = JSON.parse(await readFile(settingsPath, "utf8")) as Record<string, unknown>;
settings.readChunksOnline = true;
settings.useOnlyLocalChunk = false;
await writeFile(settingsPath, JSON.stringify(settings, null, 2));
}
async function readMetadataInfo(
databasePath: string,
vaultPath: string,
settingsPath: string,
filePath: string
): Promise<MetadataInfo> {
const infoText = await runCliOrFail(
databasePath,
"--vault",
vaultPath,
"--settings",
settingsPath,
"info",
filePath
);
const info = JSON.parse(infoText) as { id?: unknown; revision?: unknown };
assert.equal(typeof info.id, "string", "Metadata ID is missing for " + filePath + ": " + infoText);
assert.equal(typeof info.revision, "string", "Metadata revision is missing for " + filePath + ": " + infoText);
return {
id: info.id as string,
revision: info.revision as string,
};
}
async function readDatabaseSuffix(settingsPath: string): Promise<string> {
const settings = JSON.parse(await readFile(settingsPath, "utf8")) as Record<string, unknown>;
const suffix = settings.additionalSuffixOfDatabaseName;
assert.equal(typeof suffix, "string", "Fetch did not save a selected local database suffix");
assert.notEqual(suffix, "", "Fetch saved an empty local database suffix");
return suffix as string;
}
async function waitForFileBytes(filePath: string, expected: Uint8Array, timeoutMs = 45_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
let actual: Uint8Array | undefined;
while (Date.now() < deadline) {
try {
actual = await readFile(filePath);
if (Buffer.compare(actual, expected) === 0) return;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
await waitForTick();
}
throw new Error(
"Timed out waiting for exact bytes at " + filePath + "; actual=" + JSON.stringify(actual?.toString())
);
}
async function waitForMissing(filePath: string, timeoutMs = 45_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
await stat(filePath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
throw error;
}
await waitForTick();
}
throw new Error("Timed out waiting for " + filePath + " to be removed from the observer Vault");
}
async function assertMissing(filePath: string, message: string): Promise<void> {
try {
await stat(filePath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
throw error;
}
assert.fail(message);
}
/** Return DB/Vault owner identities so restart checks can detect a second state scope. */
async function readRuntimeScopePaths(databasePath: string): Promise<string[]> {
const scopesRoot = join(databasePath, ".livesync", "runtime", "scopes");
let databaseDirectories;
try {
databaseDirectories = await readdir(scopesRoot, { withFileTypes: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
throw error;
}
const scopes: string[] = [];
for (const databaseDirectory of databaseDirectories) {
if (!databaseDirectory.isDirectory()) continue;
const databaseScopePath = join(scopesRoot, databaseDirectory.name);
const vaultDirectories = await readdir(databaseScopePath, { withFileTypes: true });
for (const vaultDirectory of vaultDirectories) {
if (vaultDirectory.isDirectory()) {
scopes.push(join(databaseDirectory.name, vaultDirectory.name));
}
}
}
return scopes.sort();
}
async function assertDatabaseBytes(
databasePath: string,
vaultPath: string,
settingsPath: string,
filePath: string,
expected: Uint8Array,
message: string,
revision?: string
): Promise<void> {
const command = revision ? ["cat-rev", filePath, revision] : ["cat", filePath];
const output = await runCliOrFail(databasePath, "--vault", vaultPath, "--settings", settingsPath, ...command);
assert.deepEqual(Buffer.from(sanitiseCatStdout(output), "utf8"), expected, message);
}
// Call only after stopping the daemon. Inspect stored Metadata so reflection
// exclusions cannot hide an accidental upload from a Vault-only assertion.
async function readDatabasePaths(databasePath: string, vaultPath: string, settingsPath: string): Promise<Set<string>> {
const listing = await runCliOrFail(databasePath, "--vault", vaultPath, "--settings", settingsPath, "ls");
return new Set(
listing
.split(/\r?\n/)
.map((line) => line.split("\t", 1)[0])
.filter((filePath) => filePath !== undefined && filePath !== "")
);
}
async function assertCouchdbRevisionAncestor(
couchdbUri: string,
couchdbUser: string,
couchdbPassword: string,
dbname: string,
documentId: string,
currentRevision: string,
ancestorRevision: string
): Promise<void> {
const response = await fetch(
couchdbUri + "/" + encodeURIComponent(dbname) + "/" + encodeURIComponent(documentId) + "?revs=true",
{
headers: {
Authorization: "Basic " + Buffer.from(couchdbUser + ":" + couchdbPassword).toString("base64"),
},
}
);
const responseText = await response.text();
assert.equal(response.status, 200, "CouchDB must return the seeded baseline Metadata: " + responseText);
const document = JSON.parse(responseText) as {
_rev?: unknown;
_revisions?: { start?: unknown; ids?: unknown };
};
assert.equal(document._rev, currentRevision, "CouchDB must retain the selected baseline revision");
const ancestorMatch = /^(\d+)-(.+)$/.exec(ancestorRevision);
assert.ok(ancestorMatch, "Original revision must include its generation and hash");
const ancestorGeneration = Number(ancestorMatch[1]);
const history = document._revisions;
assert.ok(history, "CouchDB response must include the revision history");
assert.equal(typeof history.start, "number", "CouchDB revision history must include its starting generation");
assert.ok(Array.isArray(history.ids), "CouchDB revision history must include its revision hashes");
const ancestorIndex = (history.start as number) - ancestorGeneration;
assert.ok(
Number.isSafeInteger(ancestorIndex) && ancestorIndex >= 0 && ancestorIndex < history.ids.length,
"CouchDB revision history must include the original baseline generation"
);
assert.equal(
history.ids[ancestorIndex],
ancestorMatch[2],
"CouchDB revision history must retain the original baseline revision"
);
}
async function stopDaemon(daemon: BackgroundCliProcess | undefined): Promise<void> {
if (!daemon) return;
await daemon.stop().catch(() => {});
}
test(
"daemon: monitors a hidden-parent Vault through a visible symlink and retains its scope after restart",
{ timeout: 240_000 },
async () => {
await using workDir = await TempDir.create("livesync-cli-daemon-watch-paths");
const couchdbUri = envOrDefault(["COUCHDB_URI", "hostname"], "http://127.0.0.1:5989").replace(/\/$/, "");
const couchdbUser = envOrDefault(["COUCHDB_USER", "username"], "admin");
const couchdbPassword = envOrDefault(["COUCHDB_PASSWORD", "password"], "testpassword");
const dbPrefix = envOrDefault(["COUCHDB_DBNAME", "dbname"], "livesync-test-db-ci");
const dbname = (
dbPrefix +
"-daemon-watch-paths-" +
Date.now() +
"-" +
Math.floor(Math.random() * 1_000_000)
).toLowerCase();
const sourceDatabase = workDir.join("source-database");
const targetDatabase = workDir.join("target-database");
const observerDatabase = workDir.join("observer-database");
const sourceVault = workDir.join("source-vault");
const physicalVault = workDir.join(".obsidian", "physical-vault");
const visibleVault = workDir.join("visible-vault");
const observerVault = workDir.join("observer-vault");
const sourceSettings = workDir.join("source-settings.json");
const targetSettings = workDir.join("target-settings.json");
const observerSettings = workDir.join("observer-settings.json");
const baselinePath = "notes/baseline.md";
const watchedPath = "notes/created-after-fetch.md";
const restartedPath = "notes/created-after-restart.md";
const dbLikePath = "notes/archive-livesync-v2/note.md";
const hiddenPath = "notes/.private.md";
const ignoredPath = "notes/ignored.ignored.md";
const initialBytes = Buffer.from("Baseline revision one.\n", "utf8");
const baselineBytes = Buffer.from("Baseline revision two with preserved ancestry.\n", "utf8");
const createdBytes = Buffer.from("Created through the visible symlink.\n", "utf8");
const modifiedBytes = Buffer.from("Modified through the visible symlink.\n", "utf8");
const restartedBytes = Buffer.from("Created after restarting with the physical root.\n", "utf8");
const dbLikeInitialBytes = Buffer.from("Nested DB-like path before option restart.\n", "utf8");
const dbLikeModifiedBytes = Buffer.from("Nested DB-like path modified with the option.\n", "utf8");
const targetBaseline = join(physicalVault, baselinePath);
const observerBaseline = join(observerVault, baselinePath);
const observerWatched = join(observerVault, watchedPath);
const observerRestarted = join(observerVault, restartedPath);
const observerDbLike = join(observerVault, dbLikePath);
const observerHidden = join(observerVault, hiddenPath);
const observerIgnored = join(observerVault, ignoredPath);
await Promise.all([
mkdir(sourceDatabase, { recursive: true }),
mkdir(targetDatabase, { recursive: true }),
mkdir(observerDatabase, { recursive: true }),
mkdir(sourceVault, { recursive: true }),
mkdir(physicalVault, { recursive: true }),
mkdir(observerVault, { recursive: true }),
]);
await mkdir(join(physicalVault, ".livesync"), { recursive: true });
await mkdir(join(physicalVault, "notes"), { recursive: true });
await writeFile(join(physicalVault, ".livesync", "ignore"), "*.ignored.md\n");
await symlink(physicalVault, visibleVault, process.platform === "win32" ? "junction" : "dir");
assert.equal(
await realpath(visibleVault),
await realpath(physicalVault),
"Visible Vault path must resolve to the hidden physical Vault"
);
let targetDaemon: BackgroundCliProcess | undefined;
let observerDaemon: BackgroundCliProcess | undefined;
try {
await startCouchdb(couchdbUri, couchdbUser, couchdbPassword, dbname);
for (const settingsPath of [sourceSettings, targetSettings, observerSettings]) {
await initSettingsFile(settingsPath);
}
const encryptedCouchdbSettings = {
remoteType: "COUCHDB" as const,
couchdbUri,
couchdbUser,
couchdbPassword,
couchdbDbname: dbname,
encrypt: true,
passphrase: TEST_PASSPHRASE,
};
await Promise.all([
applyRemoteSyncSettings(sourceSettings, encryptedCouchdbSettings),
applyRemoteSyncSettings(targetSettings, encryptedCouchdbSettings),
applyRemoteSyncSettings(observerSettings, encryptedCouchdbSettings),
]);
await Promise.all([
setChunkRetrievalSettings(sourceSettings),
setChunkRetrievalSettings(targetSettings),
setChunkRetrievalSettings(observerSettings),
]);
await runCliWithInputOrFail(
initialBytes,
sourceDatabase,
"--vault",
sourceVault,
"--settings",
sourceSettings,
"put",
baselinePath
);
await runCliOrFail(sourceDatabase, "--vault", sourceVault, "--settings", sourceSettings, "sync");
// Check the old body on its retaining source. Fresh peers need the
// current contents and ancestry, without a guarantee of every historical body.
const originalInfo = await readMetadataInfo(sourceDatabase, sourceVault, sourceSettings, baselinePath);
await assertDatabaseBytes(
sourceDatabase,
sourceVault,
sourceSettings,
baselinePath,
initialBytes,
"Source must retain the original baseline revision before its update",
originalInfo.revision
);
await runCliWithInputOrFail(
baselineBytes,
sourceDatabase,
"--vault",
sourceVault,
"--settings",
sourceSettings,
"put",
baselinePath
);
await runCliOrFail(sourceDatabase, "--vault", sourceVault, "--settings", sourceSettings, "sync");
const seedInfo = await readMetadataInfo(sourceDatabase, sourceVault, sourceSettings, baselinePath);
assert.equal(
seedInfo.id,
originalInfo.id,
"Second put must update the original baseline Metadata document"
);
await assertDatabaseBytes(
sourceDatabase,
sourceVault,
sourceSettings,
baselinePath,
baselineBytes,
"Source must retain the current baseline revision bytes",
seedInfo.revision
);
await runCliOrFail(
targetDatabase,
"--vault",
visibleVault,
"--settings",
targetSettings,
"fetch",
"--force",
"--write-settings"
);
assert.deepEqual(await readFile(targetBaseline), baselineBytes, "Fetch must preserve exact baseline bytes");
assert.deepEqual(
await readMetadataInfo(targetDatabase, visibleVault, targetSettings, baselinePath),
seedInfo,
"Fetch must preserve the current baseline document and revision identity"
);
await assertDatabaseBytes(
targetDatabase,
visibleVault,
targetSettings,
baselinePath,
baselineBytes,
"Fetch must preserve the current baseline revision bytes",
seedInfo.revision
);
targetDaemon = startCliInBackground(
targetDatabase,
"--vault",
visibleVault,
"--settings",
targetSettings,
"daemon"
);
await targetDaemon.waitUntilContains(INITIAL_REPLICATION_COMPLETE, 45_000);
await targetDaemon.waitUntilContains(LIVESYNC_ACTIVE, 45_000);
observerDaemon = startCliInBackground(
observerDatabase,
"--vault",
observerVault,
"--settings",
observerSettings,
"daemon"
);
await observerDaemon.waitUntilContains(INITIAL_REPLICATION_COMPLETE, 45_000);
await observerDaemon.waitUntilContains(LIVESYNC_ACTIVE, 45_000);
await waitForFileBytes(observerBaseline, baselineBytes);
const targetNotesViaSymlink = join(visibleVault, "notes");
const targetNotesViaPhysicalRoot = join(physicalVault, "notes");
await writeFile(join(targetNotesViaSymlink, ".private.md"), "Default hidden-entry exclusion.\n");
await writeFile(join(targetNotesViaSymlink, "ignored.ignored.md"), "User ignore rule exclusion.\n");
await mkdir(join(targetNotesViaSymlink, "archive-livesync-v2"), { recursive: true });
await writeFile(join(targetNotesViaSymlink, "archive-livesync-v2", "note.md"), dbLikeInitialBytes);
await writeFile(join(targetNotesViaSymlink, "created-after-fetch.md"), createdBytes);
await waitForFileBytes(observerWatched, createdBytes);
assert.deepEqual(
await readFile(observerWatched),
createdBytes,
"Created note must reach the remote observer exactly"
);
await assertMissing(observerHidden, "A dot-prefixed Vault entry must remain excluded");
await assertMissing(observerIgnored, "An entry matched by .livesync/ignore must remain excluded");
await writeFile(join(targetNotesViaSymlink, "created-after-fetch.md"), modifiedBytes);
await waitForFileBytes(observerWatched, modifiedBytes);
assert.deepEqual(
await readFile(observerWatched),
modifiedBytes,
"Modified note must reach the remote observer exactly"
);
await unlink(join(targetNotesViaSymlink, "created-after-fetch.md"));
await waitForMissing(observerWatched);
assert.equal(await targetDaemon.stop(), 0, targetDaemon.combined);
targetDaemon = undefined;
await stopDaemon(observerDaemon);
observerDaemon = undefined;
// Drain remote changes before inspecting the stopped observer's DB.
// A missing reflected file alone would not establish that upload was excluded.
await runCliOrFail(observerDatabase, "--vault", observerVault, "--settings", observerSettings, "sync");
const defaultObserverPaths = await readDatabasePaths(observerDatabase, observerVault, observerSettings);
assert.equal(
defaultObserverPaths.has(dbLikePath),
false,
"Default policy must not upload a nested DB-like path to the remote database"
);
assert.equal(
defaultObserverPaths.has(hiddenPath),
false,
"Default policy must exclude dot-prefixed entries"
);
assert.equal(
defaultObserverPaths.has(ignoredPath),
false,
"Default policy must exclude entries matched by .livesync/ignore"
);
// Reuse the existing owners while changing both the root spelling and
// the naming-rule opt-in; the next phase must not create a new dataset or scope.
const selectedSuffix = await readDatabaseSuffix(targetSettings);
const scopesBeforeRestart = await readRuntimeScopePaths(targetDatabase);
assert.ok(scopesBeforeRestart.length > 0, "Alias startup must initialise a Vault runtime scope");
targetDaemon = startCliInBackground(
targetDatabase,
"--vault",
physicalVault,
"--settings",
targetSettings,
"--include-db-like-dirs",
"daemon"
);
await targetDaemon.waitUntilContains(INITIAL_REPLICATION_COMPLETE, 45_000);
await targetDaemon.waitUntilContains(LIVESYNC_ACTIVE, 45_000);
assert.equal(
await readDatabaseSuffix(targetSettings),
selectedSuffix,
"Physical-root restart must retain the selected database suffix"
);
observerDaemon = startCliInBackground(
observerDatabase,
"--vault",
observerVault,
"--settings",
observerSettings,
"--include-db-like-dirs",
"daemon"
);
await observerDaemon.waitUntilContains(INITIAL_REPLICATION_COMPLETE, 45_000);
await observerDaemon.waitUntilContains(LIVESYNC_ACTIVE, 45_000);
await waitForFileBytes(observerDbLike, dbLikeInitialBytes);
assert.deepEqual(
await readFile(observerDbLike),
dbLikeInitialBytes,
"Include option must replicate exact bytes for the previously excluded nested DB-like path"
);
await writeFile(join(targetNotesViaPhysicalRoot, "archive-livesync-v2", "note.md"), dbLikeModifiedBytes);
await waitForFileBytes(observerDbLike, dbLikeModifiedBytes);
assert.deepEqual(
await readFile(observerDbLike),
dbLikeModifiedBytes,
"Include option must monitor and replicate modifications under a nested DB-like directory"
);
await unlink(join(targetNotesViaPhysicalRoot, "archive-livesync-v2", "note.md"));
await waitForMissing(observerDbLike);
await writeFile(join(targetNotesViaPhysicalRoot, "created-after-restart.md"), restartedBytes);
await waitForFileBytes(observerRestarted, restartedBytes);
assert.deepEqual(
await readFile(observerRestarted),
restartedBytes,
"Physical-root restart must continue monitoring and replicate exact bytes"
);
assert.deepEqual(
await readRuntimeScopePaths(targetDatabase),
scopesBeforeRestart,
"Physical-root restart must reuse the existing Vault runtime scope"
);
assert.equal(await targetDaemon.stop(), 0, targetDaemon.combined);
targetDaemon = undefined;
await stopDaemon(observerDaemon);
observerDaemon = undefined;
await runCliOrFail(observerDatabase, "--vault", observerVault, "--settings", observerSettings, "sync");
for (const [databasePath, vaultPath, settingsPath] of [
[targetDatabase, visibleVault, targetSettings],
[observerDatabase, observerVault, observerSettings],
]) {
assert.deepEqual(
await readMetadataInfo(databasePath, vaultPath, settingsPath, baselinePath),
seedInfo,
"Live monitoring must preserve the current baseline document and revision identity"
);
await assertDatabaseBytes(
databasePath,
vaultPath,
settingsPath,
baselinePath,
baselineBytes,
"Live monitoring must preserve the current baseline revision bytes",
seedInfo.revision
);
}
assert.deepEqual(
await readMetadataInfo(sourceDatabase, sourceVault, sourceSettings, baselinePath),
seedInfo,
"Source must retain the selected baseline document and revision"
);
await assertDatabaseBytes(
sourceDatabase,
sourceVault,
sourceSettings,
baselinePath,
baselineBytes,
"Source must retain the selected baseline revision bytes",
seedInfo.revision
);
await assertDatabaseBytes(
sourceDatabase,
sourceVault,
sourceSettings,
baselinePath,
initialBytes,
"Source must retain the original baseline revision contents",
originalInfo.revision
);
await assertCouchdbRevisionAncestor(
couchdbUri,
couchdbUser,
couchdbPassword,
dbname,
seedInfo.id,
seedInfo.revision,
originalInfo.revision
);
await assertDatabaseBytes(
observerDatabase,
observerVault,
observerSettings,
restartedPath,
restartedBytes,
"Restarted daemon content must exist in the independently replicated database"
);
const observerPaths = await readDatabasePaths(observerDatabase, observerVault, observerSettings);
assert.equal(
observerPaths.has(watchedPath),
false,
"Deleted note must be absent from the independently replicated database"
);
assert.equal(
observerPaths.has(dbLikePath),
false,
"Deleted DB-like path must be absent from the independently replicated database"
);
assert.equal(
observerPaths.has(hiddenPath),
false,
"Dot-prefixed entry must be absent from the independently replicated database"
);
assert.equal(
observerPaths.has(ignoredPath),
false,
"Entry matched by .livesync/ignore must be absent from the independently replicated database"
);
} finally {
await stopDaemon(observerDaemon);
await stopDaemon(targetDaemon);
await stopCouchdb().catch(() => {});
}
}
);