feat: complete Nexus mission-control workflows
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user