{"record":{"id":"51638d0c6d3ca985","repo":"JuliusBrussee/caveman","slug":"arm-must-be-q-or-q","errorCode":null,"errorMessage":"arm must be %q or %q","messagePattern":"arm must be %q or %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/internal/store/learn_experiment.go","lineNumber":118,"sourceCode":"\t}\n\tid, err := result.LastInsertId()\n\tif err != nil {\n\t\treturn Experiment{}, fmt.Errorf(\"read experiment id: %w\", err)\n\t}\n\tif _, err := s.db.Exec(`INSERT INTO experiment_arms (experiment_id, arm, started_at)\n\t\tVALUES (?, ?, ?)`, id, experimentArmOn, now); err != nil {\n\t\treturn Experiment{}, fmt.Errorf(\"open first arm: %w\", err)\n\t}\n\treturn s.LoadExperiment(label)\n}\n\n// SwitchExperimentArm closes the open arm and opens the other one. Switching to\n// the arm already running is a no-op rather than an error: it keeps a repeated\n// call from fragmenting the interval history.\nfunc (s *Store) SwitchExperimentArm(label, arm string) (Experiment, error) {\n\tarm = strings.ToLower(strings.TrimSpace(arm))\n\tif arm != experimentArmOn && arm != experimentArmOff {\n\t\treturn Experiment{}, fmt.Errorf(\"arm must be %q or %q\", experimentArmOn, experimentArmOff)\n\t}\n\texperiment, err := s.LoadExperiment(label)\n\tif err != nil {\n\t\treturn Experiment{}, err\n\t}\n\tif experiment.StoppedAt != \"\" {\n\t\treturn Experiment{}, fmt.Errorf(\"experiment %q is stopped\", experiment.Label)\n\t}\n\tfor _, existing := range experiment.Arms {\n\t\tif existing.EndedAt == \"\" && existing.Arm == arm {\n\t\t\treturn experiment, nil\n\t\t}\n\t}\n\tnow := learnOutcomeClock().UTC().Format(time.RFC3339)\n\tif _, err := s.db.Exec(`UPDATE experiment_arms SET ended_at = ?\n\t\tWHERE experiment_id = ? AND ended_at IS NULL`, now, experiment.id); err != nil {\n\t\treturn Experiment{}, fmt.Errorf(\"close open arm: %w\", err)\n\t}","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/internal/store/learn_experiment.go#L100-L136","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nstore.SwitchExperimentArm(label, \"enabled\")\n// after\narm := strings.ToLower(strings.TrimSpace(userArm))\nif arm != \"on\" && arm != \"off\" {\n    return fmt.Errorf(\"--arm must be on or off\")\n}\nstore.SwitchExperimentArm(label, arm)","handlingStrategy":"validation","validationCode":"arm := strings.ToLower(strings.TrimSpace(input))\nvalid := arm == \"on\" || arm == \"off\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Normalize case and whitespace at the input boundary","Validate against on/off before calling","Use package constants, not raw strings"],"tags":["go","validation","argument","experiment"],"backgroundTag":"invalid-enum-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}