From 69f4879589cca09519c4cc8ad7085e9f20d2dd29 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 16 Jul 2026 09:26:45 +0000 Subject: [PATCH] test: cover case-only file renames in Obsidian --- docs/settings.md | 2 ++ test/e2e-obsidian/README.md | 2 +- test/e2e-obsidian/scripts/two-vault-sync.ts | 36 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/settings.md b/docs/settings.md index 754ba7e6..995aed28 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -859,6 +859,8 @@ If this enabled, all chunks will be stored with the revision made from its conte Setting key: handleFilenameCaseSensitive If this enabled, All files are handled as case-Sensitive (Previous behaviour). +When this setting is disabled, changing only the letter case of a file name within the same directory is synchronised as a rename. Changing the letter case of a directory name is not supported by this handling. + ### 4. Compatibility (Internal API Usage) #### Scan changes on customisation sync diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index d3dda631..56008f2f 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -91,7 +91,7 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- `test:e2e:obsidian:startup-scan` configures a temporary CouchDB database, stops Obsidian, writes a note directly into the vault, restarts Obsidian, and verifies from CouchDB that the boot-time scan picked up the offline file. -`test:e2e:obsidian:two-vault-sync` runs a two-vault note synchronisation workflow. It verifies note creation, update, rename, deletion, per-device target filters where one vault ignores a note that the other vault synchronises, and a separate encrypted round-trip with Path Obfuscation enabled. The optional Markdown conflict automatic merge check can be enabled with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`, but it is not part of the default local suite. +`test:e2e:obsidian:two-vault-sync` runs a two-vault note synchronisation workflow. It verifies note creation, update, ordinary rename, a case-only file name change within the same directory, deletion, per-device target filters where one vault ignores a note that the other vault synchronises, and a separate encrypted round-trip with Path Obfuscation enabled. Directory case changes deliberately remain outside this scenario because they require directory-aware rename handling. The optional Markdown conflict automatic merge check can be enabled with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`, but it is not part of the default local suite. `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. diff --git a/test/e2e-obsidian/scripts/two-vault-sync.ts b/test/e2e-obsidian/scripts/two-vault-sync.ts index f1c23b8c..52203afa 100644 --- a/test/e2e-obsidian/scripts/two-vault-sync.ts +++ b/test/e2e-obsidian/scripts/two-vault-sync.ts @@ -31,6 +31,8 @@ const updatePath = "E2E/two-vault/update.md"; const deletePath = "E2E/two-vault/delete.md"; const renameFromPath = "E2E/two-vault/rename-source.md"; const renameToPath = "E2E/two-vault/renamed/rename-target.md"; +const caseRenameFromPath = "E2E/two-vault/Case-Rename.md"; +const caseRenameToPath = "E2E/two-vault/case-rename.md"; const conflictPath = "E2E/two-vault/conflict.md"; const targetMismatchPath = "E2E/two-vault/target-mismatch.md"; const encryptedPath = "E2E/two-vault/encrypted.md"; @@ -362,6 +364,39 @@ async function runRename(context: RunnerContext, vaultA: TemporaryVault, vaultB: console.log("Two-vault note rename round-tripped."); } +async function runCaseOnlyRename( + context: RunnerContext, + vaultA: TemporaryVault, + vaultB: TemporaryVault +): Promise { + const fileContent = "# Case-only rename\n\nThe document ID should remain live.\n"; + + let session = await startConfiguredSession(context, vaultA); + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, caseRenameFromPath, fileContent); + await uploadNote(context, session, caseRenameFromPath); + await session.app.stop(); + + session = await startConfiguredSession(context, vaultB); + await syncAndApply(context, session); + await waitForPathContent(vaultB.path, caseRenameFromPath, (content) => content === fileContent); + await session.app.stop(); + + session = await startConfiguredSession(context, vaultA); + await renameNoteViaObsidian(context.cliBinary, session.cliEnv, caseRenameFromPath, caseRenameToPath); + await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, caseRenameToPath); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await session.app.stop(); + + session = await startConfiguredSession(context, vaultB); + await syncAndApply(context, session); + const renamedOnB = await waitForPathContent(vaultB.path, caseRenameToPath, (content) => content === fileContent); + await waitForPathDeleted(vaultB.path, caseRenameFromPath); + await session.app.stop(); + + assertEqual(renamedOnB, fileContent, "Case-only note rename did not round-trip to the second vault."); + console.log("Two-vault case-only note rename round-tripped without a tombstone."); +} + async function runEncryptedRoundTrip( context: RunnerContext, vaultA: TemporaryVault, @@ -498,6 +533,7 @@ async function main(): Promise { await runCreateUpdateDelete(context, vaultA, vaultB); await runRename(context, vaultA, vaultB); + await runCaseOnlyRename(context, vaultA, vaultB); if (process.env.E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT === "true") { await runMarkdownAutoMerge(context, vaultA, vaultB); }