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>
115 lines
5.1 KiB
C#
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");
|
|
}
|
|
}
|