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:
@@ -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<PathBuf, Stri
|
||||
Ok(root.join(relative_path))
|
||||
}
|
||||
|
||||
fn is_story_kit_path(path: &str) -> 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::<bool, String>(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<PathBuf, String> {
|
||||
@@ -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]
|
||||
|
||||
@@ -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<PathBuf, String> {
|
||||
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::<bool, String>(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<CommandOutput, String> {
|
||||
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();
|
||||
|
||||
@@ -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<String>,
|
||||
pub test_plan: Option<TestPlanStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -31,7 +23,6 @@ impl std::fmt::Display for StoryMetaError {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FrontMatter {
|
||||
name: Option<String>,
|
||||
test_plan: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
||||
@@ -60,11 +51,8 @@ pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaErro
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,14 +68,6 @@ pub fn parse_unchecked_todos(contents: &str) -> Vec<String> {
|
||||
.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),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user