// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests;
///
/// Unit tests for the class.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Test naming convention")]
public sealed class InMemoryActorStateStorageTests
{
private readonly InMemoryActorStateStorage _storage = new();
private readonly ActorId _testActorId = new("TestActor", "test-instance");
private readonly ActorId _anotherActorId = new("AnotherActor", "another-instance");
[Fact]
public async Task WriteStateAsync_WithSetValueOperation_ShouldStoreValueAsync()
{
// Arrange
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
var operations = new List
{
new SetValueOperation(Key, value)
};
// Act
var result = await this._storage.WriteStateAsync(this._testActorId, operations, "0", CancellationToken.None);
// Assert
Assert.True(result.Success);
Assert.NotEqual("0", result.ETag);
Assert.Equal(1, this._storage.GetKeyCount(this._testActorId));
}
[Fact]
public async Task WriteStateAsync_WithRemoveKeyOperation_ShouldRemoveValueAsync()
{
// Arrange
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
// First set a value
var setOperations = new List
{
new SetValueOperation(Key, value)
};
var setResult = await this._storage.WriteStateAsync(this._testActorId, setOperations, "0", CancellationToken.None);
// Now remove the value
var removeOperations = new List
{
new RemoveKeyOperation(Key)
};
// Act
var result = await this._storage.WriteStateAsync(this._testActorId, removeOperations, setResult.ETag, CancellationToken.None);
// Assert
Assert.True(result.Success);
Assert.NotEqual(setResult.ETag, result.ETag);
Assert.Equal(0, this._storage.GetKeyCount(this._testActorId));
}
[Fact]
public async Task WriteStateAsync_WithIncorrectETag_ShouldReturnFailureAsync()
{
// Arrange
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
var operations = new List
{
new SetValueOperation(Key, value)
};
// Act
var result = await this._storage.WriteStateAsync(this._testActorId, operations, "incorrect-etag", CancellationToken.None);
// Assert
Assert.False(result.Success);
Assert.Equal("0", result.ETag); // Should return current ETag
Assert.Equal(0, this._storage.GetKeyCount(this._testActorId));
}
[Fact]
public async Task ReadStateAsync_WithGetValueOperation_ShouldReturnValueAsync()
{
// Arrange
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
var writeOperations = new List
{
new SetValueOperation(Key, value)
};
await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None);
var readOperations = new List
{
new GetValueOperation(Key)
};
// Act
var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
// Assert
Assert.Single(result.Results);
var getValue = result.Results[0] as GetValueResult;
Assert.NotNull(getValue);
Assert.NotNull(getValue.Value);
Assert.Equal("testValue", getValue.Value?.GetString());
}
[Fact]
public async Task ReadStateAsync_WithGetValueOperationForNonExistentKey_ShouldReturnNullAsync()
{
// Arrange
var readOperations = new List
{
new GetValueOperation("nonExistentKey")
};
// Act
var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
// Assert
Assert.Single(result.Results);
var getValue = result.Results[0] as GetValueResult;
Assert.NotNull(getValue);
Assert.Null(getValue.Value);
}
[Fact]
public async Task ReadStateAsync_WithListKeysOperation_ShouldReturnAllKeysAsync()
{
// Arrange
const string Key1 = "key1";
const string Key2 = "key2";
var value1 = JsonSerializer.SerializeToElement("value1");
var value2 = JsonSerializer.SerializeToElement("value2");
var writeOperations = new List
{
new SetValueOperation(Key1, value1),
new SetValueOperation(Key2, value2)
};
await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None);
var readOperations = new List
{
new ListKeysOperation(continuationToken: null)
};
// Act
var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
// Assert
Assert.Single(result.Results);
var listKeys = result.Results[0] as ListKeysResult;
Assert.NotNull(listKeys);
Assert.Equal(2, listKeys.Keys.Count);
Assert.Contains(Key1, listKeys.Keys);
Assert.Contains(Key2, listKeys.Keys);
Assert.Null(listKeys.ContinuationToken);
}
[Fact]
public async Task ReadStateAsync_WithListKeysOperationForEmptyActor_ShouldReturnEmptyListAsync()
{
// Arrange
var readOperations = new List
{
new ListKeysOperation(continuationToken: null)
};
// Act
var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
// Assert
Assert.Single(result.Results);
var listKeys = result.Results[0] as ListKeysResult;
Assert.NotNull(listKeys);
Assert.Empty(listKeys.Keys);
Assert.Null(listKeys.ContinuationToken);
}
[Fact]
public async Task ReadStateAsync_WithListKeysOperationAndKeyPrefix_ShouldReturnFilteredKeysAsync()
{
// Arrange
const string PrefixKey1 = "prefix_key1";
const string PrefixKey2 = "prefix_key2";
const string OtherKey = "other_key";
var value1 = JsonSerializer.SerializeToElement("value1");
var value2 = JsonSerializer.SerializeToElement("value2");
var value3 = JsonSerializer.SerializeToElement("value3");
var writeOperations = new List
{
new SetValueOperation(PrefixKey1, value1),
new SetValueOperation(PrefixKey2, value2),
new SetValueOperation(OtherKey, value3)
};
await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None);
var readOperations = new List
{
new ListKeysOperation(continuationToken: null, keyPrefix: "prefix_")
};
// Act
var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
// Assert
Assert.Single(result.Results);
var listKeys = result.Results[0] as ListKeysResult;
Assert.NotNull(listKeys);
Assert.Equal(2, listKeys.Keys.Count);
Assert.Contains(PrefixKey1, listKeys.Keys);
Assert.Contains(PrefixKey2, listKeys.Keys);
Assert.DoesNotContain(OtherKey, listKeys.Keys);
Assert.Null(listKeys.ContinuationToken);
}
[Fact]
public async Task ReadStateAsync_WithListKeysOperationAndNonMatchingKeyPrefix_ShouldReturnEmptyListAsync()
{
// Arrange
const string Key1 = "key1";
const string Key2 = "key2";
var value1 = JsonSerializer.SerializeToElement("value1");
var value2 = JsonSerializer.SerializeToElement("value2");
var writeOperations = new List
{
new SetValueOperation(Key1, value1),
new SetValueOperation(Key2, value2)
};
await this._storage.WriteStateAsync(this._testActorId, writeOperations, "0", CancellationToken.None);
var readOperations = new List
{
new ListKeysOperation(continuationToken: null, keyPrefix: "prefix_")
};
// Act
var result = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
// Assert
Assert.Single(result.Results);
var listKeys = result.Results[0] as ListKeysResult;
Assert.NotNull(listKeys);
Assert.Empty(listKeys.Keys);
Assert.Null(listKeys.ContinuationToken);
}
[Fact]
public async Task MultipleOperations_ShouldBeProcessedInOrderAsync()
{
// Arrange
const string Key1 = "key1";
const string Key2 = "key2";
var value1 = JsonSerializer.SerializeToElement("value1");
var value2 = JsonSerializer.SerializeToElement("value2");
var operations = new List
{
new SetValueOperation(Key1, value1),
new SetValueOperation(Key2, value2),
new RemoveKeyOperation(Key1)
};
// Act
var result = await this._storage.WriteStateAsync(this._testActorId, operations, "0", CancellationToken.None);
// Assert
Assert.True(result.Success);
Assert.Equal(1, this._storage.GetKeyCount(this._testActorId));
// Verify remaining key
var readOperations = new List
{
new GetValueOperation(Key2)
};
var readResult = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
var getValue = readResult.Results[0] as GetValueResult;
Assert.NotNull(getValue);
Assert.Equal("value2", getValue.Value?.GetString());
}
[Fact]
public async Task DifferentActors_ShouldHaveIsolatedStateAsync()
{
// Arrange
const string Key = "sharedKey";
var value1 = JsonSerializer.SerializeToElement("value1");
var value2 = JsonSerializer.SerializeToElement("value2");
var operations1 = new List
{
new SetValueOperation(Key, value1)
};
var operations2 = new List
{
new SetValueOperation(Key, value2)
};
// Act
await this._storage.WriteStateAsync(this._testActorId, operations1, "0", CancellationToken.None);
await this._storage.WriteStateAsync(this._anotherActorId, operations2, "0", CancellationToken.None);
// Assert
Assert.Equal(2, this._storage.ActorCount);
Assert.Equal(1, this._storage.GetKeyCount(this._testActorId));
Assert.Equal(1, this._storage.GetKeyCount(this._anotherActorId));
// Verify values are different
var readOperations = new List
{
new GetValueOperation(Key)
};
var result1 = await this._storage.ReadStateAsync(this._testActorId, readOperations, CancellationToken.None);
var result2 = await this._storage.ReadStateAsync(this._anotherActorId, readOperations, CancellationToken.None);
var getValue1 = result1.Results[0] as GetValueResult;
var getValue2 = result2.Results[0] as GetValueResult;
Assert.NotNull(getValue1);
Assert.NotNull(getValue2);
Assert.Equal("value1", getValue1.Value?.GetString());
Assert.Equal("value2", getValue2.Value?.GetString());
}
[Fact]
public async Task ConcurrentOperations_ShouldBeThreadSafeAsync()
{
// Arrange
const int OperationCount = 100;
var tasks = new List();
// Act
for (int i = 0; i < OperationCount; i++)
{
var key = $"key{i}";
var value = JsonSerializer.SerializeToElement($"value{i}");
var actorId = new ActorId("TestActor", $"instance{i % 10}"); // 10 different actors
var operations = new List
{
new SetValueOperation(key, value)
};
tasks.Add(Task.Run(async () =>
{
// Retry logic to handle concurrent updates
var success = false;
var retryCount = 0;
const int MaxRetries = 10;
while (!success && retryCount < MaxRetries)
{
var currentETag = this._storage.GetETag(actorId);
var result = await this._storage.WriteStateAsync(actorId, operations, currentETag, CancellationToken.None);
success = result.Success;
retryCount++;
}
}));
}
await Task.WhenAll(tasks);
// Assert
Assert.Equal(10, this._storage.ActorCount); // 10 different actors
// Verify each actor has the expected number of keys
for (int i = 0; i < 10; i++)
{
var actorId = new ActorId("TestActor", $"instance{i}");
Assert.Equal(10, this._storage.GetKeyCount(actorId)); // Each actor should have 10 keys
}
}
[Fact]
public async Task Clear_ShouldRemoveAllStateAsync()
{
// Arrange
const string Key = "testKey";
var value = JsonSerializer.SerializeToElement("testValue");
var operations = new List
{
new SetValueOperation(Key, value)
};
await this._storage.WriteStateAsync(this._testActorId, operations, "0", CancellationToken.None);
Assert.Equal(1, this._storage.ActorCount);
// Act
this._storage.Clear();
// Assert
Assert.Equal(0, this._storage.ActorCount);
Assert.Equal(0, this._storage.GetKeyCount(this._testActorId));
Assert.Equal("0", this._storage.GetETag(this._testActorId));
}
[Fact]
public void GetETag_ForNewActor_ShouldReturnZero()
{
// Act
var etag = this._storage.GetETag(this._testActorId);
// Assert
Assert.Equal("0", etag);
}
[Fact]
public async Task WriteStateAsync_WithNullOperations_ShouldThrowArgumentNullExceptionAsync() =>
// Act & Assert
await Assert.ThrowsAsync(() =>
this._storage.WriteStateAsync(this._testActorId, null!, "0", CancellationToken.None).AsTask());
[Fact]
public async Task WriteStateAsync_WithNullETag_ShouldThrowArgumentNullExceptionAsync()
{
// Arrange
var operations = new List();
// Act & Assert
await Assert.ThrowsAsync(() =>
this._storage.WriteStateAsync(this._testActorId, operations, null!, CancellationToken.None).AsTask());
}
[Fact]
public async Task ReadStateAsync_WithNullOperations_ShouldThrowArgumentNullExceptionAsync() =>
// Act & Assert
await Assert.ThrowsAsync(() =>
this._storage.ReadStateAsync(this._testActorId, null!, CancellationToken.None).AsTask());
[Fact]
public async Task WriteStateAsync_WithCancelledToken_ShouldThrowOperationCanceledExceptionAsync()
{
// Arrange
var operations = new List();
var cancellationToken = new CancellationToken(canceled: true);
// Act & Assert
await Assert.ThrowsAsync(() =>
this._storage.WriteStateAsync(this._testActorId, operations, "0", cancellationToken).AsTask());
}
[Fact]
public async Task ReadStateAsync_WithCancelledToken_ShouldThrowOperationCanceledExceptionAsync()
{
// Arrange
var operations = new List();
var cancellationToken = new CancellationToken(canceled: true);
// Act & Assert
await Assert.ThrowsAsync(() =>
this._storage.ReadStateAsync(this._testActorId, operations, cancellationToken).AsTask());
}
[Fact]
public async Task ETagProgression_ShouldIncrementMonotonicallyAsync()
{
// Arrange
const string Key = "testKey";
var value1 = JsonSerializer.SerializeToElement("value1");
var value2 = JsonSerializer.SerializeToElement("value2");
// Act
var operations1 = new List { new SetValueOperation(Key, value1) };
var result1 = await this._storage.WriteStateAsync(this._testActorId, operations1, "0", CancellationToken.None);
var operations2 = new List { new SetValueOperation(Key, value2) };
var result2 = await this._storage.WriteStateAsync(this._testActorId, operations2, result1.ETag, CancellationToken.None);
// Assert
Assert.True(result1.Success);
Assert.True(result2.Success);
Assert.NotEqual("0", result1.ETag);
Assert.NotEqual(result1.ETag, result2.ETag);
// ETags should be numeric and increasing
Assert.True(long.Parse(result1.ETag) < long.Parse(result2.ETag));
}
[Fact]
public void ListKeysOperation_JsonSerialization_ShouldWorkCorrectly()
{
// Arrange
var operation = new ListKeysOperation(continuationToken: "token123", keyPrefix: "prefix_");
// Act - Serialize to JSON
var json = JsonSerializer.Serialize(operation);
// Deserialize back to object
var deserializedOperation = JsonSerializer.Deserialize(json);
// Assert
Assert.NotNull(deserializedOperation);
Assert.Equal("token123", deserializedOperation.ContinuationToken);
Assert.Equal("prefix_", deserializedOperation.KeyPrefix);
Assert.Equal(ActorReadOperationType.ListKeys, deserializedOperation.Type);
}
}