using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using w4c_workflows.Data; using w4c_workflows.Services.Credentials; using w4c_workflows.Services.Nodes; using w4c_workflows.Services.Nodes.Executors; using w4c_workflows.Services.Nodes.Interpolation; using Xunit; namespace w4c_workflows.Tests; /// /// End-to-end check that a node workflow run emits one audit entry per executed /// node through the real wiring. /// public class NodeWorkflowAuditTests { private const string Tenant = "audit-tenant"; private const string Yaml = """ name: audit-demo tasks: - id: prepare node: { type: core.set } parameters: mode: manual fields: - name: status value: ready next: decide - id: decide node: { type: core.if } parameters: left: "={{ $json.status }}" operator: equals right: ready edges: - { output: 0, to: ok } - { output: 1, to: nope } - id: ok node: { type: core.noop } - id: nope node: { type: core.noop } """; [Fact] public async Task Audit_records_one_entry_per_executed_node() { var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); await using var _connection = connection; await using var db = new WorkflowsDbContext(new DbContextOptionsBuilder() .UseSqlite(connection) .Options); await WorkflowsSchema.ApplyAsync(db, liteMode: true); var catalog = NodeTestData.CoreCatalog(); var workflow = WorkflowDataHelpers.CompileAndSave(db, Tenant, Yaml, "workflows/audit.yaml"); var run = WorkflowDataHelpers.AddPendingRun(db, workflow, "[{}]"); var sink = new RecordingActionAuditSink(); var executors = new List { new NoOpNodeExecutor(), new SetNodeExecutor(), new IfNodeExecutor(), }; var graphRunner = new NodeGraphRunner( new NodeExecutorRegistry(executors), new NodeParameterInterpolator()); var vault = new CredentialVault(new ReversibleTestCipher(), new CredentialTypeCatalog()); var runner = new NodeWorkflowRunner(catalog, graphRunner, vault, null, sink); var outcome = await runner.RunAsync(db, run, workflow, workingDirectory: null, default); Assert.True(outcome.Succeeded, outcome.Error); Assert.Equal(4, sink.Entries.Count); Assert.All(sink.Entries, entry => { Assert.Equal("node.executed", entry.Action); Assert.Equal("succeeded", entry.Outcome); Assert.Equal(Tenant, entry.TenantId); Assert.Equal(run.Id.ToString(), entry.RunId); }); Assert.Equal( new[] { "decide", "nope", "ok", "prepare" }, sink.Entries.Select(e => e.NodeId!).OrderBy(x => x, StringComparer.Ordinal).ToArray()); } }