feat: harden concurrency scheduling and task watchdog

This commit is contained in:
lpf
2026-03-05 11:32:06 +08:00
parent 0f3196f305
commit 2fbb98bccd
20 changed files with 1526 additions and 159 deletions

View File

@@ -4,9 +4,14 @@ import (
"context"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
"clawgo/pkg/bus"
"clawgo/pkg/config"
"clawgo/pkg/ekg"
"clawgo/pkg/providers"
)
@@ -54,6 +59,115 @@ func TestProcessPlannedMessage_AggregatesResults(t *testing.T) {
}
}
type probeProvider struct {
mu sync.Mutex
inFlight int
maxInFlight int
delayPerCall time.Duration
responseCount int
}
func (p *probeProvider) Chat(_ context.Context, _ []providers.Message, _ []providers.ToolDefinition, _ string, _ map[string]interface{}) (*providers.LLMResponse, error) {
p.mu.Lock()
p.inFlight++
if p.inFlight > p.maxInFlight {
p.maxInFlight = p.inFlight
}
p.responseCount++
p.mu.Unlock()
time.Sleep(p.delayPerCall)
p.mu.Lock()
n := p.responseCount
p.inFlight--
p.mu.Unlock()
resp := providers.LLMResponse{Content: "done-" + strconv.Itoa(n), FinishReason: "stop"}
return &resp, nil
}
func (p *probeProvider) GetDefaultModel() string { return "test-model" }
func TestRunPlannedTasks_NonConflictingKeysCanRunInParallel(t *testing.T) {
p := &probeProvider{delayPerCall: 100 * time.Millisecond}
cfg := config.DefaultConfig()
cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
cfg.Agents.Defaults.MaxToolIterations = 2
cfg.Agents.Defaults.ContextCompaction.Enabled = false
loop := NewAgentLoop(cfg, bus.NewMessageBus(), p, nil)
_, err := loop.processPlannedMessage(context.Background(), bus.InboundMessage{
Channel: "cli",
SenderID: "u",
ChatID: "direct",
SessionKey: "sess-plan-parallel",
Content: "[resource_keys: file:pkg/a.go] 修复 a[resource_keys: file:pkg/b.go] 修复 b",
})
if err != nil {
t.Fatalf("processPlannedMessage error: %v", err)
}
if p.maxInFlight < 2 {
t.Fatalf("expected parallel execution for non-conflicting keys, got maxInFlight=%d", p.maxInFlight)
}
}
func TestRunPlannedTasks_ConflictingKeysMutuallyExclusive(t *testing.T) {
p := &probeProvider{delayPerCall: 100 * time.Millisecond}
cfg := config.DefaultConfig()
cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
cfg.Agents.Defaults.MaxToolIterations = 2
cfg.Agents.Defaults.ContextCompaction.Enabled = false
loop := NewAgentLoop(cfg, bus.NewMessageBus(), p, nil)
_, err := loop.processPlannedMessage(context.Background(), bus.InboundMessage{
Channel: "cli",
SenderID: "u",
ChatID: "direct",
SessionKey: "sess-plan-locked",
Content: "[resource_keys: file:pkg/a.go] 修复 a[resource_keys: file:pkg/a.go] 补测试",
})
if err != nil {
t.Fatalf("processPlannedMessage error: %v", err)
}
if p.maxInFlight != 1 {
t.Fatalf("expected mutual exclusion for conflicting keys, got maxInFlight=%d", p.maxInFlight)
}
}
func TestRunPlannedTasks_PublishesStepProgress(t *testing.T) {
rp := &recordingProvider{responses: []providers.LLMResponse{
{Content: "done-a", FinishReason: "stop"},
{Content: "done-b", FinishReason: "stop"},
}}
loop := setupLoop(t, rp)
_, err := loop.processPlannedMessage(context.Background(), bus.InboundMessage{
Channel: "cli",
SenderID: "u",
ChatID: "direct",
SessionKey: "sess-plan-progress",
Content: "修复 pkg/a.go补充 pkg/b.go 测试",
})
if err != nil {
t.Fatalf("processPlannedMessage error: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out1, ok := loop.bus.SubscribeOutbound(ctx)
if !ok {
t.Fatalf("expected first progress outbound")
}
out2, ok := loop.bus.SubscribeOutbound(ctx)
if !ok {
t.Fatalf("expected second progress outbound")
}
all := out1.Content + "\n" + out2.Content
if !strings.Contains(all, "进度 1/2") || !strings.Contains(all, "进度 2/2") {
t.Fatalf("unexpected progress outputs:\n%s", all)
}
}
func TestFindRecentRelatedErrorEvent(t *testing.T) {
ws := filepath.Join(t.TempDir(), "workspace")
_ = os.MkdirAll(filepath.Join(ws, "memory"), 0o755)