150 lines
4.9 KiB
C#
150 lines
4.9 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ModelContextProtocol.Server;
|
|
using Nexus.Api.Controllers;
|
|
using Nexus.Api.Data;
|
|
using Nexus.Api.Models;
|
|
using Nexus.Api.Services;
|
|
using Xunit;
|
|
|
|
namespace Nexus.Api.Tests;
|
|
|
|
public sealed class NexusMcpToolsTests
|
|
{
|
|
[Fact]
|
|
public void NexusMcpTools_RegistersExpectedToolNames()
|
|
{
|
|
var toolNames = typeof(NexusMcpTools)
|
|
.GetMethods()
|
|
.Select(method => method.GetCustomAttributes(typeof(McpServerToolAttribute), inherit: false)
|
|
.OfType<McpServerToolAttribute>()
|
|
.FirstOrDefault())
|
|
.Where(attribute => attribute is not null)
|
|
.Select(attribute => attribute!.Name ?? string.Empty)
|
|
.Order()
|
|
.ToArray();
|
|
|
|
Assert.Equal(
|
|
[
|
|
"nexus_agent_overview",
|
|
"nexus_append_activity",
|
|
"nexus_create_child_task",
|
|
"nexus_create_task",
|
|
"nexus_get_activity",
|
|
"nexus_get_board",
|
|
"nexus_get_children",
|
|
"nexus_get_task",
|
|
"nexus_handoff",
|
|
"nexus_update_status"
|
|
], toolNames);
|
|
}
|
|
|
|
[Fact]
|
|
public void NexusMcpTaskState_OnlyContainsCanonicalStates()
|
|
{
|
|
Assert.Equal(
|
|
[
|
|
nameof(NexusMcpTaskState.Backlog),
|
|
nameof(NexusMcpTaskState.InProgress),
|
|
nameof(NexusMcpTaskState.Blocked),
|
|
nameof(NexusMcpTaskState.Done),
|
|
nameof(NexusMcpTaskState.Review)
|
|
], Enum.GetNames<NexusMcpTaskState>());
|
|
|
|
Assert.DoesNotContain("Delegated", Enum.GetNames<NexusMcpTaskState>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpTools_ReadAndWrite_UseBridgeService()
|
|
{
|
|
await using var fixture = await TaskWorkflowFixture.CreateAsync();
|
|
fixture.SetCallerAgent("iris");
|
|
var tools = CreateTools(fixture);
|
|
|
|
var createResult = await tools.CreateTask(
|
|
title: "MCP parent",
|
|
detail: "Created through MCP tool facade",
|
|
priority: "High",
|
|
assignedTo: "iris",
|
|
ct: CancellationToken.None);
|
|
|
|
Assert.True(createResult.Ok);
|
|
Assert.NotNull(createResult.Data);
|
|
Assert.Equal("MCP parent", createResult.Data!.Title);
|
|
|
|
var activityResult = await tools.AppendActivity(
|
|
createResult.Data.Id,
|
|
"MCP checkpoint",
|
|
"comment",
|
|
CancellationToken.None);
|
|
|
|
Assert.True(activityResult.Ok);
|
|
Assert.Equal("MCP checkpoint", activityResult.Data!.Message);
|
|
|
|
var statusResult = await tools.UpdateStatus(
|
|
createResult.Data.Id,
|
|
NexusMcpTaskState.InProgress,
|
|
CancellationToken.None);
|
|
|
|
Assert.True(statusResult.Ok);
|
|
Assert.Equal(TaskStateHelper.ToStateString(TaskState.InProgress), statusResult.Data!.State);
|
|
|
|
var board = await tools.GetBoard(CancellationToken.None);
|
|
Assert.Contains(board.InProgress, task => task.Id == createResult.Data.Id);
|
|
|
|
var taskResult = await tools.GetTask(createResult.Data.Id, CancellationToken.None);
|
|
Assert.True(taskResult.Ok);
|
|
Assert.Equal("MCP parent", taskResult.Data!.Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpTools_UpdateStatus_RejectsUnauthorizedAgent()
|
|
{
|
|
await using var fixture = await TaskWorkflowFixture.CreateAsync();
|
|
var task = await fixture.TaskService.CreateDashboardTaskAsync(
|
|
"Programmer cannot move",
|
|
"State changes stay with Iris/Bao.",
|
|
"iris",
|
|
"Normal",
|
|
"programmer",
|
|
null,
|
|
CancellationToken.None);
|
|
|
|
fixture.SetCallerAgent("programmer");
|
|
var tools = CreateTools(fixture);
|
|
|
|
var result = await tools.UpdateStatus(task.Id, NexusMcpTaskState.Done, CancellationToken.None);
|
|
|
|
Assert.False(result.Ok);
|
|
Assert.Equal("nexus_update_status", result.Command);
|
|
Assert.Contains("not authorized", result.Error, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task McpTools_ServiceKey_ResolvesAsNexusSystem()
|
|
{
|
|
await using var fixture = await TaskWorkflowFixture.CreateAsync();
|
|
fixture.HttpContextAccessor.HttpContext = TaskWorkflowFixture.CreateHttpContext(
|
|
headers: new Dictionary<string, string>
|
|
{
|
|
["X-Nexus-Api-Key"] = "test-service-key"
|
|
});
|
|
|
|
var tools = CreateTools(fixture);
|
|
var result = await tools.CreateTask(
|
|
title: "System MCP task",
|
|
assignedTo: "iris",
|
|
ct: CancellationToken.None);
|
|
|
|
Assert.True(result.Ok);
|
|
Assert.Equal("bao", result.Data!.Source);
|
|
}
|
|
|
|
private static NexusMcpTools CreateTools(TaskWorkflowFixture fixture)
|
|
=> new(
|
|
fixture.TaskBridgeService,
|
|
fixture.AgentService,
|
|
fixture.HttpContextAccessor,
|
|
fixture.Configuration,
|
|
NullLogger<NexusMcpTools>.Instance);
|
|
}
|