Story 26: Establish TDD workflow and quality gates
Add workflow engine with acceptance gates, test recording, and review queue. Frontend displays gate status (blocked/ready), test summaries, failing badges, and warnings. Proceed action is disabled when gates are not met. Includes 13 unit tests (Vitest) and 9 E2E tests (Playwright) covering all five acceptance criteria. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
111
server/src/io/story_metadata.rs
Normal file
111
server/src/io/story_metadata.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TestPlanStatus {
|
||||
Approved,
|
||||
WaitingForApproval,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct StoryMetadata {
|
||||
pub name: Option<String>,
|
||||
pub test_plan: Option<TestPlanStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum StoryMetaError {
|
||||
MissingFrontMatter,
|
||||
InvalidFrontMatter(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FrontMatter {
|
||||
name: Option<String>,
|
||||
test_plan: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
||||
let mut lines = contents.lines();
|
||||
|
||||
let first = lines.next().unwrap_or_default().trim();
|
||||
if first != "---" {
|
||||
return Err(StoryMetaError::MissingFrontMatter);
|
||||
}
|
||||
|
||||
let mut front_lines = Vec::new();
|
||||
for line in &mut lines {
|
||||
let trimmed = line.trim();
|
||||
if trimmed == "---" {
|
||||
let raw = front_lines.join("\n");
|
||||
let front: FrontMatter = serde_yaml::from_str(&raw)
|
||||
.map_err(|e| StoryMetaError::InvalidFrontMatter(e.to_string()))?;
|
||||
return Ok(build_metadata(front));
|
||||
}
|
||||
front_lines.push(line);
|
||||
}
|
||||
|
||||
Err(StoryMetaError::InvalidFrontMatter(
|
||||
"Missing closing front matter delimiter".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn build_metadata(front: FrontMatter) -> StoryMetadata {
|
||||
let test_plan = front.test_plan.as_deref().map(parse_test_plan_status);
|
||||
|
||||
StoryMetadata {
|
||||
name: front.name,
|
||||
test_plan,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_test_plan_status(value: &str) -> TestPlanStatus {
|
||||
match value {
|
||||
"approved" => TestPlanStatus::Approved,
|
||||
"waiting_for_approval" => TestPlanStatus::WaitingForApproval,
|
||||
other => TestPlanStatus::Unknown(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_front_matter_metadata() {
|
||||
let input = r#"---
|
||||
name: Establish the TDD Workflow and Gates
|
||||
test_plan: approved
|
||||
workflow: tdd
|
||||
---
|
||||
# Story 26
|
||||
"#;
|
||||
|
||||
let meta = parse_front_matter(input).expect("front matter");
|
||||
assert_eq!(
|
||||
meta,
|
||||
StoryMetadata {
|
||||
name: Some("Establish the TDD Workflow and Gates".to_string()),
|
||||
test_plan: Some(TestPlanStatus::Approved),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_front_matter() {
|
||||
let input = "# Story 26\n";
|
||||
assert_eq!(
|
||||
parse_front_matter(input),
|
||||
Err(StoryMetaError::MissingFrontMatter)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unclosed_front_matter() {
|
||||
let input = "---\nname: Test\n";
|
||||
assert!(matches!(
|
||||
parse_front_matter(input),
|
||||
Err(StoryMetaError::InvalidFrontMatter(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user