Eskalation, Konflikt und Beschluss als eigene Vorgaenge (#689, Stufe 1+2)
- Cases/Decisions in der Instanz-DB; CaseDesk fuehrt die Regeln, der Brett-Task traegt den Vorgang zum jeweiligen Halter - Eskalation nur nach oben, eine Ebene je Schritt; die Leitung entscheidet (mit Begruendung, bindet ihren Bereich), erledigt selbst oder reicht mit Einschaetzung weiter - Erst nachsehen: bindende Beschluesse werden vor dem Eskalieren gezeigt; dieselbe Frage aus derselben Einheit schliesst sich dem offenen Vorgang an - Tagesdeckel je Einheit; nach der Frist wird ein Mensch gerufen (Anschluss fuer #644) - Konflikt: zuerst die Gegenseite, dann Zustellung an die niedrigste gemeinsame Leitung; Ebenen dazwischen vermerkt, nicht befragt - Fremdinhalt-Stufe (#715): Eskalation ohne eigenen Text, kein Konflikt mit Position - Jede Ebene haengt an (#697), beim Halter gerahmt als Daten - Escalation ist ein implizites Werkzeug jedes Agenten; Briefing nennt den Weg - Scanner behaelt einen im Lauf gesetzten Status blockiert/abgebrochen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c03664cf29
commit
36fe1ad2aa
@@ -0,0 +1,59 @@
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using ClawdDotNet.Core.Tasks;
|
||||
using ClawdDotNet.Core.Tests.Infrastructure;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// #689/#715: A status the run set itself is the run's word. An agent that escalates, or sets
|
||||
/// its task to blocked to report up, must not see it overwritten with "done" when the run ends.
|
||||
/// </summary>
|
||||
public sealed class RunSetStatusTests : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
private readonly SqliteTaskRepository _repo;
|
||||
private readonly FakeTimeProvider _clock = new(new DateTimeOffset(2027, 3, 10, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
public RunSetStatusTests()
|
||||
{
|
||||
_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 */ }
|
||||
}
|
||||
|
||||
/// <summary>A run that sets its own task to a status, as the taskboard tool would.</summary>
|
||||
private sealed class SettingDispatcher(SqliteTaskRepository repo, TaskItemStatus? set) : ITaskDispatcher
|
||||
{
|
||||
public async Task<TaskLauf> DispatchAsync(TaskItem task, CancellationToken ct)
|
||||
{
|
||||
if (set is { } status)
|
||||
await repo.SetStatusAsync(task.Id, status, DateTime.UtcNow, ct);
|
||||
return TaskLauf.Erledigt;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TaskItemStatus> RunWith(TaskItemStatus? set)
|
||||
{
|
||||
await _repo.UpsertAsync(new TaskItem { Id = "t-1", Title = "Work", Assignee = "@worker" }, default);
|
||||
var scanner = new TaskScanner(_repo, new SettingDispatcher(_repo, set), NullLoggerFactory.Instance, clock: _clock);
|
||||
await scanner.ScanOnceAsync(default);
|
||||
return (await _repo.GetAsync("t-1", default))!.Status;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TaskItemStatus.Blocked)]
|
||||
[InlineData(TaskItemStatus.Canceled)]
|
||||
public async Task A_status_the_run_set_itself_is_kept(TaskItemStatus set)
|
||||
=> (await RunWith(set)).ShouldBe(set);
|
||||
|
||||
[Fact]
|
||||
public async Task A_run_that_sets_nothing_is_done()
|
||||
=> (await RunWith(null)).ShouldBe(TaskItemStatus.Done);
|
||||
}
|
||||
Reference in New Issue
Block a user