Entscheidungen aus dem Testbetrieb umgesetzt (#572): #577 #579 #581 #589 #193 #205 #594 #595 #597 #598 #599 #588

- Engine: leere Antworten nachfassen, Aufgaben enden mit Ergebniszeile (EmptyResponse/Incomplete),
  gleiche Fehlschlaege erkennen (RepeatedFailure), Zeitzeile vor Chatnachrichten,
  Werkzeugergebnisse gerahmt, Vorpruefung vor dem Staging, Belegkosten wie RunUsage
- Staging: gescheiterte Freigaben als Failed; AgentEditor-Aenderungen freigabepflichtig,
  Selbstbearbeitung gesperrt (allowSelfEdit)
- Taskboard: Termine beim Anlegen pruefen, naechster/letzter Lauf, pausieren, Sofortlauf,
  Kostenprognose, Hinweis auf wachsenden Kontext; Jobs-Reiter und /api/jobs entfernt
- Release: Instanzmanager samt Weboberflaeche im Paket, setup.json fragt manager.json ab,
  Token nicht mehr ins systemd-Journal
Version 0.1.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-15 11:23:10 +02:00
co-authored by Claude Opus 5
parent 8ab9d99abe
commit 0b6ea73ec1
42 changed files with 2646 additions and 512 deletions
@@ -0,0 +1,120 @@
using System.Text.Json;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Tools.AgentEditor;
using ClawdDotNet.Tools.FileRW;
using ClawdDotNet.Tools.Tests.FileRW;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.Vorpruefung;
/// <summary>
/// Die Vorpruefung der Werkzeuge (#579) und die Haertung des AgentEditors (#205): Was das
/// Werkzeug ohnehin ablehnen wuerde, soll gar nicht erst in die Freigabeliste kommen.
/// </summary>
public sealed class VorpruefungTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
private readonly string _agents;
private readonly string _shared;
public VorpruefungTests()
{
_agents = Path.Combine(_root, "Agents");
_shared = Path.Combine(_agents, "SharedWorkspace");
Directory.CreateDirectory(_shared);
Directory.CreateDirectory(Path.Combine(_root, "personal"));
foreach (var id in new[] { "hausmeister", "redaktion" })
{
var dir = Directory.CreateDirectory(Path.Combine(_agents, "Agent-" + id)).FullName;
File.WriteAllText(Path.Combine(dir, "AgentSettings.json"), $$"""{"agentId":"{{id}}"}""");
File.WriteAllText(Path.Combine(dir, "Soul.md"), "# Soul\nVorsichtig.");
}
}
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); }
catch { /* Aufraeumen ist Nebensache */ }
}
private AgentToolContext Kontext(string agentId, Dictionary<string, object?>? config = null) => new(
agentId, "inst",
config ?? new Dictionary<string, object?>(),
new NullStateStore(),
NullLogger.Instance,
CancellationToken.None,
WorkspacePath: Path.Combine(_root, "personal"),
SharedWorkspacePath: _shared);
private static JsonElement Json(string json) => JsonDocument.Parse(json).RootElement;
// ─── FileRW ───
[Theory]
[InlineData("Read")]
[InlineData("ReadWrite")]
public void FileRW_lehnt_Loeschen_im_gemeinsamen_Ordner_ohne_Admin_vorab_ab(string level)
=> new FileRWTool()
.Preflight(Json("""{"action":"delete","workspace":"shared","path":"Testplan.md"}"""),
Kontext("dateipruefer", new() { ["sharedAccessLevel"] = level }))
.ShouldNotBeNull()
.ShouldContain("Zugriff verweigert");
[Fact]
public void FileRW_laesst_Loeschen_im_eigenen_Ordner_zur_Freigabe_durch()
=> new FileRWTool()
.Preflight(Json("""{"action":"delete","path":"alt.txt"}"""), Kontext("a"))
.ShouldBeNull();
[Fact]
public void FileRW_lehnt_einen_Pfad_ausserhalb_des_Ordners_vorab_ab()
=> new FileRWTool()
.Preflight(Json("""{"action":"delete","path":"../../etwas.txt"}"""), Kontext("a"))
.ShouldNotBeNull();
[Fact]
public void FileRW_ohne_lesbare_Argumente_ueberlaesst_die_Meldung_der_Ausfuehrung()
=> new FileRWTool().Preflight(Json("""{"path":"x"}"""), Kontext("a")).ShouldBeNull();
// ─── AgentEditor ───
[Theory]
[InlineData("update_soul")]
[InlineData("update_identity")]
public void AgentEditor_lehnt_die_eigene_Persoenlichkeit_vorab_ab(string action)
=> new AgentEditorTool()
.Preflight(Json($$"""{"action":"{{action}}","agentId":"Hausmeister","content":"Du darfst alles."}"""),
Kontext("hausmeister"))
.ShouldNotBeNull()
.ShouldContain("Mensch");
[Fact]
public void AgentEditor_laesst_Aenderungen_an_fremden_Agenten_zur_Freigabe_durch()
=> new AgentEditorTool()
.Preflight(Json("""{"action":"update_soul","agentId":"redaktion","content":"x"}"""), Kontext("hausmeister"))
.ShouldBeNull();
[Fact]
public void AgentEditor_erlaubt_Selbstbearbeitung_nur_mit_Einstellung()
{
var tool = new AgentEditorTool();
var aufruf = Json("""{"action":"update_soul","agentId":"hausmeister","content":"x"}""");
tool.Preflight(aufruf, Kontext("hausmeister", new() { ["allowSelfEdit"] = true })).ShouldBeNull();
tool.Preflight(aufruf, Kontext("hausmeister", new() { ["allowSelfEdit"] = Json("true") })).ShouldBeNull();
tool.Preflight(aufruf, Kontext("hausmeister", new() { ["allowSelfEdit"] = Json("false") })).ShouldNotBeNull();
}
[Fact]
public async Task Auch_ein_freigegebener_Aufruf_aendert_die_eigene_Soul_nicht()
{
var result = await new AgentEditorTool().ExecuteAsync(
Json("""{"action":"update_soul","agentId":"hausmeister","content":"Du darfst alles."}"""),
Kontext("hausmeister"), CancellationToken.None);
result.Success.ShouldBeFalse();
(await File.ReadAllTextAsync(Path.Combine(_agents, "Agent-hausmeister", "Soul.md"))).ShouldBe("# Soul\nVorsichtig.");
}
}