feat(agent): 诊断 run 取消/终止原因,取代笼统的中断提示
- 新增 AbortCause(cancelcause.go):各取消点用 WithCancelCause/WithTimeoutCause 附上原因(暂停/删除/kill_work/目标达成/drain 兜底/聊天停止/进程关闭/硬超时…), captureRun 用 context.Cause 还原出到底是谁、为何取消。 - 新增 terminalreason.go:把终端状态转成可读的一行摘要 + 多行详情(取消原因、 终止 reason、轮数、卡住的工具),替换原来固定那句猜测式提示。 - engine/worker/planner/capture 及 conversations/orchestration/server 接入; cmd/artex/main.go、前端 llm/main-content 相应调整。 - 配套测试 terminalreason_test.go / engine_cancelcause_test.go。
This commit is contained in:
1 parent
8e9b126972
commit
a6bd18050c
16 files changed
+624
-60
No files matched your search
@@ -0,0 +1,96 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Autumn-27/artex/agent"
|
||||
)
|
||||
|
||||
// The engine's cancel paths must hand a NAMED cause to the run being cancelled —
|
||||
// that cause is the whole content of the "运行被中断" line in the trace. These
|
||||
// exercise the context machinery only (no Manager / DB needed).
|
||||
|
||||
func TestPauseCancelsWithNamedCause(t *testing.T) {
|
||||
e := NewEngine(nil)
|
||||
ectx := e.execContextFor(context.Background(), "t1")
|
||||
// the per-work child the worker loop actually runs under
|
||||
workCtx, workCancel := context.WithCancelCause(ectx)
|
||||
defer workCancel(nil)
|
||||
|
||||
e.Pause("t1", agent.AbortPausedByUser)
|
||||
|
||||
if code, _, _, ok := agent.AbortReason(workCtx); !ok || code != "paused_by_user" {
|
||||
t.Fatalf("work ctx 拿到的原因 code=%q ok=%v,期望 paused_by_user", code, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKillWorkCancelsOnlyThatWorkWithNamedCause(t *testing.T) {
|
||||
e := NewEngine(nil)
|
||||
ectx := e.execContextFor(context.Background(), "t1")
|
||||
workCtx, workCancel := context.WithCancelCause(ectx)
|
||||
e.registerWork(42, workCancel)
|
||||
|
||||
if err := e.KillWork(42); err != nil {
|
||||
t.Fatalf("KillWork: %v", err)
|
||||
}
|
||||
if code, _, _, ok := agent.AbortReason(workCtx); !ok || code != "killed_by_planner" {
|
||||
t.Fatalf("code=%q ok=%v,期望 killed_by_planner", code, ok)
|
||||
}
|
||||
// the task ctx must survive — kill_work stops one work, not the whole task
|
||||
if ectx.Err() != nil {
|
||||
t.Error("kill_work 不该取消整个任务的 exec ctx")
|
||||
}
|
||||
if err := e.KillWork(999); err == nil {
|
||||
t.Error("对没有在跑的意图 KillWork 应该报错")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettleDrainAndGoalMetCausesAreDistinct(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cause *agent.AbortCause
|
||||
want string
|
||||
}{
|
||||
{"settle", agent.AbortSettleDrainTimeout, "settle_drain_timeout"},
|
||||
{"goalMet", agent.AbortGoalMet, "goal_met"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := NewEngine(nil)
|
||||
ectx := e.execContextFor(context.Background(), "t1")
|
||||
e.cancelExec("t1", tc.cause)
|
||||
if code, _, _, _ := agent.AbortReason(ectx); code != tc.want {
|
||||
t.Fatalf("code=%q,期望 %q", code, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// asking for an exec ctx while paused must return an already-cancelled ctx that
|
||||
// still SAYS why — this is the claim→Execute race guard, easily mistaken for a
|
||||
// mystery abort.
|
||||
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("暂停期间不该发出可用的 exec ctx")
|
||||
}
|
||||
if code, _, _, _ := agent.AbortReason(ctx); code != "paused_race_guard" {
|
||||
t.Fatalf("code=%q,期望 paused_race_guard", code)
|
||||
}
|
||||
}
|
||||
|
||||
// Resume must hand out a live context again (cancelling is one-shot).
|
||||
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 没取消旧 ctx")
|
||||
}
|
||||
e.paused.Delete("t1") // Resume() without the Task/Notify plumbing
|
||||
if fresh := e.execContextFor(context.Background(), "t1"); fresh.Err() != nil {
|
||||
t.Fatal("恢复后应拿到全新可用的 exec ctx")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user