-
Notifications
You must be signed in to change notification settings - Fork 227
MCP Issue Labeler GitHub Triage Bot - PART I #13552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
496fb1e
first draft of issue labeler mcp using rag/llm
anannya03 db0c89d
second bunch of changes along with evaluation
anannya03 d227ef5
MCP Issues Label Prediction Modifications
anannya03 15c678c
deleted unnecessary files
anannya03 c56c319
Removed appSettings.json
anannya03 dcfdec7
Modified InitialTriage Rule for MCP and added a temp schedule job for…
anannya03 2404bc3
enable nullable
anannya03 b62250b
Merge branch 'add-mcp-labeler' into mcp-gh-issue-labeler
anannya03 562c596
GH Action change for label update
anannya03 cd28607
removed scheduleeventprocessing changes
anannya03 0f8a35d
code cleanup
anannya03 34ff588
Refactor ProcessScheduledEvent call in Program.cs
anannya03 fdcda54
test fix
anannya03 5835cf5
Merge branch 'mcp-gh-issue-labeler' of https://github.com/Azure/azure…
anannya03 63d46ce
deleted unnecessary test json
anannya03 0762ff9
Review comments fix - 1
anannya03 b7a8b49
resolved review comments- 2
anannya03 b212502
resolved eview comments- 3
anannya03 8e331b1
resolved review comments- 4
anannya03 cbe86ed
removed commented out code
anannya03 7219ceb
config driven server-team-mapping
anannya03 7c570a1
Few more changes
anannya03 c36f9ae
review comment resolution- 5
anannya03 d9fb86f
alphabetize the packages
anannya03 4388f55
sorted the usings
anannya03 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 57 additions & 45 deletions
102
...rocessor/Azure.Sdk.Tools.GitHubEventProcessor.Tests/Static/IssueCommentProcessingTests.cs
Large diffs are not rendered by default.
Oops, something went wrong.
189 changes: 102 additions & 87 deletions
189
...event-processor/Azure.Sdk.Tools.GitHubEventProcessor.Tests/Static/IssueProcessingTests.cs
Large diffs are not rendered by default.
Oops, something went wrong.
267 changes: 267 additions & 0 deletions
267
...nt-processor/Azure.Sdk.Tools.GitHubEventProcessor.Tests/Static/McpIssueProcessingTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Threading.Tasks; | ||
| using Azure.Sdk.Tools.GitHubEventProcessor.Configuration; | ||
| using Azure.Sdk.Tools.GitHubEventProcessor.Constants; | ||
| using Azure.Sdk.Tools.GitHubEventProcessor.EventProcessing; | ||
| using Azure.Sdk.Tools.GitHubEventProcessor.GitHubPayload; | ||
| using Azure.Sdk.Tools.GitHubEventProcessor.Utils; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.Logging; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Azure.Sdk.Tools.GitHubEventProcessor.Tests.Static | ||
| { | ||
| [TestFixture] | ||
| [Parallelizable(ParallelScope.Children)] | ||
| public class McpIssueProcessingTests : ProcessingTestBase | ||
| { | ||
| /// <summary> | ||
| /// Test MCP InitialIssueTriage with various scenarios including: | ||
| /// - Predicted server and tool labels | ||
| /// - User-provided labels that conflict with predictions | ||
| /// - Code owner assignment | ||
| /// - Team notification comments when no owners found | ||
| /// - Customer-reported label logic | ||
| /// - needs-triage label removal | ||
| /// </summary> | ||
| /// <param name="rule">Rule being tested</param> | ||
| /// <param name="payloadFile">JSON payload file for the event</param> | ||
| /// <param name="ruleState">Whether InitialIssueTriage rule is on/off</param> | ||
| /// <param name="predictedLabels">Labels returned from AI triage service (comma-separated)</param> | ||
| /// <param name="userProvidedLabels">Labels already on the issue when opened (comma-separated)</param> | ||
| /// <param name="ownersWithAssignPermission">Owners with permission to be assigned (comma-separated)</param> | ||
| /// <param name="hasCodeownersEntry">Whether CODEOWNERS has entry for the labels</param> | ||
| /// <param name="isMemberOfOrg">Whether issue creator is member of microsoft org</param> | ||
| /// <param name="hasWriteOrAdmin">Whether issue creator has write/admin permission</param> | ||
| [Category("static")] | ||
| [NonParallelizable] | ||
|
|
||
| // Scenario: AI predicts server-azure.mcp label, no user labels, has code owner with permission | ||
| // Expected: server-azure.mcp label added, owner assigned, needs-team-attention added, comment posted | ||
| [TestCase(RulesConstants.InitialIssueTriage, | ||
| "Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json", | ||
| RuleState.On, | ||
| "server-azure.mcp", | ||
| "", | ||
| "McpOwner1", | ||
| true, | ||
| false, | ||
| false)] | ||
|
|
||
| // Scenario: AI predicts server-azure.mcp + tool-prompts.mcp, no owners found | ||
| // Expected: Both labels added, needs-team-triage added, team notification comment posted, customer-reported + question added | ||
| [TestCase(RulesConstants.InitialIssueTriage, | ||
| "Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json", | ||
| RuleState.On, | ||
| "server-azure.mcp, tools-prompts", | ||
| "", | ||
| null, | ||
| false, | ||
| false, | ||
| false)] | ||
|
|
||
| // Scenario: AI predicts server-azure.mcp, but user already added server-fabric.mcp (conflict) | ||
| // Expected: Only user's server-fabric.mcp kept, AI prediction ignored, no server-azure.mcp added | ||
| [TestCase(RulesConstants.InitialIssueTriage, | ||
| "Tests.JsonEventPayloads/McpIssueTriage_issue_opened_with_user_label.json", | ||
| RuleState.On, | ||
| "server-azure.mcp", | ||
| "server-fabric.mcp", | ||
| null, | ||
| false, | ||
| true, | ||
| true)] | ||
|
|
||
| // Scenario: AI predicts server-azure.mcp + tool-prompts.mcp, user added tool-prompts.mcp (partial match) | ||
| // Expected: server-azure.mcp added (prediction), tool-prompts.mcp kept (user choice), needs-triage removed if present | ||
| [TestCase(RulesConstants.InitialIssueTriage, | ||
| "Tests.JsonEventPayloads/McpIssueTriage_issue_opened_with_needs_triage.json", | ||
| RuleState.On, | ||
| "server-azure.mcp, tools-prompts", | ||
| "tools-prompts, needs-triage", | ||
| "McpOwner1", | ||
| true, | ||
| true, | ||
| false)] | ||
|
|
||
| // Scenario: AI predicts no labels (empty response) | ||
| // Expected: needs-triage label added, no other processing | ||
| [TestCase(RulesConstants.InitialIssueTriage, | ||
| "Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json", | ||
| RuleState.On, | ||
| "", | ||
| "", | ||
| null, | ||
| false, | ||
| false, | ||
| false)] | ||
|
|
||
| // Scenario: AI predicts server-fabric.mcp, multiple owners, only one has permission | ||
| // Expected: server-fabric.mcp added, owner with permission assigned, comment with all owners mentioned | ||
| [TestCase(RulesConstants.InitialIssueTriage, | ||
| "Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json", | ||
| RuleState.On, | ||
| "server-fabric.mcp", | ||
| "", | ||
| "McpOwner2", | ||
| true, | ||
| true, | ||
| true)] | ||
|
|
||
| // Scenario: Rule is disabled | ||
| // Expected: No processing, no updates | ||
| [TestCase(RulesConstants.InitialIssueTriage, | ||
| "Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json", | ||
| RuleState.Off, | ||
| "server-azure.mcp", | ||
| "", | ||
| null, | ||
| false, | ||
| false, | ||
| false)] | ||
|
|
||
| public async Task TestMcpInitialIssueTriage( | ||
| string rule, | ||
| string payloadFile, | ||
| RuleState ruleState, | ||
| string predictedLabels, | ||
| string userProvidedLabels, | ||
| string ownersWithAssignPermission, | ||
| bool hasCodeownersEntry, | ||
| bool isMemberOfOrg, | ||
| bool hasWriteOrAdmin) | ||
| { | ||
| var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); | ||
| var logger = loggerFactory.CreateLogger<McpIssueProcessing>(); | ||
|
|
||
| var mockGitHubEventClient = new MockGitHubEventClient(OrgConstants.ProductHeaderName); | ||
| mockGitHubEventClient.RulesConfiguration.Rules[rule] = ruleState; | ||
| mockGitHubEventClient.UserHasPermissionsReturn = hasWriteOrAdmin; | ||
| mockGitHubEventClient.IsUserMemberOfOrgReturn = isMemberOfOrg; | ||
|
|
||
| var rawJson = TestHelpers.GetTestEventPayload(payloadFile); | ||
| var issueEventPayload = SimpleJsonSerializer.Deserialize<IssueEventGitHubPayload>(rawJson); | ||
|
|
||
| var expectedPredictedLabels = new List<string>(); | ||
| if (!string.IsNullOrEmpty(predictedLabels)) | ||
| { | ||
| expectedPredictedLabels = predictedLabels.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); | ||
| } | ||
|
|
||
| mockGitHubEventClient.AIServiceLabels = expectedPredictedLabels; | ||
| mockGitHubEventClient.AIServiceAnswer = null; | ||
| mockGitHubEventClient.AIServiceAnswerType = null; | ||
|
|
||
| if (hasCodeownersEntry) | ||
| { | ||
| CodeOwnerUtils.ResetCodeOwnerEntries(); | ||
| CodeOwnerUtils.codeOwnersFilePathOverride = "Tests.FakeCodeowners/McpCodeowners"; | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(ownersWithAssignPermission)) | ||
| { | ||
| var ownersWithPermission = ownersWithAssignPermission.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); | ||
|
|
||
| mockGitHubEventClient.OwnersWithAssignPermission = ownersWithPermission; | ||
| } | ||
|
|
||
| var mcpProcessor = new McpIssueProcessing(logger, CreateTestMcpConfiguration()); | ||
|
|
||
| await mcpProcessor.ProcessIssueEvent(mockGitHubEventClient, issueEventPayload); | ||
|
|
||
| Assert.That(mockGitHubEventClient.RulesConfiguration.RuleEnabled(rule), | ||
| Is.EqualTo(ruleState == RuleState.On), | ||
| $"Rule '{rule}' enabled should have been {ruleState == RuleState.On}"); | ||
|
|
||
| var totalUpdates = await mockGitHubEventClient.ProcessPendingUpdates( | ||
| issueEventPayload.Repository.Id, | ||
| issueEventPayload.Issue.Number); | ||
|
|
||
| if (ruleState == RuleState.Off) | ||
| { | ||
| Assert.That(totalUpdates, Is.EqualTo(0), "Expected no updates when rule is disabled"); | ||
| } | ||
| else if (string.IsNullOrEmpty(predictedLabels)) | ||
| { | ||
| Assert.That(mockGitHubEventClient.GetLabelsToAdd(), Does.Contain(TriageLabelConstants.NeedsTriage)); | ||
|
|
||
| if (!isMemberOfOrg && !hasWriteOrAdmin) | ||
| { | ||
| Assert.That(mockGitHubEventClient.GetLabelsToAdd(), Does.Contain(TriageLabelConstants.CustomerReported)); | ||
| Assert.That(mockGitHubEventClient.GetLabelsToAdd(), Does.Contain(TriageLabelConstants.Question)); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| var userLabels = string.IsNullOrEmpty(userProvidedLabels) | ||
| ? new List<string>() | ||
| : userProvidedLabels.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); | ||
|
|
||
| var labelsToAdd = mockGitHubEventClient.GetLabelsToAdd(); | ||
| var serverPredicted = expectedPredictedLabels.FirstOrDefault(l => l.StartsWith("server-", StringComparison.OrdinalIgnoreCase)); | ||
| var toolPredicted = expectedPredictedLabels.FirstOrDefault(l => l.StartsWith("tools-", StringComparison.OrdinalIgnoreCase)); | ||
|
|
||
| if (serverPredicted != null) | ||
| { | ||
| var userHasServerLabel = userLabels.Any(l => l.StartsWith("server-", StringComparison.OrdinalIgnoreCase)); | ||
| if (!userHasServerLabel) | ||
| { | ||
| Assert.That(labelsToAdd, Does.Contain(serverPredicted), | ||
| $"Expected predicted server label '{serverPredicted}' to be added"); | ||
| } | ||
| } | ||
|
|
||
| if (toolPredicted != null) | ||
| { | ||
| var userHasToolLabel = userLabels.Any(l => l.StartsWith("tools-", StringComparison.OrdinalIgnoreCase)); | ||
| if (!userHasToolLabel) | ||
| { | ||
| Assert.That(labelsToAdd, Does.Contain(toolPredicted), | ||
| $"Expected predicted tool label '{toolPredicted}' to be added"); | ||
| } | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(ownersWithAssignPermission) && hasCodeownersEntry) | ||
| { | ||
| Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.NeedsTeamAttention)); | ||
| } | ||
| else if (serverPredicted != null || toolPredicted != null) | ||
| { | ||
| Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.NeedsTeamTriage)); | ||
| } | ||
|
|
||
| if (userLabels.Contains(TriageLabelConstants.NeedsTriage, StringComparer.OrdinalIgnoreCase)) | ||
| { | ||
| var labelsToRemove = mockGitHubEventClient.GetLabelsToRemove(); | ||
| Assert.That(labelsToRemove, Does.Contain(TriageLabelConstants.NeedsTriage), | ||
| "Expected needs-triage to be removed when valid predicted labels exist"); | ||
| } | ||
|
|
||
| if (!isMemberOfOrg && !hasWriteOrAdmin) | ||
| { | ||
| Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.CustomerReported)); | ||
| Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.Question)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a test McpConfiguration with mock server team mappings. | ||
| /// </summary> | ||
| private static McpConfiguration CreateTestMcpConfiguration() | ||
| { | ||
| var configData = new Dictionary<string, string?> | ||
| { | ||
| { "microsoft/mcp:ServerTeamMappings", "server-Azure.Mcp=@microsoft/azure-mcp;server-Fabric.Mcp=@microsoft/fabric-mcp" } | ||
| }; | ||
|
|
||
| var configuration = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(configData) | ||
| .Build(); | ||
|
|
||
| return new McpConfiguration(configuration); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.