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"] = "兼容正文" });
}
}
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"] = "兼容正文" });
}
}