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");
}
}