mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 00:38:00 +00:00
Added backups of all data, restorable on another computer (#1008)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
ab45580473
commit
936043319c
144 files changed
+10417
-424
No files matched your search
@@ -0,0 +1,106 @@
|
||||
using AIStudio.Chat;
|
||||
|
||||
namespace AIStudio.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that rewriting the file paths of a chat reaches every one of them, and nothing else.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Moving a chat, copying it, and writing it into a backup or reading it back all change the paths
|
||||
/// a chat stores. A path left out points at a place where the file is no longer, and the file drops
|
||||
/// out of the chat without a word: a transcript of the message which is still being written, or an
|
||||
/// image the model no longer gets to see.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ChatThreadFilePathTests
|
||||
{
|
||||
private static readonly DateTimeOffset START = new(2026, 10, 14, 9, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static readonly string OLD_DIRECTORY = Path.Combine(Path.GetTempPath(), "old-chat");
|
||||
|
||||
private static readonly string NEW_DIRECTORY = Path.Combine(Path.GetTempPath(), "new-chat");
|
||||
|
||||
[Test]
|
||||
public void EveryPathOfTheChatIsRewritten()
|
||||
{
|
||||
var document = new FileAttachment(FileAttachmentType.DOCUMENT, "report.pdf", Old("report.pdf"), 1024);
|
||||
var transcript = new ManagedTranscriptAttachment("meeting.md", Old("meeting.md"), 512, "meeting.mp3", false);
|
||||
var picture = new FileAttachmentImage("chart.png", Old("chart.png"), 2048);
|
||||
var photo = new ContentImage { SourceType = ContentImageSource.LOCAL_PATH, Source = Old("photo.jpg") };
|
||||
var chat = new ChatThread
|
||||
{
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = "Quarterly report",
|
||||
Blocks = [Block(new ContentText { Text = "Summarize the report", FileAttachments = [document, transcript, picture] }), Block(photo)],
|
||||
};
|
||||
chat.Draft.AddAttachments([new ManagedTranscriptAttachment("call.md", Old("call.md"), 256, "call.m4a", false)]);
|
||||
|
||||
chat.RewriteFilePaths(Move);
|
||||
|
||||
var message = (ContentText)chat.Blocks[0].Content!;
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(message.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { New("report.pdf"), New("meeting.md"), New("chart.png") }));
|
||||
Assert.That(photo.Source, Is.EqualTo(New("photo.jpg")), "The model gets such an image from its path.");
|
||||
Assert.That(chat.Draft.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { New("call.md") }), "The draft is the one place every path rewrite once forgot.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhatTheRewriteLeavesAloneStaysAsItIs()
|
||||
{
|
||||
var elsewhere = new FileAttachment(FileAttachmentType.DOCUMENT, "notes.txt", Path.Combine(Path.GetTempPath(), "elsewhere", "notes.txt"), 64);
|
||||
var linked = new ContentImage { SourceType = ContentImageSource.URL, Source = "https://example.org/chart.png" };
|
||||
var chat = new ChatThread
|
||||
{
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = "Notes",
|
||||
Blocks = [Block(new ContentText { Text = "Read my notes", FileAttachments = [elsewhere] }), Block(linked)],
|
||||
};
|
||||
|
||||
chat.RewriteFilePaths(Move);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(((ContentText)chat.Blocks[0].Content!).FileAttachments.Single(), Is.SameAs(elsewhere));
|
||||
Assert.That(linked.Source, Is.EqualTo("https://example.org/chart.png"), "Only an image held by a local path has a path to rewrite.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAttachmentKeepsItsKindAtItsNewPlace()
|
||||
{
|
||||
var transcript = new ManagedTranscriptAttachment("meeting.md", Old("meeting.md"), 512, "meeting.mp3", true);
|
||||
var picture = new FileAttachmentImage("chart.png", Old("chart.png"), 2048);
|
||||
var document = new FileAttachment(FileAttachmentType.DOCUMENT, "report.pdf", Old("report.pdf"), 1024);
|
||||
|
||||
var movedTranscript = transcript.WithFilePath(New("meeting.md"));
|
||||
var movedPicture = picture.WithFilePath(New("chart.png"));
|
||||
var movedDocument = document.WithFilePath(New("report.pdf"));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(movedTranscript, Is.EqualTo(new ManagedTranscriptAttachment("meeting.md", New("meeting.md"), 512, "meeting.mp3", true)), "A transcript has to stay a transcript, or deleting the chat leaves its file behind.");
|
||||
Assert.That(movedPicture, Is.TypeOf<FileAttachmentImage>());
|
||||
Assert.That(((FileAttachmentImage)movedPicture).Source, Is.EqualTo(New("chart.png")), "The model gets the image from its source, not from its path.");
|
||||
Assert.That(movedPicture.FilePathAsUrl, Is.EqualTo(new FileAttachmentImage("chart.png", New("chart.png"), 2048).FilePathAsUrl), "The preview of the image loads it from there.");
|
||||
Assert.That(movedDocument, Is.EqualTo(new FileAttachment(FileAttachmentType.DOCUMENT, "report.pdf", New("report.pdf"), 1024)));
|
||||
});
|
||||
}
|
||||
|
||||
private static string Old(string fileName) => Path.Combine(OLD_DIRECTORY, fileName);
|
||||
|
||||
private static string New(string fileName) => Path.Combine(NEW_DIRECTORY, fileName);
|
||||
|
||||
private static string Move(string path) => path.StartsWith(OLD_DIRECTORY, StringComparison.Ordinal)
|
||||
? Path.Combine(NEW_DIRECTORY, Path.GetRelativePath(OLD_DIRECTORY, path))
|
||||
: path;
|
||||
|
||||
private static ContentBlock Block(IContent content) => new()
|
||||
{
|
||||
Time = START,
|
||||
ContentType = content is ContentImage ? ContentType.IMAGE : ContentType.TEXT,
|
||||
Content = content,
|
||||
Role = ChatRole.USER,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Checks which transcript files AI Studio deletes as its own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A transcript carries a full path, which a chat file edited outside the app may point anywhere. AI
|
||||
/// Studio therefore deletes a file only where it keeps transcripts itself: in the transcript folder of a
|
||||
/// chat, or in the staging area.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public sealed class ManagedTranscriptAttachmentTests
|
||||
{
|
||||
private string? previousDataDirectory;
|
||||
private string testDirectory = string.Empty;
|
||||
|
||||
[SetUp]
|
||||
public void PrepareDataDirectory()
|
||||
{
|
||||
// The data directory is static state of the whole application, which is why this fixture does not run alongside others:
|
||||
this.previousDataDirectory = SettingsManager.DataDirectory;
|
||||
this.testDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-transcripts-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(this.testDirectory);
|
||||
SettingsManager.DataDirectory = this.testDirectory;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void RemoveDataDirectory()
|
||||
{
|
||||
SettingsManager.DataDirectory = this.previousDataDirectory;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(this.testDirectory, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// A temporary directory we could not remove says nothing about the code under test.
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheTranscriptOfAWorkspaceChatIsDeleted()
|
||||
{
|
||||
var chatDirectory = Path.Combine(this.testDirectory, "workspaces", Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
|
||||
var transcript = WriteTranscript(ManagedTranscriptAttachment.GetTranscriptDirectory(chatDirectory));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ManagedTranscriptAttachment.TryDeleteOwnedFile(transcript), Is.True);
|
||||
Assert.That(File.Exists(transcript.FilePath), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFileInAnotherFolderOfTheChatStays()
|
||||
{
|
||||
var chatDirectory = Path.Combine(this.testDirectory, "workspaces", Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
|
||||
var file = WriteTranscript(Path.Combine(chatDirectory, "attachments", "images"));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ManagedTranscriptAttachment.TryDeleteOwnedFile(file), Is.False, "Only the transcript folder belongs to the transcripts.");
|
||||
Assert.That(File.Exists(file.FilePath), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFileOutsideTheDataDirectoryStays()
|
||||
{
|
||||
var outsideDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-elsewhere-{Guid.NewGuid():N}");
|
||||
try
|
||||
{
|
||||
var file = WriteTranscript(ManagedTranscriptAttachment.GetTranscriptDirectory(Path.Combine(outsideDirectory, "tempChats", Guid.NewGuid().ToString())));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ManagedTranscriptAttachment.TryDeleteOwnedFile(file), Is.False, "The same folders elsewhere belong to somebody else.");
|
||||
Assert.That(File.Exists(file.FilePath), Is.True);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(outsideDirectory, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a transcript into a folder.
|
||||
/// </summary>
|
||||
/// <param name="directory">Where the transcript goes.</param>
|
||||
/// <returns>The transcript, as a chat attaches it to a message.</returns>
|
||||
private static ManagedTranscriptAttachment WriteTranscript(string directory)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(directory, "meeting-transcript-0001.md");
|
||||
File.WriteAllText(path, "# Transcription: meeting.mp4");
|
||||
return new ManagedTranscriptAttachment(Path.GetFileName(path), path, new FileInfo(path).Length, "meeting.mp4", false);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user