@@ -336,8 +336,8 @@ async function testRenameDoesNotRestoreOldPath() {
336336 } ) ;
337337
338338 plugin . syncIndex . set ( oldPath , {
339- localSignature : "md:old " ,
340- remoteSignature : "sig-old " ,
339+ localSignature : "1000:16 " ,
340+ remoteSignature : "1000:16 " ,
341341 remotePath : oldRemotePath ,
342342 } ) ;
343343 plugin . uploadContentFileToRemote = async ( incomingFile , incomingRemotePath , markdownContent ) => {
@@ -353,7 +353,7 @@ async function testRenameDoesNotRestoreOldPath() {
353353 // Three-way comparison detects LOCAL_DELETED + REMOTE_UNCHANGED → deleteRemote.
354354 plugin . listRemoteTree = async ( ) => ( {
355355 files : new Map ( [
356- [ oldRemotePath , { remotePath : oldRemotePath , lastModified : 1000 , size : 16 , signature : "sig-old " } ] ,
356+ [ oldRemotePath , { remotePath : oldRemotePath , lastModified : 1000 , size : 16 , signature : "1000:16 " } ] ,
357357 ] ) ,
358358 directories : new Set ( [ plugin . normalizeFolder ( plugin . settings . vaultSyncRemoteFolder ) ] ) ,
359359 } ) ;
@@ -371,38 +371,6 @@ async function testRenameDoesNotRestoreOldPath() {
371371 // First sync should have deleted the old remote path via three-way comparison
372372 assert . ok ( deletedRemotePaths . includes ( oldRemotePath ) , "sync should delete the old remote path" ) ;
373373 assert . ok ( ! remoteStore . has ( plugin . buildUploadUrl ( oldRemotePath ) ) , "old remote should be gone after first sync" ) ;
374-
375- // Second sync: another client re-creates the old file on remote.
376- // prevSync survived from deleteRemote → LOCAL_DELETED + REMOTE_UNCHANGED → deleteRemote again.
377- remoteStore . set ( plugin . buildUploadUrl ( oldRemotePath ) , {
378- body : new TextEncoder ( ) . encode ( "# Recreated Old Title" ) . buffer ,
379- } ) ;
380- plugin . listRemoteTree = async ( ) => ( {
381- files : new Map ( [
382- [ oldRemotePath , { remotePath : oldRemotePath , lastModified : 3000 , size : 16 , signature : "sig-recreated" } ] ,
383- [ newRemotePath , { remotePath : newRemotePath , lastModified : 2000 , size : 13 , signature : newLocalSignature } ] ,
384- ] ) ,
385- directories : new Set ( [ plugin . normalizeFolder ( plugin . settings . vaultSyncRemoteFolder ) ] ) ,
386- } ) ;
387-
388- const downloadedPaths = [ ] ;
389- const originalDownload = plugin . downloadRemoteFileToVault . bind ( plugin ) ;
390- plugin . downloadRemoteFileToVault = async ( vaultPath , remote , existingFile ) => {
391- downloadedPaths . push ( vaultPath ) ;
392- if ( vaultPath === oldPath ) {
393- throw new Error ( "old path should not be restored" ) ;
394- }
395-
396- return originalDownload ( vaultPath , remote , existingFile ) ;
397- } ;
398-
399- deletedRemotePaths . length = 0 ;
400- await plugin . syncVaultContent ( false ) ;
401-
402- assert . ok ( deletedRemotePaths . includes ( oldRemotePath ) , "second sync should also delete the stale old remote path" ) ;
403- assert . ok ( ! downloadedPaths . includes ( oldPath ) , "old renamed path should not return locally" ) ;
404- assert . equal ( app . vault . getAbstractFileByPath ( oldPath ) , null , "old renamed path should stay absent locally" ) ;
405- assert . ok ( app . vault . getAbstractFileByPath ( newPath ) , "new renamed path should remain locally" ) ;
406374}
407375
408376async function testPriorityRenameSyncPushesMoveImmediately ( ) {
@@ -450,8 +418,8 @@ async function testPriorityRenameSyncPushesMoveImmediately() {
450418 } ) ;
451419
452420 plugin . syncIndex . set ( oldPath , {
453- localSignature : "md:old " ,
454- remoteSignature : "sig-old " ,
421+ localSignature : "1000:13 " ,
422+ remoteSignature : "1000:13 " ,
455423 remotePath : oldRemotePath ,
456424 } ) ;
457425 plugin . listRemoteDirectory = async ( remoteDir ) => {
@@ -526,7 +494,7 @@ async function testFullSyncRenameDoesNotRestoreOldPath() {
526494 remotePath : oldRemotePath ,
527495 lastModified : 1000 ,
528496 size : 13 ,
529- signature : "sig-old " ,
497+ signature : "1000:13 " ,
530498 } ) ;
531499 }
532500 return {
@@ -632,8 +600,8 @@ async function testBothSidesChangedCreatesConflictCopy() {
632600 const remoteState = createRemoteFileState ( remotePath , "remote edit" , 6000 ) ;
633601
634602 plugin . syncIndex . set ( file . path , {
635- localSignature : "md:old " ,
636- remoteSignature : "remote-old " ,
603+ localSignature : "1000:8:2e599d46723a6e7f " ,
604+ remoteSignature : "1000:8 " ,
637605 remotePath,
638606 } ) ;
639607 plugin . listRemoteTree = async ( ) => ( {
@@ -775,6 +743,50 @@ async function testConflictCopiesStayLocalOnlyAcrossLaterSyncs() {
775743 ) ;
776744}
777745
746+ async function testDeletedLocalFilePullsBackWhenRemoteWasModified ( ) {
747+ const deletedRemotePaths = [ ] ;
748+ const pulledPaths = [ ] ;
749+ const { plugin, app } = createHarness ( async ( ) => ( { status : 200 , headers : { } , arrayBuffer : new ArrayBuffer ( 0 ) } ) ) ;
750+
751+ const file = app . vault . addFile ( "Notes/keep-remote.md" , "local body" , { mtime : 1000 } ) ;
752+ const remotePath = plugin . syncSupport . buildVaultSyncRemotePath ( file . path ) ;
753+
754+ plugin . listRemoteTree = async ( ) => ( {
755+ files : new Map ( ) ,
756+ directories : new Set ( [ plugin . normalizeFolder ( plugin . settings . vaultSyncRemoteFolder ) ] ) ,
757+ } ) ;
758+ plugin . uploadContentFileToRemote = async ( incomingFile , incomingRemotePath , markdownContent ) => {
759+ return createRemoteFileState ( incomingRemotePath , markdownContent ?? incomingFile . content ?? "" , 2000 ) ;
760+ } ;
761+ plugin . reconcileDirectories = async ( ) => ( { createdLocal : 0 , createdRemote : 0 , deletedLocal : 0 , deletedRemote : 0 } ) ;
762+ plugin . reconcileRemoteImages = async ( ) => ( { deletedFiles : 0 , deletedDirectories : 0 } ) ;
763+ plugin . evictStaleSyncedNotes = async ( ) => 0 ;
764+
765+ await plugin . handleVaultModify ( file ) ;
766+ await plugin . syncVaultContent ( false ) ;
767+
768+ await plugin . handleVaultDelete ( file ) ;
769+ await app . vault . delete ( file ) ;
770+
771+ plugin . listRemoteTree = async ( ) => ( {
772+ files : new Map ( [ [ remotePath , createRemoteFileState ( remotePath , "remote body" , 5000 ) ] ] ) ,
773+ directories : new Set ( [ plugin . normalizeFolder ( plugin . settings . vaultSyncRemoteFolder ) ] ) ,
774+ } ) ;
775+ plugin . deleteRemoteContentFile = async ( incomingRemotePath ) => {
776+ deletedRemotePaths . push ( incomingRemotePath ) ;
777+ } ;
778+ plugin . downloadRemoteFileToVault = async ( vaultPath , remote ) => {
779+ pulledPaths . push ( vaultPath ) ;
780+ app . vault . addFile ( vaultPath , "remote body" , { mtime : remote . lastModified } ) ;
781+ } ;
782+
783+ await plugin . syncVaultContent ( false ) ;
784+
785+ assert . deepEqual ( deletedRemotePaths , [ ] , "remote modified after local deletion should not be deleted" ) ;
786+ assert . deepEqual ( pulledPaths , [ file . path ] , "remote modified after local deletion should be pulled back locally" ) ;
787+ assert . ok ( app . vault . getAbstractFileByPath ( file . path ) , "remote file should be restored locally" ) ;
788+ }
789+
778790async function testFastSyncUploadsPendingAndScannedLocalChanges ( ) {
779791 const uploadedPaths = [ ] ;
780792 const { plugin, app } = createHarness ( async ( ) => ( { status : 200 , headers : { } , arrayBuffer : new ArrayBuffer ( 0 ) } ) ) ;
@@ -1418,6 +1430,7 @@ async function run() {
14181430 [ "完整同步:本地和远端同时变化时创建冲突副本" , testBothSidesChangedCreatesConflictCopy ] ,
14191431 [ "快速同步:全量同步后本地修改不会误判冲突" , testFastSyncAfterFullSyncDoesNotCreateFalseConflictCopy ] ,
14201432 [ "冲突副本:后续同步不会把本地 conflict 备份上传到服务器" , testConflictCopiesStayLocalOnlyAcrossLaterSyncs ] ,
1433+ [ "Remotely Save 对齐:本地删除但远端已修改时应拉回远端" , testDeletedLocalFilePullsBackWhenRemoteWasModified ] ,
14211434 [ "目录同步:保留远端缺失的本地空目录" , testReconcileDirectoriesPreservesLocalEmptyDir ] ,
14221435 [ "目录同步:新本地目录上传到远端" , testReconcileDirectoriesCreatesRemoteDir ] ,
14231436 [ "目录同步:保留本地缺失的远端目录" , testReconcileDirectoriesPreservesRemoteDir ] ,
@@ -1433,6 +1446,10 @@ async function run() {
14331446 try {
14341447 Notice . messages . length = 0 ;
14351448 mockLocalforage . _reset ( ) ;
1449+ global . localforage = mockLocalforage ;
1450+ if ( typeof localStorage !== "undefined" && typeof localStorage . clear === "function" ) {
1451+ localStorage . clear ( ) ;
1452+ }
14361453 await testFn ( ) ;
14371454 console . log ( `PASS ${ name } ` ) ;
14381455 } catch ( error ) {
0 commit comments