mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-06 14:49:40 +00:00
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
220 lines
11 KiB
C#
220 lines
11 KiB
C#
using AIStudio.Provider;
|
|
using AIStudio.Settings.DataModel;
|
|
using AIStudio.Tests.Tools.Databases;
|
|
using AIStudio.Tools.Databases.IndexStore;
|
|
using AIStudio.Tools.Mail;
|
|
using AIStudio.Tools.Services;
|
|
|
|
namespace AIStudio.Tests.Tools.Mail;
|
|
|
|
/// <summary>
|
|
/// Checks which mailboxes the mail tools may read, and what they get from the index.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A mailbox holds what its owner wrote and received, so a provider below its level must never see
|
|
/// a word of it: neither the provider of the chat, which reads what is found, nor the embedding
|
|
/// provider, which gets the query. Within a mailbox, the tools rely on pages which neither repeat
|
|
/// nor skip a mail, and on a reply leading to the mail it answers.
|
|
/// </remarks>
|
|
[TestFixture]
|
|
public sealed class MailboxRetrievalTests
|
|
{
|
|
private const string MAILBOX = "3d8f1a6c-9b2e-4c7d-a5f0-6e1b8c4d2a97";
|
|
private const string OTHER_MAILBOX = "8b2c6e1f-4a9d-4f3b-b7e5-1c0d9a6f3e82";
|
|
private const string INBOX = "INBOX";
|
|
|
|
private static readonly CancellationToken TOKEN = CancellationToken.None;
|
|
private static readonly DataSourceMailbox WORK = new() { Id = MAILBOX, Name = "Work", MaxAge = MailboxMaxAge.LAST_6_MONTHS };
|
|
private static readonly DataSourceMailbox PRIVATE = new() { Id = OTHER_MAILBOX, Name = "Private", MaxAge = MailboxMaxAge.ALL };
|
|
|
|
private TemporaryIndexStore store = null!;
|
|
private string question = string.Empty;
|
|
private string answer = string.Empty;
|
|
private string newsletter = string.Empty;
|
|
private string deleted = string.Empty;
|
|
private string elsewhere = string.Empty;
|
|
|
|
[SetUp]
|
|
public async Task CreateMailboxesAsync()
|
|
{
|
|
this.store = await TemporaryIndexStore.CreateAsync();
|
|
foreach (var mailboxId in new[] { MAILBOX, OTHER_MAILBOX })
|
|
{
|
|
await this.store.Client.UpsertDataSourceAsync(mailboxId, "MAILBOX", "b0a4c4d2-1f3e-4f0a-8c9d-5a6b7c8d9e01", "signature", string.Empty, 3, TOKEN);
|
|
await this.store.Client.UpsertMailFolderAsync(mailboxId, new MailFolderRecord(INBOX, MailFolderSpecialUse.NONE, 1, null, null, 120, 4, null), TOKEN);
|
|
}
|
|
|
|
this.question = await this.store.StoreMailAsync(MAILBOX, "question", "Question", "Could you have a look at the draft?", Mail(new DateTimeOffset(2026, 9, 1, 8, 0, 0, TimeSpan.Zero), At(1, isSeen: true)) with { MessageId = "question@example.org" });
|
|
this.answer = await this.store.StoreMailAsync(MAILBOX, "answer", "Re: Question", "I had a look, see my comments.", Mail(new DateTimeOffset(2026, 9, 10, 9, 30, 0, TimeSpan.Zero), At(2, isSeen: false)) with { MessageId = "answer@example.org", InReplyTo = "question@example.org" });
|
|
this.newsletter = await this.store.StoreMailAsync(MAILBOX, "newsletter", "Newsletter August", "What happened in August.", Mail(new DateTimeOffset(2026, 8, 15, 7, 0, 0, TimeSpan.Zero), At(3, isSeen: true)));
|
|
this.deleted = await this.store.StoreMailAsync(MAILBOX, "deleted", "Old draft", "A mail which is gone from the server.", Mail(new DateTimeOffset(2026, 9, 20, 7, 0, 0, TimeSpan.Zero), At(4, isSeen: false)));
|
|
await this.store.Client.RemoveMailLocationsAsync(MAILBOX, INBOX, [4], TOKEN);
|
|
|
|
this.elsewhere = await this.store.StoreMailAsync(OTHER_MAILBOX, "elsewhere", "Holidays", "A mail in another mailbox.", Mail(new DateTimeOffset(2026, 9, 25, 7, 0, 0, TimeSpan.Zero), At(1, isSeen: false)));
|
|
}
|
|
|
|
[TearDown]
|
|
public async Task DeleteMailboxesAsync() => await this.store.DisposeAsync();
|
|
|
|
[TestCase(ConfidenceLevel.MEDIUM, ConfidenceLevel.HIGH, ConfidenceLevel.HIGH, true)]
|
|
[TestCase(ConfidenceLevel.MEDIUM, ConfidenceLevel.MEDIUM, ConfidenceLevel.MEDIUM, true)]
|
|
[TestCase(ConfidenceLevel.MEDIUM, ConfidenceLevel.LOW, ConfidenceLevel.HIGH, false)]
|
|
[TestCase(ConfidenceLevel.MEDIUM, ConfidenceLevel.HIGH, ConfidenceLevel.LOW, false)]
|
|
public void BothProvidersHaveToMeetTheLevelOfTheMailbox(ConfidenceLevel mailboxLevel, ConfidenceLevel chatProviderConfidence, ConfidenceLevel embeddingProviderConfidence, bool expected)
|
|
{
|
|
Assert.That(MailboxRetrievalService.IsReadable(mailboxLevel, chatProviderConfidence, embeddingProviderConfidence), Is.EqualTo(expected));
|
|
}
|
|
|
|
[TestCase(ConfidenceLevel.NONE)]
|
|
[TestCase(ConfidenceLevel.UNTRUSTED)]
|
|
[TestCase(ConfidenceLevel.UNKNOWN)]
|
|
public void AMailboxWithoutAValidLevelStaysClosed(ConfidenceLevel mailboxLevel)
|
|
{
|
|
Assert.That(MailboxRetrievalService.IsReadable(mailboxLevel, ConfidenceLevel.HIGH, ConfidenceLevel.HIGH), Is.False, "These levels would let almost every provider through.");
|
|
}
|
|
|
|
[Test]
|
|
public void AMailboxWithoutItsEmbeddingProviderStaysClosed()
|
|
{
|
|
Assert.That(MailboxRetrievalService.IsReadable(ConfidenceLevel.MEDIUM, ConfidenceLevel.HIGH, embeddingProviderConfidence: null), Is.False, "Nobody can tell whether a replacement would meet the level.");
|
|
}
|
|
|
|
[Test]
|
|
public void EveryMailIsListedOnceWithItsBestPassage()
|
|
{
|
|
var passages = MailboxRetrievalService.BestPassagePerMail([new("a", "a1"), new("b", "b1"), new("a", "a2"), new("c", "c1"), new("b", "b2")]);
|
|
|
|
Assert.That(passages, Is.EqualTo(new MailPassage[] { new("a", "a1"), new("b", "b1"), new("c", "c1") }));
|
|
}
|
|
|
|
[Test]
|
|
public void AChannelWhichFilledItsWindowMayHoldMoreMails()
|
|
{
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(MailboxRetrievalService.MayHoldMoreMails(1, 10, 44, 44, 3), Is.True, "Its chunks may belong to fewer mails than the page needed.");
|
|
Assert.That(MailboxRetrievalService.MayHoldMoreMails(1, 10, 44, 43, 3), Is.False, "Both channels found less than they were asked for, so they found everything.");
|
|
Assert.That(MailboxRetrievalService.MayHoldMoreMails(9, 10, 364, 364, 364), Is.False, "Paging ends at the last page.");
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task WithoutAQueryTheMailsArePagedNewestFirst()
|
|
{
|
|
var first = await MailboxRetrievalService.ListNewestFirstAsync(this.store.Client, MAILBOX, new MailFilter(), 1, 2, TOKEN);
|
|
var second = await MailboxRetrievalService.ListNewestFirstAsync(this.store.Client, MAILBOX, new MailFilter(), 2, 2, TOKEN);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(first.Hits.Select(hit => hit.Summary.MailId), Is.EqualTo(new[] { this.answer, this.question }));
|
|
Assert.That(first.HasMore, Is.True);
|
|
Assert.That(second.Hits.Select(hit => hit.Summary.MailId), Is.EqualTo(new[] { this.newsletter }), "Neither the mail gone from the server nor the one of another mailbox shows up.");
|
|
Assert.That(second.HasMore, Is.False);
|
|
Assert.That(first.Hits.Concat(second.Hits).Select(hit => hit.Passage), Is.All.Null, "Without a query, no passage matched anything.");
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task TheConditionsNarrowTheListedMails()
|
|
{
|
|
var unread = await MailboxRetrievalService.ListNewestFirstAsync(this.store.Client, MAILBOX, new MailFilter { IsUnread = true }, 1, 10, TOKEN);
|
|
|
|
Assert.That(unread.Hits.Select(hit => hit.Summary.MailId), Is.EqualTo(new[] { this.answer }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AReplyLeadsToTheMailItAnswers()
|
|
{
|
|
var reading = await MailboxRetrievalService.ReadMailAsync(this.store.Client, [WORK, PRIVATE], this.answer, TOKEN);
|
|
|
|
Assert.That(reading, Is.Not.Null);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(reading!.Mailbox.Id, Is.EqualTo(MAILBOX));
|
|
Assert.That(reading.Summary.Subject, Is.EqualTo("Re: Question"));
|
|
Assert.That(reading.Mail.MessageId, Is.EqualTo("answer@example.org"));
|
|
Assert.That(reading.InReplyToMailId, Is.EqualTo(this.question));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task AMailIsReadOnlyFromTheMailboxesGiven()
|
|
{
|
|
var fromAnotherMailbox = await MailboxRetrievalService.ReadMailAsync(this.store.Client, [WORK], this.elsewhere, TOKEN);
|
|
var fromItsOwnMailbox = await MailboxRetrievalService.ReadMailAsync(this.store.Client, [WORK, PRIVATE], this.elsewhere, TOKEN);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(fromAnotherMailbox, Is.Null, "The provider may not read the mailbox which holds it.");
|
|
Assert.That(fromItsOwnMailbox?.Mailbox.Id, Is.EqualTo(OTHER_MAILBOX));
|
|
Assert.That(fromItsOwnMailbox?.InReplyToMailId, Is.Null, "It answers no mail.");
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task AMailGoneFromTheServerIsNotRead()
|
|
{
|
|
var goneFromTheServer = await MailboxRetrievalService.ReadMailAsync(this.store.Client, [WORK], this.deleted, TOKEN);
|
|
var neverIndexed = await MailboxRetrievalService.ReadMailAsync(this.store.Client, [WORK], "4f1c9e2a-7b3d-4e8f-9a6c-2d5b8e1f3a70", TOKEN);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(goneFromTheServer, Is.Null, "It lost its last place on the server.");
|
|
Assert.That(neverIndexed, Is.Null, "Nobody ever indexed it.");
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task AMailboxNeverSyncedKnowsOnlyItsPeriod()
|
|
{
|
|
var now = new DateTimeOffset(2026, 10, 2, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
var coverage = await MailboxRetrievalService.ReadCoverageAsync(this.store.Client, WORK, now, TOKEN);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(coverage.ReceivedSinceUtc, Is.EqualTo(now.AddMonths(-6)));
|
|
Assert.That(coverage.LastCompleteSyncUtc, Is.Null, "The first sync is still running.");
|
|
Assert.That(coverage.SignInRefusedAtUtc, Is.Null);
|
|
Assert.That(coverage.PendingRemovalCount, Is.Null);
|
|
Assert.That(coverage.Folders.Select(folder => folder.Path), Is.EqualTo(new[] { INBOX }));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task TheCoverageTellsWhatTheIndexMisses()
|
|
{
|
|
var completed = new DateTimeOffset(2026, 10, 1, 9, 16, 0, TimeSpan.Zero);
|
|
var refused = new DateTimeOffset(2026, 10, 2, 8, 0, 0, TimeSpan.Zero);
|
|
await this.store.Client.CompleteMailboxSyncAsync(OTHER_MAILBOX, completed, TOKEN);
|
|
await this.store.Client.HoldBackMailRemovalAsync(OTHER_MAILBOX, 250, TOKEN);
|
|
await this.store.Client.UpsertMailboxAuthFailureAsync(OTHER_MAILBOX, new MailboxAuthFailure(refused, "Invalid credentials."), TOKEN);
|
|
|
|
var coverage = await MailboxRetrievalService.ReadCoverageAsync(this.store.Client, PRIVATE, refused, TOKEN);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(coverage.ReceivedSinceUtc, Is.Null, "The whole mailbox is indexed.");
|
|
Assert.That(coverage.LastCompleteSyncUtc, Is.EqualTo(completed));
|
|
Assert.That(coverage.SignInRefusedAtUtc, Is.EqualTo(refused), "No new mail arrives until the user deals with it.");
|
|
Assert.That(coverage.PendingRemovalCount, Is.EqualTo(250));
|
|
});
|
|
}
|
|
|
|
private static MailLocationRecord At(long uid, bool isSeen) => new(INBOX, uid, new MailFlags(isSeen, false, false));
|
|
|
|
private static MailRecord Mail(DateTimeOffset received, MailLocationRecord location) => new(
|
|
string.Empty,
|
|
string.Empty,
|
|
string.Empty,
|
|
[],
|
|
received,
|
|
received,
|
|
MailImportance.NORMAL,
|
|
MailEncryptionKind.NONE,
|
|
"mail-hash",
|
|
received,
|
|
[new MailAddressRecord(MailAddressRole.FROM, "alice@example.org", "Alice")],
|
|
[],
|
|
[location]);
|
|
} |