Abstimmung: mehrere Agenten pruefen unabhaengig und stimmen blind ab (#702)
A unit can put operations to a blind vote instead of its lead. Not opinions are counted but independent checks enforced. - UnitConfig.votes: operations, voters, approveAt, rejectAt, quorum, hours, perDay - set only by a human (OrgAdministration.SetVotes). - Core/Staging/Votes: routes a covered staged call before the lead; one task per voter with the frozen call and nothing else; cast_vote needs reasoning and evidence. Counted as soon as the result is settled; in between and without the quorum a human decides. The proposer does not vote, voters that read foreign content get no ballot, changes to agents never go to a vote. - Escalation tool: list_ballots, cast_vote. - Approvals view shows each vote with every ballot and the voters' costs; organisation view sets the rule and lists the votes held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d834e5ac38
commit
a5dd503d9b
@@ -0,0 +1,79 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using ClawdDotNet.Core.Staging;
|
||||
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>#702: A voter's side of a vote through the Escalation tool.</summary>
|
||||
public sealed class VoteToolTests : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
private readonly SqliteStagingRepository _staging;
|
||||
private readonly Votes _votes;
|
||||
private readonly EscalationTool _tool = new();
|
||||
|
||||
private readonly OrgChart _org = new(
|
||||
[
|
||||
new UnitConfig
|
||||
{
|
||||
Id = "purchasing", Name = "Purchasing",
|
||||
Votes = new UnitVotes { Operations = ["Database.insert"], Voters = ["accounting", "logistics"], ApproveAt = 2, RejectAt = 0 }
|
||||
}
|
||||
],
|
||||
[
|
||||
new AgentConfig { AgentId = "buyer", UnitId = "purchasing" },
|
||||
new AgentConfig { AgentId = "accounting", UnitId = "purchasing" },
|
||||
new AgentConfig { AgentId = "logistics", UnitId = "purchasing" }
|
||||
]);
|
||||
|
||||
public VoteToolTests()
|
||||
{
|
||||
var storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
|
||||
_staging = new SqliteStagingRepository(storage);
|
||||
var board = new TaskboardService(new SqliteTaskRepository(storage), Path.Combine(_directory, "tasks"));
|
||||
_votes = new Votes(storage, _staging, board, () => _org, _ => AgentTrust.Standard, 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,
|
||||
Votes = _votes,
|
||||
RunId = "run-" + agent
|
||||
}, CancellationToken.None);
|
||||
|
||||
[Fact]
|
||||
public async Task A_voter_lists_its_ballot_and_casts_it_with_evidence()
|
||||
{
|
||||
var call = new StagedCall { RunId = "r", AgentId = "buyer", Tool = "Database", Action = "insert", ArgumentsJson = "{\"action\":\"insert\"}" };
|
||||
var id = await _staging.AppendAsync(call, default);
|
||||
await _votes.RouteAsync(call with { Id = id }, default);
|
||||
var vote = (await _votes.ForCallsAsync([id], default))[id];
|
||||
|
||||
(await Run("accounting", new { action = "list_ballots" })).Content.ShouldContain($"vote #{vote.Id}");
|
||||
(await Run("accounting", new { action = "cast_vote", vote = vote.Id, choice = "maybe", reasoning = "x", evidence = "y" }))
|
||||
.ErrorMessage!.ShouldContain("'choice'");
|
||||
|
||||
var cast = await Run("accounting", new { action = "cast_vote", vote = vote.Id, choice = "no", reasoning = "Over budget.", evidence = "Balance 80 EUR, ledger 2026-10-01 08:10" });
|
||||
cast.Success.ShouldBeTrue(cast.ErrorMessage);
|
||||
cast.Content.ShouldContain("You will not see the others' votes");
|
||||
(await _votes.GetAsync(vote.Id, default))!.Ballots.Single(b => b.Voter == "accounting").RunId.ShouldBe("run-accounting");
|
||||
(await Run("logistics", new { action = "list_ballots" })).Content.ShouldNotContain("Over budget", customMessage: "blind");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user