feat(tasks): add templates findings workflows and batch controls

This commit is contained in:
pphua committed 2026-08-21 21:01:39 +08:00
1 parent b7e0b93023
commit d3183dbbd2
65 files changed
+11603 -2198

No files matched your search

+146 -2
View File
@@ -42,7 +42,7 @@ func TestEngineDeleteBarrierRejectsNewTaskOperations(t *testing.T) {
if e.BeginDelete(taskID) {
t.Fatal("duplicate delete must not acquire the barrier")
}
e.AbortDelete(taskID)
e.AbortDelete(taskID, false)
if e.IsPaused(taskID) {
t.Fatal("aborted delete should restore a previously running task")
}
@@ -58,12 +58,130 @@ func TestAbortDeletePreservesExistingPause(t *testing.T) {
if !e.BeginDelete("42") {
t.Fatal("delete barrier should be installed")
}
e.AbortDelete("42")
e.AbortDelete("42", true)
if !e.IsPaused("42") {
t.Fatal("aborted delete must preserve a pre-existing user pause")
}
}
func TestBeginTaskDeleteSerializesWithLifecycleLock(t *testing.T) {
s := &Server{engine: NewEngine(nil)}
s.concMu.Lock()
done := make(chan bool, 1)
go func() { done <- s.beginTaskDelete("42") }()
select {
case <-done:
t.Fatal("delete barrier bypassed the lifecycle lock")
case <-time.After(20 * time.Millisecond):
}
if s.engine.IsDeleting("42") {
t.Fatal("delete barrier was installed before concMu was released")
}
s.concMu.Unlock()
select {
case ok := <-done:
if !ok {
t.Fatal("delete barrier was not installed after lifecycle lock release")
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for delete barrier")
}
s.engine.AbortDelete("42", false)
}
func TestDeletingRunningTaskKeepsConcurrencySlotUntilCommit(t *testing.T) {
task := &Task{ID: "42", Status: "running"}
s := &Server{engine: NewEngine(nil)}
if !s.engine.BeginDelete(task.ID) {
t.Fatal("install delete barrier")
}
if !s.occupiesConcurrencySlot(task) {
t.Fatal("temporary delete pause released the running task's slot before commit")
}
s.engine.AbortDelete(task.ID, false)
}
func TestTaskLifecycleRechecksDeleteBarrierAfterConcLock(t *testing.T) {
task := &Task{ID: "42", notify: make(chan struct{}, 1)}
m := &Manager{tasks: map[string]*Task{task.ID: task}}
s := &Server{m: m, engine: NewEngine(m)}
s.concMu.Lock()
started := make(chan struct{})
pauseDone := make(chan error, 1)
go func() {
close(started)
_, err := s.applyTaskControl(task, "pause")
pauseDone <- err
}()
<-started
if !s.engine.BeginDelete(task.ID) {
t.Fatal("install delete barrier")
}
s.concMu.Unlock()
if err := <-pauseDone; err == nil || !strings.Contains(err.Error(), "正在删除") {
t.Fatalf("pause passed a delete barrier installed while waiting for concMu: %v", err)
}
s.engine.AbortDelete(task.ID, false)
s.concMu.Lock()
started = make(chan struct{})
admitDone := make(chan error, 1)
go func() {
close(started)
_, err := s.admitTask(task, "resume")
admitDone <- err
}()
<-started
if !s.engine.BeginDelete(task.ID) {
t.Fatal("install second delete barrier")
}
s.concMu.Unlock()
if err := <-admitDone; err == nil || !strings.Contains(err.Error(), "being deleted") {
t.Fatalf("admission passed a delete barrier installed while waiting for concMu: %v", err)
}
s.engine.AbortDelete(task.ID, false)
}
func TestAbortTaskDeleteUsesPersistedPauseAndQueueState(t *testing.T) {
m, err := NewManager(t.TempDir(), "")
if err != nil {
t.Skipf("postgres unavailable (%v) - skipping", err)
}
defer m.Close()
task, err := m.CreateTask("delete abort state", "restore persisted barrier", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
defer func() { _, _ = m.DeleteTask(task.ID, DeleteTaskOptions{}) }()
s := &Server{m: m, engine: NewEngine(m)}
// An engine-only pause is not authoritative. The committed task is running,
// so aborting deletion must clear this stale in-memory barrier.
s.engine.Pause(task.ID, agent.AbortPausedByUser)
if !s.beginTaskDelete(task.ID) {
t.Fatal("install first delete barrier")
}
s.abortTaskDelete(task.ID)
if s.engine.IsPaused(task.ID) {
t.Fatal("stale engine pause survived although the persisted task is running")
}
// A queued row must retain its execution barrier even when BeginDelete saw the
// engine as running. This specifically guards against restoring a stale snapshot.
if err := m.EnqueueTask(task.ID, "resume"); err != nil {
t.Fatal(err)
}
if !s.beginTaskDelete(task.ID) {
t.Fatal("install second delete barrier")
}
s.abortTaskDelete(task.ID)
if !s.engine.IsPaused(task.ID) {
t.Fatal("persisted queued task lost its execution barrier after abort")
}
}
func TestStopTaskCancelsRuntimeAndClearsLifecycleState(t *testing.T) {
e := NewEngine(nil)
const taskID = "42"
@@ -182,6 +300,32 @@ func TestWaitTaskQuiescentHonorsContext(t *testing.T) {
}
}
func TestTaskChatStatusUsesAuthoritativeBusyState(t *testing.T) {
m := &Manager{tasks: map[string]*Task{"7": {ID: "7"}}}
s := &Server{
m: m,
chatBusy: map[string]bool{"7": true},
chatCancel: map[string]context.CancelCauseFunc{},
}
req := httptest.NewRequest(http.MethodGet, "/api/tasks/7/chat/status", nil)
req.SetPathValue("id", "7")
rec := httptest.NewRecorder()
s.taskChatStatus(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s, want 200", rec.Code, rec.Body.String())
}
var got struct {
Running bool `json:"running"`
}
if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if !got.Running {
t.Fatal("busy main-agent turn was reported idle")
}
}
func TestTaskChatUploadRejectsDeleteBarrierBeforeCreatingDirectory(t *testing.T) {
dataDir := t.TempDir()
m := &Manager{dir: dataDir, tasks: map[string]*Task{"7": {ID: "7"}}}