mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-22 09:03:21 +00:00
Document and test NFC reconstruction with logical deletions
This commit is contained in:
1 parent
6412dd6d6b
commit
612e3eb9d5
6 files changed
+81
-10
No files matched your search
@@ -172,7 +172,7 @@ Set `E2E_OBSIDIAN_ONLY_DIFFERENT_CHUNK_ID_KEYS=true` to run the focused case whe
|
||||
|
||||
Set `E2E_OBSIDIAN_UNICODE_PATHS=true` to exercise path identity version 1 on Linux. This variant uses an NFD directory and a mixed NFC/NFD file name, checks reflection at Obsidian's NFC path, and returns an update and deletion to the original CLI physical path. Updates use `sync` and `mirror`; deletions use the polling daemon, because `mirror` retains tombstoned local files by default. It uses unencrypted Metadata by default to match issue #1259; add `E2E_OBSIDIAN_UNICODE_ENCRYPTION=true` for the encrypted variant. These checks do not establish native macOS or iOS filesystem behaviour.
|
||||
|
||||
Set `E2E_OBSIDIAN_UNICODE_MIGRATION=true` to begin with two CLI databases using legacy path identity. The source runs `rebuild --force --write-settings` after selecting version 1, and the second client runs `fetch --force --write-settings`. The scenario checks that the old document ID is replaced, the NFC feature is declared, and both physical NFD filenames are preserved before continuing the real Obsidian update and deletion checks.
|
||||
Set `E2E_OBSIDIAN_UNICODE_MIGRATION=true` to begin with two CLI databases using legacy path identity and an obsolete, logically deleted NFC-equivalent Metadata document. The source runs `rebuild --force --write-settings` after selecting version 1, and the second client runs `fetch --force --write-settings`. The scenario checks that the old document ID is replaced, the NFC feature is declared, and both physical NFD filenames are preserved. In real Obsidian it also seeds an unconflicted logical deletion under the obsolete ID, runs Streaming Fast Fetch, and verifies replacement of that local Metadata with unchanged file content before continuing the update and deletion checks. The CouchDB fixture must allow browser CORS requests; this case rejects the native Request API fallback.
|
||||
|
||||
The isolated Obsidian session starts with its CouchDB settings and device-local compatibility acknowledgement already in place. This keeps the scenario focused on cross-runtime data compatibility; unconfigured start-up and visible CouchDB onboarding are covered by their dedicated workflows.
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ type CliFileInfo = {
|
||||
id: string;
|
||||
children: string[];
|
||||
expectedCtime?: number;
|
||||
legacyId?: string;
|
||||
};
|
||||
|
||||
function parseCommandLine(value: string): string[] {
|
||||
@@ -130,14 +131,15 @@ function resolveLiveSyncCliCommand(): LiveSyncCliCommand {
|
||||
return { executable: process.execPath, prefixArgs: [liveSyncCli] };
|
||||
}
|
||||
|
||||
async function runLiveSyncCli(command: LiveSyncCliCommand, args: string[]): Promise<CliResult> {
|
||||
async function runLiveSyncCli(command: LiveSyncCliCommand, args: string[], input?: string): Promise<CliResult> {
|
||||
return await new Promise((resolvePromise, reject) => {
|
||||
const timeoutMs = Number(process.env.E2E_LIVESYNC_CLI_TIMEOUT_MS ?? 60000);
|
||||
const child = spawn(command.executable, [...command.prefixArgs, ...args], {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
child.stdin.end(input);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk: Buffer) => {
|
||||
@@ -219,6 +221,7 @@ async function configureLiveSyncCli(
|
||||
passphrase: e2eePassphrase,
|
||||
usePathObfuscation: useEncryption,
|
||||
encryptInternalMetadata: useEncryption,
|
||||
deleteMetadataOfDeletedFiles: false,
|
||||
pathNormalizationVersion,
|
||||
doctorProcessedVersion: "0.25.27",
|
||||
isConfigured: true,
|
||||
@@ -339,6 +342,27 @@ async function migrateCliDatabases(
|
||||
dbName: string,
|
||||
legacyFileInfo: CliFileInfo
|
||||
): Promise<CliFileInfo> {
|
||||
await runLiveSyncCli(
|
||||
command,
|
||||
[databasePath, "--settings", settingsPath, "put", receivedNotePath],
|
||||
"Obsolete composed-path content.\n"
|
||||
);
|
||||
const aliasInfo = await runLiveSyncCli(command, [
|
||||
databasePath,
|
||||
"--settings",
|
||||
settingsPath,
|
||||
"info",
|
||||
receivedNotePath,
|
||||
]);
|
||||
const deletedAliasId = (JSON.parse(aliasInfo.stdout) as CliFileInfo).id;
|
||||
if (deletedAliasId === legacyFileInfo.id)
|
||||
throw new Error("The legacy Unicode fixture needs distinct Metadata IDs.");
|
||||
await runLiveSyncCli(command, [databasePath, "--settings", settingsPath, "rm", receivedNotePath]);
|
||||
await runLiveSyncCli(command, [databasePath, "--settings", settingsPath, "sync"]);
|
||||
await waitForCouchDbDocs(couchDb, dbName, (docs) =>
|
||||
docs.some((doc) => doc._id === deletedAliasId && doc.deleted === true)
|
||||
);
|
||||
|
||||
const secondaryDatabasePath = join(cliState, "secondary-database");
|
||||
const secondarySettingsPath = join(cliState, "secondary-settings.json");
|
||||
await mkdir(secondaryDatabasePath, { recursive: true });
|
||||
@@ -406,8 +430,8 @@ async function migrateCliDatabases(
|
||||
}
|
||||
);
|
||||
}
|
||||
console.log("CLI Rebuild and Fetch migrated legacy document IDs while preserving both NFD physical files.");
|
||||
return { ...fileInfo, expectedCtime: legacyFileInfo.expectedCtime };
|
||||
console.log("CLI Rebuild and Fetch passed with obsolete logical deletions and preserved both NFD physical files.");
|
||||
return { ...fileInfo, expectedCtime: legacyFileInfo.expectedCtime, legacyId: legacyFileInfo.id };
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -509,6 +533,31 @@ async function main(): Promise<void> {
|
||||
|
||||
const received = await waitForVaultContent(vault.path, receivedNotePath);
|
||||
assertEqual(received, editedNoteContent, "The real Obsidian plug-in did not materialise the CLI-edited note.");
|
||||
if (cliFileInfo.legacyId) {
|
||||
const fetched = await evalObsidianJson<{ obsoleteRetained: boolean; content: string }>(
|
||||
obsidianCli.binary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"if(core.settings.useRequestAPI) throw new Error('This case requires Streaming Fast Fetch.');",
|
||||
"await core.services.setting.applyPartial({suspendParseReplicationResult:true,suspendFileWatching:true});",
|
||||
`const legacyId=${JSON.stringify(cliFileInfo.legacyId)};`,
|
||||
`const {_rev,...current}=await core.localDatabase.localDatabase.get(${JSON.stringify(cliFileInfo.id)});`,
|
||||
`await core.localDatabase.localDatabase.put({...current,_id:legacyId,path:${JSON.stringify(notePath)},deleted:true});`,
|
||||
"await core.serviceModules.rebuilder.$fetchLocalDBFast(true);",
|
||||
"const rows=await core.localDatabase.localDatabase.allDocs();",
|
||||
"return JSON.stringify({",
|
||||
"obsoleteRetained:rows.rows.some(row=>row.id===legacyId),",
|
||||
`content:await app.vault.adapter.read(${JSON.stringify(receivedNotePath)}),`,
|
||||
"});",
|
||||
"})()",
|
||||
].join(""),
|
||||
session.cliEnv
|
||||
);
|
||||
assertEqual(fetched.obsoleteRetained, false, "Fast Fetch retained obsolete local Metadata.");
|
||||
assertEqual(fetched.content, editedNoteContent, "Fast Fetch changed the synchronised file content.");
|
||||
console.log("Real Obsidian Fast Fetch replaced obsolete logical deletions and retained the expected file.");
|
||||
}
|
||||
if (cliFileInfo.expectedCtime !== undefined) {
|
||||
const receivedCtime = await evalObsidianJson<number>(
|
||||
obsidianCli.binary,
|
||||
|
||||
Reference in new issue
Block a user