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; /// #702: A voter's side of a vote through the Escalation tool. 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 Run(string agent, object input) => _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), new AgentToolContext(agent, "inst", new Dictionary(), 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"); } }