use serde::Deserialize;
use super::GitHubIssue;
use crate::error::GitHubError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActionType {
Implement,
Survey,
Bugfix,
Refactor,
Docs,
}
impl ActionType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Implement => "implement",
Self::Survey => "survey",
Self::Bugfix => "bugfix",
Self::Refactor => "refactor",
Self::Docs => "docs",
}
}
#[must_use]
pub const fn icon(&self) -> &'static str {
match self {
Self::Implement => "+",
Self::Survey => "?",
Self::Bugfix => "!",
Self::Refactor => "~",
Self::Docs => "#",
}
}
}
impl std::fmt::Display for ActionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ActionChoice {
pub branch: String,
pub action: ActionType,
pub prompt: String,
}
#[derive(Debug, Deserialize)]
struct ClaudeResponse {
structured_output: StructuredOutput,
}
#[derive(Debug, Deserialize)]
struct StructuredOutput {
choices: Vec<ActionChoice>,
}
const JSON_SCHEMA: &str = r#"{"type":"object","properties":{"choices":{"type":"array","items":{"type":"object","properties":{"branch":{"type":"string"},"action":{"type":"string","enum":["implement","survey","bugfix","refactor","docs"]},"prompt":{"type":"string"}},"required":["branch","action","prompt"]},"minItems":2,"maxItems":3}},"required":["choices"]}"#;
pub async fn generate_choices(issue: &GitHubIssue) -> Result<Vec<ActionChoice>, GitHubError> {
let prompt = build_prompt(issue);
let response: ClaudeResponse = super::run_command(
"claude",
&[
"-p",
"--output-format",
"json",
"--json-schema",
JSON_SCHEMA,
"--model",
"haiku",
&prompt,
],
"claude -p",
)
.await?;
Ok(response.structured_output.choices)
}
#[must_use]
pub fn build_prompt(issue: &GitHubIssue) -> String {
let labels = issue.label_names().join(", ");
format!(
r#"GitHub Issue #{}: {}
{}
Labels: {}
IMPORTANT: DO NOT search or explore any codebase. Generate actions based ONLY on the issue information above.
Generate 2-3 action choices with different approaches for this issue.
Each choice must include:
- branch: descriptive branch name (e.g., "feat/issue-integration", "fix/auth-bug")
- action: one of implement, survey, bugfix, refactor, docs
- prompt: initial prompt for Claude Code session (MUST include issue number #{}, 10 words max)
Respond immediately with practical, actionable approaches."#,
issue.number, issue.title, issue.body, labels, issue.number
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(ActionType::Implement, "implement", "+")]
#[case(ActionType::Survey, "survey", "?")]
#[case(ActionType::Bugfix, "bugfix", "!")]
#[case(ActionType::Refactor, "refactor", "~")]
#[case(ActionType::Docs, "docs", "#")]
fn action_type_variants(#[case] action: ActionType, #[case] str_val: &str, #[case] icon: &str) {
#[derive(serde::Deserialize)]
struct W {
action: ActionType,
}
assert_eq!(action.as_str(), str_val);
assert_eq!(format!("{action}"), str_val);
assert_eq!(action.icon(), icon);
let w: W = serde_json::from_str(&format!(r#"{{"action": "{str_val}"}}"#)).unwrap();
assert_eq!(w.action, action);
}
#[test]
fn action_choice_deserialize() {
let json = r#"{
"branch": "feat/issue-22",
"action": "implement",
"prompt": "Implement issue #22"
}"#;
let choice: ActionChoice = serde_json::from_str(json).unwrap();
assert_eq!(choice.branch, "feat/issue-22");
assert_eq!(choice.action, ActionType::Implement);
assert_eq!(choice.prompt, "Implement issue #22");
}
#[test]
fn build_prompt_includes_issue_number() {
let issue = GitHubIssue {
number: 42,
title: "Test Issue".to_string(),
body: "Test body".to_string(),
labels: vec![],
state: super::super::IssueState::Open,
};
let prompt = build_prompt(&issue);
assert!(prompt.contains("#42"));
assert!(prompt.contains("Test Issue"));
assert!(prompt.contains("Test body"));
}
#[test]
fn claude_response_deserialize() {
let json = r#"{
"structured_output": {
"choices": [
{"branch": "feat/test", "action": "implement", "prompt": "Test #1"},
{"branch": "survey/test", "action": "survey", "prompt": "Survey #1"}
]
}
}"#;
let response: ClaudeResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.structured_output.choices.len(), 2);
}
#[test]
fn build_prompt_with_labels() {
use super::super::GitHubLabel;
let issue = GitHubIssue {
number: 42,
title: "Test Issue".to_string(),
body: "Test body".to_string(),
labels: vec![
GitHubLabel {
name: "bug".to_string(),
},
GitHubLabel {
name: "enhancement".to_string(),
},
],
state: super::super::IssueState::Open,
};
let prompt = build_prompt(&issue);
assert!(prompt.contains("bug, enhancement"));
assert!(prompt.contains("#42"));
}
}