huskies: merge 1158 story Reject boilerplate user stories at save time

This commit is contained in:
Huskies Agent
2026-07-16 00:03:44 +00:00
parent 6cceec9c26
commit e67eff17ad
9 changed files with 158 additions and 16 deletions
+25
View File
@@ -11,6 +11,7 @@ pub(crate) mod io;
pub mod state_machine;
use crate::io::wizard::{StepStatus, WizardState, WizardStep, format_wizard_state};
use crate::validation::check_boilerplate_user_story;
use std::path::Path;
// ── Error type ────────────────────────────────────────────────────────────────
@@ -21,6 +22,7 @@ use std::path::Path;
/// - [`Error::NotActive`] → 404 Not Found
/// - [`Error::InvalidStateTransition`] → 400 Bad Request
/// - [`Error::MissingInput`] → 400 Bad Request
/// - [`Error::InvalidContent`] → 400 Bad Request
/// - [`Error::GenerationFailure`] → 500 Internal Server Error
/// - [`Error::PersistenceFailure`] → 500 Internal Server Error
#[derive(Debug)]
@@ -32,6 +34,8 @@ pub enum Error {
InvalidStateTransition(String),
/// Required input was absent (e.g. content not staged before confirming).
MissingInput(String),
/// Staged content failed validation (e.g. boilerplate user-story template).
InvalidContent(String),
/// The LLM or agent failed to generate content for the step.
GenerationFailure(String),
/// A filesystem read or write operation failed.
@@ -46,6 +50,7 @@ impl std::fmt::Display for Error {
}
Self::InvalidStateTransition(msg) => write!(f, "Invalid state transition: {msg}"),
Self::MissingInput(msg) => write!(f, "Missing input: {msg}"),
Self::InvalidContent(msg) => write!(f, "Invalid content: {msg}"),
Self::GenerationFailure(msg) => write!(f, "Generation failed: {msg}"),
Self::PersistenceFailure(msg) => write!(f, "Persistence error: {msg}"),
}
@@ -104,6 +109,8 @@ pub fn status(root: &Path) -> Result<String, Error> {
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::InvalidContent`] if `content` matches the boilerplate
/// "As a …, I want …, so that …" user-story template.
/// - [`Error::PersistenceFailure`] if saving state fails.
pub fn generate(root: &Path, content: Option<&str>) -> Result<String, Error> {
let mut state = io::load(root).ok_or(Error::NotActive)?;
@@ -116,6 +123,9 @@ pub fn generate(root: &Path, content: Option<&str>) -> Result<String, Error> {
let step = state.steps[current_idx].step;
if let Some(c) = content {
if let Some(err) = check_boilerplate_user_story("content", c) {
return Err(Error::InvalidContent(err.to_string()));
}
state.set_step_status(step, StepStatus::AwaitingConfirmation, Some(c.to_string()));
io::save(&state, root)?;
return Ok(format!(
@@ -381,6 +391,21 @@ mod tests {
assert_eq!(state.steps[1].status, StepStatus::AwaitingConfirmation);
}
#[test]
fn generate_rejects_boilerplate_user_story_content() {
let dir = TempDir::new().unwrap();
init_wizard(&dir);
let err = generate(
dir.path(),
Some("As a user, I want this, so that I get value."),
)
.unwrap_err();
assert!(matches!(err, Error::InvalidContent(_)));
// Content must not be staged when rejected.
let state = get_state(dir.path()).unwrap();
assert_eq!(state.steps[1].status, StepStatus::Pending);
}
#[test]
fn generate_no_content_marks_generating() {
let dir = TempDir::new().unwrap();