2026-10-20 18:50:09 +02:00
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.Services ;
using AIStudio.Tools.Services.Indexing ;
using Microsoft.Extensions.Logging.Abstractions ;
namespace AIStudio.Tests.Tools ;
public sealed class FileSourceRelocationTests
{
private string testRoot = string . Empty ;
[SetUp]
public void SetUp ()
{
this . testRoot = Path . Combine ( Path . GetTempPath (), $"ai-studio-relocation-{Guid.NewGuid():N}" );
Directory . CreateDirectory ( this . testRoot );
}
[TearDown]
public void TearDown () => Directory . Delete ( this . testRoot , true );
[Test]
public void AMovedFolderFindsAnUnchangedNestedDocument ()
{
var oldRoot = Path . Combine ( this . testRoot , "original" );
var oldFile = Path . Combine ( oldRoot , "nested" , "notes.md" );
Directory . CreateDirectory ( Path . GetDirectoryName ( oldFile )!);
File . WriteAllText ( oldFile , "Some indexed text" );
var record = Record ( oldFile , Path . Combine ( "nested" , "notes.md" ));
var newRoot = Path . Combine ( this . testRoot , "moved" );
Directory . Move ( oldRoot , newRoot );
var found = FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalDirectory { Path = newRoot }, oldFile , record );
Assert . That ( found ?. FullName , Is . EqualTo ( Path . Combine ( newRoot , "nested" , "notes.md" )));
}
[TestCase("size")]
[TestCase("modification time")]
[TestCase("missing file")]
public void AChangedOrMissingDocumentCannotReuseItsEmbeddings ( string change )
{
var file = Path . Combine ( this . testRoot , "notes.md" );
File . WriteAllText ( file , "Some indexed text" );
var record = Record ( file , "notes.md" );
switch ( change )
{
case "size" :
File . AppendAllText ( file , " with more text" );
File . SetLastWriteTimeUtc ( file , record . LastWriteUtc . UtcDateTime );
break ;
case "modification time" :
File . SetLastWriteTimeUtc ( file , record . LastWriteUtc . UtcDateTime . AddMinutes ( 1 ));
break ;
case "missing file" :
File . Delete ( file );
break ;
}
// The old place lies outside the selected folder, so only the change can prevent the reuse:
var oldPath = Path . Combine ( Path . GetDirectoryName ( this . testRoot )!, "moved-away" , "notes.md" );
Assert . That ( FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalDirectory { Path = this . testRoot }, oldPath , record ), Is . Null );
}
[Test]
public void AStoredPathBelowTheSelectedFolderIsNotMappedToACopy ()
{
// The folder grows to its parent, which holds a copy with the same size and time at the
// place the old relative path names. The original is still there and stays the document.
var original = Path . Combine ( this . testRoot , "project" , "notes.md" );
Directory . CreateDirectory ( Path . GetDirectoryName ( original )!);
File . WriteAllText ( original , "Some indexed text" );
var copy = Path . Combine ( this . testRoot , "notes.md" );
File . Copy ( original , copy );
File . SetLastWriteTimeUtc ( copy , File . GetLastWriteTimeUtc ( original ));
Assert . That ( FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalDirectory { Path = this . testRoot }, original , Record ( original , "notes.md" )), Is . Null );
}
[Test]
public void ARenamedSingleFileCanReuseItsEmbeddings ()
{
var oldFile = Path . Combine ( this . testRoot , "notes.md" );
File . WriteAllText ( oldFile , "Some indexed text" );
var record = Record ( oldFile , "notes.md" );
var newFile = Path . Combine ( this . testRoot , "renamed.md" );
File . Move ( oldFile , newFile );
Assert . That ( FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalFile { FilePath = newFile }, oldFile , record )?. FullName , Is . EqualTo ( newFile ));
}
[Test]
public void ARelativePathCannotEscapeTheConfiguredFolder ()
{
var outside = Path . Combine ( this . testRoot , "notes.md" );
File . WriteAllText ( outside , "Some indexed text" );
var record = Record ( outside , Path . Combine ( ".." , "notes.md" ));
var folder = Path . Combine ( this . testRoot , "selected" );
Directory . CreateDirectory ( folder );
Assert . That ( FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalDirectory { Path = folder }, Path . Combine ( this . testRoot , "old" , "notes.md" ), record ), Is . Null );
}
private static EmbeddedFileRecord Record ( string path , string relativePath )
{
var file = new FileInfo ( path );
return new ( "old-fingerprint" , file . Length , new DateTimeOffset ( file . LastWriteTimeUtc ), DateTimeOffset . UtcNow , 1 , RelativePath : relativePath , ParentFileId : Guid . NewGuid (). ToString ());
}
[TestCase(@"E:\Documents\nested\notes.md", @"nested\notes.md")]
[TestCase(@"\\server\share\Documents\nested\notes.md", @"nested\notes.md")]
[TestCase(@"\\?\E:\Documents\nested\notes.md", @"nested\notes.md")]
[TestCase("E:/Documents/nested/notes.md", "nested/notes.md")]
[TestCase("/home/alice/Documents/nested/notes.md", "nested/notes.md")]
[TestCase("/Volumes/External drive/Documents/nested/notes.md", "nested/notes.md")]
public void ABackupFromAnotherPathSyntaxFindsANestedFile ( string oldPath , string relativePath )
{
var localFile = Path . Combine ( this . testRoot , "nested" , "notes.md" );
Directory . CreateDirectory ( Path . GetDirectoryName ( localFile )!);
File . WriteAllText ( localFile , "Some indexed text" );
var found = FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalDirectory { Path = this . testRoot }, oldPath , Record ( localFile , relativePath ));
Assert . That ( found ?. FullName , Is . EqualTo ( localFile ));
}
[TestCase(@"C:\a.folder\README")]
[TestCase(@"\\server\a.folder\README")]
[TestCase("/media/a.folder/README")]
public void AForeignDirectoryNameDoesNotBecomeTheExtensionOfAnExtensionlessFile ( string oldPath )
{
var localFile = Path . Combine ( this . testRoot , "README" );
File . WriteAllText ( localFile , "Some indexed text" );
Assert . That ( FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalFile { FilePath = localFile }, oldPath , Record ( localFile , "README" ))?. FullName , Is . EqualTo ( localFile ));
}
[TestCase(@"..\notes.md")]
[TestCase("../notes.md")]
[TestCase(@"nested\..\..\notes.md")]
[TestCase(@"C:\notes.md")]
[TestCase(@"C:notes.md")]
[TestCase(@"\\server\share\notes.md")]
[TestCase("/etc/notes.md")]
public void AStoredWindowsPathCannotEscapeTheNewRoot ( string relativePath )
{
Assert . That ( RelocatedFilePaths . Resolve ( this . testRoot , @"E:\Documents\notes.md" , relativePath ), Is . Null );
}
[Test]
public void AnExternalDriveRootIsAValidRelocationDestination ()
{
// No file needs to be written at the drive root: this checks resolving the destination.
var root = Path . GetPathRoot ( this . testRoot )!;
Assert . That ( RelocatedFilePaths . Resolve ( root , @"E:\notes.md" , "notes.md" ), Is . EqualTo ( Path . Combine ( root , "notes.md" )));
}
[Test]
public void AUnixBackslashInAFileNameIsNotReinterpretedAsADirectory ()
{
if ( OperatingSystem . IsWindows ())
{
Assert . That ( RelocatedFilePaths . Resolve ( this . testRoot , @"/home/alice/notes\draft.md" , @"notes\draft.md" ), Is . Null , "A Windows filename cannot contain a backslash from a Unix filename." );
return ;
}
var localFile = Path . Combine ( this . testRoot , @"notes\draft.md" );
File . WriteAllText ( localFile , "Some indexed text" );
Assert . That ( FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalDirectory { Path = this . testRoot }, @"/home/alice/notes\draft.md" , Record ( localFile , @"notes\draft.md" ))?. FullName , Is . EqualTo ( localFile ));
}
[Test]
public void AnOldForeignRootIsNotResolvedRelativeToTheWorkingDirectory ()
{
var foreignRoot = OperatingSystem . IsWindows () ? "/home/alice/Documents" : @"E:\Documents" ;
Assert . That ( RelocatedFilePaths . Resolve ( foreignRoot , @"E:\Documents\notes.md" , "notes.md" ), Is . Null );
}
[Test]
public void ReducedTimestampPrecisionOnAnotherDriveDoesNotSilentlyReuseOldContent ()
{
var localFile = Path . Combine ( this . testRoot , "notes.md" );
File . WriteAllText ( localFile , "Some indexed text" );
var record = Record ( localFile , "notes.md" );
// The old filesystem stored a finer timestamp than the copied file now reports. Without
// a stored content hash, equal size alone is not enough to prove that it is unchanged.
record = record with { LastWriteUtc = record . LastWriteUtc . AddTicks ( 1 ) };
Assert . That ( FileSourceIndexer . FindRelocatedFile ( new DataSourceLocalDirectory { Path = this . testRoot }, @"E:\Documents\notes.md" , record ), Is . Null );
}
[Test]
public void ANewFileAtAnOldLocationCannotOverwriteTheRelocatedDocument ()
{
var source = new DataSourceLocalDirectory { Id = Guid . NewGuid (). ToString (), Path = this . testRoot };
var originalPath = Path . Combine ( this . testRoot , "nested" , "notes.md" );
var relocatedPath = Path . Combine ( this . testRoot , "notes.md" );
var originalId = IndexedDocumentIds . CreateParentId ( source . Id , originalPath );
var manifest = new DataSourceEmbeddingManifest ();
manifest . Files [ relocatedPath ] = new ( "fingerprint" , 100 , DateTimeOffset . UnixEpoch , DateTimeOffset . UnixEpoch , 1 , RelativePath : "notes.md" , ParentFileId : originalId );
// Only identity assignment is exercised; none of the run's services are called.
var context = new IndexedRunContext ( source , null !, null !, null !, null !, manifest ,
new RunShare ( RunShare . DEFAULT_DURATION ), new IndexingPass ( new DataSourceRunSignature ( "signature" , "selection" )), null !, null !, _ => { }, NullLogger . Instance );
var newId = context . GetFileParentId ( originalPath );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-20 18:50:09 +02:00
{
Assert . That ( context . GetFileParentId ( relocatedPath ), Is . EqualTo ( originalId ));
Assert . That ( newId , Is . Not . EqualTo ( originalId ));
Assert . That ( context . GetFileParentId ( originalPath ), Is . EqualTo ( newId ), "A media file may ask for its identity once before and once after transcription." );
2026-11-01 20:32:16 +01:00
}
2026-10-20 18:50:09 +02:00
}
}