// Milestone 2 service-layer tests for the chatbot session preview // turn-limit math (plan §3, §10 M2, §11). // // Two contracts are locked in here: // // 1. Turn-limit derivation: the service caller hands the repository // turn_limit = min(request.limit, cfg.RecentTurns) per plan §3. // This test exercises the boundary set so a future refactor cannot // silently drop the cap. // 2. Config validation: a service constructed with a config whose // RecentTurns is out of [1, 5] must not be accepted. The // authoritative validator lives in // internal/serviceconfig/chatbot.ChatbotConfig.Validate; this test // proves that path catches the boundaries plan §8 mandates. // // Compile contract: this file references bot.Service.New plus the // bot.SessionListInput / bot.MinTurnLimit symbols that backend-eng // emits during M2. Until then the file fails to compile — the failing // state the team-lead M2 dispatch endorsed. package bot_test import ( "testing" "queryorchestration/internal/bot" "queryorchestration/internal/serviceconfig/chatbot" "github.com/stretchr/testify/require" ) // TestSessionList_TurnLimitMin proves min(request.limit, cfg.RecentTurns) // at the service layer. The bot package exports MinTurnLimit (or // equivalently named helper) so the service and any other caller agree // on the cap arithmetic. // // With cfg.RecentTurns=5, the brief lists these (request.limit, want) // pairs: (2, 2), (3, 3), (5, 5), (7, 5), (10, 5). func TestSessionList_TurnLimitMin(t *testing.T) { t.Parallel() const recentTurns = 5 cases := []struct { name string requestLimit int want int }{ {name: "below_cap_two", requestLimit: 2, want: 2}, {name: "below_cap_three", requestLimit: 3, want: 3}, {name: "at_cap_five", requestLimit: 5, want: 5}, {name: "above_cap_seven", requestLimit: 7, want: 5}, {name: "above_cap_ten", requestLimit: 10, want: 5}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := bot.MinTurnLimit(c.requestLimit, recentTurns) require.Equal(t, c.want, got, "plan §3: turn_limit = min(request.limit, cfg.RecentTurns); "+ "min(%d, %d) expected %d, got %d", c.requestLimit, recentTurns, c.want, got) }) } } // TestSessionList_RejectsConfigOutOfRange proves that the chatbot // config validator catches the same boundaries plan §8 lists, and // that bot.New refuses a config whose Validate fails. Two layers, // one assertion each — keeps the test focused on the service-level // contract. func TestSessionList_RejectsConfigOutOfRange(t *testing.T) { t.Parallel() cases := []struct { name string recentTurns int }{ {name: "zero_rejected", recentTurns: 0}, {name: "six_rejected", recentTurns: 6}, {name: "negative_rejected", recentTurns: -1}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { cfg := chatbot.ChatbotConfig{ ServiceURL: "http://chatbot.local:8000", APIKey: "k", RequestTimeoutSeconds: 60, MaxTurnChars: 2000, RecentTurns: c.recentTurns, } require.Error(t, cfg.Validate(), "plan §8: ChatbotConfig.Validate must reject RecentTurns=%d", c.recentTurns) // bot.New must refuse the same invalid config so the // service can never be constructed with an out-of-range // turn-limit cap. The exact error type is not asserted // here — only that construction fails. _, err := bot.New(cfg, nil) require.Errorf(t, err, "bot.New must refuse a config with RecentTurns=%d", c.recentTurns) }) } }