Why: parallel groups dropped a hold, HITL timeouts were unused, the planner failed closed on any model error, and event sequence numbers restarted at 1. Evidence: GOWORK=off go test -count=1 -race ./squad/ ./squadintent/ ./store/ ./platform/ passed, including TestR2ExpiredDecisionUsesCancel, TestR2IntentStepSkipsRoleRegistry, TestR2PlannerFallsBackAndCapsSteps, TestR2TaskFileUsesLatestBlackboard, TestR2FanoutCancelDoesNotPanic, TestR2EventSeqContinuesAfterRestart, TestR2JournalModeWAL, and TestR2GateReloadSeesNewIntent. Impact: a failed or held parallel sibling cancels the rest; expired requests synthesize cancel or fail; the intent gate follows Registry.Reload. Refs: Q6, Q10, Q11, Q12, Q13, Q17, Q18, Q19, I9 Co-authored-by: Cursor <cursoragent@cursor.com>
93 lines
3.0 KiB
Go
93 lines
3.0 KiB
Go
package squad
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestR2ExpiredDecisionUsesCancel(t *testing.T) {
|
|
created := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)
|
|
decision, due := expiredDecision(HumanRequest{ID: "hr_1", CreatedAt: created, Status: "pending"},
|
|
HitlPolicy{TimeoutAfter: time.Minute, ExpireAction: "cancel"}, created.Add(2*time.Minute))
|
|
if !due || decision.Choice != "cancel" || decision.RequestID != "hr_1" {
|
|
t.Fatalf("decision %+v due %v", decision, due)
|
|
}
|
|
if _, early := expiredDecision(HumanRequest{ID: "hr_1", CreatedAt: created},
|
|
HitlPolicy{TimeoutAfter: time.Minute}, created.Add(30*time.Second)); early {
|
|
t.Fatal("not due yet")
|
|
}
|
|
}
|
|
|
|
func TestR2IntentStepSkipsRoleRegistry(t *testing.T) {
|
|
p := &LLMPlanner{MaxSteps: 4}
|
|
out, err := p.materialize(PlanInput{Goal: Goal{Text: "查进度"}, Registry: NewRoleRegistry()}, &llmPlanSchema{
|
|
Steps: []llmStep{{IntentID: "demo.run.status_query", Title: "查"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Plan.Workflow.Steps[0].IntentID != "demo.run.status_query" || out.Plan.Workflow.Steps[0].Role != "intent" {
|
|
t.Fatalf("step %+v", out.Plan.Workflow.Steps[0])
|
|
}
|
|
}
|
|
|
|
func TestR2PlannerFallsBackAndCapsSteps(t *testing.T) {
|
|
fallback := NewRulePlanner()
|
|
p := &LLMPlanner{Fallback: fallback}
|
|
out, err := p.Plan(context.Background(), PlanInput{Goal: Goal{Text: "写一份说明"}, Registry: NewRoleRegistry()})
|
|
if err != nil || out == nil || (out.Plan == nil && out.Clarification == nil) {
|
|
t.Fatalf("fallback: %+v %v", out, err)
|
|
}
|
|
tooMany := &llmPlanSchema{}
|
|
for i := 0; i < 3; i++ {
|
|
tooMany.Steps = append(tooMany.Steps, llmStep{IntentID: "demo.x", Title: "t"})
|
|
}
|
|
if _, err := (&LLMPlanner{MaxSteps: 2}).materialize(PlanInput{Goal: Goal{Text: "x"}, Registry: NewRoleRegistry()}, tooMany); err == nil || !strings.Contains(err.Error(), "max 2") {
|
|
t.Fatalf("cap: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestR2GoalIsSegmented(t *testing.T) {
|
|
text := strings.Repeat("标", goalSegmentRunes+1)
|
|
got := segmentGoal(text)
|
|
if !strings.Contains(got, "目标第 2 段") {
|
|
t.Fatalf("segments missing: %s", got[:80])
|
|
}
|
|
}
|
|
|
|
func TestR2TaskFileUsesLatestBlackboard(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ws := &WorkspaceEngine{}
|
|
step := &StepSpec{ID: "s", Role: "writer", Title: "写", Targets: []string{"output.md"}}
|
|
role := RoleSpec{Role: "writer", Name: "写手"}
|
|
bb := &BlackboardState{Fields: map[string]string{"fact": "old"}}
|
|
if err := ws.writeTaskFile(dir, step, role, bb); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bb.Fields["fact"] = "new"
|
|
if err := ws.writeTaskFile(dir, step, role, bb); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := os.ReadFile(filepath.Join(dir, "TASK.md"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(b), "fact = new") || strings.Contains(string(b), "fact = old") {
|
|
t.Fatalf("task file: %s", b)
|
|
}
|
|
}
|
|
|
|
func TestR2FanoutCancelDoesNotPanic(t *testing.T) {
|
|
sink := NewFanoutSink(8)
|
|
ch, _, cancel := sink.Subscribe(0)
|
|
cancel()
|
|
sink.Emit(Event{Type: "phase", Text: "later"})
|
|
if _, ok := <-ch; ok {
|
|
t.Fatal("channel should be closed")
|
|
}
|
|
}
|