Files
nexus/backend-tests/PostgresOpenClawOperationAuditStoreTests.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

176 lines
6.2 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Nexus.Api.Data;
using Nexus.Api.Services;
using Xunit;
namespace Nexus.Api.Tests;
public sealed class PostgresOpenClawOperationAuditStoreTests
{
[Fact]
public async Task Completed_claim_replays_from_database_without_writing_jsonl()
{
await using var fixture = Fixture.Create();
var context = Context("database-replay");
var operation = Operation("cron.run", "job-1", "intent-a");
var started = await fixture.Store.ClaimAsync(context, operation);
await fixture.Store.CompleteAsync(
context,
operation,
true,
"enqueued",
"This message is deliberately not persisted.");
var replay = await fixture.Store.ClaimAsync(context, operation);
Assert.Equal(OpenClawOperationClaimDisposition.Started, started.Disposition);
Assert.Equal(OpenClawOperationClaimDisposition.Replayed, replay.Disposition);
Assert.True(replay.PreviousOk);
Assert.Equal("enqueued", replay.PreviousState);
Assert.False(File.Exists(fixture.AuditPath));
await using var scope = fixture.Provider.CreateAsyncScope();
var claim = await scope.ServiceProvider
.GetRequiredService<NexusDbContext>()
.OperationClaims
.SingleAsync();
Assert.Equal("openclaw.mutation", claim.Operation);
Assert.Equal("completed", claim.State);
Assert.Equal("enqueued", claim.ResultCode);
Assert.DoesNotContain("database-replay", claim.IdempotencyKeyHash);
Assert.DoesNotContain("This message", claim.ResultCode);
}
[Fact]
public async Task Same_key_with_different_intent_conflicts()
{
await using var fixture = Fixture.Create();
var context = Context("conflict-key");
await fixture.Store.ClaimAsync(
context,
Operation("cron.run", "job-1", "intent-a"));
var conflict = await fixture.Store.ClaimAsync(
context,
Operation("cron.run", "job-2", "intent-b"));
Assert.Equal(
OpenClawOperationClaimDisposition.Conflict,
conflict.Disposition);
}
[Fact]
public async Task Expired_incomplete_claim_remains_in_doubt()
{
await using var fixture = Fixture.Create();
var context = Context("in-doubt-key");
var operation = Operation("agents.files.set", "agent/SOUL.md", "intent");
await fixture.Store.ClaimAsync(context, operation);
await fixture.ExpireClaimAsync();
var replay = await fixture.Store.ClaimAsync(context, operation);
Assert.Equal(
OpenClawOperationClaimDisposition.InDoubt,
replay.Disposition);
}
[Fact]
public async Task Expired_terminal_claim_can_be_reclaimed()
{
await using var fixture = Fixture.Create();
var context = Context("expired-terminal-key");
var operation = Operation("config.patch", "primary", "intent");
await fixture.Store.ClaimAsync(context, operation);
await fixture.Store.CompleteAsync(
context,
operation,
false,
"conflict",
"Not persisted.");
await fixture.ExpireClaimAsync();
var reclaimed = await fixture.Store.ClaimAsync(context, operation);
Assert.Equal(
OpenClawOperationClaimDisposition.Started,
reclaimed.Disposition);
await using var scope = fixture.Provider.CreateAsyncScope();
Assert.Single(await scope.ServiceProvider
.GetRequiredService<NexusDbContext>()
.OperationClaims
.ToArrayAsync());
}
[Fact]
public async Task Database_failure_is_not_treated_as_a_started_claim()
{
var fixture = Fixture.Create();
var store = fixture.Store;
await fixture.DisposeAsync();
await Assert.ThrowsAnyAsync<ObjectDisposedException>(() =>
store.ClaimAsync(
Context("database-down"),
Operation("cron.run", "job-1", "intent")));
}
private static OpenClawInvocationContext Context(string key)
=> OpenClawInvocationContext.Create(
actor: "owner",
idempotencyKey: key,
correlationId: $"correlation-{key}");
private static OpenClawOperationDescriptor Operation(
string method,
string targetId,
string intent)
=> new(
method,
"test-resource",
targetId,
OpenClawInvocationContextFactory.Hash(intent));
private sealed class Fixture(
ServiceProvider provider,
PostgresOpenClawOperationAuditStore store,
string auditPath) : IAsyncDisposable
{
public ServiceProvider Provider { get; } = provider;
public PostgresOpenClawOperationAuditStore Store { get; } = store;
public string AuditPath { get; } = auditPath;
public static Fixture Create()
{
var services = new ServiceCollection();
var databaseName = $"operation-claims-{Guid.NewGuid():N}";
services.AddDbContext<NexusDbContext>(options =>
options.UseInMemoryDatabase(databaseName));
var provider = services.BuildServiceProvider();
var auditPath = Path.Combine(
Path.GetTempPath(),
$"nexus-operation-archive-{Guid.NewGuid():N}.jsonl");
var store = new PostgresOpenClawOperationAuditStore(
provider.GetRequiredService<IServiceScopeFactory>(),
Options.Create(new GatewayConnectorOptions
{
OperationAuditPath = auditPath
}));
return new Fixture(provider, store, auditPath);
}
public async Task ExpireClaimAsync()
{
await using var scope = Provider.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<NexusDbContext>();
var claim = await db.OperationClaims.SingleAsync();
claim.ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(-1);
await db.SaveChangesAsync();
}
public ValueTask DisposeAsync() => Provider.DisposeAsync();
}
}