Add read-only task context inheritance, ordered task-level LLM failover, live Worker controls, cascading task cleanup, FIFO task concurrency, cancellation diagnostics, UI updates, tests, build script, and changelog.
110 lines
3.1 KiB
Go
110 lines
3.1 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/Autumn-27/artex/agent"
|
|
)
|
|
|
|
func TestPauseCancelsWithNamedCause(t *testing.T) {
|
|
e := NewEngine(nil)
|
|
execCtx := e.execContextFor(context.Background(), "t1")
|
|
workCtx, workCancel := context.WithCancelCause(execCtx)
|
|
defer workCancel(nil)
|
|
e.Pause("t1", agent.AbortPausedByUser)
|
|
if code, _, _, ok := agent.AbortReason(workCtx); !ok || code != "paused_by_user" {
|
|
t.Fatalf("code=%q ok=%v, want paused_by_user", code, ok)
|
|
}
|
|
}
|
|
|
|
func TestKillWorkCancelsOnlyThatWorkWithNamedCause(t *testing.T) {
|
|
e := NewEngine(nil)
|
|
execCtx := e.execContextFor(context.Background(), "t1")
|
|
workCtx, workCancel := context.WithCancelCause(execCtx)
|
|
e.registerWork(42, workCancel)
|
|
if err := e.KillWork(42); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if code, _, _, ok := agent.AbortReason(workCtx); !ok || code != "killed_by_planner" {
|
|
t.Fatalf("code=%q ok=%v, want killed_by_planner", code, ok)
|
|
}
|
|
if execCtx.Err() != nil {
|
|
t.Fatal("kill_work cancelled the entire task context")
|
|
}
|
|
_, complete := e.detachWork(42)
|
|
complete()
|
|
}
|
|
|
|
func TestControlWorkCarriesUserActionCause(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
action string
|
|
code string
|
|
}{
|
|
{action: "pause", code: "work_paused_by_user"},
|
|
{action: "cancel", code: "work_cancelled_by_user"},
|
|
} {
|
|
t.Run(tc.action, func(t *testing.T) {
|
|
e := NewEngine(nil)
|
|
ctx, cancel := context.WithCancelCause(context.Background())
|
|
e.registerWork(42, cancel)
|
|
done := make(chan error, 1)
|
|
go func() { done <- e.ControlWork(42, tc.action) }()
|
|
<-ctx.Done()
|
|
if code, _, _, _ := agent.AbortReason(ctx); code != tc.code {
|
|
t.Fatalf("code=%q, want %q", code, tc.code)
|
|
}
|
|
action, complete := e.detachWork(42)
|
|
if action != tc.action {
|
|
t.Fatalf("action=%q, want %q", action, tc.action)
|
|
}
|
|
complete()
|
|
if err := <-done; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSettleDrainAndGoalMetCausesAreDistinct(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
cause *agent.AbortCause
|
|
code string
|
|
}{
|
|
{agent.AbortSettleDrainTimeout, "settle_drain_timeout"},
|
|
{agent.AbortGoalMet, "goal_met"},
|
|
} {
|
|
e := NewEngine(nil)
|
|
ctx := e.execContextFor(context.Background(), "t1")
|
|
e.cancelExec("t1", tc.cause)
|
|
if code, _, _, _ := agent.AbortReason(ctx); code != tc.code {
|
|
t.Fatalf("code=%q, want %q", code, tc.code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExecContextWhilePausedCarriesRaceGuardCause(t *testing.T) {
|
|
e := NewEngine(nil)
|
|
e.Pause("t1", agent.AbortPausedByUser)
|
|
ctx := e.execContextFor(context.Background(), "t1")
|
|
if ctx.Err() == nil {
|
|
t.Fatal("paused task received a live execution context")
|
|
}
|
|
if code, _, _, _ := agent.AbortReason(ctx); code != "paused_race_guard" {
|
|
t.Fatalf("code=%q, want paused_race_guard", code)
|
|
}
|
|
}
|
|
|
|
func TestResumeYieldsFreshContext(t *testing.T) {
|
|
e := NewEngine(nil)
|
|
old := e.execContextFor(context.Background(), "t1")
|
|
e.Pause("t1", agent.AbortPausedByUser)
|
|
if old.Err() == nil {
|
|
t.Fatal("Pause did not cancel old context")
|
|
}
|
|
e.paused.Delete("t1")
|
|
if fresh := e.execContextFor(context.Background(), "t1"); fresh.Err() != nil {
|
|
t.Fatal("resume did not create a fresh execution context")
|
|
}
|
|
}
|