// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Functions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Moq;
namespace Microsoft.Agents.AI.UnitTests.Functions;
///
/// Contains unit tests for the class.
///
public sealed class FunctionStoreTests
{
private readonly Mock _vectorStoreMock;
private readonly Mock>> _collectionMock;
public FunctionStoreTests()
{
this._vectorStoreMock = new Mock(MockBehavior.Strict);
this._collectionMock = new Mock>>(MockBehavior.Strict);
this._vectorStoreMock
.Setup(vs => vs.GetDynamicCollection(It.IsAny(), It.IsAny()))
.Returns(this._collectionMock.Object);
this._collectionMock
.Setup(c => c.CollectionExistsAsync(It.IsAny()))
.ReturnsAsync(true);
this._collectionMock
.Setup(c => c.EnsureCollectionExistsAsync(It.IsAny()))
.Returns(Task.CompletedTask);
this._collectionMock
.Setup(c => c.UpsertAsync(It.IsAny>>(), It.IsAny()))
.Returns(Task.CompletedTask);
this._collectionMock
.Setup(c => c.SearchAsync(It.IsAny(), It.IsAny(), null, It.IsAny()))
.Returns(AsyncEnumerable.Empty>>());
}
[Fact]
public void Constructor_ShouldThrowOnInvalidArguments()
{
var functions = new List { CreateFunction("f1") };
Assert.Throws(() => new FunctionStore(null!, "col", 1, functions, 3));
Assert.Throws(() => new FunctionStore(this._vectorStoreMock.Object, "", 1, functions, 3));
Assert.Throws(() => new FunctionStore(this._vectorStoreMock.Object, "col", 0, functions, 3));
Assert.Throws(() => new FunctionStore(this._vectorStoreMock.Object, "col", 1, null!, 3));
}
[Fact]
public async Task SaveAsync_ShouldUpsertFunctionsAsync()
{
// Arrange
var functions = new List
{
CreateFunction("f1", "desc1"),
CreateFunction("f2", "desc2")
};
this._collectionMock.Setup(c => c.UpsertAsync(It.IsAny>>(), It.IsAny()))
.Returns(Task.CompletedTask)
.Verifiable();
var store = new FunctionStore(this._vectorStoreMock.Object, "col", 3, functions, 3);
// Act
await store.SaveAsync();
// Assert
this._collectionMock.Verify(c => c.EnsureCollectionExistsAsync(It.IsAny()), Times.Once);
this._collectionMock.Verify(c => c.UpsertAsync(It.Is>>(records =>
records.Count() == 2 &&
records.Any(r => (r["Name"] as string) == "f1") &&
records.Any(r => (r["Name"] as string) == "f2")
), It.IsAny()), Times.Once);
}
[Fact]
public async Task SearchAsync_ShouldReturnMatchingFunctionsAsync()
{
// Arrange
var functions = new List
{
CreateFunction("f1", "desc1"),
CreateFunction("f2", "desc2"),
CreateFunction("f3", "desc3")
};
var searchResults = new List>>
{
new(new Dictionary { ["Name"] = "f3" }, 0.3),
new(new Dictionary { ["Name"] = "f2" }, 0.2),
new(new Dictionary { ["Name"] = "f1" }, 0.1)
};
this._collectionMock.Setup(c => c.SearchAsync(It.IsAny(), It.IsAny(), null, It.IsAny()))
.Returns(searchResults.ToAsyncEnumerable());
var store = new FunctionStore(this._vectorStoreMock.Object, "col", 3, functions, 3);
// Act
var result = await store.SearchAsync("desc3");
// Assert
var resultList = result.ToList();
Assert.Equal(3, resultList.Count);
Assert.Equal("f3", resultList[0].Name);
Assert.Equal("f2", resultList[1].Name);
Assert.Equal("f1", resultList[2].Name);
}
[Fact]
public async Task SearchAsync_ShouldThrowIfCollectionDoesNotExistAsync()
{
// Arrange
var functions = new List { CreateFunction("f1") };
this._collectionMock.Setup(c => c.CollectionExistsAsync(It.IsAny()))
.ReturnsAsync(false);
var store = new FunctionStore(this._vectorStoreMock.Object, "col", 3, functions, 3);
// Act & Assert
await Assert.ThrowsAsync(() => store.SearchAsync("query"));
}
private static AIFunction CreateFunction(string name, string description = "desc")
{
return AIFunctionFactory.Create(() => { }, name, description);
}
}