story-kit: merge 247_story_human_qa_gate_with_rejection_flow
This commit is contained in:
@@ -9,6 +9,7 @@ pub struct StoryMetadata {
|
||||
pub merge_failure: Option<String>,
|
||||
pub agent: Option<String>,
|
||||
pub review_hold: Option<bool>,
|
||||
pub manual_qa: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -33,6 +34,7 @@ struct FrontMatter {
|
||||
merge_failure: Option<String>,
|
||||
agent: Option<String>,
|
||||
review_hold: Option<bool>,
|
||||
manual_qa: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
||||
@@ -67,6 +69,7 @@ fn build_metadata(front: FrontMatter) -> StoryMetadata {
|
||||
merge_failure: front.merge_failure,
|
||||
agent: front.agent,
|
||||
review_hold: front.review_hold,
|
||||
manual_qa: front.manual_qa,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +196,32 @@ pub fn set_front_matter_field(contents: &str, key: &str, value: &str) -> String
|
||||
result
|
||||
}
|
||||
|
||||
/// Append rejection notes to a story file body.
|
||||
///
|
||||
/// Adds a `## QA Rejection Notes` section at the end of the file so the coder
|
||||
/// agent can see what needs fixing.
|
||||
pub fn write_rejection_notes(path: &Path, notes: &str) -> Result<(), String> {
|
||||
let contents =
|
||||
fs::read_to_string(path).map_err(|e| format!("Failed to read story file: {e}"))?;
|
||||
|
||||
let section = format!("\n\n## QA Rejection Notes\n\n{notes}\n");
|
||||
let updated = format!("{contents}{section}");
|
||||
fs::write(path, &updated).map_err(|e| format!("Failed to write story file: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether a story requires manual QA (defaults to true).
|
||||
pub fn requires_manual_qa(path: &Path) -> bool {
|
||||
let contents = match fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return true,
|
||||
};
|
||||
match parse_front_matter(&contents) {
|
||||
Ok(meta) => meta.manual_qa.unwrap_or(true),
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_unchecked_todos(contents: &str) -> Vec<String> {
|
||||
contents
|
||||
.lines()
|
||||
@@ -367,4 +396,45 @@ workflow: tdd
|
||||
assert!(contents.contains("review_hold: true"));
|
||||
assert!(contents.contains("name: My Spike"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_manual_qa_from_front_matter() {
|
||||
let input = "---\nname: Story\nmanual_qa: false\n---\n# Story\n";
|
||||
let meta = parse_front_matter(input).expect("front matter");
|
||||
assert_eq!(meta.manual_qa, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_qa_defaults_to_none() {
|
||||
let input = "---\nname: Story\n---\n# Story\n";
|
||||
let meta = parse_front_matter(input).expect("front matter");
|
||||
assert_eq!(meta.manual_qa, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_manual_qa_defaults_true() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("story.md");
|
||||
std::fs::write(&path, "---\nname: Test\n---\n# Story\n").unwrap();
|
||||
assert!(requires_manual_qa(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_manual_qa_false_when_set() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("story.md");
|
||||
std::fs::write(&path, "---\nname: Test\nmanual_qa: false\n---\n# Story\n").unwrap();
|
||||
assert!(!requires_manual_qa(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_rejection_notes_appends_section() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("story.md");
|
||||
std::fs::write(&path, "---\nname: Test\n---\n# Story\n").unwrap();
|
||||
write_rejection_notes(&path, "Button color is wrong").unwrap();
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(contents.contains("## QA Rejection Notes"));
|
||||
assert!(contents.contains("Button color is wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user