226 lines
7.7 KiB
Rust
226 lines
7.7 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|