using AIStudio.Tools.Databases; using AIStudio.Tools.Databases.VectorStore; namespace AIStudio.Tests.Tools; /// /// Keeps the identities and locations of existing vectors. New embeddings are forbidden: these /// tests reconnect previously indexed files and must never contact an embedding provider. /// internal sealed class RelocationVectorStore() : VectorStoreClient("Relocation test vectors", string.Empty) { public Dictionary Locations { get; } = new(StringComparer.Ordinal); public List RelocatedPaths { get; } = []; public TaskCompletionSource RelocationStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); public bool BlockNextRelocation { get; set; } public int CanceledRelocations { get; private set; } public int DeletedStores { get; private set; } public int DeletedDocuments { get; private set; } /// /// How often a failed attempt was discarded while the document kept its stored version. /// public int DiscardedAttempts { get; private set; } public int InsertedBatches { get; private set; } public override async Task RelocateEmbeddingFileAsync(string storeName, string parentFileId, string absolutePath, string relativePath, string fingerprint, CancellationToken token) { token.ThrowIfCancellationRequested(); Assert.That(this.Locations.ContainsKey(parentFileId), Is.True, "Relocation must address an existing vector identity."); this.Locations[parentFileId] = absolutePath; this.RelocatedPaths.Add(absolutePath); if (!this.BlockNextRelocation) return; this.BlockNextRelocation = false; this.RelocationStarted.TrySetResult(null); try { await Task.Delay(Timeout.InfiniteTimeSpan, token); } catch (OperationCanceledException) { this.CanceledRelocations++; throw; } } public override Task EnsureVectorStoreExists(string storeName, string dataSourceName, int vectorSize, CancellationToken token) => Task.FromResult(new VectorStoreEnsureResult(false)); public override Task VectorStoreExists(string storeName, CancellationToken token) => Task.FromResult(true); public override Task InsertEmbedding(string storeName, IReadOnlyList points, CancellationToken token) { this.InsertedBatches++; throw new InvalidOperationException("Existing unchanged files must not be embedded again."); } public override Task DeleteEmbeddingByFile(string storeName, string filePath, string parentFileId, IReadOnlyCollection keepPointIds, CancellationToken token) { // Only identities are kept here, so points to keep mean the document keeps its identity: if (keepPointIds.Count > 0) { this.DiscardedAttempts++; return Task.CompletedTask; } this.DeletedDocuments++; foreach (var id in this.Locations.Where(entry => entry.Key == parentFileId || entry.Value == filePath).Select(entry => entry.Key).ToArray()) this.Locations.Remove(id); return Task.CompletedTask; } public override Task DeleteVectorStore(string storeName, CancellationToken token) { this.DeletedStores++; this.Locations.Clear(); return Task.CompletedTask; } public override Task OptimizeVectorStore(string storeName, CancellationToken token) => Task.CompletedTask; public override Task> SearchEmbeddingAsync(string storeName, IReadOnlyList vector, int maxMatches, IReadOnlyList hiddenChunks, CancellationToken token) => throw new NotSupportedException(); public override Task> SearchEmbeddingAsync(string storeName, IReadOnlyList vector, int maxMatches, VectorSearchFilter filter, IReadOnlyList hiddenChunks, CancellationToken token) => throw new NotSupportedException(); // Only the identities and locations are kept here, never a vector: public override Task> RetrieveVectorsAsync(string storeName, IReadOnlyList pointIds, CancellationToken token) => Task.FromResult>([]); public override Task PrepareBackupAsync(CancellationToken token) => throw new NotSupportedException(); public override Task CopyStoreForBackupAsync(string backupId, string storeName, CancellationToken token) => throw new NotSupportedException(); public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() { await Task.CompletedTask; yield break; } public override void Dispose() { } }