Fix workflow rule validation logging
PR and Push Build/Test / build-and-test (push) Failing after 9m19s

This commit is contained in:
2026-06-03 17:33:12 +02:00
parent 40853115c5
commit 351ed2eb50
9 changed files with 243 additions and 102 deletions
@@ -538,34 +538,20 @@ public sealed class WorkflowRulesEditorTests
[Fact]
public async Task RulesEditorToolsRefuseInvalidYamlWithoutOverwritingFile()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
await File.WriteAllTextAsync(rulesPath, "rules: []");
var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
});
var fixture = await CreateRulesEditorFixtureAsync();
var result = await tools.WriteRules("rules:\n- name: [", replace_file: true);
var result = await fixture.Tools.WriteRules("rules:\n- name: [", replace_file: true);
Assert.StartsWith("Refused: workflow rules YAML is invalid.", result);
Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath));
Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath));
}
[Fact]
public async Task RulesEditorToolsRefuseWorkflowRulesThatWouldFailAtRuntime()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
await File.WriteAllTextAsync(rulesPath, "rules: []");
var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
});
var fixture = await CreateRulesEditorFixtureAsync();
var result = await tools.WriteRules("""
var result = await fixture.Tools.WriteRules("""
rules:
- name: broken-step
on:
@@ -579,22 +565,37 @@ public sealed class WorkflowRulesEditorTests
Assert.Contains("broken-step", result);
Assert.Contains("send_email", result);
Assert.Contains("add_attendee", result);
Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath));
Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath));
}
[Fact]
public async Task RulesEditorToolsLogWorkflowRuleValidationFailures()
{
var logger = new CapturingLogger();
var fixture = await CreateRulesEditorFixtureAsync(logger);
var result = await fixture.Tools.WriteRules("""
rules:
- name: broken-step
on:
- created: {}
steps:
- uses: send_email
value: Manuel
""", replace_file: true);
Assert.StartsWith("Refused: workflow rules are invalid.", result);
Assert.Contains(logger.Messages, message =>
message.Contains(nameof(WorkflowRulesEditorTools.WriteRules), StringComparison.Ordinal) &&
message.Contains("send_email", StringComparison.Ordinal));
}
[Fact]
public async Task RulesEditorToolsExplainUnsupportedWorkflowProperties()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
await File.WriteAllTextAsync(rulesPath, "rules: []");
var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
});
var fixture = await CreateRulesEditorFixtureAsync();
var result = await tools.WriteRules("""
var result = await fixture.Tools.WriteRules("""
rules:
- name: broken-property
on:
@@ -609,22 +610,15 @@ public sealed class WorkflowRulesEditorTests
Assert.Contains("broken-property", result);
Assert.Contains("meeting.status", result);
Assert.Contains("title", result);
Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath));
Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath));
}
[Fact]
public async Task RulesEditorToolsRefuseTranscriptLinePropertyOutsideTranscriptLineTrigger()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
await File.WriteAllTextAsync(rulesPath, "rules: []");
var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
});
var fixture = await CreateRulesEditorFixtureAsync();
var result = await tools.WriteRules("""
var result = await fixture.Tools.WriteRules("""
rules:
- name: broken-transcript-property
on:
@@ -639,22 +633,37 @@ public sealed class WorkflowRulesEditorTests
Assert.Contains("broken-transcript-property", result);
Assert.Contains("transcript.line", result);
Assert.Contains("transcript_line", result);
Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath));
Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath));
}
[Fact]
public async Task RulesEditorToolsAcceptAndParseMaskedProfanityRedactionRule()
{
var fixture = await CreateRulesEditorFixtureAsync();
var result = await fixture.Tools.WriteRules(MeetingWorkflowTestRules.MaskedProfanityRedactionYaml, replace_file: true);
Assert.Equal(fixture.RulesPath, result);
var provider = new FileMeetingWorkflowRulesProvider(
NullLogger<FileMeetingWorkflowRulesProvider>.Instance);
var rule = Assert.Single(await provider.GetRulesAsync(fixture.Options, CancellationToken.None));
Assert.Equal("redact-masked-profanity", rule.Name);
Assert.NotNull(Assert.Single(rule.On).TranscriptLine);
Assert.Equal("contains(transcript.line, '*****')", Assert.Single(rule.If).Condition);
var step = Assert.Single(rule.Steps);
Assert.Equal("set_property", step.Uses);
Assert.Equal("transcript.line", step.Property);
Assert.Equal(
"""@Model.Transcript.Line.Replace("*****", "[redacted]")""",
step.Value);
}
[Fact]
public async Task RulesEditorToolsRefuseMalformedRazorStepValues()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
await File.WriteAllTextAsync(rulesPath, "rules: []");
var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
});
var fixture = await CreateRulesEditorFixtureAsync();
var result = await tools.WriteRules("""
var result = await fixture.Tools.WriteRules("""
rules:
- name: broken-razor
on:
@@ -668,21 +677,15 @@ public sealed class WorkflowRulesEditorTests
Assert.Contains("broken-razor", result);
Assert.Contains("Razor", result);
Assert.Contains("ThisIsNotAModelVar", result);
Assert.Equal("rules: []", await File.ReadAllTextAsync(rulesPath));
Assert.Equal("rules: []", await File.ReadAllTextAsync(fixture.RulesPath));
}
[Fact]
public async Task RulesEditorToolsAllowValidRazorAndEmailStepValues()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
var tools = new WorkflowRulesEditorTools(new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
});
var fixture = await CreateRulesEditorFixtureAsync(initialContent: "");
var result = await tools.WriteRules("""
var result = await fixture.Tools.WriteRules("""
rules:
- name: valid-razor
on:
@@ -692,9 +695,9 @@ public sealed class WorkflowRulesEditorTests
value: "Speaker @Model.Speaker.Name can contact Support@example.com"
""", replace_file: true);
Assert.Equal(rulesPath, result);
Assert.Contains("@Model.Speaker.Name", await File.ReadAllTextAsync(rulesPath));
Assert.Contains("Support@example.com", await File.ReadAllTextAsync(rulesPath));
Assert.Equal(fixture.RulesPath, result);
Assert.Contains("@Model.Speaker.Name", await File.ReadAllTextAsync(fixture.RulesPath));
Assert.Contains("Support@example.com", await File.ReadAllTextAsync(fixture.RulesPath));
}
[Fact]
@@ -1159,6 +1162,55 @@ public sealed class WorkflowRulesEditorTests
}
}
private sealed class CapturingLogger : ILogger
{
public List<string> Messages { get; } = [];
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
Messages.Add(formatter(state, exception));
}
}
private static async Task<RulesEditorFixture> CreateRulesEditorFixtureAsync(
ILogger? logger = null,
string initialContent = "rules: []")
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var rulesPath = Path.Combine(root, "rules.yaml");
await File.WriteAllTextAsync(rulesPath, initialContent);
var options = new MeetingAssistantOptions
{
Automation = new AutomationOptions { RulesPath = rulesPath }
};
return new RulesEditorFixture(
rulesPath,
options,
new WorkflowRulesEditorTools(options, logger: logger));
}
private sealed record RulesEditorFixture(
string RulesPath,
MeetingAssistantOptions Options,
WorkflowRulesEditorTools Tools);
private sealed class WorkflowRulesEditorIdentityFixture : IAsyncDisposable
{
private readonly string tempDirectory;