Merge branch 'feature/story-28-ui-show-test-todos'
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::io::story_metadata::{StoryMetadata, parse_front_matter};
|
||||
use crate::io::story_metadata::{StoryMetadata, parse_front_matter, parse_unchecked_todos};
|
||||
use crate::workflow::{
|
||||
CoverageReport, StoryTestResults, TestCaseResult, TestStatus,
|
||||
evaluate_acceptance_with_coverage, parse_coverage_json, summarize_results,
|
||||
@@ -87,6 +87,18 @@ struct ReviewListResponse {
|
||||
pub stories: Vec<ReviewStory>,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct StoryTodosResponse {
|
||||
pub story_id: String,
|
||||
pub story_name: Option<String>,
|
||||
pub todos: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct TodoListResponse {
|
||||
pub stories: Vec<StoryTodosResponse>,
|
||||
}
|
||||
|
||||
fn load_current_story_metadata(ctx: &AppContext) -> Result<Vec<(String, StoryMetadata)>, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let current_dir = root.join(".story_kit").join("stories").join("current");
|
||||
@@ -403,6 +415,51 @@ impl WorkflowApi {
|
||||
}))
|
||||
}
|
||||
|
||||
/// List unchecked acceptance criteria (TODOs) for all current stories.
|
||||
#[oai(path = "/workflow/todos", method = "get")]
|
||||
async fn story_todos(&self) -> OpenApiResult<Json<TodoListResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let current_dir = root.join(".story_kit").join("stories").join("current");
|
||||
|
||||
if !current_dir.exists() {
|
||||
return Ok(Json(TodoListResponse {
|
||||
stories: Vec::new(),
|
||||
}));
|
||||
}
|
||||
|
||||
let mut stories = Vec::new();
|
||||
let mut entries: Vec<_> = fs::read_dir(¤t_dir)
|
||||
.map_err(|e| bad_request(format!("Failed to read current stories: {e}")))?
|
||||
.filter_map(|e| e.ok())
|
||||
.collect();
|
||||
entries.sort_by_key(|e| e.file_name());
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|ext| ext.to_str()) != Some("md") {
|
||||
continue;
|
||||
}
|
||||
let story_id = path
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let contents = fs::read_to_string(&path)
|
||||
.map_err(|e| bad_request(format!("Failed to read {}: {e}", path.display())))?;
|
||||
let story_name = parse_front_matter(&contents)
|
||||
.ok()
|
||||
.and_then(|m| m.name);
|
||||
let todos = parse_unchecked_todos(&contents);
|
||||
stories.push(StoryTodosResponse {
|
||||
story_id,
|
||||
story_name,
|
||||
todos,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(TodoListResponse { stories }))
|
||||
}
|
||||
|
||||
/// Ensure a story can be accepted; returns an error when gates fail.
|
||||
#[oai(path = "/workflow/acceptance/ensure", method = "post")]
|
||||
async fn ensure_acceptance(
|
||||
|
||||
@@ -59,6 +59,18 @@ fn build_metadata(front: FrontMatter) -> StoryMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_unchecked_todos(contents: &str) -> Vec<String> {
|
||||
contents
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let trimmed = line.trim();
|
||||
trimmed
|
||||
.strip_prefix("- [ ] ")
|
||||
.map(|text| text.to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_test_plan_status(value: &str) -> TestPlanStatus {
|
||||
match value {
|
||||
"approved" => TestPlanStatus::Approved,
|
||||
@@ -108,4 +120,31 @@ workflow: tdd
|
||||
Err(StoryMetaError::InvalidFrontMatter(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_unchecked_todos_mixed() {
|
||||
let input = "## AC\n- [ ] First thing\n- [x] Done thing\n- [ ] Second thing\n";
|
||||
assert_eq!(
|
||||
parse_unchecked_todos(input),
|
||||
vec!["First thing", "Second thing"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_unchecked_todos_all_checked() {
|
||||
let input = "- [x] Done\n- [x] Also done\n";
|
||||
assert!(parse_unchecked_todos(input).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_unchecked_todos_no_checkboxes() {
|
||||
let input = "# Story\nSome text\n- A bullet\n";
|
||||
assert!(parse_unchecked_todos(input).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_unchecked_todos_leading_whitespace() {
|
||||
let input = " - [ ] Indented item\n";
|
||||
assert_eq!(parse_unchecked_todos(input), vec!["Indented item"]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user