feat: ship agent-first mission control v0.2.57
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s

This commit is contained in:
AzuTear
2026-07-31 22:39:47 +02:00
parent 3bc7622977
commit f5552218bc
535 changed files with 95242 additions and 8791 deletions
+77 -159
View File
@@ -1,214 +1,132 @@
using Nexus.Api.Services;
using Nexus.Api.Integrations;
using Nexus.Api.Data;
using Microsoft.Extensions.Configuration;
using Nexus.Api.Models;
using Nexus.Api.Services;
using Xunit;
namespace Nexus.Api.Tests;
public class AgentServiceTests
public sealed class AgentServiceTests
{
[Fact]
public async Task GetAgentsAsync_ReturnsCorrectCount()
public async Task GetAgentsAsync_ProjectsOnlyLiveOpenClawInventory()
{
var configPath = CreateAgentConfigFile();
var config = CreateConfiguration(configPath);
var runtime = new FakeRuntime();
var service = new AgentService(config, runtime);
var service = new AgentService(new StubOpenClawControlService());
var agents = await service.GetAgentsAsync(CancellationToken.None);
Assert.True(agents.Count >= 4, $"Expected at least 4 agents, got {agents.Count}");
Assert.Equal(6, agents.Count);
Assert.Contains(agents, agent =>
agent.Id == "product-owner" &&
agent.Workspace == "/workspace-po" &&
agent.Role == "Product Owner");
}
[Fact]
public async Task GetAgentAsync_Iris_ReturnsOrchestrator()
{
var configPath = CreateAgentConfigFile();
var config = CreateConfiguration(configPath);
var runtime = new FakeRuntime();
var service = new AgentService(config, runtime);
var service = new AgentService(new StubOpenClawControlService());
var agent = await service.GetAgentAsync("iris", CancellationToken.None);
Assert.NotNull(agent);
Assert.Equal("Orchestrator", agent.Role);
Assert.Equal("/workspace/iris", agent.Workspace);
Assert.Null(agent.AgentDir);
}
[Fact]
public async Task GetAgentAsync_Unknown_ReturnsNull()
{
var configPath = CreateAgentConfigFile();
var config = CreateConfiguration(configPath);
var runtime = new FakeRuntime();
var service = new AgentService(config, runtime);
var service = new AgentService(new StubOpenClawControlService());
var agent = await service.GetAgentAsync("nonexistent", CancellationToken.None);
Assert.Null(agent);
}
[Fact]
public async Task GetAllowedAgentIdsAsync_IncludesProductOwnerAndProgrammerFast()
public async Task GetAllowedAgentIdsAsync_UsesLiveIds()
{
var configPath = CreateAgentConfigFile();
var config = CreateConfiguration(configPath);
var runtime = new FakeRuntime();
var service = new AgentService(config, runtime);
var control = new StubOpenClawControlService(
agents:
[
Agent("custom-live", "Custom"),
Agent("product-owner", "Product Owner")
]);
var service = new AgentService(control);
var ids = await service.GetAllowedAgentIdsAsync(CancellationToken.None);
Assert.Equal(2, ids.Count);
Assert.Contains("custom-live", ids);
Assert.Contains("product-owner", ids);
Assert.Contains("programmer-fast", ids);
Assert.DoesNotContain("iris", ids);
}
[Fact]
public async Task GetAgentAsync_ProgrammerFast_UsesPrimaryModelAndDeveloperRole()
public async Task GetAgentAsync_LatestSessionModelOverridesInventoryModel()
{
var configPath = CreateAgentConfigFile();
var config = CreateConfiguration(configPath);
var runtime = new FakeRuntime();
var service = new AgentService(config, runtime);
var older = DateTimeOffset.UtcNow.AddMinutes(-10);
var newer = DateTimeOffset.UtcNow;
var control = new StubOpenClawControlService(
agents: [Agent("programmer-fast", "Programmer Fast", "openai/default")],
sessions:
[
Session("old", "programmer-fast", "openai/gpt-5.3", older),
Session("new", "programmer-fast", "openai/gpt-5.5", newer)
]);
var service = new AgentService(control);
var agent = await service.GetAgentAsync("programmer-fast", CancellationToken.None);
Assert.NotNull(agent);
Assert.Equal("Developer", agent.Role);
Assert.Equal("openai/gpt-5.3-codex-spark", agent.Model);
Assert.Equal("openai/gpt-5.5", agent.Model);
Assert.Equal(newer, agent.LastSeen);
}
[Fact]
public async Task GetAgentAsync_LegacyStringModel_IsSupported()
public async Task GetAgentsAsync_DisconnectedGatewayMarksAgentsOffline()
{
var configPath = CreateAgentConfigFile(
"""
{
"agents": {
"defaults": {
"workspace": "/workspace/default",
"model": "deepseek/deepseek-v4-flash"
},
"list": [
{
"id": "iris",
"name": "iris",
"model": "openai/gpt-5.5"
}
]
}
}
""");
var config = CreateConfiguration(configPath);
var service = new AgentService(config, new FakeRuntime());
var service = new AgentService(new StubOpenClawControlService(connected: false));
var agent = await service.GetAgentAsync("iris", CancellationToken.None);
var agents = await service.GetAgentsAsync(CancellationToken.None);
Assert.NotNull(agent);
Assert.Equal("openai/gpt-5.5", agent!.Model);
Assert.All(agents, agent => Assert.Equal(OperationalStatus.Offline, agent.Status));
}
[Fact]
public async Task GetAgentAsync_ObjectModel_InheritsStringDefaultModel()
{
var configPath = CreateAgentConfigFile(
"""
{
"agents": {
"defaults": {
"workspace": "/workspace/default",
"model": "openai/gpt-5.5-mini"
},
"list": [
{
"id": "reviewer",
"name": "reviewer"
}
]
}
}
""");
var config = CreateConfiguration(configPath);
var service = new AgentService(config, new FakeRuntime());
private static OpenClawAgentDto Agent(
string id,
string name,
string model = "openai/gpt-5.5")
=> new(
Id: id,
Name: name,
Description: null,
Model: model,
Provider: "openai",
Workspace: $"/live/{id}",
Status: "ready");
var agent = await service.GetAgentAsync("reviewer", CancellationToken.None);
Assert.NotNull(agent);
Assert.Equal("openai/gpt-5.5-mini", agent!.Model);
}
private static IConfiguration CreateConfiguration(string configPath)
=> new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["AgentConfigPath"] = configPath
})
.Build();
private static string CreateAgentConfigFile(string? json = null)
{
var path = Path.Combine(Path.GetTempPath(), $"agent-config-{Guid.NewGuid():N}.json");
File.WriteAllText(path, json ??
"""
{
"agents": {
"defaults": {
"workspace": "/workspace/default",
"model": {
"primary": "deepseek/deepseek-v4-flash"
}
},
"list": [
{
"id": "iris",
"name": "iris",
"model": { "primary": "openai/gpt-5.5" }
},
{
"id": "product-owner",
"name": "product-owner",
"model": { "primary": "openai/gpt-5.5" }
},
{
"id": "programmer",
"name": "programmer",
"model": { "primary": "openai/gpt-5.4" }
},
{
"id": "programmer-fast",
"name": "programmer-fast",
"model": { "primary": "openai/gpt-5.3-codex-spark" }
},
{
"id": "reviewer",
"name": "reviewer",
"model": { "primary": "openai/gpt-5.5" }
},
{
"id": "architekt",
"name": "architekt",
"model": { "primary": "openai/gpt-5.5" }
}
]
}
}
""");
return path;
}
}
public sealed class FakeRuntime : IAgentRuntime
{
public string Name => "FakeRuntime";
public Task<AgentRuntimeStatus> GetStatusAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new AgentRuntimeStatus(
Runtime: "OpenClaw",
Status: OperationalStatus.Online,
Latency: TimeSpan.FromMilliseconds(10),
Detail: "Fake runtime for testing"));
public Task<AgentChatResult> ChatAsync(string message, string conversationId, string agentId, CancellationToken cancellationToken = default)
=> Task.FromResult(new AgentChatResult(
Runtime: "OpenClaw",
private static OpenClawSessionDto Session(
string key,
string agentId,
string model,
DateTimeOffset updatedAt)
=> new(
Key: key,
SessionId: key,
AgentId: agentId,
ConversationId: conversationId,
Content: "Echo: " + message));
Title: key,
Status: "active",
Kind: "agent",
Channel: null,
Model: model,
Provider: "openai",
RunId: null,
UpdatedAt: updatedAt,
InputTokens: null,
OutputTokens: null,
TotalTokens: null,
CanAbort: true);
}