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

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
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;
using XFEWorkflow.Infrastructure.Execution;
using XFEWorkflow.Infrastructure.Plugins;
using XFEWorkflow.Plugins.Standard;

namespace XFEWorkflow.Test;

[TestFixture]
public class GraphPluginTests
{
    [Test]
    public async Task GraphPluginHasTypedArgumentsMultipleReturnsAndRunsInIndependentRunner()
    {
        await using var workspace = new TestWorkspace(); var plugins = new PluginManager(workspace.Paths, workspace.State);
        var code = new NodeDefinition { CapabilityId = "core.csharp", Code = "return new JsonObject { [\"text\"] = context.Inputs[\"name\"]?.DeepClone(), [\"count\"] = 3, [\"ok\"] = true };" };
        var method = TestSupport.Graph(code); method.Inputs = [new() { Name = "name", Label = "名称", Type = WorkflowValueType.String, Required = true }];
        method.Outputs = [new() { Name = "text", Type = WorkflowValueType.String }, new() { Name = "count", Type = WorkflowValueType.Number }, new() { Name = "ok", Type = WorkflowValueType.Boolean }];
        method.Graph.Edges.Add(new() { Kind = PortKind.Data, FromNode = method.Graph.Nodes[0].Id, FromPort = "inputs", ToNode = code.Id, ToPort = "inputs" });
        foreach (var port in method.Outputs) method.Graph.Nodes[^1].Parameters[port.Name] = new JsonObject { ["$node"] = code.Id + ".result." + port.Name };
        var project = new GraphPluginProject { Id = "user.fixture", Name = "测试方法", Methods = [new() { Workflow = method }] };
        var package = Path.Combine(workspace.Paths.Temp, "methods.xfeplugin"); var manifest = await new GraphPluginBuilder(plugins).BuildAsync(project, package);
        await plugins.InstallAsync(package);
        var parent = TestSupport.Graph(new NodeDefinition { CapabilityId = "user.fixture.run", Parameters = new() { ["name"] = "图形参数" } });
        foreach (var port in method.Outputs) parent.Graph.Nodes[^1].Parameters[port.Name] = new JsonObject { ["$node"] = parent.Graph.Nodes[1].Id + "." + port.Name };
        await using var coordinator = new RunCoordinator(workspace.Paths, plugins, new(workspace.State), new(), TestSupport.FindOutput("XFEWorkflow.Runner", "XFEWorkflow.Runner.exe"));
        var graphRun = await coordinator.RunAsync(parent); Assert.Equal(RunStatus.Succeeded, graphRun.Status, graphRun.Error); Assert.Equal("图形参数", graphRun.Outputs.String("text"));
        parent.Kind = WorkflowKind.CSharp;
        parent.Code = "using System.Threading; using System.Threading.Tasks; using System.Text.Json.Nodes; using XFEWorkflow.Abstractions; public class Fixture : IWorkflow { public async ValueTask<JsonObject> ExecuteAsync(IWorkflowContext context, CancellationToken ct) { var (a,b,c) = await " + PluginMethodBindings.CallName(manifest.Capabilities[0]) + "(context, \"元组参数\", ct); return new JsonObject { [\"a\"] = a, [\"b\"] = b, [\"c\"] = c }; } }";
        var codeRun = await coordinator.RunAsync(parent); Assert.Equal(RunStatus.Succeeded, codeRun.Status, codeRun.Error);
        Assert.Equal("元组参数", codeRun.Outputs.String("a")); Assert.Equal(3, codeRun.Outputs.Integer("b")); Assert.True(codeRun.Outputs.Boolean("c"));
    }
    [Test]
    public async Task SelectedNodesPreserveCrossBoundaryBindingsAndLockTheirPluginDependencies()
    {
        await using var workspace = new TestWorkspace(); var plugins = new PluginManager(workspace.Paths, workspace.State);
        await plugins.InstallAsync(TestSupport.Package(workspace.Paths.Temp, new FilePlugin().Manifest));
        var file = Path.Combine(workspace.Paths.Temp, "数据.txt"); await File.WriteAllTextAsync(file, "边界结果");
        var source = new NodeDefinition { CapabilityId = "core.set", Parameters = new() { ["name"] = "source", ["value"] = file } };
        var read = new NodeDefinition { CapabilityId = "xfe.file.read", Parameters = new() { ["path"] = new JsonObject { ["$node"] = source.Id + ".value" } } };
        var flow = TestSupport.Graph(source, read); flow.Graph.Nodes[^1].Parameters["result"] = new JsonObject { ["$node"] = read.Id + ".text" };
        var extracted = new NodeEncapsulation(flow.Graph, [read.Id], plugins.Catalog(), "读取并处理");
        Assert.Equal(1, extracted.Method.Inputs.Count); Assert.Equal(1, extracted.Method.Outputs.Count);
        var project = new GraphPluginProject { Id = "user.read", Name = "自定义文件", Methods = [new() { Workflow = extracted.Method }] };
        var package = Path.Combine(workspace.Paths.Temp, "read.xfeplugin"); var manifest = await new GraphPluginBuilder(plugins).BuildAsync(project, package);
        Assert.Equal("1.0.0", manifest.Dependencies.Single().ExactVersion); await plugins.InstallAsync(package);
        flow.Graph = extracted.Replace(flow.Graph, "user.read.run");
        await using var coordinator = new RunCoordinator(workspace.Paths, plugins, new(workspace.State), new(), TestSupport.FindOutput("XFEWorkflow.Runner", "XFEWorkflow.Runner.exe"));
        var result = await coordinator.RunAsync(flow); Assert.Equal(RunStatus.Succeeded, result.Status, result.Error);
        Assert.Equal("边界结果", result.Outputs.String("result")); Assert.Equal(0, GraphValidator.Validate(flow, plugins.Catalog()).Count);
    }
    [Test]
    public async Task DependencyResolverBacktracksAcrossSharedRangesAndRejectsCyclesBeforeInstalling()
    {
        PluginManifest P(string id, string version, params PluginDependency[] dependencies) => new() { Id = id, Name = id, Version = version, Assembly = "fixture.dll", EntryType = "Fixture", Dependencies = dependencies.ToList() };
        var root = P("user.root", "1.0.0", new PluginDependency { Id = "user.a" }, new PluginDependency { Id = "user.b" });
        var a2 = P("user.a", "2.0.0", new PluginDependency { Id = "user.shared", MinimumVersion = "2.0.0" });
        var a1 = P("user.a", "1.0.0", new PluginDependency { Id = "user.shared", MaximumVersionExclusive = "2.0.0" });
        var b = P("user.b", "1.0.0", new PluginDependency { Id = "user.shared", MaximumVersionExclusive = "2.0.0" });
        var versions = new[] { a2, a1, b, P("user.shared", "2.0.0"), P("user.shared", "1.0.0") };
        var plan = await PluginDependencyResolver.ResolveAsync(root, (id, _) => Task.FromResult(versions.Where(x => x.Id == id).ToList()));
        Assert.Equal("1.0.0", plan.Single(x => x.Id == "user.a").Version); Assert.Equal("user.root", plan[^1].Id);
        b.Dependencies = [new PluginDependency { Id = "user.root" }];
        await Assert.ThrowsAsync<InvalidOperationException>(() => PluginDependencyResolver.ResolveAsync(root, (id, _) => Task.FromResult(versions.Where(x => x.Id == id).ToList())));
    }
    [Test]
    public async Task NestedSelectionPreservesWorkflowInputsExternalNodesAndLoopVariables()
    {
        await using var workspace = new TestWorkspace(); var plugins = new PluginManager(workspace.Paths, workspace.State);
        var source = new NodeDefinition { CapabilityId = "core.set", Parameters = new() { ["name"] = "source", ["value"] = "外部" } };
        var body = TestSupport.Graph(); var bodyEnd = body.Graph.Nodes[^1];
        bodyEnd.Parameters["result"] = new JsonObject { ["source"] = new JsonObject { ["$node"] = source.Id + ".value" }, ["note"] = new JsonObject { ["$input"] = "note" }, ["item"] = new JsonObject { ["$var"] = "item" } };
        var loop = new NodeDefinition { CapabilityId = "core.foreach", Parameters = new() { ["items"] = new JsonArray(1, 2) }, Children = new() { ["body"] = body.Graph } };
        var flow = TestSupport.Graph(source, loop); flow.Graph.Nodes[^1].Parameters["result"] = new JsonObject { ["$node"] = bodyEnd.Id + ".result" };
        var selection = new NodeEncapsulation(flow.Graph, [loop.Id], plugins.Catalog(), "嵌套方法");
        var project = new GraphPluginProject { Id = "user.nested", Name = "嵌套流程", Methods = [new() { Workflow = selection.Method }] };
        var package = Path.Combine(workspace.Paths.Temp, "nested.xfeplugin"); await new GraphPluginBuilder(plugins).BuildAsync(project, package); await plugins.InstallAsync(package);
        flow.Graph = selection.Replace(flow.Graph, "user.nested.run");
        await using var coordinator = new RunCoordinator(workspace.Paths, plugins, new(workspace.State), new(), TestSupport.FindOutput("XFEWorkflow.Runner", "XFEWorkflow.Runner.exe"));
        var run = await coordinator.RunAsync(flow, inputs: new() { ["note"] = "输入" }); Assert.Equal(RunStatus.Succeeded, run.Status, run.Error);
        Assert.Equal("外部", run.Outputs["result"]!["source"]!.GetValue<string>()); Assert.Equal("输入", run.Outputs["result"]!["note"]!.GetValue<string>()); Assert.Equal(2, run.Outputs["result"]!["item"]!.GetValue<int>());
        var branch = new NodeDefinition { CapabilityId = "core.if", Parameters = new() { ["left"] = true } }; var unsafeFlow = TestSupport.Graph(branch); unsafeFlow.Graph.Edges.Single(x => x.FromNode == branch.Id).FromPort = "true";
        Assert.Throws<ArgumentException>(() => new NodeEncapsulation(unsafeFlow.Graph, [branch.Id], plugins.Catalog(), "不能改变提前结束语义"));
    }
    [Test]
    public async Task MethodsWithinOnePluginComposeWithoutSelfDependencyOrUnusedGraphLocks()
    {
        await using var workspace = new TestWorkspace(); var plugins = new PluginManager(workspace.Paths, workspace.State);
        var first = TestSupport.Graph(); first.Outputs = [new() { Name = "text", Type = WorkflowValueType.String }]; first.Graph.Nodes[^1].Parameters["text"] = "方法返回值";
        first.PluginVersions["unused.plugin"] = "9.0.0";
        var call = new NodeDefinition { CapabilityId = "user.methods.first" }; var second = TestSupport.Graph(call); second.Outputs = WorkflowJson.Clone(first.Outputs);
        second.Graph.Nodes[^1].Parameters["text"] = new JsonObject { ["$node"] = call.Id + ".text" };
        var project = new GraphPluginProject { Id = "user.methods", Name = "组合方法", Methods = [new() { Id = "first", Workflow = first }, new() { Id = "second", Workflow = second }] };
        var package = Path.Combine(workspace.Paths.Temp, "methods.xfeplugin"); var manifest = await new GraphPluginBuilder(plugins).BuildAsync(project, package); Assert.Equal(0, manifest.Dependencies.Count); await plugins.InstallAsync(package);
        var node = new NodeDefinition { CapabilityId = "user.methods.second" }; var flow = TestSupport.Graph(node); flow.Graph.Nodes[^1].Parameters["result"] = new JsonObject { ["$node"] = node.Id + ".text" };
        await using var coordinator = new RunCoordinator(workspace.Paths, plugins, new(workspace.State), new(), TestSupport.FindOutput("XFEWorkflow.Runner", "XFEWorkflow.Runner.exe"));
        var run = await coordinator.RunAsync(flow); Assert.Equal(RunStatus.Succeeded, run.Status, run.Error); Assert.Equal("方法返回值", run.Outputs.String("result"));
    }
    [Test]
    public async Task RuntimeDependencyResolutionBacktracksAndHonorsExplicitLocks()
    {
        await using var workspace = new TestWorkspace(); var plugins = new PluginManager(workspace.Paths, workspace.State);
        InstalledPlugin P(string id, string version, params PluginDependency[] dependencies) => new() { Manifest = new() { Id = id, Name = id, Version = version, Assembly = "fixture.dll", EntryType = "Fixture", Dependencies = dependencies.ToList(), Capabilities = [new() { Id = id + ".run", PluginId = id }] } };
        workspace.State.Store.Update(p => p.InstancePlugins = [P("user.a", "2.0.0", new PluginDependency { Id = "user.shared", MinimumVersion = "2.0.0" }), P("user.a", "1.0.0", new PluginDependency { Id = "user.shared", MaximumVersionExclusive = "2.0.0" }), P("user.b", "1.0.0", new PluginDependency { Id = "user.shared", MaximumVersionExclusive = "2.0.0" }), P("user.shared", "2.0.0"), P("user.shared", "1.0.0")]);
        var flow = TestSupport.Graph(new NodeDefinition { CapabilityId = "user.a.run" }, new NodeDefinition { CapabilityId = "user.b.run" }); var resolved = plugins.Resolve(flow);
        Assert.Equal("1.0.0", resolved.Single(x => x.Manifest.Id == "user.a").Manifest.Version); Assert.Equal("1.0.0", resolved.Single(x => x.Manifest.Id == "user.shared").Manifest.Version);
        flow.PluginVersions["user.a"] = "2.0.0"; Assert.Throws<InvalidOperationException>(() => plugins.Resolve(flow));
    }
}