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

  1. Pass exactly "on" or "off" (case-insensitive; surrounding whitespace is tolerated).
  2. Trim and strings.ToLower the value at the caller boundary before calling.
  3. Validate user input against the allowed set before invoking the API.
  4. 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

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


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)