feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -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")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user