{"record":{"id":"069b8aa097fa5676","repo":"jackwener/OpenCLI","slug":"operation-must-be-one-of-action-choices-join","errorCode":null,"errorMessage":"operation must be one of: ${ACTION_CHOICES.join(', ')}","messagePattern":"operation must be one of: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/midjourney/capabilities.js","lineNumber":296,"sourceCode":"  if (estimatedMinutes > max) {\n    throw new ArgumentError(`Estimated GPU cost ${estimatedMinutes} minutes exceeds --max-minutes ${max}`);\n  }\n  const remainingCredits = Number(account?.total_credits ?? account?.credits_total);\n  const remainingMinutes = creditsToMinutes(remainingCredits);\n  if (Number.isFinite(remainingMinutes) && remainingMinutes - estimatedMinutes < reserve) {\n    throw new CommandExecutionError(\n      `Midjourney budget guard would leave ${Number((remainingMinutes - estimatedMinutes).toFixed(2))} minutes, below the ${reserve}-minute reserve`,\n    );\n  }\n  return { maxMinutes: max, reserveMinutes: reserve, remainingMinutes };\n}\n\nexport function estimateAction(\n  operation,\n  { videoResolution = 'sd', batchSize = 1, sourceIsVideo = false, sourceUsesOmni = false } = {},\n) {\n  if (!ACTION_CHOICES.includes(operation)) {\n    throw new ArgumentError(`operation must be one of: ${ACTION_CHOICES.join(', ')}`);\n  }\n  if (operation === 'open-editor' || operation === 'cancel') return 0;\n  if (operation === 'rerun' && sourceIsVideo) {\n    if (!['sd', 'hd'].includes(videoResolution)) throw new ArgumentError('--video-resolution must be sd or hd');\n    if (![1, 2, 4].includes(batchSize)) throw new ArgumentError('--batch-size must be 1, 2, or 4');\n    return GPU_COST_MINUTES[`video${videoResolution === 'hd' ? 'Hd' : 'Sd'}${batchSize}`];\n  }\n  if (sourceUsesOmni && ['rerun', 'vary-subtle', 'vary-strong'].includes(operation)) {\n    return GPU_COST_MINUTES.omni;\n  }\n  if (Object.prototype.hasOwnProperty.call(ACTION_COSTS, operation)) return ACTION_COSTS[operation];\n  if (['animate-low', 'animate-high', 'loop-low', 'loop-high', 'extend-low', 'extend-high'].includes(operation)) {\n    if (!['sd', 'hd'].includes(videoResolution)) throw new ArgumentError('--video-resolution must be sd or hd');\n    if (![1, 2, 4].includes(batchSize)) throw new ArgumentError('--batch-size must be 1, 2, or 4');\n    return GPU_COST_MINUTES[`video${videoResolution === 'hd' ? 'Hd' : 'Sd'}${batchSize}`];\n  }\n  throw new ArgumentError(`No cost model is defined for action \"${operation}\"`);\n}","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/midjourney/capabilities.js#L278-L314","documentation":"estimateAction validates the Midjourney operation name before computing its GPU-minute cost. If operation is not a member of ACTION_CHOICES, an ArgumentError is thrown listing all valid actions. This is a fail-fast guard so unsupported/typo'd subcommands never reach Playwright automation.","triggerScenarios":"Calling estimateAction(operation) (directly or via estimated()) with a string not in ACTION_CHOICES, e.g. 'upscale', 'imagine-x', or a typo like 'vary-subtle ' with trailing whitespace or wrong casing.","commonSituations":"Typos in CLI subcommand wiring; renaming an action without updating call sites; passing a user-supplied action string straight through without pre-validation; adding a new action to a switch/caller but forgetting to add it to ACTION_CHOICES.","solutions":["Check the error message's comma-separated list and use an exact supported operation string","Add the new action to ACTION_CHOICES (and ACTION_COSTS or a cost branch) in clis/midjourney/capabilities.js if it is a legitimately new capability","Trim/lowercase-and-normalize user input before calling estimateAction","Wrap the CLI arg-parsing step to validate operation against ACTION_CHOICES before invoking estimateAction"],"exampleFix":"// before\nconst minutes = estimateAction('upscale', opts);\n// after\nif (!ACTION_CHOICES.includes('upscale')) throw new Error('unsupported');\nconst minutes = estimateAction('vary-subtle', opts); // use a listed action","handlingStrategy":"validation","validationCode":"import { ACTION_CHOICES, estimateAction } from './clis/midjourney/capabilities.js';\nif (!ACTION_CHOICES.includes(operation)) {\n  throw new Error(`unsupported operation: ${operation}; expected one of ${ACTION_CHOICES.join(', ')}`);\n}\nconst minutes = estimateAction(operation, opts);","typeGuard":"function isMidjourneyAction(op) {\n  return typeof op === 'string' && ACTION_CHOICES.includes(op);\n}","tryCatchPattern":"try {\n  const minutes = estimateAction(operation, opts);\n} catch (err) {\n  if (err instanceof ArgumentError && /operation must be one of/.test(err.message)) {\n    console.error(`Invalid operation \"${operation}\". Valid: ${ACTION_CHOICES.join(', ')}`);\n    process.exitCode = 2;\n  } else throw err;\n}","preventionTips":["Always validate user-supplied action names against ACTION_CHOICES before calling estimateAction","Normalize input (trim + toLowerCase) to catch whitespace/casing typos","Keep ACTION_CHOICES and all cost branches in sync when adding actions","Add a test enumerating every supported action through estimateAction"],"tags":["argument-error","validation","cli","midjourney"],"backgroundTag":"invalid-argument-value","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}