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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b75b68c786
commit
b22272345e
@@ -0,0 +1,110 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user