feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar

This commit is contained in:
Richard
2026-08-10 10:48:34 +02:00
parent a0e18d2a57
commit b5bf97ae74
187 changed files with 20054 additions and 882 deletions
@@ -1,6 +1,8 @@
using ClawdDotNet.Core.Audit;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Security;
using ClawdDotNet.Core.Staging;
using ClawdDotNet.Core.Tools;
namespace ClawdDotNet.Core.Tests.Infrastructure;
@@ -18,14 +20,19 @@ internal sealed class EngineFixture
private readonly List<AgentConfig> _agents = new();
public EngineFixture()
/// <summary>Die registrierten Agenten — für Tests, die einen Dispatcher selbst bauen.</summary>
public IReadOnlyList<AgentConfig> Agents => _agents;
public EngineFixture(IAuditRepository? audit = null, StagingGate? staging = null)
{
Engine = new AgentEngine(
Client,
Registry,
new PermissionGate(),
StateStore,
TestLogging.Factory);
TestLogging.Factory,
auditRepository: audit,
stagingGate: staging);
// Kein Agent-Verzeichnis → keine Persistenz auf die Platte während der Tests.
Engine.SetAgentConfigProvider(() => _agents, "test-instance", _ => null);
@@ -0,0 +1,19 @@
namespace ClawdDotNet.Core.Tests.Infrastructure;
/// <summary>
/// Ein steuerbarer <see cref="TimeProvider"/> für die Zeit-abhängigen Tests (R2 der
/// Teststrategie). Bewusst schlank: die Tests treiben den Scanner über
/// <c>ScanOnceAsync</c> direkt, sie brauchen nur eine feste, verstellbare Uhr.
/// </summary>
internal sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _now;
public FakeTimeProvider(DateTimeOffset start) => _now = start;
public override DateTimeOffset GetUtcNow() => _now;
public void SetUtcNow(DateTimeOffset value) => _now = value;
public void Advance(TimeSpan delta) => _now += delta;
}
@@ -0,0 +1,40 @@
using System.Collections.Concurrent;
using ClawdDotNet.Core.Audit;
namespace ClawdDotNet.Core.Tests.Infrastructure;
/// <summary>
/// In-Memory-Attrappe des Audit-Logs für Engine-Tests — hält Einträge und Belege in
/// Listen, damit ein Test prüfen kann, was die Engine stempelt, ohne eine Datei.
/// </summary>
internal sealed class InMemoryAuditRepository : IAuditRepository
{
public ConcurrentQueue<AuditEntry> Entries { get; } = new();
public ConcurrentQueue<RunReceipt> Receipts { get; } = new();
public Task AppendAsync(AuditEntry entry, CancellationToken ct)
{
Entries.Enqueue(entry);
return Task.CompletedTask;
}
public Task RecordReceiptAsync(RunReceipt receipt, CancellationToken ct)
{
Receipts.Enqueue(receipt);
return Task.CompletedTask;
}
public Task<IReadOnlyList<AuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<AuditEntry>>(Entries.Reverse().Take(limit).ToList());
public Task<IReadOnlyList<AuditEntry>> ListForRunAsync(string runId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<AuditEntry>>(Entries.Where(e => e.RunId == runId).ToList());
public Task<RunReceipt?> GetReceiptForRunAsync(string runId, CancellationToken ct)
=> Task.FromResult(Receipts.LastOrDefault(r => r.RunId == runId));
public Task<IReadOnlyList<RunReceipt>> ListReceiptsForTaskAsync(string taskId, CancellationToken ct)
=> Task.FromResult<IReadOnlyList<RunReceipt>>(Receipts.Where(r => r.TaskId == taskId).ToList());
public Task<int> CountAsync(CancellationToken ct) => Task.FromResult(Entries.Count);
}
@@ -0,0 +1,52 @@
using System.Collections.Concurrent;
using ClawdDotNet.Core.Staging;
namespace ClawdDotNet.Core.Tests.Infrastructure;
/// <summary>In-Memory-Attrappe der Staging-Warteschlange für Engine-Tests.</summary>
internal sealed class InMemoryStagingRepository : IStagingRepository
{
private readonly ConcurrentDictionary<long, StagedCall> _calls = new();
private long _nextId;
private readonly Lock _lock = new();
public Task<long> AppendAsync(StagedCall call, CancellationToken ct)
{
var id = Interlocked.Increment(ref _nextId);
_calls[id] = call with { Id = id, Status = StagingStatus.Pending, CreatedAt = DateTime.UtcNow };
return Task.FromResult(id);
}
public Task<StagedCall?> GetAsync(long id, CancellationToken ct)
=> Task.FromResult(_calls.GetValueOrDefault(id));
public Task<IReadOnlyList<StagedCall>> ListPendingAsync(CancellationToken ct)
=> Task.FromResult<IReadOnlyList<StagedCall>>(
_calls.Values.Where(c => c.Status == StagingStatus.Pending).OrderBy(c => c.Id).ToList());
public Task<bool> TryTransitionAsync(
long id, StagingStatus from, StagingStatus to,
string? decidedBy, string? rejectionReason, DateTime now, CancellationToken ct)
{
lock (_lock)
{
if (!_calls.TryGetValue(id, out var call) || call.Status != from)
return Task.FromResult(false);
_calls[id] = call with
{
Status = to, DecidedBy = decidedBy, DecidedAt = now, RejectionReason = rejectionReason
};
return Task.FromResult(true);
}
}
public Task FinalizeAsync(long id, StagingStatus status, string? resultRef, DateTime now, CancellationToken ct)
{
if (_calls.TryGetValue(id, out var call))
_calls[id] = call with { Status = status, ResultRef = resultRef, DecidedAt = now };
return Task.CompletedTask;
}
public Task<int> CountPendingAsync(CancellationToken ct)
=> Task.FromResult(_calls.Values.Count(c => c.Status == StagingStatus.Pending));
}
@@ -0,0 +1,31 @@
namespace ClawdDotNet.Core.Tests.Infrastructure;
/// <summary>
/// Ein Test, der nur unter Linux etwas aussagt.
///
/// Nötig geworden mit der Linux-Portierung: Groß-/Kleinschreibung und symbolische
/// Verknüpfungen verhalten sich je Dateisystem unterschiedlich. Solche Fälle unter
/// Windows mitlaufen zu lassen, würde entweder fehlschlagen oder — schlimmer — grün
/// sein, ohne das Gemeinte geprüft zu haben.
///
/// Bewusst ein Übersprung mit Begründung statt eines stillen <c>return</c>: Im
/// Testbericht ist dann sichtbar, dass hier etwas <em>nicht</em> geprüft wurde.
/// </summary>
public sealed class LinuxFactAttribute : FactAttribute
{
public LinuxFactAttribute()
{
if (!OperatingSystem.IsLinux())
Skip = "Nur unter Linux aussagekräftig (Groß-/Kleinschreibung, Verknüpfungen).";
}
}
/// <summary>Ein Test, der nur unter Windows etwas aussagt. Siehe <see cref="LinuxFactAttribute"/>.</summary>
public sealed class WindowsFactAttribute : FactAttribute
{
public WindowsFactAttribute()
{
if (!OperatingSystem.IsWindows())
Skip = "Nur unter Windows aussagekräftig.";
}
}
@@ -0,0 +1,33 @@
using System.Runtime.CompilerServices;
using ClawdDotNet.Core.Security;
namespace ClawdDotNet.Core.Tests.Infrastructure;
/// <summary>
/// Hält den Schlüssel von <see cref="SecretKeyStore"/> aus den Tests heraus.
///
/// Zwei Dinge müssen zusammenkommen:
///
/// 1. <see cref="Init"/> verlegt den Schlüssel <b>einmal beim Laden</b> der
/// Testassembly in ein Wegwerfverzeichnis. Ohne das würden die Tests den echten
/// Benutzerschlüssel anlegen oder lesen — auf dem Entwicklungsrechner wie auf dem
/// Bauserver.
///
/// 2. Wer den Schlüssel <em>während</em> eines Tests wechselt (etwa um zu prüfen, dass
/// ein fremder Schlüssel nicht liest), muss in dieser Sammlung laufen. Der Speicher
/// ist statisch; ohne abgeschaltete Parallelität zöge ein solcher Wechsel einer
/// gleichzeitig laufenden Klasse den Schlüssel unter den Füßen weg. Genau das ist
/// zwischen den Geheimnis- und den Sicherungstests passiert.
/// </summary>
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class SecretKeyCollection
{
public const string Name = "SecretKey";
[ModuleInitializer]
internal static void Init()
{
SecretKeyStore.UseDirectory(Path.Combine(
Path.GetTempPath(), "clawd-tests", "keys", Guid.NewGuid().ToString("N")));
}
}