diff --git a/examples/ExampleJobSummaryConvention.cs b/examples/ExampleJobSummaryConvention.cs
new file mode 100644
index 0000000000..5d61928f6e
--- /dev/null
+++ b/examples/ExampleJobSummaryConvention.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections;
+using PSRule.Definitions;
+using PSRule.Definitions.Conventions;
+using PSRule.Runtime;
+
+namespace PSRule.Examples;
+
+///
+/// Example convention that demonstrates how to contribute content to job summaries.
+/// This shows how convention authors can implement IJobSummaryContributor to extend job summary output.
+///
+internal sealed class ExampleJobSummaryConvention : BaseConvention, IConventionV1, IJobSummaryContributor
+{
+ private int _processedCount = 0;
+ private int _successCount = 0;
+
+ public ExampleJobSummaryConvention(ISourceFile source, string name) : base(source, name)
+ {
+ }
+
+ public override void Process(LegacyRunspaceContext context, IEnumerable input)
+ {
+ // Example: Track some metrics during processing
+ foreach (var item in input)
+ {
+ _processedCount++;
+ // Simulate some processing logic
+ if (_processedCount % 2 == 0)
+ _successCount++;
+ }
+ }
+
+ public IEnumerable? GetJobSummaryContent()
+ {
+ // Return custom sections for the job summary
+ var sections = new List();
+
+ // Add a metrics section
+ var metricsContent = $@"The example convention processed {_processedCount} items with {_successCount} successful operations.
+
+### Breakdown
+- Total items: {_processedCount}
+- Successful: {_successCount}
+- Success rate: {(_processedCount > 0 ? (_successCount * 100.0 / _processedCount):0):F1}%";
+
+ sections.Add(new JobSummarySection("Convention Metrics", new InfoString(metricsContent)));
+
+ // Add an additional information section
+ var additionalInfo = @"This section demonstrates how conventions can contribute custom content to job summaries.
+
+Convention authors can:
+- Add custom metrics and statistics
+- Provide configuration summaries
+- Include environment information
+- Display custom analysis results
+
+For more information, see the [PSRule conventions documentation](https://microsoft.github.io/PSRule/concepts/conventions/).";
+
+ sections.Add(new JobSummarySection("Convention Information", new InfoString(additionalInfo)));
+
+ return sections;
+ }
+
+ #region IConventionV1 implementation (required but not relevant for this example)
+
+ public IResourceHelpInfo Info => throw new NotImplementedException();
+ public ResourceFlags Flags => ResourceFlags.None;
+ public ISourceExtent Extent => throw new NotImplementedException();
+ public ResourceKind Kind => ResourceKind.Convention;
+ public string ApiVersion => "v1";
+ public ResourceId? Ref => null;
+ public ResourceId[]? Alias => null;
+ public IResourceTags? Tags => null;
+ public IResourceLabels? Labels => null;
+
+ #endregion
+}
\ No newline at end of file
diff --git a/examples/JobSummaryContribution.md b/examples/JobSummaryContribution.md
new file mode 100644
index 0000000000..8bee0eca43
--- /dev/null
+++ b/examples/JobSummaryContribution.md
@@ -0,0 +1,57 @@
+# PSRule Job Summary Convention Extension Example
+
+This example demonstrates how conventions can contribute additional information to PSRule job summaries.
+
+## Overview
+
+The new `IJobSummaryContributor` interface allows conventions to append custom sections to job summary output. This enables:
+
+- Custom metrics and statistics
+- Environment information
+- Configuration summaries
+- Additional analysis results
+
+## Usage
+
+Conventions can implement the `IJobSummaryContributor` interface to contribute content:
+
+```csharp
+public class MyConvention : BaseConvention, IConventionV1, IJobSummaryContributor
+{
+ public IEnumerable? GetJobSummaryContent()
+ {
+ return new[]
+ {
+ new JobSummarySection("Custom Metrics", "- Processed: 100 items\n- Success rate: 95%"),
+ new JobSummarySection("Environment", "- OS: Windows\n- Runtime: .NET 8.0")
+ };
+ }
+
+ // ... other convention implementation
+}
+```
+
+## Example Output
+
+The job summary will include additional sections after the standard PSRule content:
+
+```markdown
+# PSRule result summary
+
+ā PSRule completed with an overall result of 'Fail' with 10 rule(s) and 5 target(s) in 00:00:02.123.
+
+## Analysis
+...
+
+## Custom Metrics
+- Processed: 100 items
+- Success rate: 95%
+
+## Environment
+- OS: Windows
+- Runtime: .NET 8.0
+```
+
+## Backward Compatibility
+
+This feature is fully backward compatible. Existing conventions and job summaries will continue to work unchanged. Only conventions that explicitly implement `IJobSummaryContributor` will contribute additional content.
\ No newline at end of file
diff --git a/src/PSRule.Types/Runtime/IConventionContext.cs b/src/PSRule.Types/Runtime/IConventionContext.cs
new file mode 100644
index 0000000000..3c67d40ea6
--- /dev/null
+++ b/src/PSRule.Types/Runtime/IConventionContext.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace PSRule.Runtime;
+
+///
+/// An interface for context available while executing a convention.
+///
+public interface IConventionContext
+{
+ ///
+ /// A collection of items to add to the run summary.
+ ///
+ SummaryCollection Summary { get; }
+}
diff --git a/src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs b/src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs
index 40c76d0de3..4201262ff2 100644
--- a/src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs
+++ b/src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs
@@ -33,4 +33,11 @@ void AddService()
/// A unique name of the service instance.
/// An instance of the service.
void AddService(string instanceName, object instance);
+
+ ///
+ /// Add a convention.
+ ///
+ /// The convention type to add.
+ void AddConvention()
+ where TConvention : class;
}
diff --git a/src/PSRule/Definitions/IJobSummaryContributor.cs b/src/PSRule/Definitions/IJobSummaryContributor.cs
new file mode 100644
index 0000000000..ad4c01e992
--- /dev/null
+++ b/src/PSRule/Definitions/IJobSummaryContributor.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace PSRule.Definitions;
+
+///
+/// Defines an interface for conventions that can contribute additional information to job summaries.
+///
+public interface IJobSummaryContributor
+{
+ ///
+ /// Gets additional content to include in the job summary.
+ /// This method is called during job summary generation to collect custom content from conventions.
+ ///
+ ///
+ /// A collection of job summary sections, where each section contains a title and content.
+ /// The content can be formatted differently for different output formats (plain text for console, markdown for files).
+ /// Returns null or empty collection if no additional content should be added.
+ ///
+ IEnumerable? GetJobSummaryContent();
+}
diff --git a/src/PSRule/Definitions/JobSummarySection.cs b/src/PSRule/Definitions/JobSummarySection.cs
new file mode 100644
index 0000000000..35d16c56cc
--- /dev/null
+++ b/src/PSRule/Definitions/JobSummarySection.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace PSRule.Definitions;
+
+///
+/// Represents a section of content to be included in a job summary.
+///
+/// The title for the section (will be rendered as H2 header).
+/// The content for the section.
+public sealed class JobSummarySection(string title, InfoString content)
+{
+ ///
+ /// Gets the title for this section.
+ ///
+ public string Title { get; } = title ?? throw new ArgumentNullException(nameof(title));
+
+ ///
+ /// Gets the content for this section.
+ ///
+ public InfoString Content { get; } = content ?? throw new ArgumentNullException(nameof(content));
+}
diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj
index 471bd0c14b..0943a44679 100644
--- a/src/PSRule/PSRule.csproj
+++ b/src/PSRule/PSRule.csproj
@@ -33,7 +33,7 @@
- $(version)
+ 0.0.1
true
diff --git a/src/PSRule/Pipeline/AssertPipelineBuilder.cs b/src/PSRule/Pipeline/AssertPipelineBuilder.cs
index 805fc6c025..4e4c263cbf 100644
--- a/src/PSRule/Pipeline/AssertPipelineBuilder.cs
+++ b/src/PSRule/Pipeline/AssertPipelineBuilder.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using PSRule.Configuration;
+using PSRule.Definitions;
using PSRule.Pipeline.Output;
using PSRule.Rules;
@@ -59,10 +60,13 @@ private bool ShouldOutput()
if (!RequireModules() || !RequireWorkspaceCapabilities() || !RequireSources())
return null;
- var context = PrepareContext(PipelineHookActions.Default, writer: HandleJobSummary(writer ?? PrepareWriter()), checkModuleCapabilities: true);
+ var context = PrepareContext(PipelineHookActions.Default, writer: writer, checkModuleCapabilities: true);
if (context == null)
return null;
+ // Update job summary writer to include conventions
+ writer = HandleJobSummary(writer, context);
+
return new InvokeRulePipeline
(
context: context,
@@ -71,17 +75,21 @@ private bool ShouldOutput()
);
}
- private IPipelineWriter HandleJobSummary(IPipelineWriter writer)
+ private IPipelineWriter HandleJobSummary(IPipelineWriter writer, PipelineContext context)
{
if (string.IsNullOrEmpty(Option.Output.JobSummaryPath))
return writer;
+ // Get conventions that contribute to job summaries
+ var contributors = context.ResourceCache.OfType().ToArray();
+
return new JobSummaryWriter
(
inner: writer,
option: Option,
shouldProcess: ShouldProcess,
- source: Source
+ source: Source,
+ contributors: contributors.Length > 0 ? contributors : null
);
}
}
diff --git a/src/PSRule/Pipeline/Output/JobSummaryWriter.cs b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs
index 7926e722d5..6b1e2241d1 100644
--- a/src/PSRule/Pipeline/Output/JobSummaryWriter.cs
+++ b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs
@@ -3,6 +3,7 @@
using System.Text;
using PSRule.Configuration;
+using PSRule.Definitions;
using PSRule.Resources;
using PSRule.Rules;
@@ -23,12 +24,13 @@ internal sealed class JobSummaryWriter : ResultOutputWriter
private readonly Encoding _Encoding;
private readonly JobSummaryFormat _JobSummary;
private readonly Source[]? _Source;
+ private readonly IJobSummaryContributor[]? _Contributors;
private Stream? _Stream;
- private StreamWriter _Writer;
+ private StreamWriter? _Writer;
private bool _IsDisposed;
- public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string? outputPath = null, Stream? stream = null, Source[]? source = null)
+ public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string? outputPath = null, Stream? stream = null, Source[]? source = null, IJobSummaryContributor[]? contributors = null)
: base(inner, option, shouldProcess)
{
_OutputPath = outputPath ?? Environment.GetRootedPath(Option.Output.JobSummaryPath);
@@ -36,6 +38,7 @@ public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProces
_JobSummary = JobSummaryFormat.Default;
_Stream = stream;
_Source = source;
+ _Contributors = contributors;
if (Option.Output.As == ResultFormat.Summary && inner != null)
inner.WriteError(new PipelineConfigurationException("Output.As", PSRuleResources.PSR0002), "PSRule.Output.AsOutputSerialization", System.Management.Automation.ErrorCategory.InvalidOperation);
@@ -135,6 +138,47 @@ private void Complete()
FinalResult(results);
Source();
Analysis(results);
+ AdditionalInformation();
+ }
+
+ private void AdditionalInformation()
+ {
+ if (_Contributors == null || _Contributors.Length == 0)
+ return;
+
+ var sections = new List();
+
+ // Collect content from all contributors
+ for (var i = 0; i < _Contributors.Length; i++)
+ {
+ try
+ {
+ var contributorSections = _Contributors[i].GetJobSummaryContent();
+ if (contributorSections != null)
+ {
+ sections.AddRange(contributorSections);
+ }
+ }
+ catch
+ {
+ // Ignore exceptions from individual contributors to prevent them from breaking the entire job summary
+ continue;
+ }
+ }
+
+ // Write sections if any content was provided
+ if (sections.Count > 0)
+ {
+ foreach (var section in sections)
+ {
+ if (!string.IsNullOrWhiteSpace(section.Title) && section.Content != null && section.Content.HasValue)
+ {
+ H2(section.Title);
+ WriteLine(section.Content.Markdown);
+ WriteLine();
+ }
+ }
+ }
}
private void Analysis(InvokeResult[] o)
diff --git a/src/PSRule/Pipeline/ResultOutputWriter.cs b/src/PSRule/Pipeline/ResultOutputWriter.cs
index 7714b14880..76e4af17e9 100644
--- a/src/PSRule/Pipeline/ResultOutputWriter.cs
+++ b/src/PSRule/Pipeline/ResultOutputWriter.cs
@@ -13,7 +13,7 @@ internal abstract class ResultOutputWriter : PipelineWriter
protected ResultOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess)
: base(inner, option, shouldProcess)
{
- _Result = new List();
+ _Result = [];
}
public override void WriteObject(object sendToPipeline, bool enumerateCollection)
diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs
index 5ed4edc679..ba1edc6bfc 100644
--- a/src/PSRule/Runtime/Assert.cs
+++ b/src/PSRule/Runtime/Assert.cs
@@ -1523,7 +1523,7 @@ private static string GetFileType(string value)
///
/// Determine line comment prefix by file extension
///
- private static string DetectLinePrefix(string extension)
+ private static string DetectLinePrefix(string? extension)
{
extension = extension?.ToLower();
switch (extension)
diff --git a/src/PSRule/Runtime/ILanguageScope.cs b/src/PSRule/Runtime/ILanguageScope.cs
index 80ccc3bce6..f93682fe7c 100644
--- a/src/PSRule/Runtime/ILanguageScope.cs
+++ b/src/PSRule/Runtime/ILanguageScope.cs
@@ -67,6 +67,11 @@ internal interface ILanguageScope : IDisposable
///
IEnumerable GetEmitters();
+ ///
+ /// Get any conventions added to the scope.
+ ///
+ IEnumerable GetConventions();
+
ITargetBindingResult? Bind(ITargetObject targetObject);
///
diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs
index 86150de253..5c22010281 100644
--- a/src/PSRule/Runtime/LanguageScope.cs
+++ b/src/PSRule/Runtime/LanguageScope.cs
@@ -19,6 +19,7 @@ internal sealed class LanguageScope(string name, RuntimeFactoryContainer? contai
private readonly RuntimeFactoryContainer? _Container = container;
private readonly Dictionary _Service = [];
private readonly List _EmitterTypes = [];
+ private readonly List _ConventionTypes = [];
private readonly Dictionary _Filter = [];
private IDictionary? _Configuration = new Dictionary(StringComparer.OrdinalIgnoreCase);
@@ -138,6 +139,12 @@ public IEnumerable GetEmitters()
return _EmitterTypes;
}
+ ///
+ public IEnumerable GetConventions()
+ {
+ return _ConventionTypes;
+ }
+
public ITargetBindingResult? Bind(ITargetObject targetObject)
{
return _TargetBinder?.Bind(targetObject);
@@ -236,5 +243,11 @@ void IRuntimeServiceCollection.AddService()
_EmitterTypes.Add(typeof(TService));
}
+ ///
+ void IRuntimeServiceCollection.AddConvention()
+ {
+ _ConventionTypes.Add(typeof(TConvention));
+ }
+
#endregion IRuntimeServiceCollection
}
diff --git a/tests/PSRule.Tests/MockLanguageScope.cs b/tests/PSRule.Tests/MockLanguageScope.cs
index b1cf9fcfce..97dd5cbb4d 100644
--- a/tests/PSRule.Tests/MockLanguageScope.cs
+++ b/tests/PSRule.Tests/MockLanguageScope.cs
@@ -63,6 +63,11 @@ public IEnumerable GetEmitters()
throw new NotImplementedException();
}
+ public IEnumerable GetConventions()
+ {
+ throw new NotImplementedException();
+ }
+
public IResourceFilter GetFilter(ResourceKind kind)
{
throw new NotImplementedException();
diff --git a/tests/PSRule.Tests/Pipeline/Output/JobSummaryWriterTests.cs b/tests/PSRule.Tests/Pipeline/Output/JobSummaryWriterTests.cs
index 46e54df696..c78fdc375a 100644
--- a/tests/PSRule.Tests/Pipeline/Output/JobSummaryWriterTests.cs
+++ b/tests/PSRule.Tests/Pipeline/Output/JobSummaryWriterTests.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.IO;
+using PSRule.Definitions;
using PSRule.Definitions.Rules;
namespace PSRule.Pipeline.Output;
@@ -22,7 +23,7 @@ public void JobSummary()
result.Add(GetPass());
result.Add(GetFail());
result.Add(GetFail("rid-003", SeverityLevel.Warning, ruleId: "TestModule\\Rule-003"));
- var writer = new JobSummaryWriter(output, option, null, outputPath: "reports/summary.md", stream: stream);
+ var writer = new JobSummaryWriter(output, option, null, outputPath: "reports/summary.md", stream: stream, source: null, contributors: null);
writer.Begin();
writer.WriteObject(result, false);
context.RunTime.Stop();
@@ -33,4 +34,71 @@ public void JobSummary()
var s = reader.ReadToEnd().Replace(System.Environment.NewLine, "\r\n");
Assert.Equal($"# PSRule result summary\r\n\r\nā PSRule completed with an overall result of 'Fail' with 3 rule(s) and 1 target(s) in {context.RunTime.Elapsed}.\r\n\r\n## Analysis\r\n\r\nThe following results were reported with fail or error results.\r\n\r\nName | Target name | Synopsis\r\n---- | ----------- | --------\r\nrule-002 | TestObject1 | This is rule 002.\r\nRule-003 | TestObject1 | This is rule 002.\r\n", s);
}
+
+ [Fact]
+ public void JobSummaryWithContributors()
+ {
+ using var stream = new MemoryStream();
+ var option = GetOption();
+ var output = new TestWriter(option);
+ var result = new InvokeResult(GetRun());
+ var context = GetPipelineContext(option: option, writer: output, resourceCache: GetResourceCache());
+ result.Add(GetPass());
+ result.Add(GetFail());
+
+ // Create mock contributors
+ var contributors = new IJobSummaryContributor[]
+ {
+ new TestJobSummaryContributor("Custom Section", "This is custom content from a convention.\n\n- Item 1\n- Item 2"),
+ new TestJobSummaryContributor("Another Section", "Additional information:\n\n| Key | Value |\n|-----|-------|\n| Status | Success |\n| Count | 42 |")
+ };
+
+ var writer = new JobSummaryWriter(output, option, null, outputPath: "reports/summary.md", stream: stream, source: null, contributors: contributors);
+ writer.Begin();
+ writer.WriteObject(result, false);
+ context.RunTime.Stop();
+ writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None));
+
+ stream.Seek(0, SeekOrigin.Begin);
+ using var reader = new StreamReader(stream);
+ var s = reader.ReadToEnd().Replace(System.Environment.NewLine, "\r\n");
+
+ // Verify that the custom sections are included
+ Assert.Contains("## Custom Section", s);
+ Assert.Contains("This is custom content from a convention.", s);
+ Assert.Contains("## Another Section", s);
+ Assert.Contains("Additional information:", s);
+ Assert.Contains("| Status | Success |", s);
+ }
+
+ [Fact]
+ public void JobSummaryWithEmptyContributors()
+ {
+ using var stream = new MemoryStream();
+ var option = GetOption();
+ var output = new TestWriter(option);
+ var result = new InvokeResult(GetRun());
+ var context = GetPipelineContext(option: option, writer: output, resourceCache: GetResourceCache());
+ result.Add(GetPass());
+
+ // Create contributor that returns no content
+ var contributors = new IJobSummaryContributor[]
+ {
+ new TestJobSummaryContributor("", "") // Should be ignored due to empty strings
+ };
+
+ var writer = new JobSummaryWriter(output, option, null, outputPath: "reports/summary.md", stream: stream, source: null, contributors: contributors);
+ writer.Begin();
+ writer.WriteObject(result, false);
+ context.RunTime.Stop();
+ writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None));
+
+ stream.Seek(0, SeekOrigin.Begin);
+ using var reader = new StreamReader(stream);
+ var s = reader.ReadToEnd().Replace(System.Environment.NewLine, "\r\n");
+
+ // Should only contain standard content, no additional sections
+ Assert.Contains("# PSRule result summary", s);
+ Assert.DoesNotContain("## Custom Section", s);
+ }
}
diff --git a/tests/PSRule.Tests/Pipeline/Output/TestJobSummaryContributor.cs b/tests/PSRule.Tests/Pipeline/Output/TestJobSummaryContributor.cs
new file mode 100644
index 0000000000..c27e189caf
--- /dev/null
+++ b/tests/PSRule.Tests/Pipeline/Output/TestJobSummaryContributor.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using PSRule.Definitions;
+
+namespace PSRule.Pipeline.Output;
+
+#nullable enable
+
+///
+/// Test implementation of IJobSummaryContributor for testing purposes.
+///
+internal sealed class TestJobSummaryContributor(string title, string content) : IJobSummaryContributor
+{
+ private readonly string _Title = title;
+ private readonly string _Content = content;
+
+ public IEnumerable? GetJobSummaryContent()
+ {
+ if (string.IsNullOrWhiteSpace(_Title) || string.IsNullOrWhiteSpace(_Content))
+ return null;
+
+ return [new JobSummarySection(_Title, new InfoString(_Content))];
+ }
+}
+
+#nullable restore
diff --git a/tests/PSRule.Tests/Runtime/ConventionRegistrationTests.cs b/tests/PSRule.Tests/Runtime/ConventionRegistrationTests.cs
new file mode 100644
index 0000000000..bd60c68705
--- /dev/null
+++ b/tests/PSRule.Tests/Runtime/ConventionRegistrationTests.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Linq;
+using PSRule.Runtime;
+
+namespace PSRule.Tests.Runtime;
+
+///
+/// Tests for convention registration via IRuntimeServiceCollection.
+///
+public sealed class ConventionRegistrationTests
+{
+ ///
+ /// Test that conventions can be registered via AddConvention method.
+ ///
+ [Fact]
+ public void AddConvention_WhenCalled_ShouldRegisterConvention()
+ {
+ // Arrange
+ var scope = new LanguageScope("test", null);
+ var serviceCollection = scope as IRuntimeServiceCollection;
+
+ // Act
+ serviceCollection.AddConvention();
+
+ // Assert
+ var conventions = scope.GetConventions();
+ Assert.Single(conventions);
+ Assert.Equal(typeof(TestConvention), conventions.First());
+ }
+
+ ///
+ /// Test that multiple conventions can be registered.
+ ///
+ [Fact]
+ public void AddConvention_WhenMultipleConventions_ShouldRegisterAll()
+ {
+ // Arrange
+ var scope = new LanguageScope("test", null);
+ var serviceCollection = scope as IRuntimeServiceCollection;
+
+ // Act
+ serviceCollection.AddConvention();
+ serviceCollection.AddConvention();
+
+ // Assert
+ var conventions = scope.GetConventions().ToArray();
+ Assert.Equal(2, conventions.Length);
+ Assert.Contains(typeof(TestConvention), conventions);
+ Assert.Contains(typeof(TestConvention2), conventions);
+ }
+
+ ///
+ /// Test that GetConventions returns empty collection when no conventions are registered.
+ ///
+ [Fact]
+ public void GetConventions_WhenNoConventions_ShouldReturnEmpty()
+ {
+ // Arrange
+ var scope = new LanguageScope("test", null);
+
+ // Act
+ var conventions = scope.GetConventions();
+
+ // Assert
+ Assert.Empty(conventions);
+ }
+
+ ///
+ /// Test convention class for testing.
+ ///
+ private sealed class TestConvention
+ {
+ }
+
+ ///
+ /// Test convention class for testing.
+ ///
+ private sealed class TestConvention2
+ {
+ }
+}
diff --git a/tests/PSRule.Tests/TestData/Conventions/JobSummary.Convention.ps1 b/tests/PSRule.Tests/TestData/Conventions/JobSummary.Convention.ps1
new file mode 100644
index 0000000000..471510c86a
--- /dev/null
+++ b/tests/PSRule.Tests/TestData/Conventions/JobSummary.Convention.ps1
@@ -0,0 +1,12 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# Test convention that demonstrates job summary contribution capabilities
+# Note: To actually contribute to job summaries, a convention would need to be implemented in C#
+# and inherit from BaseConvention while implementing IJobSummaryContributor
+
+Convention 'Test.JobSummaryConvention' {
+ Begin {
+ Write-Host "Test convention for job summary contribution"
+ }
+}