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));
}
}
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));
}
}