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") } }