Merged in feature/chatbot-limits (pull request #228)

fix chatbot limits and test

* add tests
This commit is contained in:
Jay Brown
2026-05-28 19:23:39 +00:00
parent 451da3d26d
commit 33b0931b8c
6 changed files with 173 additions and 55 deletions
+15
View File
@@ -15,6 +15,16 @@ import "fmt"
// 64 KiB matches the plan literal `64 * 1024`.
const StateMaxBytes = 64 * 1024
// MaxTurnCharsCeiling is the largest legal value for MaxTurnChars.
// It mirrors the OpenAPI schema constraint on the prompt body at
// serviceAPIs/queryAPI.yaml (BotTurnAddRequest.prompt.maxLength=65536).
// Setting CHATBOT_MAX_TURN_CHARS above this ceiling would create a
// silent inconsistency: the schema validator would reject oversized
// prompts before the service-layer length guard ran, leaving operators
// with no signal that the configured cap is unreachable. Any change to
// this value must be made in lockstep with the OpenAPI spec.
const MaxTurnCharsCeiling = 65536
// ChatbotConfig is the queryAPI-side configuration block for the FastAPI
// chatbot agent service. Fields, env-var names, defaults, and the
// notEmpty/required tags are dictated verbatim by plan §8.
@@ -53,6 +63,11 @@ func (c ChatbotConfig) Validate() error {
if c.MaxTurnChars < 1 {
return fmt.Errorf("chatbot config: MaxTurnChars must be >= 1, got %d", c.MaxTurnChars)
}
if c.MaxTurnChars > MaxTurnCharsCeiling {
return fmt.Errorf(
"chatbot config: MaxTurnChars must be <= %d (OpenAPI prompt.maxLength), got %d",
MaxTurnCharsCeiling, c.MaxTurnChars)
}
if c.RecentTurns < 1 || c.RecentTurns > 5 {
return fmt.Errorf("chatbot config: RecentTurns must be in [1, 5], got %d", c.RecentTurns)
}
@@ -266,6 +266,96 @@ func TestChatbotConfig_Validate(t *testing.T) {
}
}
// TestChatbotConfig_MaxTurnChars_NonDefault_Loaded covers Gap 1: prior
// to this test no case asserted that CHATBOT_MAX_TURN_CHARS=5000 (a
// non-default value) was both loaded by env.ParseWithOptions and
// accepted by Validate(). The plan permits any operator-chosen value
// >= 1; this test pins that contract for a representative larger cap
// so a regression that hard-codes the default cannot pass silently.
func TestChatbotConfig_MaxTurnChars_NonDefault_Loaded(t *testing.T) {
t.Parallel()
envMap := requiredEnvBase()
envMap["CHATBOT_MAX_TURN_CHARS"] = "5000"
cfg, err := loadConfig(t, envMap)
require.NoError(t, err, "loader must accept CHATBOT_MAX_TURN_CHARS=5000")
require.Equal(t, 5000, cfg.MaxTurnChars,
"non-default CHATBOT_MAX_TURN_CHARS must be honored by the loader")
require.NoError(t, cfg.Validate(),
"Validate must accept a non-default MaxTurnChars within the legal range")
}
// TestChatbotConfig_MaxTurnChars_AtOpenAPILimit_Accepted locks the
// upper-edge boundary: MaxTurnChars=65536 (exactly the OpenAPI
// prompt.maxLength) must load and validate. Pinned alongside the
// rejection cases so an off-by-one in the ceiling check fails loudly.
func TestChatbotConfig_MaxTurnChars_AtOpenAPILimit_Accepted(t *testing.T) {
t.Parallel()
envMap := requiredEnvBase()
envMap["CHATBOT_MAX_TURN_CHARS"] = "65536"
cfg, err := loadConfig(t, envMap)
require.NoError(t, err, "loader must accept CHATBOT_MAX_TURN_CHARS=65536")
require.Equal(t, chatbot.MaxTurnCharsCeiling, cfg.MaxTurnChars,
"loaded value must equal the documented ceiling")
require.NoError(t, cfg.Validate(),
"Validate must accept MaxTurnChars exactly at MaxTurnCharsCeiling")
}
// TestChatbotConfig_MaxTurnChars_AboveOpenAPILimit_Rejected covers Gap 2:
// the OpenAPI schema (serviceAPIs/queryAPI.yaml: prompt.maxLength=65536)
// caps the prompt body at 65,536 runes. If CHATBOT_MAX_TURN_CHARS is set
// above that ceiling the system enters an inconsistent state — the
// generated request validator silently truncates the cap to 65,536 while
// the service-layer guard believes a higher cap is in force. Validate()
// MUST refuse this configuration so the inconsistency is caught at boot,
// not on the first oversized prompt. Expected to FAIL until backend-eng
// adds an upper-bound check tied to the OpenAPI maxLength.
func TestChatbotConfig_MaxTurnChars_AboveOpenAPILimit_Rejected(t *testing.T) {
t.Parallel()
envMap := requiredEnvBase()
envMap["CHATBOT_MAX_TURN_CHARS"] = "65537"
cfg, err := loadConfig(t, envMap)
require.NoError(t, err, "loader must parse the integer value")
validateErr := cfg.Validate()
require.Error(t, validateErr,
"Validate must reject MaxTurnChars > OpenAPI prompt.maxLength=65536; "+
"otherwise oversized prompts are silently rejected by the schema validator "+
"with no operator-visible signal that the configured cap is unreachable")
require.True(t,
strings.Contains(validateErr.Error(), "MaxTurnChars"),
"error %q must name the offending field for actionable diagnostics",
validateErr.Error())
}
// TestChatbotConfig_MaxTurnChars_GrosslyOverLimit_Rejected covers Gap 3:
// Validate() currently only enforces MaxTurnChars >= 1 — there is no
// upper bound at all. An operator typo such as CHATBOT_MAX_TURN_CHARS=
// 10000000 loads cleanly today, silently exceeds the OpenAPI ceiling,
// and creates the inconsistency described in Gap 2 at scale. This test
// asserts that any value above the documented ceiling is rejected.
// Expected to FAIL until backend-eng adds the upper-bound check.
func TestChatbotConfig_MaxTurnChars_GrosslyOverLimit_Rejected(t *testing.T) {
t.Parallel()
envMap := requiredEnvBase()
envMap["CHATBOT_MAX_TURN_CHARS"] = "10000000"
cfg, err := loadConfig(t, envMap)
require.NoError(t, err, "loader must parse the integer value")
validateErr := cfg.Validate()
require.Error(t, validateErr,
"Validate must reject grossly over-limit MaxTurnChars to fail-fast on operator typos")
require.True(t,
strings.Contains(validateErr.Error(), "MaxTurnChars"),
"error %q must name the offending field",
validateErr.Error())
}
// TestChatbotConfig_InflightGraceSeconds locks down the derived value
// from plan §8: "InflightGraceSeconds = 2 * RequestTimeoutSeconds". The
// helper is consumed by AddTurn (M4) to size the grace window passed to