feat(executor): trigger pinned_output overrides triggerItems on manual runs

This commit is contained in:
Claude
2026-08-30 14:17:29 +00:00
parent 8a68b13a70
commit e24bdb012b
2 changed files with 70 additions and 2 deletions

View File

@@ -99,7 +99,7 @@ public sealed class Executor
try
{
await RunAsync(eid, workflowId, graph, triggerNodeId, triggerItems, answerChannelId, logger, ct);
await RunAsync(eid, workflowId, graph, triggerNodeId, triggerKind, triggerItems, answerChannelId, logger, ct);
}
catch (Exception ex)
{
@@ -113,11 +113,21 @@ public sealed class Executor
private async Task RunAsync(
string executionId, string workflowId, WorkflowGraph graph,
string triggerNodeId, IReadOnlyList<Item> triggerItems,
string triggerNodeId, string triggerKind, IReadOnlyList<Item> triggerItems,
Guid? answerChannelId, ILogger logger, CancellationToken ct)
{
var triggerNode = graph.Nodes.FirstOrDefault(n => n.Id == triggerNodeId)
?? throw new InvalidOperationException($"Trigger node {triggerNodeId} not found in graph.");
// Manual runs honor the trigger's pinned_output — replaces the
// endpoint-passed triggerItems entirely. Live runs (webhook, NATS, etc.)
// ignore the pin so a stale value never corrupts real traffic.
if (triggerKind == "manual" && triggerNode.PinnedOutputJson is { } pinJson)
{
using var pinDoc = JsonDocument.Parse(pinJson);
triggerItems = WrapPin(pinDoc.RootElement);
}
var triggerStart = DateTimeOffset.UtcNow;
_neRepo.Create(executionId, triggerNodeId,
inputsJson: "[]",

View File

@@ -416,4 +416,62 @@ public class ExecutorTests
Assert.Single(items);
Assert.Equal("42", items[0].Json.GetRawText());
}
// --- trigger pinned_output on manual runs ------------------------------
[Fact]
public async Task Trigger_pinned_output_overrides_trigger_items_on_manual_run()
{
var rec = new Recorder { Id = "sink.trig_pin_manual" };
var (er, nr, exec, _) = Build(new TestTrigger(), rec);
var graph = new WorkflowGraph
{
Nodes = new[]
{
new WorkflowNode("t", "test.trigger", null, "{}",
PinnedOutputJson: """{"pinned":true}"""),
new WorkflowNode("s", "sink.trig_pin_manual", null, "{}"),
},
Edges = new[] { new WorkflowEdge("t", 0, "s", 0) },
};
var passedIn = new Core.Item(JsonSerializer.SerializeToElement(new { pinned = false }));
var eid = await exec.StartExecutionAsync("wf", graph, "t", "manual",
new[] { passedIn }, null, null, CancellationToken.None);
Assert.Equal("completed", er.Get(eid)!.Status);
var sinkRow = nr.ListByExecution(eid).Single(r => r.NodeId == "s");
using var inputs = JsonDocument.Parse(sinkRow.InputsJson);
var port0Item0Json = inputs.RootElement[0][0].GetProperty("json");
Assert.True(port0Item0Json.GetProperty("pinned").GetBoolean());
}
[Fact]
public async Task Trigger_pinned_output_is_ignored_on_non_manual_run()
{
var rec = new Recorder { Id = "sink.trig_pin_webhook" };
var (er, nr, exec, _) = Build(new TestTrigger(), rec);
var graph = new WorkflowGraph
{
Nodes = new[]
{
new WorkflowNode("t", "test.trigger", null, "{}",
PinnedOutputJson: """{"pinned":true}"""),
new WorkflowNode("s", "sink.trig_pin_webhook", null, "{}"),
},
Edges = new[] { new WorkflowEdge("t", 0, "s", 0) },
};
var passedIn = new Core.Item(JsonSerializer.SerializeToElement(new { pinned = false }));
var eid = await exec.StartExecutionAsync("wf", graph, "t", "http.webhook",
new[] { passedIn }, null, null, CancellationToken.None);
Assert.Equal("completed", er.Get(eid)!.Status);
var sinkRow = nr.ListByExecution(eid).Single(r => r.NodeId == "s");
using var inputs = JsonDocument.Parse(sinkRow.InputsJson);
var port0Item0Json = inputs.RootElement[0][0].GetProperty("json");
Assert.False(port0Item0Json.GetProperty("pinned").GetBoolean());
}
}