| 1 | using Microsoft.CodeAnalysis; |
| 2 | using Microsoft.CodeAnalysis.Text; |
| 3 | using Xunit; |
| 4 | |
| 5 | namespace AgentForge.PluginAnalyzers.Tests; |
| 6 | |
| 7 | public sealed class ForgePluginAnalyzerTests |
| 8 | { |
| 9 | [Fact] |
| 10 | public async Task InternalsVisibleTo_reports_FORGE001() |
| 11 | { |
| 12 | const string source = """ |
| 13 | [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AgentForge.Plugin.Issue")] |
| 14 | namespace AgentForge.Plugin.Test; |
| 15 | """; |
| 16 | |
| 17 | var diagnostics = await AnalyzerTestHelper.GetDiagnosticsAsync(source); |
| 18 | Assert.True(AnalyzerTestHelper.HasDiagnostic(diagnostics, ForgePluginAnalyzer.InternalsVisibleToId, DiagnosticSeverity.Error)); |
| 19 | } |
| 20 | |
| 21 | [Fact] |
| 22 | public async Task DuplicateDoi_reports_FORGE002() |
| 23 | { |
| 24 | const string source = """ |
| 25 | using AgentForge.Abstractions; |
| 26 | namespace AgentForge.Plugin.Test; |
| 27 | public static class Commands |
| 28 | { |
| 29 | public static ForgeCommandDescriptor[] All => |
| 30 | [ |
| 31 | new ForgeCommandDescriptor { Domain = "forge", Object = "issue", Intent = "open", Help = "h", Category = "c", Surfaces = ["global"] }, |
| 32 | new ForgeCommandDescriptor { Domain = "forge", Object = "issue", Intent = "open", Help = "h2", Category = "c2", Surfaces = ["global"] }, |
| 33 | ]; |
| 34 | } |
| 35 | """; |
| 36 | |
| 37 | var diagnostics = await AnalyzerTestHelper.GetDiagnosticsAsync( |
| 38 | source, |
| 39 | extraReferences: [AnalyzerTestHelper.SdkReference]); |
| 40 | |
| 41 | Assert.True(AnalyzerTestHelper.HasDiagnostic(diagnostics, ForgePluginAnalyzer.DuplicateDoiId, DiagnosticSeverity.Error)); |
| 42 | } |
| 43 | |
| 44 | [Fact] |
| 45 | public async Task MapGetCreateRoute_reports_FORGE004() |
| 46 | { |
| 47 | const string source = """ |
| 48 | namespace AgentForge.Plugin.Test; |
| 49 | public static class Routes |
| 50 | { |
| 51 | public static void Map(object app) => app.MapGet("/view/issues/new", () => ""); |
| 52 | } |
| 53 | public static class AppExtensions |
| 54 | { |
| 55 | public static object MapGet(this object app, string route, System.Func<object> handler) => app; |
| 56 | } |
| 57 | """; |
| 58 | |
| 59 | var diagnostics = await AnalyzerTestHelper.GetDiagnosticsAsync(source); |
| 60 | Assert.True(AnalyzerTestHelper.HasDiagnostic(diagnostics, ForgePluginAnalyzer.AntiJiraCreateRouteId, DiagnosticSeverity.Error)); |
| 61 | } |
| 62 | |
| 63 | [Fact] |
| 64 | public async Task MapGetCiNewRoute_in_PluginCi_skips_FORGE004() |
| 65 | { |
| 66 | const string source = """ |
| 67 | namespace AgentForge.Plugin.Ci; |
| 68 | public static class Routes |
| 69 | { |
| 70 | public static void Map(object app) => app.MapGet("/view/repos/{name}/ci/new", () => ""); |
| 71 | } |
| 72 | public static class AppExtensions |
| 73 | { |
| 74 | public static object MapGet(this object app, string route, System.Func<object> handler) => app; |
| 75 | } |
| 76 | """; |
| 77 | |
| 78 | var diagnostics = await AnalyzerTestHelper.GetDiagnosticsAsync( |
| 79 | source, |
| 80 | assemblyName: "AgentForge.Plugin.Ci"); |
| 81 | |
| 82 | Assert.False(AnalyzerTestHelper.HasDiagnostic(diagnostics, ForgePluginAnalyzer.AntiJiraCreateRouteId)); |
| 83 | } |
| 84 | |
| 85 | [Fact] |
| 86 | public async Task EmbeddedSlashScript_reports_FORGE007() |
| 87 | { |
| 88 | const string source = """ |
| 89 | namespace AgentForge.Plugin.Test; |
| 90 | public static class Plugin; |
| 91 | """; |
| 92 | |
| 93 | var additional = new AdditionalTextImplementation("wwwroot/forge-slash.js", "console.log('x');"); |
| 94 | var diagnostics = await AnalyzerTestHelper.GetDiagnosticsAsync( |
| 95 | source, |
| 96 | additionalFiles: [additional]); |
| 97 | |
| 98 | Assert.True(AnalyzerTestHelper.HasDiagnostic(diagnostics, ForgePluginAnalyzer.EmbeddedSlashScriptId, DiagnosticSeverity.Error)); |
| 99 | } |
| 100 | |
| 101 | [Fact] |
| 102 | public async Task ViewShell_skips_FORGE007_for_slash_script() |
| 103 | { |
| 104 | const string source = """ |
| 105 | namespace AgentForge.Plugin.Test; |
| 106 | public static class Plugin; |
| 107 | """; |
| 108 | |
| 109 | var additional = new AdditionalTextImplementation("wwwroot/forge-slash.js", "console.log('x');"); |
| 110 | var diagnostics = await AnalyzerTestHelper.GetDiagnosticsAsync( |
| 111 | source, |
| 112 | assemblyName: "AgentForge.Plugin.ViewShell", |
| 113 | additionalFiles: [additional]); |
| 114 | |
| 115 | Assert.False(AnalyzerTestHelper.HasDiagnostic(diagnostics, ForgePluginAnalyzer.EmbeddedSlashScriptId)); |
| 116 | } |
| 117 | |
| 118 | [Fact] |
| 119 | public async Task MissingHelp_reports_FORGE008() |
| 120 | { |
| 121 | const string source = """ |
| 122 | using AgentForge.Abstractions; |
| 123 | namespace AgentForge.Plugin.Test; |
| 124 | public static class Commands |
| 125 | { |
| 126 | public static ForgeCommandDescriptor One => |
| 127 | new ForgeCommandDescriptor { Domain = "forge", Object = "issue", Intent = "open", Surfaces = ["global"] }; |
| 128 | } |
| 129 | """; |
| 130 | |
| 131 | var diagnostics = await AnalyzerTestHelper.GetDiagnosticsAsync( |
| 132 | source, |
| 133 | extraReferences: [AnalyzerTestHelper.SdkReference]); |
| 134 | |
| 135 | Assert.True(AnalyzerTestHelper.HasDiagnostic(diagnostics, ForgePluginAnalyzer.MissingCommandHelpId, DiagnosticSeverity.Info)); |
| 136 | } |
| 137 | |
| 138 | private sealed class AdditionalTextImplementation(string path, string content) : AdditionalText |
| 139 | { |
| 140 | public override SourceText? GetText(CancellationToken cancellationToken = default) => |
| 141 | SourceText.From(content); |
| 142 | |
| 143 | public override string Path { get; } = path; |
| 144 | } |
| 145 | } |
| 146 | |