use shared_kernel::{normalize_optional_string, normalize_required_string}; use crate::{ CreativeAgentError, CreativeAgentMessageAppendInput, CreativeAgentMessageKind, CreativeAgentMessageRole, CreativeAgentStage, CreativeAgentStageUpdateInput, CreativeAgentTargetBindInput, CreativeAgentTemplateConfirmInput, CreativeTargetPlayType, }; pub fn validate_create_session( session_id: &str, owner_user_id: &str, ) -> Result<(String, String), CreativeAgentError> { let session_id = normalize_required_string(session_id).ok_or(CreativeAgentError::MissingSessionId)?; let owner_user_id = normalize_required_string(owner_user_id).ok_or(CreativeAgentError::MissingOwnerUserId)?; Ok((session_id, owner_user_id)) } pub fn validate_append_message( input: &CreativeAgentMessageAppendInput, ) -> Result<(), CreativeAgentError> { validate_create_session(&input.session_id, &input.owner_user_id)?; if normalize_required_string(&input.message_id).is_none() { return Err(CreativeAgentError::MissingMessageId); } if normalize_required_string(&input.text).is_none() { return Err(CreativeAgentError::MissingMessageText); } Ok(()) } pub fn validate_stage_update( current: CreativeAgentStage, input: &CreativeAgentStageUpdateInput, ) -> Result<(), CreativeAgentError> { validate_create_session(&input.session_id, &input.owner_user_id)?; validate_stage_transition(current, input.stage) } pub fn validate_template_confirmation( current: CreativeAgentStage, input: &CreativeAgentTemplateConfirmInput, ) -> Result<(), CreativeAgentError> { validate_create_session(&input.session_id, &input.owner_user_id)?; if normalize_required_string(&input.template_selection_json).is_none() { return Err(CreativeAgentError::MissingTemplateSelection); } if !input.template_selection_json.contains("\"costRange\"") && !input.template_selection_json.contains("\"cost_range\"") { return Err(CreativeAgentError::MissingCostRange); } validate_stage_transition(current, CreativeAgentStage::PlanningPuzzleLevels) } pub fn validate_target_binding( current_stage: CreativeAgentStage, template_selection_json: Option<&str>, input: &CreativeAgentTargetBindInput, ) -> Result<(), CreativeAgentError> { validate_create_session(&input.session_id, &input.owner_user_id)?; if input.play_type != CreativeTargetPlayType::Puzzle { return Err(CreativeAgentError::UnsupportedTargetPlayType); } if normalize_required_string(&input.target_session_id).is_none() { return Err(CreativeAgentError::MissingTargetSessionId); } if normalize_optional_string(template_selection_json.map(str::to_string)).is_none() { return Err(CreativeAgentError::TemplateNotConfirmed); } // 中文注释:绑定目标 session 是“草稿已创建”的持久化标记,只允许在行动链路之后发生。 if !matches!( current_stage, CreativeAgentStage::PlanningPuzzleLevels | CreativeAgentStage::Acting | CreativeAgentStage::Reflecting | CreativeAgentStage::Collaborating | CreativeAgentStage::TargetReady ) { return Err(CreativeAgentError::InvalidStageTransition); } Ok(()) } pub fn validate_stage_transition( current: CreativeAgentStage, next: CreativeAgentStage, ) -> Result<(), CreativeAgentError> { if current == next { return Ok(()); } if matches!( next, CreativeAgentStage::Failed | CreativeAgentStage::WaitingUser ) { return Ok(()); } let allowed = matches!( (current, next), (CreativeAgentStage::Idle, CreativeAgentStage::Perceiving) | ( CreativeAgentStage::Idle, CreativeAgentStage::SelectingPuzzleTemplate ) | (CreativeAgentStage::Perceiving, CreativeAgentStage::Thinking) | ( CreativeAgentStage::Thinking, CreativeAgentStage::Remembering ) | ( CreativeAgentStage::Thinking, CreativeAgentStage::SelectingPuzzleTemplate ) | ( CreativeAgentStage::Remembering, CreativeAgentStage::SelectingPuzzleTemplate ) | ( CreativeAgentStage::SelectingPuzzleTemplate, CreativeAgentStage::WaitingTemplateConfirmation ) | ( CreativeAgentStage::WaitingTemplateConfirmation, CreativeAgentStage::PlanningPuzzleLevels ) | ( CreativeAgentStage::PlanningPuzzleLevels, CreativeAgentStage::Acting ) | (CreativeAgentStage::Acting, CreativeAgentStage::Reflecting) | ( CreativeAgentStage::Reflecting, CreativeAgentStage::Collaborating ) | ( CreativeAgentStage::Collaborating, CreativeAgentStage::Acting ) | ( CreativeAgentStage::Reflecting, CreativeAgentStage::TargetReady ) | (CreativeAgentStage::Acting, CreativeAgentStage::TargetReady) | ( CreativeAgentStage::PlanningPuzzleLevels, CreativeAgentStage::TargetReady ) ); if allowed { Ok(()) } else { Err(CreativeAgentError::InvalidStageTransition) } } pub fn normalize_message_role(value: CreativeAgentMessageRole) -> &'static str { value.as_str() } pub fn normalize_message_kind(value: CreativeAgentMessageKind) -> &'static str { value.as_str() } #[cfg(test)] mod tests { use super::*; use crate::{CreativeAgentTargetBindInput, CreativeTargetStage}; #[test] fn template_confirmation_requires_cost_range() { let input = CreativeAgentTemplateConfirmInput { session_id: "creative-session-1".to_string(), owner_user_id: "user-1".to_string(), template_selection_json: r#"{"templateId":"puzzle.default-creative"}"#.to_string(), updated_at_micros: 1, }; assert_eq!( validate_template_confirmation(CreativeAgentStage::WaitingTemplateConfirmation, &input,), Err(CreativeAgentError::MissingCostRange) ); } #[test] fn target_binding_requires_confirmed_template() { let input = CreativeAgentTargetBindInput { binding_id: "creative-binding-1".to_string(), session_id: "creative-session-1".to_string(), owner_user_id: "user-1".to_string(), play_type: CreativeTargetPlayType::Puzzle, target_session_id: "puzzle-session-1".to_string(), target_stage: CreativeTargetStage::PuzzleResult, result_profile_id: None, created_at_micros: 1, }; assert_eq!( validate_target_binding(CreativeAgentStage::Acting, None, &input), Err(CreativeAgentError::TemplateNotConfirmed) ); } #[test] fn phase1_stage_path_allows_template_to_target_ready() { assert!( validate_stage_transition( CreativeAgentStage::WaitingTemplateConfirmation, CreativeAgentStage::PlanningPuzzleLevels, ) .is_ok() ); assert!( validate_stage_transition( CreativeAgentStage::PlanningPuzzleLevels, CreativeAgentStage::Acting ) .is_ok() ); assert!( validate_stage_transition(CreativeAgentStage::Acting, CreativeAgentStage::TargetReady) .is_ok() ); } }