XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEExtension.NetCore.XUnit.Assertions;
using XFEExtension.NetCore.XUnit.Attributes;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Core.Runtime;
using XFEWorkflow.Infrastructure.Compilation;

namespace XFEWorkflow.Test;

[TestFixture]
public class PortTypeTests
{
    [Test]
    public void ConcreteTypesRequireAnExactMatchAndNominalMessagesRejectPlainObjects()
    {
        foreach (var source in Enum.GetValues<WorkflowValueType>())
        foreach (var target in Enum.GetValues<WorkflowValueType>())
            Assert.Equal(source == target || target == WorkflowValueType.Any, GraphValidator.Compatible(new PortDefinition { Type = source }, new PortDefinition { Type = target }));
        var message = new PortDefinition { Type = WorkflowValueType.Object, TypeId = "xfe.wecom.message" };
        Assert.True(GraphValidator.Compatible(message, WorkflowJson.Clone(message)));
        Assert.True(!GraphValidator.Compatible(message, new() { Type = WorkflowValueType.Object }));
        Assert.True(!GraphValidator.Compatible(new() { Type = WorkflowValueType.Object }, message));
        Assert.True(!GraphValidator.Compatible(message, new() { Type = WorkflowValueType.Object, TypeId = "other.message" }));
        Assert.True(GraphValidator.Compatible(message, new() { Type = WorkflowValueType.Any }));
    }

    [Test]
    public async Task TriggerDeclaredFieldsPathsAndLegacyAliasesReachIndependentTypedInputs()
    {
        var registry = new CapabilityRegistry(); new Plugins.WeCom.WeComPlugin().Register(registry);
        JsonObject? captured = null;
        registry.RegisterAction(new() { Id = "test.capture", Inputs =
            [P("message", WorkflowValueType.Object, "xfe.wecom.message"), P("legacy", WorkflowValueType.Object, "xfe.wecom.message"),
             P("content", WorkflowValueType.String), P("groupId", WorkflowValueType.String), P("senderId", WorkflowValueType.String), P("hasContent", WorkflowValueType.Boolean)] },
            () => new Capture(input => captured = WorkflowJson.Copy(input)));
        var receive = new NodeDefinition { Id = "receive", CapabilityId = "xfe.wecom.message.received", Trigger = new() { Kind = TriggerKind.Plugin, CapabilityId = "xfe.wecom.message.received" } };
        var capture = new NodeDefinition { Id = "capture", CapabilityId = "test.capture" };
        var workflow = new WorkflowDefinition { Graph = new() { Nodes = [receive, capture], Edges = [new() { FromNode = receive.Id, ToNode = capture.Id }] } };
        foreach (var name in new[] { "message", "content", "groupId", "senderId", "hasContent" }) workflow.Graph.Edges.Add(new() { Kind = PortKind.Data, FromNode = receive.Id, FromPort = name, ToNode = capture.Id, ToPort = name });
        workflow.Graph.Edges.Add(new() { Kind = PortKind.Data, FromNode = receive.Id, FromPort = "event", ToNode = capture.Id, ToPort = "legacy" });
        var data = new JsonObject { ["dispatchId"] = "fixture", ["content"] = "正文", ["conversationId"] = "群", ["senderId"] = "成员", ["hasContent"] = true };
        var context = TestSupport.Context(registry, new() { ["trigger"] = new JsonObject { ["data"] = data.DeepClone() } }, entryNodeId: receive.Id);
        using var compiler = new CodeCompiler(); var catalog = CoreCapabilities.Catalog(registry.Descriptors.Values);
        Assert.True(catalog[receive.CapabilityId].Outputs.Count > 5);
        Assert.True(!catalog[receive.CapabilityId].Outputs.Any(x => x.Name == "event"));
        Assert.True(!catalog[receive.CapabilityId].Inputs.Any());
        Assert.Equal(0, GraphValidator.Validate(workflow, catalog).Count);
        await new WorkflowEngine(compiler, catalog).ExecuteAsync(workflow, context, default);
        Assert.True(captured is not null && JsonNode.DeepEquals(data, captured["message"]) && JsonNode.DeepEquals(data, captured["legacy"]));
        Assert.Equal("正文", captured!.String("content")); Assert.Equal("群", captured.String("groupId")); Assert.Equal("成员", captured.String("senderId")); Assert.True(captured.Boolean("hasContent"));
        Assert.True(JsonNode.DeepEquals(data, context.NodeOutputs[receive.Id]["event"]));
    }

    [Test]
    public void TriggerProjectionDoesNotModifyThePluginAndObjectCannotFeedMessageInput()
    {
        var plugin = new Plugins.WeCom.WeComPlugin().Manifest;
        var original = plugin.Capabilities.Single(x => x.IsTrigger); var descriptor = WorkflowTriggerNodes.AsNodeDescriptor(original);
        descriptor.Outputs[0].Label = "changed";
        Assert.True(original.Outputs[0].Label != "changed");
        var flow = Plugins.WeCom.WeComWorkflowImport.Template(); var receive = flow.Graph.Nodes[0];
        flow.Graph.Nodes[1].CapabilityId = "xfe.wecom.quoteReply"; flow.Graph.Nodes[1].Parameters["content"] = "1";
        flow.Graph.Edges.First(x => x.Kind == PortKind.Data).ToPort = "message";
        var catalog = CoreCapabilities.Catalog(plugin.Capabilities);
        flow.Graph.Edges.First(x => x.Kind == PortKind.Data).FromPort = "content";
        Assert.True(GraphValidator.Validate(flow, catalog).Any(x => x.Message.Contains("类型不兼容")));
        flow.Graph.Edges.First(x => x.Kind == PortKind.Data).FromPort = "event";
        Assert.Equal(0, GraphValidator.Validate(flow, catalog).Count);
        var fallback = WorkflowTriggerNodes.AsNodeDescriptor(new() { Id = "test.legacy", IsTrigger = true });
        Assert.Equal("", fallback.Outputs.Single(x => x.Name == "event").EventPath);
        var end = new NodeDefinition { CapabilityId = "core.end" }; flow.Graph.Nodes.Add(end);
        flow.Graph.Edges.Add(new() { Kind = PortKind.Data, FromNode = receive.Id, FromPort = "content", ToNode = end.Id, ToPort = "result" });
        Assert.Equal(WorkflowValueType.String, WorkflowPortCatalog.Resolve(flow.Graph, end.Id, "result", false, catalog)!.Type);
    }

    [Test]
    public async Task OlderTriggerManifestsKeepTheirWholeEventAndInputsBindings()
    {
        var legacy = new CapabilityDescriptor { Id = "legacy.received", IsTrigger = true, Outputs = [P("event", WorkflowValueType.Object)] };
        var catalog = CoreCapabilities.Catalog([legacy]);
        var receive = new NodeDefinition { CapabilityId = legacy.Id, Trigger = new() { Kind = TriggerKind.Plugin, CapabilityId = legacy.Id } };
        var end = new NodeDefinition { CapabilityId = "core.end" };
        var flow = new WorkflowDefinition { Graph = new() { Nodes = [receive, end], Edges = [new() { FromNode = receive.Id, ToNode = end.Id },
            new() { Kind = PortKind.Data, FromNode = receive.Id, FromPort = "event", ToNode = end.Id, ToPort = "result" }] } };
        using var compiler = new CodeCompiler(); var context = TestSupport.Context(inputs: new() { ["trigger"] = new JsonObject { ["data"] = new JsonObject { ["messageId"] = "old" } } }, entryNodeId: receive.Id);
        var result = await new WorkflowEngine(compiler, catalog).ExecuteAsync(flow, context, default);
        Assert.Equal("old", result["result"]!["messageId"]!.GetValue<string>());
        Assert.True(catalog[legacy.Id].Outputs.Any(x => x.Name == "inputs"));
        var timer = CoreCapabilities.Catalog(new Plugins.Standard.TimerPlugin().Manifest.Capabilities)["xfe.timer.interval"];
        Assert.True(timer.Outputs.Any(x => x.Name == "timestamp") && timer.Outputs.Any(x => x.Name == "event"));
    }

    [Test]
    public void ConversionsParseInvariantValuesAndRejectMalformedOrWrongShapes()
    {
        var culture = CultureInfo.CurrentCulture;
        try
        {
            CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
            Assert.Equal(12.5m, WorkflowConversions.Convert(JsonValue.Create("12.5"), "number")!.GetValue<decimal>());
            Assert.True(WorkflowConversions.Convert(JsonValue.Create("TRUE"), "boolean")!.GetValue<bool>());
            Assert.True(!WorkflowConversions.Convert(JsonValue.Create(0), "boolean")!.GetValue<bool>());
            Assert.Equal("中文", WorkflowConversions.Convert(JsonValue.Create("中文"), "string")!.GetValue<string>());
            Assert.Equal(2, WorkflowConversions.Convert(JsonValue.Create("[1,2]"), "array")!.AsArray().Count);
            Assert.Equal(1, WorkflowConversions.Convert(JsonValue.Create("{\"n\":1}"), "object")!["n"]!.GetValue<int>());
            Assert.Equal("null", WorkflowConversions.Convert(null, "json")!.GetValue<string>());
            var obj = new JsonObject { ["x"] = "a" }; var copy = WorkflowConversions.Convert(obj, "object")!.AsObject(); copy["x"] = "b"; Assert.Equal("a", obj.String("x"));
            foreach (var invalid in new[] { "NaN", "Infinity", "1,25", "wrong" }) Assert.Throws<ArgumentException>(() => WorkflowConversions.Convert(JsonValue.Create(invalid), "number"));
            Assert.Throws<ArgumentException>(() => WorkflowConversions.Convert(null, "number"));
            Assert.Throws<ArgumentException>(() => WorkflowConversions.Convert(JsonValue.Create("yes"), "boolean"));
            Assert.Throws<ArgumentException>(() => WorkflowConversions.Convert(JsonValue.Create("[]"), "object"));
            Assert.Throws<ArgumentException>(() => WorkflowConversions.Convert(JsonValue.Create("{}"), "array"));
            Assert.Throws<JsonException>(() => WorkflowConversions.Convert(JsonValue.Create("broken"), "object"));
        }
        finally { CultureInfo.CurrentCulture = culture; }
    }

    [Test]
    public async Task ActionPortAliasesPreserveConnectionsAndNodeBindings()
    {
        var registry = new CapabilityRegistry();
        registry.RegisterAction(new() { Id = "test.renamed", Outputs = [new() { Name = "content", Type = WorkflowValueType.String, Aliases = ["oldContent"] }] }, () => new RenamedAction());
        registry.RegisterAction(new() { Id = "test.consume", Inputs = [new() { Name = "message", Type = WorkflowValueType.String, Aliases = ["oldMessage"] }, P("binding", WorkflowValueType.String)] },
            () => new Capture(input => { Assert.Equal("兼容正文", input.String("message")); Assert.Equal("兼容正文", input.String("binding")); }));
        var source = new NodeDefinition { Id = "source", CapabilityId = "test.renamed" };
        var target = new NodeDefinition { CapabilityId = "test.consume", Parameters = new() { ["binding"] = new JsonObject { ["$node"] = "source.oldContent" } } };
        var workflow = TestSupport.Graph(source, target);
        workflow.Graph.Edges.Add(new() { Kind = PortKind.Data, FromNode = source.Id, FromPort = "oldContent", ToNode = target.Id, ToPort = "oldMessage" });
        using var compiler = new CodeCompiler();
        await new WorkflowEngine(compiler, CoreCapabilities.Catalog(registry.Descriptors.Values)).ExecuteAsync(workflow, TestSupport.Context(registry), default);
    }

    [Test]
    public async Task ExplicitConversionWorksInGraphAndGeneratedCodeButUnknownAnyCannotFeedTypedInput()
    {
        var set = new NodeDefinition { CapabilityId = "core.set", Parameters = new() { ["name"] = "n", ["value"] = "12.5" } };
        var convert = new NodeDefinition { CapabilityId = "core.convert.number" }; var workflow = TestSupport.Graph(set, convert);
        workflow.Outputs = [P("result", WorkflowValueType.Number)];
        workflow.Graph.Edges.AddRange([new() { Kind = PortKind.Data, FromNode = set.Id, FromPort = "value", ToNode = convert.Id, ToPort = "value" },
            new() { Kind = PortKind.Data, FromNode = convert.Id, FromPort = "value", ToNode = workflow.Graph.Nodes[^1].Id, ToPort = "result" }]);
        using var compiler = new CodeCompiler(); var catalog = CoreCapabilities.Catalog();
        var result = await new WorkflowEngine(compiler, catalog).ExecuteAsync(workflow, TestSupport.Context(), default);
        Assert.Equal(12.5m, result["result"]!.GetValue<decimal>());
        workflow.Code = GraphCodeGenerator.Generate(workflow.Graph, workflow.Name); workflow.Kind = WorkflowKind.CSharp;
        var codeResult = await new WorkflowEngine(compiler, catalog).ExecuteAsync(workflow, TestSupport.Context(), default);
        Assert.Equal(12.5m, codeResult["result"]!.GetValue<decimal>());
        workflow.Kind = WorkflowKind.Graph; workflow.Graph.Edges[^1].FromNode = set.Id;
        Assert.True(GraphValidator.Validate(workflow, catalog).Any(x => x.Message.Contains("类型不兼容")));
    }
    private static PortDefinition P(string name, WorkflowValueType type, string typeId = "") => new() { Name = name, Type = type, TypeId = typeId, Required = true };
    private sealed class Capture(Action<JsonObject> capture) : IWorkflowAction
    {
        public ValueTask<JsonObject> ExecuteAsync(JsonObject input, IWorkflowContext context, CancellationToken ct) { capture(input); return ValueTask.FromResult(new JsonObject()); }
    }
    private sealed class RenamedAction : IWorkflowAction
    {
        public ValueTask<JsonObject> ExecuteAsync(JsonObject input, IWorkflowContext context, CancellationToken ct) => ValueTask.FromResult(new JsonObject { ["content"] = "兼容正文" });
    }
}