| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/agent/testutil" |
| 9 | "reasonix/internal/event" |
| 10 | "reasonix/internal/provider" |
| 11 | "reasonix/internal/tool" |
| 12 | ) |
| 13 | |
| 14 | func TestGoalRunHasNoDefaultModelRoundCeiling(t *testing.T) { |
| 15 | reg := tool.NewRegistry() |
| 16 | reg.Add(fakeTool{name: "read_file", readOnly: true}) |
| 17 | turns := make([]testutil.Turn, 0, 102) |
| 18 | for i := range 101 { |
| 19 | turns = append(turns, testutil.Turn{ToolCalls: []provider.ToolCall{{ |
| 20 | ID: fmt.Sprintf("r%d", i), Name: "read_file", Arguments: fmt.Sprintf(`{"path":"file-%d"}`, i), |
| 21 | }}}) |
| 22 | } |
| 23 | turns = append(turns, testutil.Turn{Text: "Done."}) |
| 24 | prov := testutil.NewMock("m", turns...) |
| 25 | a := New(prov, reg, NewSession(""), Options{}, event.Discard) |
| 26 | ctx := WithDeliveryExecutionScope(context.Background(), DeliveryExecutionScope{ID: "goal-1", TaskText: "work"}) |
| 27 | if err := a.Run(ctx, "work"); err != nil { |
| 28 | t.Fatalf("Goal run stopped at a default round boundary: %v", err) |
| 29 | } |
| 30 | if prov.CallCount() != 102 { |
| 31 | t.Fatalf("provider calls = %d, want 101 tool rounds plus final", prov.CallCount()) |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | func TestExplicitMaxStepsStillAppliesToGoal(t *testing.T) { |
| 36 | reg := tool.NewRegistry() |
| 37 | reg.Add(fakeTool{name: "read_file", readOnly: true}) |
| 38 | prov := testutil.NewMock("m", |
| 39 | testutil.Turn{ToolCalls: []provider.ToolCall{{ID: "r1", Name: "read_file", Arguments: `{"path":"a"}`}}}, |
| 40 | testutil.Turn{ToolCalls: []provider.ToolCall{{ID: "r2", Name: "read_file", Arguments: `{"path":"b"}`}}}, |
| 41 | testutil.Turn{ToolCalls: []provider.ToolCall{{ID: "r3", Name: "read_file", Arguments: `{"path":"c"}`}}}, |
| 42 | testutil.Turn{Text: "Done."}, |
| 43 | ) |
| 44 | a := New(prov, reg, NewSession(""), Options{MaxSteps: 3}, event.Discard) |
| 45 | ctx := WithDeliveryExecutionScope(context.Background(), DeliveryExecutionScope{ID: "goal-1", TaskText: "work"}) |
| 46 | err := a.Run(ctx, "work") |
| 47 | info, ok := InspectRunPause(err) |
| 48 | if !ok || info.Kind != "max_steps" || info.HostOwned || info.Limit != 3 { |
| 49 | t.Fatalf("explicit MaxSteps pause = %+v ok=%v err=%v", info, ok, err) |
| 50 | } |
| 51 | if prov.CallCount() != 4 { |
| 52 | t.Fatalf("provider calls = %d, want explicit three rounds plus summary", prov.CallCount()) |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | func TestUnboundedGoalParentLeavesChildUnbounded(t *testing.T) { |
| 57 | task := &TaskTool{} |
| 58 | if got := task.childMaxStepsForContext(context.Background(), 0); got != 0 { |
| 59 | t.Fatalf("child steps = %d, want unlimited", got) |
| 60 | } |
| 61 | if got := task.childMaxStepsForContext(context.Background(), 3); got != 3 { |
| 62 | t.Fatalf("explicit child steps = %d, want 3", got) |
| 63 | } |
| 64 | task.maxSteps = 16 |
| 65 | if got := task.childMaxStepsForContext(context.Background(), 0); got != 8 { |
| 66 | t.Fatalf("explicit parent child steps = %d, want 8", got) |
| 67 | } |
| 68 | } |
| 69 |