diff --git a/aevatar-framework.sln b/aevatar-framework.sln index f292ad46..9645bb64 100644 --- a/aevatar-framework.sln +++ b/aevatar-framework.sln @@ -20,6 +20,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.TestBase", "test\Ae EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.GAgents.Tests", "test\Aevatar.GAgents.Tests\Aevatar.GAgents.Tests.csproj", "{B06AEDD8-DCE4-4F23-B730-C353E627A8E4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.Plugins.Test", "test\Aevatar.Plugins.Test\Aevatar.Plugins.Test.csproj", "{8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.ProxyGAgent.Sdk", "test\Aevatar.ProxyGAgent.Sdk\Aevatar.ProxyGAgent.Sdk.csproj", "{41D14456-6FBB-4432-BBA0-C01DA8DABE2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aevatar.ProxyGAgent", "src\Aevatar.ProxyGAgent\Aevatar.ProxyGAgent.csproj", "{DA353205-DE92-45CA-995B-D5C492D55528}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,6 +40,9 @@ Global {6545F4B2-EB81-4CF6-8FEC-C5FF8E116C7C} = {96D7B45D-F041-44DD-BFCC-E17048911357} {55EF840B-F16D-4D15-9770-B3006ADB6EFB} = {96D7B45D-F041-44DD-BFCC-E17048911357} {B06AEDD8-DCE4-4F23-B730-C353E627A8E4} = {96D7B45D-F041-44DD-BFCC-E17048911357} + {8D9680A9-0BD0-46EC-AE4C-8FE666900C2E} = {96D7B45D-F041-44DD-BFCC-E17048911357} + {41D14456-6FBB-4432-BBA0-C01DA8DABE2E} = {94D0A422-54BA-48E4-9AAA-4C59BFF72F06} + {DA353205-DE92-45CA-995B-D5C492D55528} = {94D0A422-54BA-48E4-9AAA-4C59BFF72F06} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {70485291-29F6-4FBA-BFE3-3F400184B3C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -68,5 +77,17 @@ Global {B06AEDD8-DCE4-4F23-B730-C353E627A8E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {B06AEDD8-DCE4-4F23-B730-C353E627A8E4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B06AEDD8-DCE4-4F23-B730-C353E627A8E4}.Release|Any CPU.Build.0 = Release|Any CPU + {8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D9680A9-0BD0-46EC-AE4C-8FE666900C2E}.Release|Any CPU.Build.0 = Release|Any CPU + {41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41D14456-6FBB-4432-BBA0-C01DA8DABE2E}.Release|Any CPU.Build.0 = Release|Any CPU + {DA353205-DE92-45CA-995B-D5C492D55528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA353205-DE92-45CA-995B-D5C492D55528}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA353205-DE92-45CA-995B-D5C492D55528}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA353205-DE92-45CA-995B-D5C492D55528}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyGAgentInitialization.cs b/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyGAgentInitialization.cs new file mode 100644 index 00000000..60e410fe --- /dev/null +++ b/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyGAgentInitialization.cs @@ -0,0 +1,7 @@ +namespace Aevatar.Core.Abstractions.ProxyGAgent; + +[GenerateSerializer] +public class ProxyGAgentInitialization : InitializationEventBase +{ + [Id(0)] public byte[] PluginCode { get; set; } +} \ No newline at end of file diff --git a/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyGAgentState.cs b/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyGAgentState.cs new file mode 100644 index 00000000..9855c4e5 --- /dev/null +++ b/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyGAgentState.cs @@ -0,0 +1,8 @@ +namespace Aevatar.Core.Abstractions.ProxyGAgent; + +[GenerateSerializer] +public class ProxyGAgentState : StateBase +{ + [Id(0)] public byte[]? PluginCode { get; set; } + [Id(1)] public Dictionary? Database { get; set; } +} \ No newline at end of file diff --git a/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyStateLogEvent.cs b/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyStateLogEvent.cs new file mode 100644 index 00000000..187fa3f1 --- /dev/null +++ b/src/Aevatar.Core.Abstractions/ProxyGAgent/ProxyStateLogEvent.cs @@ -0,0 +1,7 @@ +namespace Aevatar.Core.Abstractions.ProxyGAgent; + +[GenerateSerializer] +public class ProxyStateLogEvent : StateLogEventBase +{ + [Id(0)] public Dictionary Data { get; set; } +} \ No newline at end of file diff --git a/src/Aevatar.ProxyGAgent/Aevatar.ProxyGAgent.csproj b/src/Aevatar.ProxyGAgent/Aevatar.ProxyGAgent.csproj new file mode 100644 index 00000000..8d3ceade --- /dev/null +++ b/src/Aevatar.ProxyGAgent/Aevatar.ProxyGAgent.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + enable + enable + Aevatar.ProxyGAgent + + + + + + + + + diff --git a/src/Aevatar.ProxyGAgent/ProxyCodeLoadContext.cs b/src/Aevatar.ProxyGAgent/ProxyCodeLoadContext.cs new file mode 100644 index 00000000..ea376848 --- /dev/null +++ b/src/Aevatar.ProxyGAgent/ProxyCodeLoadContext.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Aevatar.ProxyGAgent; + +public class ProxyCodeLoadContext : AssemblyLoadContext +{ + private readonly ISdkStreamManager _sdkStreamManager; + + public ProxyCodeLoadContext(ISdkStreamManager sdkStreamManager) : base(true) + { + _sdkStreamManager = sdkStreamManager; + } + + protected override Assembly Load(AssemblyName assemblyName) + { + return LoadFromFolderOrDefault(assemblyName); + } + + private Assembly LoadFromFolderOrDefault(AssemblyName assemblyName) + { + if (assemblyName.Name.StartsWith("Aevatar.ProxyGAgent.Sdk")) + { + // Sdk assembly should NOT be shared + using var stream = _sdkStreamManager.GetStream(assemblyName); + return LoadFromStream(stream); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Aevatar.ProxyGAgent/ProxyGAgent.cs b/src/Aevatar.ProxyGAgent/ProxyGAgent.cs new file mode 100644 index 00000000..d07ca745 --- /dev/null +++ b/src/Aevatar.ProxyGAgent/ProxyGAgent.cs @@ -0,0 +1,160 @@ +using System.Reflection; +using System.Runtime.Loader; +using Aevatar.Core; +using Aevatar.Core.Abstractions; +using Aevatar.Core.Abstractions.ProxyGAgent; +using Aevatar.ProxyGAgent.Sdk; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Aevatar.ProxyGAgent; + +[GAgent("proxy")] +public class ProxyGAgent : GAgentBase +{ + public ProxyGAgent(ILogger logger) : base(logger) + { + AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve!; + } + + private static Assembly? OnAssemblyResolve(object sender, ResolveEventArgs args) + { + var folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (folderPath == null) return null; + var assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll"); + if (!File.Exists(assemblyPath)) return null; + var assembly = Assembly.LoadFrom(assemblyPath); + return assembly; + } + + public override Task GetDescriptionAsync() + { + return Task.FromResult("This is a proxy GAgent for executing C# code."); + } + + public override async Task InitializeAsync(ProxyGAgentInitialization initializeDto) + { + RaiseEvent(new SetPluginCode + { + PluginCode = initializeDto.PluginCode + }); + await ConfirmEvents(); + } + + [GenerateSerializer] + public class SetPluginCode : ProxyStateLogEvent + { + [Id(0)] public byte[] PluginCode { get; set; } + } + + protected override void GAgentTransitionState(ProxyGAgentState state, StateLogEventBase @event) + { + if (@event is SetPluginCode setPluginCode) + { + State.PluginCode = setPluginCode.PluginCode; + return; + } + + if (State.PluginCode.IsNullOrEmpty()) + { + return; + } + + var assembly = Assembly.Load(State.PluginCode!); + var logEventConsistencyTypes = GetLogEventConsistencyTypes(assembly); + if (logEventConsistencyTypes == null) return; + foreach (var logEventConsistencyType in logEventConsistencyTypes) + { + var logEventConsistencyInstance = Activator.CreateInstance(logEventConsistencyType)!; + if (logEventConsistencyInstance is not ILogEventConsistency logEventConsistency) continue; + logEventConsistency.State = State; + logEventConsistency.Apply((ProxyStateLogEvent)@event); + } + } + + [AllEventHandler] + public async Task ExecuteEventHandlersAsync(EventWrapperBase eventData) + { + if (State.PluginCode.IsNullOrEmpty()) + { + return; + } + + var assembly = Assembly.Load(State.PluginCode!); + var handlerTypes = GetHandlerTypes(assembly); + + foreach (var handlerType in handlerTypes) + { + var interfaceType = GetHandlerInterfaceType(handlerType); + var eventType = interfaceType.GetGenericArguments()[0]; + + if (IsMatchingEventType(eventData, eventType)) + { + var handlerInstance = Activator.CreateInstance(handlerType); + var handleMethod = interfaceType.GetMethod(nameof(IGAgentEventHandler.HandleEventAsync)); + + if (handleMethod != null) + { + await InvokeHandleMethodAsync(handleMethod, handlerInstance!, eventData, eventType); + } + } + } + } + + private IEnumerable GetHandlerTypes(Assembly assembly) + { + var handlerInterfaceType = typeof(IGAgentEventHandler<>); + return assembly.GetTypes() + .Where(t => t.GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == handlerInterfaceType) && + t is { IsInterface: false, IsAbstract: false }); + } + + private IEnumerable? GetLogEventConsistencyTypes(Assembly? assembly) + { + if (assembly == null) return null; + var handlerInterfaceType = typeof(ILogEventConsistency); + return assembly.GetTypes() + .Where(t => handlerInterfaceType.IsAssignableFrom(t) + && t is { IsInterface: false, IsAbstract: false }); + } + + private Type GetHandlerInterfaceType(Type handlerType) + { + var handlerInterfaceType = typeof(IGAgentEventHandler<>); + return handlerType.GetInterfaces() + .First(i => i.IsGenericType && i.GetGenericTypeDefinition() == handlerInterfaceType); + } + + private bool IsMatchingEventType(EventWrapperBase eventData, Type eventType) + { + return eventData.GetType().IsGenericType && + eventData.GetType().GetGenericTypeDefinition() == typeof(EventWrapper<>) && + ((EventWrapper)eventData).Event.GetType().FullName == eventType.FullName; + } + + private async Task InvokeHandleMethodAsync(MethodInfo handleMethod, object handlerInstance, + EventWrapperBase eventData, Type eventType) + { + dynamic eventWrapper = eventData; + var eventJson = JsonConvert.SerializeObject(eventWrapper.Event); + var eventObject = JsonConvert.DeserializeObject(eventJson, eventType); + + var result = await (Task)handleMethod.Invoke(handlerInstance, new object[] { eventObject })!; + if (!result.StateLogEventList.IsNullOrEmpty()) + { + foreach (var stateLogEvent in result.StateLogEventList) + { + RaiseEvent(stateLogEvent); + } + } + + if (!result.GAgentEventBase.IsNullOrEmpty()) + { + foreach (var eventBase in result.GAgentEventBase) + { + await PublishAsync(eventBase); + } + } + } +} \ No newline at end of file diff --git a/src/Aevatar.ProxyGAgent/SdkStreamManager.cs b/src/Aevatar.ProxyGAgent/SdkStreamManager.cs new file mode 100644 index 00000000..4af29408 --- /dev/null +++ b/src/Aevatar.ProxyGAgent/SdkStreamManager.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace Aevatar.ProxyGAgent; + +public interface ISdkStreamManager +{ + Stream GetStream(AssemblyName assemblyName); +} + +public class SdkStreamManager(string sdkDir) : ISdkStreamManager +{ + private readonly ConcurrentDictionary _cachedSdkStreams = new(); + + public Stream GetStream(AssemblyName assemblyName) + { + var path = Path.Combine(sdkDir, assemblyName.Name + ".dll"); + if (!File.Exists(path)) + { + var assembly = Assembly.Load(assemblyName); + + path = assembly.Location; + } + + if (!_cachedSdkStreams.TryGetValue(path, out var buffer)) + { + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read); + var length = (int)fs.Length; + buffer = new byte[length]; + fs.ReadExactly(buffer, 0, length); + _cachedSdkStreams.TryAdd(path, buffer); + } + + return new MemoryStream(buffer); + } +} \ No newline at end of file diff --git a/test/Aevatar.Core.Tests/TestGAgents/NaiveTestGAgent.cs b/test/Aevatar.Core.Tests/TestGAgents/NaiveTestGAgent.cs index 133847bd..d838ecbd 100644 --- a/test/Aevatar.Core.Tests/TestGAgents/NaiveTestGAgent.cs +++ b/test/Aevatar.Core.Tests/TestGAgents/NaiveTestGAgent.cs @@ -10,13 +10,14 @@ public class NaiveTestGAgentState : StateBase [Id(0)] public List Content { get; set; } } +[GenerateSerializer] public class NaiveTestStateLogEvent : StateLogEventBase { [Id(0)] public Guid Id { get; set; } } [GAgent("naiveTest")] -public class NaiveTestGAgent : GAgentBase +public class NaiveTestGAgent : GAgentBase { public NaiveTestGAgent(ILogger logger) : base(logger) { @@ -27,13 +28,13 @@ public override Task GetDescriptionAsync() return Task.FromResult("This is a naive test GAgent"); } - public override async Task InitializeAsync(NaiveGAgentInitialize initialize) + public override async Task InitializeAsync(NaiveGAgentInitializationEvent initializationEvent) { if (State.Content.IsNullOrEmpty()) { State.Content = []; } - State.Content.Add(initialize.InitialGreeting); + State.Content.Add(initializationEvent.InitialGreeting); } } \ No newline at end of file diff --git a/test/Aevatar.Core.Tests/TestInitializeDtos/NaiveGAgentInitializeDto.cs b/test/Aevatar.Core.Tests/TestInitializeDtos/NaiveGAgentInitializationEvent.cs similarity index 70% rename from test/Aevatar.Core.Tests/TestInitializeDtos/NaiveGAgentInitializeDto.cs rename to test/Aevatar.Core.Tests/TestInitializeDtos/NaiveGAgentInitializationEvent.cs index 0d416ec6..0000faee 100644 --- a/test/Aevatar.Core.Tests/TestInitializeDtos/NaiveGAgentInitializeDto.cs +++ b/test/Aevatar.Core.Tests/TestInitializeDtos/NaiveGAgentInitializationEvent.cs @@ -3,7 +3,7 @@ namespace Aevatar.Core.Tests.TestInitializeDtos; [GenerateSerializer] -public class NaiveGAgentInitialize : InitializationEventBase +public class NaiveGAgentInitializationEvent : InitializationEventBase { [Id(0)] public string InitialGreeting { get; set; } } \ No newline at end of file diff --git a/test/Aevatar.Core.Tests/TestGEvents/GroupGEvent.cs b/test/Aevatar.Core.Tests/TestStateLogEvents/GroupStateLogEvent.cs similarity index 100% rename from test/Aevatar.Core.Tests/TestGEvents/GroupGEvent.cs rename to test/Aevatar.Core.Tests/TestStateLogEvents/GroupStateLogEvent.cs diff --git a/test/Aevatar.Core.Tests/TestGEvents/MessageGEvent.cs b/test/Aevatar.Core.Tests/TestStateLogEvents/MessageStateLogEvent.cs similarity index 100% rename from test/Aevatar.Core.Tests/TestGEvents/MessageGEvent.cs rename to test/Aevatar.Core.Tests/TestStateLogEvents/MessageStateLogEvent.cs diff --git a/test/Aevatar.Core.Tests/TestGEvents/PublishingGEvent.cs b/test/Aevatar.Core.Tests/TestStateLogEvents/PublishingStateLogEvent.cs similarity index 100% rename from test/Aevatar.Core.Tests/TestGEvents/PublishingGEvent.cs rename to test/Aevatar.Core.Tests/TestStateLogEvents/PublishingStateLogEvent.cs diff --git a/test/Aevatar.Core.Tests/TestGEvents/ReceiveMessageTestGEvent.cs b/test/Aevatar.Core.Tests/TestStateLogEvents/ReceiveMessageTestStateLogEvent.cs similarity index 100% rename from test/Aevatar.Core.Tests/TestGEvents/ReceiveMessageTestGEvent.cs rename to test/Aevatar.Core.Tests/TestStateLogEvents/ReceiveMessageTestStateLogEvent.cs diff --git a/test/Aevatar.GAgents.Tests/Aevatar.GAgents.Tests.csproj b/test/Aevatar.GAgents.Tests/Aevatar.GAgents.Tests.csproj index 46c0e53b..b37f1e44 100644 --- a/test/Aevatar.GAgents.Tests/Aevatar.GAgents.Tests.csproj +++ b/test/Aevatar.GAgents.Tests/Aevatar.GAgents.Tests.csproj @@ -26,7 +26,9 @@ + + @@ -34,6 +36,10 @@ Always + + + Always + diff --git a/test/Aevatar.GAgents.Tests/GAgentFactoryTests.cs b/test/Aevatar.GAgents.Tests/GAgentFactoryTests.cs index 5c55b5a7..6219faaf 100644 --- a/test/Aevatar.GAgents.Tests/GAgentFactoryTests.cs +++ b/test/Aevatar.GAgents.Tests/GAgentFactoryTests.cs @@ -42,7 +42,7 @@ public async Task CreateGAgentByGenericTypeTest() } { - var gAgent = await _gAgentFactory.GetGAgentAsync>(); + var gAgent = await _gAgentFactory.GetGAgentAsync>(); gAgent.ShouldNotBeNull(); Should.NotThrow(() => gAgent.GetPrimaryKey()); gAgent.GetGrainId().ShouldBe(GrainId.Create("aevatar/naiveTest", gAgent.GetPrimaryKey().ToString("N"))); @@ -63,14 +63,14 @@ public async Task CreateGAgentWithInitializeMethodTest() { // Arrange & Act. var guid = Guid.NewGuid(); - var gAgent = await _gAgentFactory.GetGAgentAsync>(guid, - new NaiveGAgentInitialize + var gAgent = await _gAgentFactory.GetGAgentAsync>(guid, + new NaiveGAgentInitializationEvent { InitialGreeting = "Test" }); var initializeDtoType = await gAgent.GetInitializationTypeAsync(); - initializeDtoType.ShouldBe(typeof(NaiveGAgentInitialize)); + initializeDtoType.ShouldBe(typeof(NaiveGAgentInitializationEvent)); await TestHelper.WaitUntilAsync(_ => CheckState(gAgent), TimeSpan.FromSeconds(20)); @@ -94,7 +94,7 @@ public async Task CreateGAgentByAliasTest() } { - var gAgent = await _gAgentFactory.GetGAgentAsync("naiveTest", initializeDto: new NaiveGAgentInitialize + var gAgent = await _gAgentFactory.GetGAgentAsync("naiveTest", initializeDto: new NaiveGAgentInitializationEvent { InitialGreeting = "Test" }); @@ -119,7 +119,7 @@ public async Task GetAvailableGAgentTypesTest() availableGAgents.Count.ShouldBeGreaterThan(20); } - private async Task CheckState(IStateGAgent gAgent) + private async Task CheckState(IStateGAgent gAgent) { var state = await gAgent.GetStateAsync(); return !state.Content.IsNullOrEmpty(); diff --git a/test/Aevatar.GAgents.Tests/ProxyGAgentPlugins/Aevatar.Plugins.Test.dll b/test/Aevatar.GAgents.Tests/ProxyGAgentPlugins/Aevatar.Plugins.Test.dll new file mode 100644 index 00000000..80488d94 Binary files /dev/null and b/test/Aevatar.GAgents.Tests/ProxyGAgentPlugins/Aevatar.Plugins.Test.dll differ diff --git a/test/Aevatar.GAgents.Tests/ProxyGAgentTests.cs b/test/Aevatar.GAgents.Tests/ProxyGAgentTests.cs new file mode 100644 index 00000000..7050b059 --- /dev/null +++ b/test/Aevatar.GAgents.Tests/ProxyGAgentTests.cs @@ -0,0 +1,54 @@ +using Aevatar.Core; +using Aevatar.Core.Abstractions; +using Aevatar.Core.Abstractions.ProxyGAgent; +using Aevatar.Core.Tests.TestGAgents; +using Aevatar.Plugins.Test; +using Shouldly; + +namespace Aevatar.GAgents.Tests; + +public class ProxyGAgentTests : AevatarGAgentsTestBase +{ + private readonly IGAgentFactory _gAgentFactory; + + public ProxyGAgentTests() + { + _gAgentFactory = GetRequiredService(); + } + + [Fact] + public async Task ProxyGAgentEventHandlerTest() + { + // Arrange. + var code = await File.ReadAllBytesAsync("ProxyGAgentPlugins/Aevatar.Plugins.Test.dll"); + var proxyGAgent = await _gAgentFactory.GetGAgentAsync("proxy", initializeDto: new ProxyGAgentInitialization + { + PluginCode = code + }); + var proxyTestGAgent = await _gAgentFactory.GetGAgentAsync>(); + var publishingGAgent = await _gAgentFactory.GetGAgentAsync(); + + // Act. + await publishingGAgent.RegisterAsync(proxyGAgent); + await publishingGAgent.RegisterAsync(proxyTestGAgent); + await publishingGAgent.PublishEventAsync(new PluginTestEvent()); + await TestHelper.WaitUntilAsync(_ => CheckCount(proxyTestGAgent, 1), TimeSpan.FromSeconds(30)); + + // Assert. + var proxyTestGAgentState = await proxyTestGAgent.GetStateAsync(); + proxyTestGAgentState.Content.Count.ShouldBe(1); + proxyTestGAgentState.Content[0].ShouldBe("Hello from TestEventHandler"); + var proxyGAgentWithState = + await _gAgentFactory.GetGAgentAsync>(proxyGAgent.GetPrimaryKey()); + var proxyGAgentState = await proxyGAgentWithState.GetStateAsync(); + proxyGAgentState.Database.ShouldNotBeNull(); + proxyGAgentState.Database.Count.ShouldBe(1); + proxyGAgentState.Database["Test"].ToString().ShouldBe("Raised event from TestEventHandler"); + } + + private async Task CheckCount(IStateGAgent gAgent, int expectedCount) + { + var state = await gAgent.GetStateAsync(); + return state.Content.Count == expectedCount; + } +} \ No newline at end of file diff --git a/test/Aevatar.GAgents.Tests/ProxyTestGAgent.cs b/test/Aevatar.GAgents.Tests/ProxyTestGAgent.cs new file mode 100644 index 00000000..aaa0b88c --- /dev/null +++ b/test/Aevatar.GAgents.Tests/ProxyTestGAgent.cs @@ -0,0 +1,44 @@ +using Aevatar.Core; +using Aevatar.Core.Abstractions; +using Aevatar.Core.Tests.TestInitializeDtos; +using Aevatar.Plugins.Test; +using Aevatar.ProxyGAgent.Sdk; +using Microsoft.Extensions.Logging; + +namespace Aevatar.GAgents.Tests; + +[GenerateSerializer] +public class ProxyTestGAgentState : StateBase +{ + [Id(0)] public List Content { get; set; } +} + +[GenerateSerializer] +public class ProxyTestStateLogEvent : StateLogEventBase +{ + [Id(0)] public Guid Id { get; set; } +} + +[GAgent("proxyTest")] +public class ProxyTestGAgent : GAgentBase +{ + public ProxyTestGAgent(ILogger logger) : base(logger) + { + } + + public override Task GetDescriptionAsync() + { + return Task.FromResult("This is a proxy test GAgent"); + } + + public async Task HandleEventAsync(ProxyGAgentEvent eventData) + { + if (State.Content.IsNullOrEmpty()) + { + State.Content = []; + } + + var data = eventData.EventData["Greeting"].ToString()!; + State.Content.Add(data); + } +} \ No newline at end of file diff --git a/test/Aevatar.Plugins.Test/Aevatar.Plugins.Test.csproj b/test/Aevatar.Plugins.Test/Aevatar.Plugins.Test.csproj new file mode 100644 index 00000000..b81c53e5 --- /dev/null +++ b/test/Aevatar.Plugins.Test/Aevatar.Plugins.Test.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/test/Aevatar.Plugins.Test/TestEventHandler.cs b/test/Aevatar.Plugins.Test/TestEventHandler.cs new file mode 100644 index 00000000..e77b94c2 --- /dev/null +++ b/test/Aevatar.Plugins.Test/TestEventHandler.cs @@ -0,0 +1,41 @@ +using Aevatar.Core.Abstractions; +using Aevatar.Core.Abstractions.ProxyGAgent; +using Aevatar.ProxyGAgent.Sdk; + +namespace Aevatar.Plugins.Test; + +[GenerateSerializer] +public class PluginTestEvent : EventBase +{ + [Id(0)] public string Greeting { get; set; } +} + +public class TestEventHandler : IGAgentEventHandler +{ + public async Task HandleEventAsync(PluginTestEvent eventBase) + { + return new EventHandleResult + { + GAgentEventBase = + [ + new ProxyGAgentEvent + { + EventData = new Dictionary + { + ["Greeting"] = "Hello from TestEventHandler" + } + } + ], + StateLogEventList = + [ + new ProxyStateLogEvent + { + Data = new Dictionary + { + ["Test"] = "Raised event from TestEventHandler" + } + } + ] + }; + } +} \ No newline at end of file diff --git a/test/Aevatar.Plugins.Test/TestLogEventConsistency.cs b/test/Aevatar.Plugins.Test/TestLogEventConsistency.cs new file mode 100644 index 00000000..f97ac814 --- /dev/null +++ b/test/Aevatar.Plugins.Test/TestLogEventConsistency.cs @@ -0,0 +1,15 @@ +using Aevatar.Core.Abstractions; +using Aevatar.Core.Abstractions.ProxyGAgent; +using Aevatar.ProxyGAgent.Sdk; + +namespace Aevatar.Plugins.Test; + +public class TestLogEventConsistency : ILogEventConsistency +{ + public ProxyGAgentState State { get; set; } + public void Apply(ProxyStateLogEvent eventData) + { + State.Database ??= new Dictionary(); + State.Database["Test"] = eventData.Data["Test"]; + } +} \ No newline at end of file diff --git a/test/Aevatar.ProxyGAgent.Sdk/Aevatar.ProxyGAgent.Sdk.csproj b/test/Aevatar.ProxyGAgent.Sdk/Aevatar.ProxyGAgent.Sdk.csproj new file mode 100644 index 00000000..d6385534 --- /dev/null +++ b/test/Aevatar.ProxyGAgent.Sdk/Aevatar.ProxyGAgent.Sdk.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/test/Aevatar.ProxyGAgent.Sdk/IGAgentEventHandler.cs b/test/Aevatar.ProxyGAgent.Sdk/IGAgentEventHandler.cs new file mode 100644 index 00000000..5ca5a603 --- /dev/null +++ b/test/Aevatar.ProxyGAgent.Sdk/IGAgentEventHandler.cs @@ -0,0 +1,15 @@ +using Aevatar.Core.Abstractions; +using Aevatar.Core.Abstractions.ProxyGAgent; + +namespace Aevatar.ProxyGAgent.Sdk; + +public interface IGAgentEventHandler where T : EventBase +{ + Task HandleEventAsync(T eventBase); +} + +public class EventHandleResult +{ + public List StateLogEventList { get; set; } + public List GAgentEventBase { get; set; } +} \ No newline at end of file diff --git a/test/Aevatar.ProxyGAgent.Sdk/ILogEventConsistency.cs b/test/Aevatar.ProxyGAgent.Sdk/ILogEventConsistency.cs new file mode 100644 index 00000000..1bceb864 --- /dev/null +++ b/test/Aevatar.ProxyGAgent.Sdk/ILogEventConsistency.cs @@ -0,0 +1,10 @@ +using Aevatar.Core.Abstractions; +using Aevatar.Core.Abstractions.ProxyGAgent; + +namespace Aevatar.ProxyGAgent.Sdk; + +public interface ILogEventConsistency +{ + ProxyGAgentState State { set; } + void Apply(ProxyStateLogEvent eventData); +} \ No newline at end of file diff --git a/test/Aevatar.ProxyGAgent.Sdk/ProxyGAgentEvent.cs b/test/Aevatar.ProxyGAgent.Sdk/ProxyGAgentEvent.cs new file mode 100644 index 00000000..f7cd4b36 --- /dev/null +++ b/test/Aevatar.ProxyGAgent.Sdk/ProxyGAgentEvent.cs @@ -0,0 +1,9 @@ +using Aevatar.Core.Abstractions; + +namespace Aevatar.ProxyGAgent.Sdk; + +[GenerateSerializer] +public class ProxyGAgentEvent : EventBase +{ + [Id(0)] public Dictionary EventData { get; set; } +} \ No newline at end of file