feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user