JuliusBrussee/caveman · error
arm must be or
Error message
arm must be %q or %q
What it means
Validation error in SwitchExperimentArm: the requested arm is not one of the two recognized experiment arms ('on'/'off' style literals filled into the %q slots). Arm switching is binary, so any other label is rejected before the interval history is touched.
Solutions
- Pass exactly "on" or "off" (case-insensitive; surrounding whitespace is tolerated).
- Trim and strings.ToLower the value at the caller boundary before calling.
- Validate user input against the allowed set before invoking the API.
- Use the package's experimentArmOn/experimentArmOff constants rather than string literals.
Example fix
// before
store.SwitchExperimentArm(label, "enabled")
// after
arm := strings.ToLower(strings.TrimSpace(userArm))
if arm != "on" && arm != "off" {
return fmt.Errorf("--arm must be on or off")
}
store.SwitchExperimentArm(label, arm) Defensive patterns
Strategy: validation
Validate before calling
arm := strings.ToLower(strings.TrimSpace(input)) valid := arm == "on" || arm == "off"
Prevention
- Normalize case and whitespace at the input boundary
- Validate against on/off before calling
- Use package constants, not raw strings
When it happens
Trigger: Calling Store.SwitchExperimentArm(label, arm) with any value other than "on" or "off" (case-insensitive after trim), e.g. "ON " is fine but "enabled" or "" is not.
Common situations: Wiring a CLI flag or API parameter straight through without normalizing; typos like "off " vs "offically"; passing an internal numeric enum converted to string incorrectly.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- ccr max storage bytes must be positive
- experiment is stopped
- rewriter: theta must not be negative, got
- sink has no measurable fix kind; pass --fix-kind explicitly
- artifactId is required
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/51638d0c6d3ca985.
Report an issue: GitHub.
Appendix: source
Thrown at proxy/internal/store/learn_experiment.go:118
}
id, err := result.LastInsertId()
if err != nil {
return Experiment{}, fmt.Errorf("read experiment id: %w", err)
}
if _, err := s.db.Exec(`INSERT INTO experiment_arms (experiment_id, arm, started_at)
VALUES (?, ?, ?)`, id, experimentArmOn, now); err != nil {
return Experiment{}, fmt.Errorf("open first arm: %w", err)
}
return s.LoadExperiment(label)
}
// SwitchExperimentArm closes the open arm and opens the other one. Switching to
// the arm already running is a no-op rather than an error: it keeps a repeated
// call from fragmenting the interval history.
func (s *Store) SwitchExperimentArm(label, arm string) (Experiment, error) {
arm = strings.ToLower(strings.TrimSpace(arm))
if arm != experimentArmOn && arm != experimentArmOff {
return Experiment{}, fmt.Errorf("arm must be %q or %q", experimentArmOn, experimentArmOff)
}
experiment, err := s.LoadExperiment(label)
if err != nil {
return Experiment{}, err
}
if experiment.StoppedAt != "" {
return Experiment{}, fmt.Errorf("experiment %q is stopped", experiment.Label)
}
for _, existing := range experiment.Arms {
if existing.EndedAt == "" && existing.Arm == arm {
return experiment, nil
}
}
now := learnOutcomeClock().UTC().Format(time.RFC3339)
if _, err := s.db.Exec(`UPDATE experiment_arms SET ended_at = ?
WHERE experiment_id = ? AND ended_at IS NULL`, now, experiment.id); err != nil {
return Experiment{}, fmt.Errorf("close open arm: %w", err)
}View on GitHub (pinned to 3ee70a1026)