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:
@@ -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<String> {
|
||||
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)
|
||||
|
||||
@@ -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<Value>) -> 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<String, String
|
||||
))
|
||||
}
|
||||
|
||||
fn tool_set_test_plan(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
@@ -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())
|
||||
|
||||
@@ -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(¤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"));
|
||||
|
||||
@@ -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