Files
nexus/backend-tests/OpenClawContentServicesTests.cs
T
AzuTear f5552218bc
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
feat: ship agent-first mission control v0.2.57
2026-07-31 22:39:47 +02:00

349 lines
12 KiB
C#

using System.Collections.Concurrent;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Nexus.Api.Controllers;
using Nexus.Api.Models;
using Nexus.Api.Services;
using Xunit;
namespace Nexus.Api.Tests;
public sealed class OpenClawContentServicesTests
{
[Fact]
public async Task Memory_uses_agent_files_and_workspace_rpc_with_source_metadata()
{
var gateway = new ContentConfigurationStub();
gateway.AgentFiles =
[
new OpenClawAgentFileSummaryDto(
"MEMORY.md",
false,
15,
DateTimeOffset.UtcNow,
"hash")
];
gateway.Listings["memory"] =
[
Entry("memory/2026-07-31.md", 24)
];
gateway.AgentFileContent = "Long term memory";
gateway.Files["memory/2026-07-31.md"] =
File("bao-agent", "memory/2026-07-31.md", "Daily memory needle");
var service = new MemoryService(gateway);
var listed = await service.GetAllAsync("bao-agent");
var found = await service.SearchAsync("needle", "bao-agent");
var longTerm = await service.GetFileAsync("MEMORY.md", "bao-agent");
Assert.Collection(
listed,
item =>
{
Assert.Equal("MEMORY.md", item.WorkspacePath);
Assert.Equal("bao-agent", item.SourceAgentId);
},
item =>
{
Assert.Equal("memory/2026-07-31.md", item.WorkspacePath);
Assert.Equal("2026-07-31.md", item.Path);
});
Assert.Equal(
"memory/2026-07-31.md",
Assert.Single(found).WorkspacePath);
Assert.Equal("MEMORY.md", longTerm?.WorkspacePath);
Assert.DoesNotContain(
listed.Select(item => item.WorkspacePath),
path => path.StartsWith('/'));
}
[Fact]
public async Task Memory_search_limits_live_file_reads_to_four()
{
var gateway = new ContentConfigurationStub
{
ReadDelay = TimeSpan.FromMilliseconds(20)
};
gateway.Listings["memory"] = Enumerable.Range(1, 12)
.Select(index => Entry($"memory/{index:00}.md", 20))
.ToArray();
foreach (var entry in gateway.Listings["memory"])
{
gateway.Files[entry.Path] =
File("iris", entry.Path, $"needle {entry.Name}");
}
var results = await new MemoryService(gateway)
.SearchAsync("needle", "iris");
Assert.Equal(12, results.Count);
Assert.InRange(gateway.MaxConcurrentReads, 1, 4);
}
[Fact]
public async Task Missing_daily_memory_directory_preserves_memory_file()
{
var gateway = new ContentConfigurationStub
{
MissingMemoryDirectory = true,
AgentFileContent = "Long term needle"
};
gateway.AgentFiles =
[
new OpenClawAgentFileSummaryDto(
"MEMORY.md",
false,
16,
DateTimeOffset.UtcNow,
"hash")
];
var service = new MemoryService(gateway);
var listed = await service.GetAllAsync("iris");
var searched = await service.SearchAsync("needle", "iris");
Assert.Equal("MEMORY.md", Assert.Single(listed).WorkspacePath);
Assert.Equal("MEMORY.md", Assert.Single(searched).WorkspacePath);
}
[Fact]
public async Task Docs_and_incidents_use_workspace_relative_rpc_paths()
{
var gateway = new ContentConfigurationStub();
gateway.Listings[""] = [Entry("README.md", 30)];
gateway.Listings["nexus-phases"] =
[Entry("nexus-phases/phase-1.md", 30)];
gateway.Listings["skills"] = [];
gateway.Listings["nexus"] = [];
gateway.Listings["nexus/phases"] = [];
gateway.Listings["memory/incidents"] =
[Entry("memory/incidents/2026-07-31-gateway.md", 90)];
gateway.Files["README.md"] =
File("iris", "README.md", "# Nexus");
gateway.Files["memory/incidents/2026-07-31-gateway.md"] =
File(
"iris",
"memory/incidents/2026-07-31-gateway.md",
"# Gateway outage\n**Severity:** high\n\nRecovered.");
var docs = await new DocService(gateway).GetAllAsync("iris");
var incidents = await new IncidentService(gateway).GetAllAsync("iris");
Assert.Contains(
docs,
item => item.WorkspacePath == "README.md"
&& item.SourceAgentId == "iris");
var incident = Assert.Single(incidents);
Assert.Equal("high", incident.Severity);
Assert.Equal(
"memory/incidents/2026-07-31-gateway.md",
incident.WorkspacePath);
}
[Fact]
public void Sensitive_content_controllers_are_owner_only()
{
Type[] controllers =
[
typeof(MemoryController),
typeof(DocsController),
typeof(IncidentsController)
];
foreach (var controller in controllers)
{
var authorize = Assert.Single(
controller.GetCustomAttributes<AuthorizeAttribute>());
Assert.Equal("owner", authorize.Roles);
}
}
[Fact]
public async Task Adapter_reports_gateway_unavailable_instead_of_empty_success()
{
var controller = new MemoryController(new UnavailableMemoryService());
var result = await controller.GetAll();
Assert.Equal(
StatusCodes.Status503ServiceUnavailable,
Assert.IsAssignableFrom<IStatusCodeHttpResult>(result)
.StatusCode);
}
private static OpenClawWorkspaceEntryDto Entry(string path, long size)
=> new(
path,
Path.GetFileName(path),
"file",
size,
DateTimeOffset.UtcNow);
private static OpenClawWorkspaceFileDto File(
string agentId,
string path,
string content)
=> new(
agentId,
path,
Path.GetFileName(path),
System.Text.Encoding.UTF8.GetByteCount(content),
DateTimeOffset.UtcNow,
"text/markdown",
"utf8",
content,
"hash",
DateTimeOffset.UtcNow);
private sealed class ContentConfigurationStub
: IOpenClawAgentConfigurationService
{
private int activeReads;
private int maxConcurrentReads;
public IReadOnlyList<OpenClawAgentFileSummaryDto> AgentFiles { get; set; }
= [];
public string AgentFileContent { get; set; } = string.Empty;
public ConcurrentDictionary<
string,
IReadOnlyList<OpenClawWorkspaceEntryDto>> Listings { get; } =
new(StringComparer.Ordinal);
public ConcurrentDictionary<string, OpenClawWorkspaceFileDto> Files { get; }
= new(StringComparer.Ordinal);
public TimeSpan ReadDelay { get; set; }
public bool MissingMemoryDirectory { get; set; }
public int MaxConcurrentReads => Volatile.Read(ref maxConcurrentReads);
public Task<OpenClawAgentFileCollectionDto> GetAgentFilesAsync(
string agentId,
CancellationToken cancellationToken = default)
=> Task.FromResult(new OpenClawAgentFileCollectionDto(
agentId,
AgentFiles,
DateTimeOffset.UtcNow));
public Task<OpenClawAgentFileDto> GetAgentFileAsync(
string agentId,
string fileName,
CancellationToken cancellationToken = default)
=> Task.FromResult(new OpenClawAgentFileDto(
agentId,
fileName,
false,
System.Text.Encoding.UTF8.GetByteCount(AgentFileContent),
DateTimeOffset.UtcNow,
AgentFileContent,
"hash",
DateTimeOffset.UtcNow));
public Task<OpenClawWorkspaceCollectionDto> GetWorkspaceAsync(
string agentId,
string? path,
int offset,
int limit,
CancellationToken cancellationToken = default)
{
var normalized = path ?? string.Empty;
if (MissingMemoryDirectory
&& string.Equals(normalized, "memory", StringComparison.Ordinal))
{
throw new OpenClawGatewayRpcException(
"PATH_NOT_FOUND",
"Optional memory directory is absent.");
}
Listings.TryGetValue(normalized, out var entries);
entries ??= [];
return Task.FromResult(new OpenClawWorkspaceCollectionDto(
agentId,
normalized,
null,
entries.Take(limit).ToArray(),
entries.Count,
offset,
DateTimeOffset.UtcNow));
}
public async Task<OpenClawWorkspaceFileDto> GetWorkspaceFileAsync(
string agentId,
string path,
CancellationToken cancellationToken = default)
{
var active = Interlocked.Increment(ref activeReads);
UpdateMaximum(active);
try
{
if (ReadDelay > TimeSpan.Zero)
await Task.Delay(ReadDelay, cancellationToken);
return Files[path];
}
finally
{
Interlocked.Decrement(ref activeReads);
}
}
private void UpdateMaximum(int value)
{
while (true)
{
var current = Volatile.Read(ref maxConcurrentReads);
if (value <= current
|| Interlocked.CompareExchange(
ref maxConcurrentReads,
value,
current) == current)
{
return;
}
}
}
public Task<OpenClawAgentFileWriteDto> SetAgentFileAsync(
string agentId,
string fileName,
UpdateOpenClawAgentFileRequest request,
OpenClawInvocationContext invocationContext,
CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<OpenClawConfigSchemaLookupDto> GetConfigSchemaAsync(
string path,
CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<OpenClawConfigSnapshotDto> GetConfigAsync(
CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<OpenClawConfigPatchDto> PatchConfigAsync(
PatchOpenClawConfigRequest request,
OpenClawInvocationContext invocationContext,
CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
private sealed class UnavailableMemoryService : IMemoryService
{
public Task<IReadOnlyList<MemoryFileInfo>> GetAllAsync(
string agentId = "iris",
CancellationToken cancellationToken = default)
=> throw new OpenClawAgentConfigurationUnavailableException(
"disconnected",
"agents.files.list",
"operator.read",
"Gateway unavailable.");
public Task<IReadOnlyList<MemorySearchResult>> SearchAsync(
string query,
string agentId = "iris",
CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<MemoryFileContent?> GetFileAsync(
string name,
string agentId = "iris",
CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
}
}