Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/Taskboard/ParentTaskTests.cs
T
RichardandClaude Opus 5.5 b22272345e Zielkette, Stufe 1: jede Aufgabe kann sagen, wofuer sie da ist (#714)
- TaskItem.ParentId, in der Datei als parent, in der DB als ParentId. Die
  Elternaufgabe ist der Zweck ("wofuer"), blocked_by die Voraussetzung
  ("wann").
- GoalChain: Eltern bis zu acht Stufen, Zyklen brechen den Gang ab und
  werden beim Setzen abgelehnt. Oben steht das Ziel der Einheit - der
  obersten Aufgabe, die zu einer Einheit gehoert (ein Ziel, das ein Mensch
  auf der obersten Ebene setzt, gehoert keiner), sonst der naechsten Einheit
  darueber mit Ziel.
- Der Lauf einer Aufgabe bekommt den Absatz "Why this task exists" und die
  Bitte, zu melden, wenn die Aufgabe ihrem Zweck nicht mehr dient.
- Taskboard-Tool: parent bei task_create/task_update, geprueft auf Existenz,
  Sichtbarkeit (#684) und Zyklen; task_list zeigt "Serves".
- WebUI: Feld im Aufgabendialog, Anzeige auf der Karte; die Verwaltung
  prueft wie das Tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 18:45:15 +02:00

111 lines
4.1 KiB
C#

using System.Text.Json;
using ClawdDotNet.Core.Config;
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>#714: Sub-tasks under the task they serve — through the tool, with its checks.</summary>
public sealed class ParentTaskTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
private readonly SqliteTaskRepository _repo;
private readonly TaskboardTool _tool = new();
private readonly OrgChart _org = new(
[
new UnitConfig { Id = "marketing", Name = "Marketing" },
new UnitConfig { Id = "accounting", Name = "Accounting" }
],
[
new AgentConfig { AgentId = "ads", UnitId = "marketing" },
new AgentConfig { AgentId = "bookkeeper", UnitId = "accounting" }
]);
public ParentTaskTests()
{
_repo = new SqliteTaskRepository(new SqliteStorage(Path.Combine(_directory, "state.db")));
}
public void Dispose()
{
SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is secondary */ }
}
private Task<ToolResult> Run(string agentId, object input)
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), new AgentToolContext(
agentId, "inst", new Dictionary<string, object?>(), new NullStateStore(), NullLogger.Instance,
CancellationToken.None, SharedWorkspacePath: Path.Combine(_directory, "SharedWorkspace"), Tasks: _repo)
{
Organisation = _org
}, CancellationToken.None);
private async Task<string> Create(string agentId, string title, string? parent = null)
{
var result = await Run(agentId, new { action = "task_create", title, parent, force = true });
result.Success.ShouldBeTrue(result.ErrorMessage);
return result.Content.Split(' ', 4)[2];
}
[Fact]
public async Task A_sub_task_is_created_under_the_task_it_serves_and_listed_so()
{
var top = await Create("ads", "Spring collection");
var sub = await Create("ads", "Listings for the collection", top);
(await _repo.GetAsync(sub, default))!.ParentId.ShouldBe(top);
(await Run("ads", new { action = "task_list" })).Content.ShouldContain($"Serves: {top}");
}
[Fact]
public async Task A_parent_that_does_not_exist_is_refused()
{
var result = await Run("ads", new { action = "task_create", title = "Orphan", parent = "t-nowhere" });
result.Success.ShouldBeFalse();
result.ErrorMessage!.ShouldContain("No task found");
}
[Fact]
public async Task A_parent_on_another_board_is_answered_like_one_that_does_not_exist()
{
var foreign = await Create("bookkeeper", "Close the books");
var result = await Run("ads", new { action = "task_create", title = "Sneaky", parent = foreign });
result.Success.ShouldBeFalse();
result.ErrorMessage.ShouldBe($"No task found with ID '{foreign}' to serve as parent.");
}
[Fact]
public async Task A_task_cannot_be_moved_below_its_own_sub_task()
{
var top = await Create("ads", "Spring collection");
var sub = await Create("ads", "Listings", top);
var result = await Run("ads", new { action = "task_update", id = top, parent = sub });
result.Success.ShouldBeFalse();
result.ErrorMessage!.ShouldContain("cannot serve itself");
}
[Fact]
public async Task An_empty_parent_removes_it()
{
var top = await Create("ads", "Spring collection");
var sub = await Create("ads", "Listings", top);
(await Run("ads", new { action = "task_update", id = sub, parent = "" })).Success.ShouldBeTrue();
(await _repo.GetAsync(sub, default))!.ParentId.ShouldBeEmpty();
}
}