Remove test_plan gate from the codebase

The test_plan field was a gate from the old interactive web UI workflow
where a human would approve a test plan before the LLM could write code.
With autonomous coder agents, this gate is dead weight — coders sometimes
obey the README's "wait for approval" instruction and produce no code.

Removes: TestPlanStatus enum, ensure_test_plan_approved checks in fs/shell,
set_test_plan MCP tool + handler, test_plan from story/bug front matter
creation, test_plan validation in validate_story_dirs, and all related tests.
Updates README to remove Step 2 (Test Planning) and renumber steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-23 19:12:05 +00:00
parent cc2511b792
commit 31037f5bf5
7 changed files with 23 additions and 363 deletions

View File

@@ -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<String> = 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(&current).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(&current).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(&current).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(&current).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(&current).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(&current).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"));