| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "reasonix/internal/agent/testutil" |
| 11 | "reasonix/internal/event" |
| 12 | "reasonix/internal/provider" |
| 13 | "reasonix/internal/tool" |
| 14 | ) |
| 15 | |
| 16 | // A review child under an uncapped parent takes as many rounds as the review |
| 17 | // needs; a multi-file review that reads more than a handful of files must not |
| 18 | // stop at a budget the user has no setting for. |
| 19 | func TestReviewSubagentUnderUncappedParentFinishesPastEightRounds(t *testing.T) { |
| 20 | const rounds = 12 |
| 21 | turns := make([]testutil.Turn, 0, rounds+1) |
| 22 | for i := range rounds { |
| 23 | turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ |
| 24 | ID: fmt.Sprintf("read-%d", i), Name: "echo", Arguments: fmt.Sprintf(`{"text":"file %d"}`, i), |
| 25 | }}}) |
| 26 | } |
| 27 | turns = append(turns, testutil.Turn{Text: "verdict: approve"}) |
| 28 | parent := tool.NewRegistry() |
| 29 | parent.Add(echoTool{}) |
| 30 | task := NewTaskTool(testutil.NewMock("sub", turns...), nil, parent, 0, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil). |
| 31 | WithTranscripts(mustSubagentStore(t), t.TempDir(), "base", "high"). |
| 32 | WithProfileLookup(func(name string) (ProfileDefinition, bool) { |
| 33 | return ProfileDefinition{Name: name, Body: "You review changes."}, name == "review" |
| 34 | }) |
| 35 | |
| 36 | out, err := task.Execute(withCallContext(context.Background(), "c", event.Discard, nil, false), |
| 37 | json.RawMessage(`{"prompt":"review the change","profile":"review"}`)) |
| 38 | if err != nil { |
| 39 | t.Fatalf("review task: %v", err) |
| 40 | } |
| 41 | if strings.Contains(out, "paused after") || !strings.Contains(out, "verdict: approve") { |
| 42 | t.Fatalf("review child must finish its review, got:\n%s", out) |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | func TestReviewBudgetKeepsInheritedStepsAndBoundsOutput(t *testing.T) { |
| 47 | for _, tc := range []struct { |
| 48 | parent, requested, want int |
| 49 | }{ |
| 50 | {parent: 0, requested: 0, want: 0}, |
| 51 | {parent: 40, requested: 0, want: 20}, |
| 52 | {parent: 0, requested: 24, want: 24}, |
| 53 | } { |
| 54 | task := &TaskTool{maxSteps: tc.parent} |
| 55 | spec := ProfileExecSpec{Worker: WorkerSpec{Profile: "review"}, Sched: SchedulerPolicy{MaxSteps: tc.requested}} |
| 56 | ctx, steps := task.childMaxStepsForSpec(context.Background(), &spec) |
| 57 | if steps != tc.want { |
| 58 | t.Fatalf("parent=%d requested=%d: steps = %d, want %d", tc.parent, tc.requested, steps, tc.want) |
| 59 | } |
| 60 | if childOutputBudgetFrom(ctx) != defaultReviewOutputTokens { |
| 61 | t.Fatalf("review output budget = %d", childOutputBudgetFrom(ctx)) |
| 62 | } |
| 63 | } |
| 64 | } |
| 65 |