test: cover on-demand Fetch recovery in CLI and Obsidian

This commit is contained in:
vorotamoroz committed 2026-11-08 16:46:27 +00:00
1 parent c2e4e9198b
commit a43882d743
11 files changed
+592

No files matched your search

+15
View File
@@ -77,6 +77,21 @@ 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 Fetch with on-demand chunks
Build the CLI, then run `node test/e2e-cli/run-task.ts test:fetch-on-demand-chunks`
with Node.js 26 and Docker available. The standard CLI CI suite includes this task.
It uses the existing disposable CouchDB test container and the CLI fixture
connection variables, with local test defaults when they are absent.
The scenario fetches into an empty Vault, repeats Fetch with the same files, and
checks exact bytes, Metadata revisions, and removal of the reconstruction record.
It then prohibits direct Chunk requests, verifies that unavailable content keeps
Fetch failed and prevents daemon startup, and retries with requests permitted.
The final daemon startup must preserve the original bytes and revision. Commonlib
unit tests separately verify that the prohibition sends no direct request and
that equal-time, equal-size edits retain their recorded revision ancestry.
## CLI Fetch database continuity
The standard CLI CI suite and direct task runner execute
+1
View File
@@ -16,6 +16,7 @@
"test:e2e-matrix:minio-enc0",
"test:e2e-matrix:minio-enc1",
"test:fetch-database-continuity",
"test:fetch-on-demand-chunks",
"test:journal-chunk-recovery",
"test:journal-receive-failure"
]
+3
View File
@@ -32,6 +32,9 @@
"test:fetch-database-continuity": {
"test": ["test-fetch-database-continuity.ts"]
},
"test:fetch-on-demand-chunks": {
"test": ["test-fetch-on-demand-chunks.ts"]
},
"test:remote-administration-exit-codes": {
"test": ["test-remote-administration-exit-codes.ts"]
},
+236
View File
@@ -0,0 +1,236 @@
import assert from "node:assert/strict";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { test } from "node:test";
import { startCliInBackground, type BackgroundCliProcess } from "./helpers/backgroundCli.ts";
import { runCli, runCliOrFail } from "./helpers/cli.ts";
import { startCouchdb, stopCouchdb } from "./helpers/docker.ts";
import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts";
import { TempDir } from "./helpers/temp.ts";
function envOrDefault(keys: string[], fallback: string): string {
for (const key of keys) {
const value = process.env[key]?.trim();
if (value) return value;
}
return fallback;
}
async function setChunkRetrievalSettings(settingsPath: string, useOnlyLocalChunk: boolean): Promise<void> {
const settings = JSON.parse(await readFile(settingsPath, "utf8")) as Record<string, unknown>;
settings.readChunksOnline = true;
settings.useOnlyLocalChunk = useOnlyLocalChunk;
await writeFile(settingsPath, JSON.stringify(settings, null, 2));
}
async function assertMarkerAbsent(markerPath: string, message: string): Promise<void> {
let exists = true;
try {
await stat(markerPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") exists = false;
else throw error;
}
assert.equal(exists, false, message);
}
async function readMetadataRevision(
databasePath: string,
vaultPath: string,
settingsPath: string,
filePath: string
): Promise<string> {
const infoText = await runCliOrFail(
databasePath,
"--vault",
vaultPath,
"--settings",
settingsPath,
"info",
filePath
);
const info = JSON.parse(infoText) as { revision?: unknown };
assert.equal(typeof info.revision, "string", "Metadata revision is missing for " + filePath + ": " + infoText);
return info.revision as string;
}
async function stopDaemon(daemon: BackgroundCliProcess | undefined): Promise<void> {
if (!daemon) return;
await daemon.stop().catch(() => {});
}
test(
"fetch: on-demand chunks preserve existing Vault Metadata and recover after an interrupted fetch",
{ timeout: 180_000 },
async () => {
await using workDir = await TempDir.create("livesync-cli-fetch-on-demand-chunks");
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 + "-fetch-on-demand-chunks-" + Date.now() + "-" + Math.floor(Math.random() * 1_000_000))
.toLowerCase();
const sourceDatabase = workDir.join("source-database");
const receiverDatabase = workDir.join("receiver-database");
const sourceVault = workDir.join("source-vault");
const receiverVault = workDir.join("receiver-vault");
const sourceSettings = workDir.join("source-settings.json");
const receiverSettings = workDir.join("receiver-settings.json");
const notePath = "notes/on-demand-chunks.md";
const sourceNote = join(sourceVault, notePath);
const receiverNote = join(receiverVault, notePath);
const expectedBytes = Buffer.from("Repeated fetch must preserve this ASCII note.\n", "utf8");
const sourceMarker = join(sourceDatabase, ".livesync", "reconstruction.json");
const receiverMarker = join(receiverDatabase, ".livesync", "reconstruction.json");
await Promise.all([
mkdir(sourceDatabase, { recursive: true }),
mkdir(receiverDatabase, { recursive: true }),
mkdir(sourceVault, { recursive: true }),
mkdir(receiverVault, { recursive: true }),
mkdir(join(sourceVault, "notes"), { recursive: true }),
]);
await writeFile(sourceNote, expectedBytes);
let daemon: BackgroundCliProcess | undefined;
try {
await startCouchdb(couchdbUri, couchdbUser, couchdbPassword, dbname);
await Promise.all([initSettingsFile(sourceSettings), initSettingsFile(receiverSettings)]);
await Promise.all([
applyCouchdbSettings(sourceSettings, couchdbUri, couchdbUser, couchdbPassword, dbname, true),
applyCouchdbSettings(receiverSettings, couchdbUri, couchdbUser, couchdbPassword, dbname, true),
]);
await Promise.all([
setChunkRetrievalSettings(sourceSettings, false),
setChunkRetrievalSettings(receiverSettings, false),
]);
await runCliOrFail(
sourceDatabase,
"--vault",
sourceVault,
"--settings",
sourceSettings,
"rebuild",
"--force",
"--write-settings"
);
await assertMarkerAbsent(sourceMarker, "Source rebuild should clear reconstruction.json");
assert.deepEqual(await readFile(sourceNote), expectedBytes, "Source rebuild must preserve the Vault bytes");
await runCliOrFail(
receiverDatabase,
"--vault",
receiverVault,
"--settings",
receiverSettings,
"fetch",
"--force",
"--write-settings"
);
assert.deepEqual(await readFile(receiverNote), expectedBytes, "Initial fetch must reproduce the note bytes");
await assertMarkerAbsent(receiverMarker, "Initial fetch should clear reconstruction.json");
const originalRevision = await readMetadataRevision(receiverDatabase, receiverVault, receiverSettings, notePath);
await runCliOrFail(
receiverDatabase,
"--vault",
receiverVault,
"--settings",
receiverSettings,
"fetch",
"--force",
"--write-settings"
);
assert.deepEqual(await readFile(receiverNote), expectedBytes, "Repeated fetch must preserve the Vault bytes");
assert.equal(
await readMetadataRevision(receiverDatabase, receiverVault, receiverSettings, notePath),
originalRevision,
"Repeated fetch must retain the original Metadata revision"
);
await assertMarkerAbsent(receiverMarker, "Repeated fetch should clear reconstruction.json");
await setChunkRetrievalSettings(receiverSettings, true);
const omittedChunkFetch = await runCli(
"-v",
receiverDatabase,
"--vault",
receiverVault,
"--settings",
receiverSettings,
"fetch",
"--force",
"--write-settings"
);
assert.notEqual(omittedChunkFetch.code, 0, "Fetch must fail when local-only chunk reads omit the remote chunk");
assert.match(omittedChunkFetch.combined, /useOnlyLocalChunk=true/);
assert.match(omittedChunkFetch.combined, /Could not reconcile/);
assert.deepEqual(
await readFile(receiverNote),
expectedBytes,
"A failed fetch must preserve the receiver Vault bytes"
);
const failedMarker = JSON.parse(await readFile(receiverMarker, "utf8")) as {
command?: unknown;
phase?: unknown;
};
assert.equal(failedMarker.command, "fetch", "Failed fetch should retain its recovery marker");
assert.equal(failedMarker.phase, "running", "Failed fetch marker should remain in the running phase");
const blockedDaemon = await runCli(
receiverDatabase,
"--vault",
receiverVault,
"--settings",
receiverSettings,
"daemon"
);
assert.notEqual(blockedDaemon.code, 0, "Daemon must refuse to start while fetch recovery is required");
assert.match(blockedDaemon.combined, /interrupted fetch requires recovery/i);
await setChunkRetrievalSettings(receiverSettings, false);
await runCliOrFail(
receiverDatabase,
"--vault",
receiverVault,
"--settings",
receiverSettings,
"fetch",
"--force",
"--write-settings"
);
await assertMarkerAbsent(receiverMarker, "Successful recovery fetch should clear reconstruction.json");
assert.deepEqual(await readFile(receiverNote), expectedBytes, "Recovery fetch must preserve the Vault bytes");
assert.equal(
await readMetadataRevision(receiverDatabase, receiverVault, receiverSettings, notePath),
originalRevision,
"Recovery fetch must retain the original Metadata revision"
);
daemon = startCliInBackground(
receiverDatabase,
"--vault",
receiverVault,
"--settings",
receiverSettings,
"daemon"
);
await daemon.waitUntilContains("[Daemon] Initial replication complete", 45_000);
assert.equal(await daemon.stop(), 0, daemon.combined);
daemon = undefined;
assert.deepEqual(await readFile(receiverNote), expectedBytes, "Daemon startup must preserve the Vault bytes");
assert.equal(
await readMetadataRevision(receiverDatabase, receiverVault, receiverSettings, notePath),
originalRevision,
"Daemon startup must not create an unnecessary Metadata revision"
);
await assertMarkerAbsent(receiverMarker, "Recovered operation should leave reconstruction.json absent");
} finally {
await stopDaemon(daemon);
await stopCouchdb().catch(() => {});
}
}
);
+13
View File
@@ -85,6 +85,7 @@ npm run test:e2e:obsidian:focused -- stale-file-restart
npm run test:e2e:obsidian:focused -- folder-batch
npm run test:e2e:obsidian:focused -- security-seed-reconnect
npm run test:e2e:obsidian:focused -- couchdb-http-cache
npm run test:e2e:obsidian:focused -- fetch-on-demand-chunks
npm run test:e2e:obsidian:focused -- replication-status
npm run test:e2e:obsidian:focused -- sync-guard
```
@@ -396,6 +397,18 @@ Set `E2E_OBSIDIAN_ARTIFACT_ROOT` to a directory containing the exact release `ma
When these checks pass for the release artefacts, BRAT acceptance can be limited to installation or update, enabling the plug-in, and a cold start. Check any other changes included in that release using their own regression scenarios. The diagnostic export checks verify report payloads; they do not exercise the operating system's clipboard or download implementation on iOS or Android.
### Fetch with on-demand Chunks
`fetch-on-demand-chunks` checks Standard Fetch finalisation for an existing Vault
when native Request API selection makes Fast Setup fall back from Streaming
replication. It uses encrypted CouchDB, obfuscated paths, internal Metadata
encryption, Markdown and attachment files, and on-demand Chunk retrieval. The
scenario requires local Chunks to be absent before the strict scan, then checks
remote delivery, exact file bytes, unchanged Metadata revisions, provenance,
readiness, and released suspension after finalisation. It covers the shared
failure path reported in #1329 and #1332; a Linux Obsidian run does not establish
verification on the reporter's iPadOS/WebKit device.
### Environment variables
- `OBSIDIAN_BINARY`: explicit Obsidian executable path.
@@ -0,0 +1,313 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import {
assertCouchDbReachable,
createCouchDbDatabase,
deleteCouchDbDatabase,
loadCouchDbConfig,
makeUniqueDatabaseName,
} from "../runner/couchdb.ts";
import { requireObsidianBinary, requireObsidianCli } from "../runner/environment.ts";
import {
createE2eCouchDbPluginData,
createE2eObsidianDeviceLocalState,
prepareRemote,
pushLocalChanges,
waitForLiveSyncCoreReady,
} from "../runner/liveSyncWorkflow.ts";
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
import { captureObsidianPage, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts";
import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts";
type FileMetadata = {
path: string;
id: string;
revision: string;
children: string[];
};
type InitialState = {
appVersion: string;
files: FileMetadata[];
};
type FetchOutcome = {
scanResult: boolean;
finalised: boolean;
chunksPresentBeforeScan: number;
chunksPresentAfterScan: number;
totalChunkIds: number;
remoteChunkRequests: number[];
appReady: boolean;
appSuspended: boolean;
suspendFileWatching: boolean;
files: Array<{
path: string;
id: string;
revision: string;
conflicts: string[];
provenanceRevision: string | null;
}>;
};
const paths = [
"E2E/plain.md",
"E2E/drawing.excalidraw.md",
"E2E/attachment.pdf",
"E2E/image.png",
];
const contents = [
Buffer.from("# Existing note\n\n日本語 and English.\n".repeat(200)),
Buffer.from("# Excalidraw fixture\n\nExisting drawing data.\n".repeat(200)),
Buffer.from("%PDF-1.4\n" + "Synthetic attachment content.\n".repeat(2048)),
Buffer.from(Array.from({ length: 65536 }, (_, index) => (index * 13 + 17) % 256)),
];
const digest = (bytes: ArrayLike<number>) => createHash("sha256").update(Uint8Array.from(bytes)).digest("hex");
async function main(): Promise<void> {
const binary = requireObsidianBinary();
const cliBinary = requireObsidianCli();
const couchDb = await loadCouchDbConfig();
await assertCouchDbReachable(couchDb);
const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "fetch-on-demand-chunks");
const connection = { ...couchDb, dbName };
let databaseCreated = false;
let vault: TemporaryVault | undefined;
let session: ObsidianLiveSyncSession | undefined;
try {
await createCouchDbDatabase(couchDb, dbName);
databaseCreated = true;
vault = await createTemporaryVault("obsidian-livesync-fetch-on-demand-");
const vaultPath = vault.path;
const remoteDebuggingPort = obsidianRemoteDebuggingPort(process.env);
const sessionEnv = {
...process.env,
E2E_OBSIDIAN_REMOTE_DEBUGGING_PORT: String(remoteDebuggingPort),
};
session = await startObsidianLiveSyncSession({
binary,
cliBinary,
vault,
artifactRoot: process.env.E2E_OBSIDIAN_ARTIFACT_ROOT ?? process.cwd(),
pluginData: createE2eCouchDbPluginData(connection, {
encrypt: true,
E2EEAlgorithm: "v2",
passphrase: "synthetic-fetch-on-demand-fixture",
usePathObfuscation: true,
encryptInternalMetadata: true,
doctorProcessedVersion: "1.0.33",
idDerivationVersion: 0,
pathNormalizationVersion: 0,
useRequestAPI: true,
useOnlyLocalChunk: false,
readChunksOnline: true,
useIndexedDBAdapter: false,
concurrencyOfReadChunksOnline: 40,
minimumIntervalOfReadChunksOnline: 50,
showVerboseLog: true,
lessInformationInLog: false,
doNotSuspendOnFetching: false,
}),
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
env: sessionEnv,
});
await waitForLiveSyncCoreReady(cliBinary, session.cliEnv);
await prepareRemote(cliBinary, session.cliEnv);
const evaluate = async <T>(expression: string): Promise<T> => {
if (!session) throw new Error("No active Obsidian session.");
let result: string | undefined;
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
result = await page.evaluate<string>(expression);
});
if (result === undefined) throw new Error("The renderer returned no result.");
return JSON.parse(result) as T;
};
const seededSettings = await evaluate<Record<string, unknown>>(
[
"(async()=>{",
"const core=app.plugins.plugins['obsidian-livesync'].core;",
`const paths=${JSON.stringify(paths)};`,
`const contents=${JSON.stringify(contents.map((bytes) => bytes.toString("base64")))};`,
"await app.vault.createFolder('E2E');",
"for(let index=0;index<paths.length;index++){",
" const bytes=Uint8Array.from(atob(contents[index]),character=>character.charCodeAt(0));",
" if(paths[index].endsWith('.md')) await app.vault.create(paths[index],new TextDecoder().decode(bytes));",
" else await app.vault.createBinary(paths[index],bytes.buffer);",
"}",
"await core.services.fileProcessing.commitPendingFileEvents();",
"for(const path of paths){",
" const file=app.vault.getAbstractFileByPath(path);",
" if(!file) throw new Error('Fixture file is missing: '+path);",
" if(!(await core.fileHandler.storeFileToDB(file))) throw new Error('Could not store fixture '+path);",
"}",
"const settings=core.services.setting.currentSettings();",
"return JSON.stringify({",
" encrypt:settings.encrypt,E2EEAlgorithm:settings.E2EEAlgorithm,",
" usePathObfuscation:settings.usePathObfuscation,encryptInternalMetadata:settings.encryptInternalMetadata,",
" idDerivationVersion:settings.idDerivationVersion,pathNormalizationVersion:settings.pathNormalizationVersion,",
" readChunksOnline:settings.readChunksOnline,useOnlyLocalChunk:settings.useOnlyLocalChunk,",
" useRequestAPI:settings.useRequestAPI,useIndexedDBAdapter:settings.useIndexedDBAdapter",
"});",
"})()",
].join("")
);
assert.deepEqual(seededSettings, {
encrypt: true,
E2EEAlgorithm: "v2",
usePathObfuscation: true,
encryptInternalMetadata: true,
idDerivationVersion: 0,
pathNormalizationVersion: 0,
readChunksOnline: true,
useOnlyLocalChunk: false,
useRequestAPI: true,
useIndexedDBAdapter: false,
});
await pushLocalChanges(cliBinary, session.cliEnv);
const before = await evaluate<InitialState>(
[
"(async()=>{",
"const core=app.plugins.plugins['obsidian-livesync'].core;",
`const paths=${JSON.stringify(paths)};`,
"return JSON.stringify({appVersion:core.services.API.getAppVersion(),files:",
"await Promise.all(paths.map(async path=>{",
" const meta=await core.localDatabase.getDBEntryMeta(path,undefined,true);",
" if(!meta) throw new Error('Fixture Metadata is missing: '+path);",
" return {path,id:meta._id,revision:meta._rev,children:meta.children};",
"}))});",
"})()",
].join("")
);
assert.equal(before.files.length, paths.length, "The remote fixture must contain all four files.");
assert.ok(before.files.every((file) => file.children.length > 0), "Every fixture file must have Chunks.");
const diskBefore = await Promise.all(
paths.map(async (path) => ({ path, hash: digest(await readFile(join(vaultPath, path))) }))
);
assert.deepEqual(
diskBefore,
paths.map((path, index) => ({ path, hash: digest(contents[index]) })),
"The seeded Vault bytes must match the fixture."
);
const outcome = await evaluate<FetchOutcome>(
[
"(async()=>{",
"const core=app.plugins.plugins['obsidian-livesync'].core;",
`const paths=${JSON.stringify(paths)};`,
"await core.rebuilder.$fetchLocalDBFast(false);",
"const db=core.localDatabase.localDatabase;",
"const metadata=await Promise.all(paths.map(path=>core.localDatabase.getDBEntryMeta(path,undefined,true)));",
"const ids=[...new Set(metadata.flatMap(meta=>meta.children))];",
"const initialChunks=await db.allDocs({keys:ids,include_docs:true});",
"const chunksPresentBeforeScan=initialChunks.rows.filter(row=>row.doc&&!row.value?.deleted).length;",
"const replicator=core.services.replicator.getActiveReplicator();",
"if(!replicator||typeof replicator.fetchRemoteChunks!=='function') throw new Error('Remote Chunk fetch is unavailable');",
"const originalFetchRemoteChunks=replicator.fetchRemoteChunks;",
"const ownDescriptor=Object.getOwnPropertyDescriptor(replicator,'fetchRemoteChunks');",
"const remoteChunkRequests=[];",
"replicator.fetchRemoteChunks=async function(...args){",
" remoteChunkRequests.push(args[0]?.length??0);",
" return await originalFetchRemoteChunks.apply(this,args);",
"};",
"let scanResult;let finalised;",
"try{",
" scanResult=await core.services.vault.scanVault(true,true,false,{strictContentCheck:true});",
" finalised=await core.rebuilder.finishRebuild();",
"}finally{",
" if(ownDescriptor) Object.defineProperty(replicator,'fetchRemoteChunks',ownDescriptor);",
" else delete replicator.fetchRemoteChunks;",
"}",
"const afterMetadata=await Promise.all(paths.map(path=>core.localDatabase.getDBEntryMeta(path,undefined,true)));",
"const afterChunks=await db.allDocs({keys:ids,include_docs:true});",
"const provenance=core.services.keyValueDB.openSimpleStore('file-reflection-provenance-v1');",
"const files=await Promise.all(afterMetadata.map(async(meta,index)=>({",
" path:paths[index],id:meta._id,revision:meta._rev,",
" conflicts:(await db.get(meta._id,{conflicts:true}))._conflicts??[],",
" provenanceRevision:(await provenance.get(paths[index]))?.revision??null",
"})));",
"return JSON.stringify({scanResult,finalised,chunksPresentBeforeScan,",
" chunksPresentAfterScan:afterChunks.rows.filter(row=>row.doc&&!row.value?.deleted).length,",
" totalChunkIds:ids.length,remoteChunkRequests,",
" appReady:core.services.appLifecycle.isReady(),",
" appSuspended:core.services.appLifecycle.isSuspended(),",
" suspendFileWatching:core.services.setting.currentSettings().suspendFileWatching,files});",
"})()",
].join("")
);
const diskAfter = await Promise.all(
paths.map(async (path) => ({ path, hash: digest(await readFile(join(vaultPath, path))) }))
);
console.log(
JSON.stringify(
{
scenario: "fetch-on-demand-chunks",
appVersion: before.appVersion,
files: before.files.map(({ path, revision, children }) => ({ path, revision, chunkCount: children.length })),
scanResult: outcome.scanResult,
finalised: outcome.finalised,
chunksPresentBeforeScan: outcome.chunksPresentBeforeScan,
chunksPresentAfterScan: outcome.chunksPresentAfterScan,
remoteChunkRequestCount: outcome.remoteChunkRequests.length,
appReady: outcome.appReady,
appSuspended: outcome.appSuspended,
suspendFileWatching: outcome.suspendFileWatching,
},
null,
2
)
);
assert.equal(outcome.chunksPresentBeforeScan, 0, "The fetch must omit Chunks until strict scanning.");
assert.ok(outcome.totalChunkIds > 0, "The Metadata must reference Chunks.");
assert.equal(outcome.chunksPresentAfterScan, outcome.totalChunkIds, "Strict scanning must retrieve every Chunk.");
assert.equal(outcome.scanResult, true, "The strict Vault scan must succeed.");
assert.equal(outcome.finalised, true, "The Standard Fetch rebuild must finalise.");
assert.ok(outcome.remoteChunkRequests.some((count) => count > 0), "Strict scanning must request remote Chunks.");
assert.equal(outcome.appReady, true, "The application must become ready after Fetch.");
assert.equal(outcome.appSuspended, false, "The application must not remain suspended after Fetch.");
assert.equal(outcome.suspendFileWatching, false, "File watching must resume after Fetch.");
assert.deepEqual(diskAfter, diskBefore, "Fetch must preserve the exact physical Vault bytes.");
assert.equal(outcome.files.length, before.files.length);
for (let index = 0; index < before.files.length; index++) {
assert.equal(outcome.files[index].id, before.files[index].id, "Fetch must preserve the Metadata ID.");
assert.equal(
outcome.files[index].revision,
before.files[index].revision,
"Fetch must preserve the exact Metadata revision."
);
assert.deepEqual(outcome.files[index].conflicts, [], "Fetch must not create Metadata conflicts.");
assert.equal(
outcome.files[index].provenanceRevision,
before.files[index].revision,
"File reflection provenance must identify the exact Metadata revision."
);
}
} catch (error) {
if (session) {
await captureObsidianPage(session.remoteDebuggingPort, "fetch-on-demand-chunks-failure.png", async () => {}).catch(
(captureError) => console.error("Could not capture the failed Obsidian session:", captureError)
);
}
throw error;
} finally {
try {
if (session) await session.app.stop();
} finally {
try {
if (vault) await vault.dispose();
} finally {
if (databaseCreated) await deleteCouchDbDatabase(couchDb, dbName);
}
}
}
}
await main();
+1
View File
@@ -28,6 +28,7 @@ const testSteps: Step[] = [
{ name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] },
{ name: "joined devices with CouchDB", args: ["run", "test:e2e:obsidian:joined-devices"] },
{ name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] },
{ name: "on-demand Chunk fetch", args: ["run", "test:e2e:obsidian:fetch-on-demand-chunks"] },
{
name: "manual CouchDB setup workflow",
args: ["run", "test:e2e:obsidian:couchdb-manual-setup-workflow"],
+1
View File
@@ -26,6 +26,7 @@ const focusedScenarios = new Set([
"third-party-chunks",
"couchdb-upload",
"couchdb-http-cache",
"fetch-on-demand-chunks",
"chunk-fetch-retry",
"couchdb-manual-setup-workflow",
"cli-to-obsidian-sync",