diff --git a/.story_kit/README.md b/.story_kit/README.md index e7978a8..4621ced 100644 --- a/.story_kit/README.md +++ b/.story_kit/README.md @@ -26,7 +26,7 @@ Instead of ephemeral chat prompts ("Fix this", "Add that"), we work through pers * **Tests** define the *Truth*. * **Code** defines the *Reality*. -**The Golden Rule:** You are not allowed to write code until the Acceptance Criteria are captured in the story and the test plan is approved. +**The Golden Rule:** You are not allowed to write code until the Acceptance Criteria are captured in the story. --- @@ -91,14 +91,12 @@ When the user asks for a feature, follow this 4-step loop strictly: ### Step 1: The Story (Ingest) * **User Input:** "I want the robot to dance." * **Action:** Create a story via MCP tool `create_story` (guarantees correct front matter and auto-assigns the story number). -* **Front Matter (Required):** Every work item file MUST begin with YAML front matter containing `name` and `test_plan` fields: +* **Front Matter (Required):** Every work item file MUST begin with YAML front matter containing a `name` field: ```yaml --- name: Short Human-Readable Story Name - test_plan: pending --- ``` - The `test_plan` field tracks approval status: `pending` → `approved` (after Step 2). * **Move to Current:** Once the story is validated and ready for coding, move it to `work/2_current/`. * **Tracking:** Mark Acceptance Criteria as tested directly in the story file as tests are completed. * **Content:** @@ -108,21 +106,12 @@ When the user asks for a feature, follow this 4-step loop strictly: * **Story Quality (INVEST):** Stories should be Independent, Negotiable, Valuable, Estimable, Small, and Testable. * **Git:** The `start_agent` MCP tool automatically creates a worktree under `.story_kit/worktrees/`, checks out a feature branch, moves the story to `work/2_current/`, and spawns the agent. No manual branch or worktree creation is needed. -### Step 2: Test Planning (TDD) -* **Action:** Define the test plan for the Story before any implementation. -* **Logic:** - * Identify required unit tests and integration tests. - * Confirm test frameworks and commands from `specs/tech/STACK.md`. - * Ensure Acceptance Criteria are testable and mapped to planned tests. - * Each Acceptance Criterion must be traceable to a specific test. -* **Output:** Show the user the test plan. **Wait for approval.** - -### Step 3: The Implementation (Code) +### Step 2: The Implementation (Code) * **Action:** Write the code to satisfy the approved tests and Acceptance Criteria. * **Constraint:** adhere strictly to `specs/tech/STACK.md` (e.g., if it forbids certain patterns, you must not use them). * **Full-Stack Completion:** Every story must be completed across all components of the stack. If a feature touches the backend, frontend, and API layer, all three must be fully implemented and working end-to-end before the story can be accepted. Partial implementations (e.g., backend logic with no frontend wiring, or UI scaffolding with no real data) do not satisfy acceptance criteria. -### Step 4: Verification (Close) +### Step 3: Verification (Close) * **Action:** For each Acceptance Criterion in the story, write a failing test (red), mark the criterion as tested, make the test pass (green), and refactor if needed. Keep only one failing test at a time. * **Action:** Run compilation and make sure it succeeds without errors. Consult `specs/tech/STACK.md` and run all required linters listed there (treat warnings as errors). Run tests and make sure they all pass before proceeding. Ask questions here if needed. * **Action:** Do not accept stories yourself. Ask the user if they accept the story. If they agree, move the story file to `work/5_archived/`. @@ -233,7 +222,7 @@ If a user hands you this document and says "Apply this process to my project": ## 6. Code Quality -**MANDATORY:** Before completing Step 4 (Verification) of any story, you MUST run all applicable linters, formatters, and test suites and fix ALL errors and warnings. Zero tolerance for warnings or errors. +**MANDATORY:** Before completing Step 3 (Verification) of any story, you MUST run all applicable linters, formatters, and test suites and fix ALL errors and warnings. Zero tolerance for warnings or errors. **AUTO-RUN CHECKS:** Always run the required lint/test/build checks as soon as relevant changes are made. Do not ask for permission to run them—run them automatically and fix any failures. diff --git a/server/src/agents.rs b/server/src/agents.rs index 325630e..fe0580f 100644 --- a/server/src/agents.rs +++ b/server/src/agents.rs @@ -249,21 +249,11 @@ impl AgentPool { } } - // Read story content from the project root (the worktree may not have - // .story_kit/work/) and inject it into the agent prompt so the coder - // knows what to implement. - let story_content = read_story_content(project_root, story_id); - // Spawn the agent process let wt_path_str = wt_info.path.to_string_lossy().to_string(); let (command, args, mut prompt) = config.render_agent_args(&wt_path_str, story_id, Some(&resolved_name), Some(&wt_info.base_branch))?; - // Prepend story content so the agent sees the requirements first. - if let Some(content) = story_content { - prompt = format!("## Story Requirements\n\n{content}\n\n---\n\n{prompt}"); - } - // Append resume context if this is a restart with failure information. if let Some(ctx) = resume_context { prompt.push_str(ctx); @@ -1369,20 +1359,6 @@ fn item_type_from_id(item_id: &str) -> &'static str { } /// Return the source directory path for a work item (always work/1_upcoming/). -/// Read story/bug content from any pipeline stage directory. -/// Returns the file contents if found, or None if the file doesn't exist anywhere. -fn read_story_content(project_root: &Path, story_id: &str) -> Option { - let sk = project_root.join(".story_kit").join("work"); - let filename = format!("{story_id}.md"); - for stage in &["2_current", "1_upcoming", "3_qa", "4_merge"] { - let path = sk.join(stage).join(&filename); - if let Ok(content) = std::fs::read_to_string(&path) { - return Some(content); - } - } - None -} - fn item_source_dir(project_root: &Path, _item_id: &str) -> PathBuf { project_root.join(".story_kit").join("work").join("1_upcoming") } @@ -2799,7 +2775,7 @@ mod tests { let merge_dir = repo.join(".story_kit/work/4_merge"); fs::create_dir_all(&merge_dir).unwrap(); let story_file = merge_dir.join("23_test.md"); - fs::write(&story_file, "---\nname: Test\ntest_plan: approved\n---\n").unwrap(); + fs::write(&story_file, "---\nname: Test\n---\n").unwrap(); Command::new("git") .args(["add", "."]) .current_dir(repo) diff --git a/server/src/http/mcp.rs b/server/src/http/mcp.rs index 1cf39f3..40dca7b 100644 --- a/server/src/http/mcp.rs +++ b/server/src/http/mcp.rs @@ -4,7 +4,7 @@ use crate::http::context::AppContext; use crate::http::settings::get_editor_command_from_store; use crate::http::workflow::{ check_criterion_in_file, create_bug_file, create_story_file, list_bug_files, - load_upcoming_stories, set_test_plan_in_file, validate_story_dirs, + load_upcoming_stories, validate_story_dirs, }; use crate::worktree; use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos}; @@ -614,24 +614,6 @@ fn handle_tools_list(id: Option) -> JsonRpcResponse { "required": ["story_id", "criterion_index"] } }, - { - "name": "set_test_plan", - "description": "Update the test_plan front-matter field of a story file and auto-commit to master. Common values: 'pending', 'approved'.", - "inputSchema": { - "type": "object", - "properties": { - "story_id": { - "type": "string", - "description": "Story identifier (filename stem, e.g. '28_my_story')" - }, - "status": { - "type": "string", - "description": "New value for the test_plan field (e.g. 'approved', 'pending')" - } - }, - "required": ["story_id", "status"] - } - }, { "name": "create_bug", "description": "Create a bug file in .story_kit/bugs/ with a deterministic filename and auto-commit to master. Returns the bug_id.", @@ -787,7 +769,6 @@ async fn handle_tools_call( "accept_story" => tool_accept_story(&args, ctx), // Story mutation tools (auto-commit to master) "check_criterion" => tool_check_criterion(&args, ctx), - "set_test_plan" => tool_set_test_plan(&args, ctx), // Bug lifecycle tools "create_bug" => tool_create_bug(&args, ctx), "list_bugs" => tool_list_bugs(ctx), @@ -1193,24 +1174,6 @@ fn tool_check_criterion(args: &Value, ctx: &AppContext) -> Result Result { - let story_id = args - .get("story_id") - .and_then(|v| v.as_str()) - .ok_or("Missing required argument: story_id")?; - let status = args - .get("status") - .and_then(|v| v.as_str()) - .ok_or("Missing required argument: status")?; - - let root = ctx.state.get_project_root()?; - set_test_plan_in_file(&root, story_id, status)?; - - Ok(format!( - "test_plan set to '{status}' for story '{story_id}'. Committed to master." - )) -} - // ── Bug lifecycle tool implementations ─────────────────────────── fn tool_create_bug(args: &Value, ctx: &AppContext) -> Result { @@ -1536,14 +1499,13 @@ mod tests { assert!(!names.contains(&"report_completion")); assert!(names.contains(&"accept_story")); assert!(names.contains(&"check_criterion")); - assert!(names.contains(&"set_test_plan")); assert!(names.contains(&"create_bug")); assert!(names.contains(&"list_bugs")); assert!(names.contains(&"close_bug")); assert!(names.contains(&"merge_agent_work")); assert!(names.contains(&"move_story_to_merge")); assert!(names.contains(&"request_qa")); - assert_eq!(tools.len(), 26); + assert_eq!(tools.len(), 25); } #[test] @@ -1626,7 +1588,7 @@ mod tests { fs::create_dir_all(¤t_dir).unwrap(); fs::write( current_dir.join("1_test.md"), - "---\nname: Test\ntest_plan: approved\n---\n## AC\n- [ ] First\n- [x] Done\n- [ ] Second\n", + "---\nname: Test\n---\n## AC\n- [ ] First\n- [x] Done\n- [ ] Second\n", ) .unwrap(); @@ -2105,7 +2067,7 @@ mod tests { let current_dir = tmp.path().join(".story_kit/work/2_current"); std::fs::create_dir_all(¤t_dir).unwrap(); let story_file = current_dir.join("24_story_test.md"); - std::fs::write(&story_file, "---\nname: Test\ntest_plan: approved\n---\n").unwrap(); + std::fs::write(&story_file, "---\nname: Test\n---\n").unwrap(); std::process::Command::new("git") .args(["add", "."]) .current_dir(tmp.path()) diff --git a/server/src/http/workflow.rs b/server/src/http/workflow.rs index 98e43a1..26557c8 100644 --- a/server/src/http/workflow.rs +++ b/server/src/http/workflow.rs @@ -165,7 +165,6 @@ pub fn create_story_file( let mut content = String::new(); content.push_str("---\n"); content.push_str(&format!("name: \"{}\"\n", name.replace('"', "\\\""))); - content.push_str("test_plan: pending\n"); content.push_str("---\n\n"); content.push_str(&format!("# Story {story_number}: {name}\n\n")); @@ -240,7 +239,6 @@ pub fn create_bug_file( let mut content = String::new(); content.push_str("---\n"); content.push_str(&format!("name: \"{}\"\n", name.replace('"', "\\\""))); - content.push_str("test_plan: pending\n"); content.push_str("---\n\n"); content.push_str(&format!("# Bug {bug_number}: {name}\n\n")); content.push_str("## Description\n\n"); @@ -406,60 +404,6 @@ pub fn check_criterion_in_file( Ok(()) } -/// Update the `test_plan` front-matter field in a story file and auto-commit. -pub fn set_test_plan_in_file( - project_root: &Path, - story_id: &str, - status: &str, -) -> Result<(), String> { - let filepath = find_story_file(project_root, story_id)?; - let contents = fs::read_to_string(&filepath) - .map_err(|e| format!("Failed to read story file: {e}"))?; - - let mut in_front_matter = false; - let mut front_matter_started = false; - let mut found = false; - - let new_lines: Vec = contents - .lines() - .map(|line| { - if line.trim() == "---" { - if !front_matter_started { - front_matter_started = true; - in_front_matter = true; - } else if in_front_matter { - in_front_matter = false; - } - return line.to_string(); - } - if in_front_matter { - let trimmed = line.trim_start(); - if trimmed.starts_with("test_plan:") { - found = true; - return format!("test_plan: {status}"); - } - } - line.to_string() - }) - .collect(); - - if !found { - return Err(format!( - "Story '{story_id}' does not have a 'test_plan' field in its front matter." - )); - } - - let mut new_str = new_lines.join("\n"); - if contents.ends_with('\n') { - new_str.push('\n'); - } - fs::write(&filepath, &new_str) - .map_err(|e| format!("Failed to write story file: {e}"))?; - - // Watcher handles the git commit asynchronously. - Ok(()) -} - fn slugify_name(name: &str) -> String { let slug: String = name .chars() @@ -558,9 +502,6 @@ pub fn validate_story_dirs( if meta.name.is_none() { errors.push("Missing 'name' field".to_string()); } - if meta.test_plan.is_none() { - errors.push("Missing 'test_plan' field".to_string()); - } if errors.is_empty() { results.push(StoryValidationResult { story_id, @@ -607,7 +548,7 @@ mod tests { fs::create_dir_all(&dir).unwrap(); fs::write( dir.join(format!("{id}.md")), - format!("---\nname: {id}\ntest_plan: pending\n---\n"), + format!("---\nname: {id}\n---\n"), ) .unwrap(); } @@ -647,7 +588,7 @@ mod tests { fs::create_dir_all(¤t).unwrap(); fs::write( current.join("10_story_test.md"), - "---\nname: Test Story\ntest_plan: approved\n---\n# Story\n", + "---\nname: Test Story\n---\n# Story\n", ) .unwrap(); @@ -673,7 +614,7 @@ mod tests { fs::create_dir_all(¤t).unwrap(); fs::write( current.join("11_story_done.md"), - "---\nname: Done Story\ntest_plan: approved\n---\n# Story\n", + "---\nname: Done Story\n---\n# Story\n", ) .unwrap(); @@ -698,7 +639,7 @@ mod tests { fs::create_dir_all(¤t).unwrap(); fs::write( current.join("12_story_pending.md"), - "---\nname: Pending Story\ntest_plan: approved\n---\n# Story\n", + "---\nname: Pending Story\n---\n# Story\n", ) .unwrap(); @@ -720,12 +661,12 @@ mod tests { fs::create_dir_all(&upcoming).unwrap(); fs::write( upcoming.join("31_story_view_upcoming.md"), - "---\nname: View Upcoming\ntest_plan: pending\n---\n# Story\n", + "---\nname: View Upcoming\n---\n# Story\n", ) .unwrap(); fs::write( upcoming.join("32_story_worktree.md"), - "---\nname: Worktree Orchestration\ntest_plan: pending\n---\n# Story\n", + "---\nname: Worktree Orchestration\n---\n# Story\n", ) .unwrap(); @@ -746,7 +687,7 @@ mod tests { fs::write(upcoming.join(".gitkeep"), "").unwrap(); fs::write( upcoming.join("31_story_example.md"), - "---\nname: A Story\ntest_plan: pending\n---\n", + "---\nname: A Story\n---\n", ) .unwrap(); @@ -765,12 +706,12 @@ mod tests { fs::create_dir_all(&upcoming).unwrap(); fs::write( current.join("28_story_todos.md"), - "---\nname: Show TODOs\ntest_plan: approved\n---\n# Story\n", + "---\nname: Show TODOs\n---\n# Story\n", ) .unwrap(); fs::write( upcoming.join("36_story_front_matter.md"), - "---\nname: Enforce Front Matter\ntest_plan: pending\n---\n# Story\n", + "---\nname: Enforce Front Matter\n---\n# Story\n", ) .unwrap(); @@ -805,26 +746,6 @@ mod tests { assert!(!results[0].valid); let err = results[0].error.as_deref().unwrap(); assert!(err.contains("Missing 'name' field")); - assert!(err.contains("Missing 'test_plan' field")); - } - - #[test] - fn validate_story_dirs_missing_test_plan_only() { - let tmp = tempfile::tempdir().unwrap(); - let current = tmp.path().join(".story_kit/work/2_current"); - fs::create_dir_all(¤t).unwrap(); - fs::write( - current.join("28_story_todos.md"), - "---\nname: A Story\n---\n# Story\n", - ) - .unwrap(); - - let results = validate_story_dirs(tmp.path()).unwrap(); - assert_eq!(results.len(), 1); - assert!(!results[0].valid); - let err = results[0].error.as_deref().unwrap(); - assert!(err.contains("Missing 'test_plan' field")); - assert!(!err.contains("Missing 'name' field")); } #[test] @@ -922,7 +843,6 @@ mod tests { let mut content = String::new(); content.push_str("---\n"); content.push_str("name: \"My New Feature\"\n"); - content.push_str("test_plan: pending\n"); content.push_str("---\n\n"); content.push_str(&format!("# Story {number}: My New Feature\n\n")); content.push_str("## User Story\n\n"); @@ -936,7 +856,7 @@ mod tests { fs::write(&filepath, &content).unwrap(); let written = fs::read_to_string(&filepath).unwrap(); - assert!(written.starts_with("---\nname: \"My New Feature\"\ntest_plan: pending\n---")); + assert!(written.starts_with("---\nname: \"My New Feature\"\n---")); assert!(written.contains("# Story 37: My New Feature")); assert!(written.contains("- [ ] It works")); assert!(written.contains("- [ ] It is tested")); @@ -998,7 +918,7 @@ mod tests { } fn story_with_criteria(n: usize) -> String { - let mut s = "---\nname: Test Story\ntest_plan: pending\n---\n\n## Acceptance Criteria\n\n".to_string(); + let mut s = "---\nname: Test Story\n---\n\n## Acceptance Criteria\n\n".to_string(); for i in 0..n { s.push_str(&format!("- [ ] Criterion {i}\n")); } @@ -1083,66 +1003,6 @@ mod tests { assert!(result.unwrap_err().contains("out of range")); } - // ── set_test_plan_in_file tests ─────────────────────────────────────────── - - #[test] - fn set_test_plan_updates_pending_to_approved() { - let tmp = tempfile::tempdir().unwrap(); - setup_git_repo(tmp.path()); - let current = tmp.path().join(".story_kit/work/2_current"); - fs::create_dir_all(¤t).unwrap(); - let filepath = current.join("4_test.md"); - fs::write( - &filepath, - "---\nname: Test Story\ntest_plan: pending\n---\n\n## Body\n", - ) - .unwrap(); - std::process::Command::new("git") - .args(["add", "."]) - .current_dir(tmp.path()) - .output() - .unwrap(); - std::process::Command::new("git") - .args(["commit", "-m", "add story"]) - .current_dir(tmp.path()) - .output() - .unwrap(); - - set_test_plan_in_file(tmp.path(), "4_test", "approved").unwrap(); - - let contents = fs::read_to_string(&filepath).unwrap(); - assert!(contents.contains("test_plan: approved"), "should be updated to approved"); - assert!(!contents.contains("test_plan: pending"), "old value should be replaced"); - } - - #[test] - fn set_test_plan_missing_field_returns_error() { - let tmp = tempfile::tempdir().unwrap(); - setup_git_repo(tmp.path()); - let current = tmp.path().join(".story_kit/work/2_current"); - fs::create_dir_all(¤t).unwrap(); - let filepath = current.join("5_test.md"); - fs::write( - &filepath, - "---\nname: Test Story\n---\n\n## Body\n", - ) - .unwrap(); - std::process::Command::new("git") - .args(["add", "."]) - .current_dir(tmp.path()) - .output() - .unwrap(); - std::process::Command::new("git") - .args(["commit", "-m", "add story"]) - .current_dir(tmp.path()) - .output() - .unwrap(); - - let result = set_test_plan_in_file(tmp.path(), "5_test", "approved"); - assert!(result.is_err(), "should fail if test_plan field is missing"); - assert!(result.unwrap_err().contains("test_plan")); - } - #[test] fn find_story_file_searches_current_then_upcoming() { let tmp = tempfile::tempdir().unwrap(); @@ -1271,7 +1131,7 @@ mod tests { assert!(filepath.exists()); let contents = fs::read_to_string(&filepath).unwrap(); assert!( - contents.starts_with("---\nname: \"Login Crash\"\ntest_plan: pending\n---"), + contents.starts_with("---\nname: \"Login Crash\"\n---"), "bug file must start with YAML front matter" ); assert!(contents.contains("# Bug 1: Login Crash")); @@ -1314,7 +1174,7 @@ mod tests { let filepath = tmp.path().join(".story_kit/work/1_upcoming/1_bug_some_bug.md"); let contents = fs::read_to_string(&filepath).unwrap(); assert!( - contents.starts_with("---\nname: \"Some Bug\"\ntest_plan: pending\n---"), + contents.starts_with("---\nname: \"Some Bug\"\n---"), "bug file must have YAML front matter" ); assert!(contents.contains("- [ ] Bug is fixed and verified")); diff --git a/server/src/io/fs.rs b/server/src/io/fs.rs index 54c1dbc..e1961f9 100644 --- a/server/src/io/fs.rs +++ b/server/src/io/fs.rs @@ -1,4 +1,3 @@ -use crate::io::story_metadata::{TestPlanStatus, parse_front_matter}; use crate::state::SessionState; use crate::store::StoreOps; use serde::Serialize; @@ -412,34 +411,6 @@ fn resolve_path_impl(root: PathBuf, relative_path: &str) -> Result bool { - path == ".story_kit" || path.starts_with(".story_kit/") -} - -async fn ensure_test_plan_approved(root: PathBuf) -> Result<(), String> { - let approved = tokio::task::spawn_blocking(move || { - let story_path = root - .join(".story_kit") - .join("stories") - .join("current") - .join("26_establish_tdd_workflow_and_gates.md"); - let contents = fs::read_to_string(&story_path) - .map_err(|e| format!("Failed to read story file for test plan approval: {e}"))?; - let metadata = parse_front_matter(&contents) - .map_err(|e| format!("Failed to parse story front matter: {e:?}"))?; - - Ok::(matches!(metadata.test_plan, Some(TestPlanStatus::Approved))) - }) - .await - .map_err(|e| format!("Task failed: {e}"))??; - - if approved { - Ok(()) - } else { - Err("Test plan is not approved for the current story.".to_string()) - } -} - /// Resolves a relative path against the active project root. /// Returns error if no project is open or if path attempts traversal (..). fn resolve_path(state: &SessionState, relative_path: &str) -> Result { @@ -666,9 +637,6 @@ async fn write_file_impl(full_path: PathBuf, content: String) -> Result<(), Stri pub async fn write_file(path: String, content: String, state: &SessionState) -> Result<(), String> { let root = state.get_project_root()?; - if !is_story_kit_path(&path) { - ensure_test_plan_approved(root.clone()).await?; - } let full_path = resolve_path_impl(root, &path)?; write_file_impl(full_path, content).await } @@ -767,16 +735,6 @@ mod tests { assert!(result.unwrap_err().contains("traversal")); } - // --- is_story_kit_path --- - - #[test] - fn is_story_kit_path_matches_root_and_children() { - assert!(is_story_kit_path(".story_kit")); - assert!(is_story_kit_path(".story_kit/stories/current/26.md")); - assert!(!is_story_kit_path("src/main.rs")); - assert!(!is_story_kit_path(".story_kit_other")); - } - // --- open/close/get project --- #[tokio::test] @@ -936,24 +894,6 @@ mod tests { assert_eq!(fs::read_to_string(&file).unwrap(), "content"); } - #[tokio::test] - async fn write_file_requires_approved_test_plan() { - let dir = tempdir().expect("tempdir"); - let state = SessionState::default(); - - { - let mut root = state.project_root.lock().expect("lock project root"); - *root = Some(dir.path().to_path_buf()); - } - - let result = write_file("notes.txt".to_string(), "hello".to_string(), &state).await; - - assert!( - result.is_err(), - "expected write to be blocked when test plan is not approved" - ); - } - // --- list directory --- #[tokio::test] diff --git a/server/src/io/shell.rs b/server/src/io/shell.rs index f45bc73..8219d77 100644 --- a/server/src/io/shell.rs +++ b/server/src/io/shell.rs @@ -1,7 +1,5 @@ -use crate::io::story_metadata::{TestPlanStatus, parse_front_matter}; use crate::state::SessionState; use serde::Serialize; -use std::fs; use std::path::PathBuf; use std::process::Command; @@ -10,30 +8,6 @@ fn get_project_root(state: &SessionState) -> Result { state.get_project_root() } -async fn ensure_test_plan_approved(root: PathBuf) -> Result<(), String> { - let approved = tokio::task::spawn_blocking(move || { - let story_path = root - .join(".story_kit") - .join("stories") - .join("current") - .join("26_establish_tdd_workflow_and_gates.md"); - let contents = fs::read_to_string(&story_path) - .map_err(|e| format!("Failed to read story file for test plan approval: {e}"))?; - let metadata = parse_front_matter(&contents) - .map_err(|e| format!("Failed to parse story front matter: {e:?}"))?; - - Ok::(matches!(metadata.test_plan, Some(TestPlanStatus::Approved))) - }) - .await - .map_err(|e| format!("Task failed: {e}"))??; - - if approved { - Ok(()) - } else { - Err("Test plan is not approved for the current story.".to_string()) - } -} - #[derive(Serialize, Debug, poem_openapi::Object)] pub struct CommandOutput { pub stdout: String, @@ -80,7 +54,6 @@ pub async fn exec_shell( state: &SessionState, ) -> Result { let root = get_project_root(state)?; - ensure_test_plan_approved(root.clone()).await?; exec_shell_impl(command, args, root).await } @@ -89,24 +62,6 @@ mod tests { use super::*; use tempfile::tempdir; - #[tokio::test] - async fn exec_shell_requires_approved_test_plan() { - let dir = tempdir().expect("tempdir"); - let state = SessionState::default(); - - { - let mut root = state.project_root.lock().expect("lock project root"); - *root = Some(dir.path().to_path_buf()); - } - - let result = exec_shell("ls".to_string(), Vec::new(), &state).await; - - assert!( - result.is_err(), - "expected shell execution to be blocked when test plan is not approved" - ); - } - #[tokio::test] async fn exec_shell_impl_rejects_disallowed_command() { let dir = tempdir().unwrap(); diff --git a/server/src/io/story_metadata.rs b/server/src/io/story_metadata.rs index 723f109..dd5776a 100644 --- a/server/src/io/story_metadata.rs +++ b/server/src/io/story_metadata.rs @@ -1,16 +1,8 @@ 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, - pub test_plan: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -31,7 +23,6 @@ impl std::fmt::Display for StoryMetaError { #[derive(Debug, Deserialize)] struct FrontMatter { name: Option, - test_plan: Option, } pub fn parse_front_matter(contents: &str) -> Result { @@ -60,11 +51,8 @@ pub fn parse_front_matter(contents: &str) -> Result StoryMetadata { - let test_plan = front.test_plan.as_deref().map(parse_test_plan_status); - StoryMetadata { name: front.name, - test_plan, } } @@ -80,14 +68,6 @@ pub fn parse_unchecked_todos(contents: &str) -> Vec { .collect() } -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::*; @@ -96,7 +76,6 @@ mod tests { fn parses_front_matter_metadata() { let input = r#"--- name: Establish the TDD Workflow and Gates -test_plan: approved workflow: tdd --- # Story 26 @@ -107,7 +86,6 @@ workflow: tdd meta, StoryMetadata { name: Some("Establish the TDD Workflow and Gates".to_string()), - test_plan: Some(TestPlanStatus::Approved), } ); }