Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/Taskboard/SpotCheckToolTests.cs
T
RichardandClaude Opus 5.5 68e723a9c0 Kontrollpflicht der Leitung: Stichproben gegen das Audit (#693)
A lead looks at the runs of the agents it leads - held against the task and
against what the engine stamped, never by asking the agent.

- RunInspector (Core/Inspection): the mechanical check and first profile of
  the inspector foundation (#204). Flags failed runs, tool errors, refusals,
  unfinished calls, tasks answered with bookkeeping alone, and operations the
  agent names but never called in the run (the 2026-09-14 pattern).
- SpotChecks: picks runs per unit (rate, denser for new agents, new models and
  after a failure, daily cap), deterministic by run id; one task per lead.
- Escalation tool: spot_checks, inspect_run, check_done, propose_measure.
  Measures go to a human; nothing is carried out automatically. A security
  suspicion is its own case, not a personnel matter.
- Organisation view: settings per unit, recent checks, measures to decide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:40:20 +02:00

115 lines
5.1 KiB
C#

using System.Text.Json;
using ClawdDotNet.Core.Audit;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Inspection;
using ClawdDotNet.Core.Organisation;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Tools.Taskboard;
using ClawdDotNet.Tools.Tests.FileRW;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.Taskboard;
/// <summary>#693: The lead's side of the spot checks through the Escalation tool.</summary>
public sealed class SpotCheckToolTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
private readonly SqliteStorage _storage;
private readonly SqliteAuditRepository _audit;
private readonly SpotChecks _checks;
private readonly EscalationTool _tool = new();
private readonly OrgChart _org = new(
[
new UnitConfig { Id = "purchasing", Name = "Purchasing", LeaderAgentId = "buy-lead", Inspection = new UnitInspection { Rate = 1, DenseRate = 1, PerDay = 5 } },
new UnitConfig { Id = "sales", Name = "Sales", LeaderAgentId = "sales-lead" }
],
[
new AgentConfig { AgentId = "buy-lead", UnitId = "purchasing" },
new AgentConfig { AgentId = "buyer", UnitId = "purchasing" },
new AgentConfig { AgentId = "sales-lead", UnitId = "sales" },
new AgentConfig { AgentId = "seller", UnitId = "sales" }
]);
public SpotCheckToolTests()
{
_storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
_audit = new SqliteAuditRepository(_storage);
var tasks = new SqliteTaskRepository(_storage);
_checks = new SpotChecks(_storage, _audit,
new RunInspector(_audit, tasks, null, () => RunInspector.VocabularyOf([_tool])),
() => _org, new TaskboardService(tasks, Path.Combine(_directory, "tasks")), NullLogger.Instance);
}
public void Dispose()
{
SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is secondary */ }
}
private Task<ToolResult> Run(string agent, object input)
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input),
new AgentToolContext(agent, "inst", new Dictionary<string, object?>(), new NullStateStore(), NullLogger.Instance, CancellationToken.None)
{
Organisation = _org,
SpotChecks = _checks
}, CancellationToken.None);
private async Task RecordAsync(string runId, string agent, string said)
{
await _audit.AppendAsync(new AuditEntry
{
RunId = runId, AgentId = agent, Tool = "FileRW", Arguments = "{\"action\":\"append\"}",
Status = AuditStatus.Ok, OccurredAt = DateTime.UtcNow.AddMinutes(-2)
}, default);
await _audit.RecordReceiptAsync(new RunReceipt
{
RunId = runId, AgentId = agent, Model = "m", Status = "Completed", StepCount = 2,
ResultRef = said, OccurredAt = DateTime.UtcNow.AddMinutes(-1)
}, default);
}
[Fact]
public async Task The_lead_sees_its_checks_with_the_audit_findings_and_closes_them()
{
await RecordAsync("run-b", "buyer", "Escalation doctrine_set was refused.");
var check = (await _checks.SampleAsync(default)).Single();
var list = await Run("buy-lead", new { action = "spot_checks" });
list.Content.ShouldContain($"check {check.Id}");
list.Content.ShouldContain("doctrine_set", customMessage: "the claim without a call is shown from the audit");
var detail = await Run("buy-lead", new { action = "inspect_run", run = "run-b" });
detail.Content.ShouldContain("FileRW.append: Ok");
detail.Content.ShouldContain("its claim, not evidence");
(await Run("buy-lead", new { action = "check_done", check = check.Id, verdict = "finding", note = "Reports a refusal that never happened." }))
.Success.ShouldBeTrue();
(await Run("buy-lead", new { action = "spot_checks" })).Content.ShouldContain("No spot check waits for you");
}
[Fact]
public async Task A_lead_sees_no_runs_of_agents_it_does_not_lead()
{
await RecordAsync("run-s", "seller", "Done.");
(await Run("buy-lead", new { action = "inspect_run", run = "run-s" })).ErrorMessage!.ShouldContain("do not lead");
(await Run("buyer", new { action = "inspect_run", run = "run-s" })).Success.ShouldBeFalse();
}
[Fact]
public async Task A_measure_needs_a_known_kind_and_goes_to_a_human()
{
(await Run("buy-lead", new { action = "propose_measure", agent = "buyer", measure = "fire", reasoning = "x" }))
.ErrorMessage!.ShouldContain("'measure'");
var proposed = await Run("buy-lead", new { action = "propose_measure", agent = "buyer", measure = "change_model", reasoning = "Fails every table task." });
proposed.Success.ShouldBeTrue(proposed.ErrorMessage);
proposed.Content.ShouldContain("with a human");
}
}