use super::Config;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnrichmentStage {
Voice,
Persona,
Targeting,
}
impl EnrichmentStage {
pub fn label(self) -> &'static str {
match self {
Self::Voice => "Voice",
Self::Persona => "Persona",
Self::Targeting => "Targeting",
}
}
pub fn description(self) -> &'static str {
match self {
Self::Voice => "shapes every LLM-generated reply and tweet",
Self::Persona => "makes content authentic with opinions and experiences",
Self::Targeting => "focuses discovery on specific accounts and competitors",
}
}
pub fn all() -> &'static [EnrichmentStage] {
&[Self::Voice, Self::Persona, Self::Targeting]
}
}
impl std::fmt::Display for EnrichmentStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
pub struct ProfileCompleteness {
pub stages: Vec<(EnrichmentStage, bool)>,
}
impl ProfileCompleteness {
pub fn completed_count(&self) -> usize {
self.stages.iter().filter(|(_, done)| *done).count()
}
pub fn total_count(&self) -> usize {
self.stages.len()
}
pub fn is_fully_enriched(&self) -> bool {
self.stages.iter().all(|(_, done)| *done)
}
pub fn next_incomplete(&self) -> Option<EnrichmentStage> {
self.stages
.iter()
.find(|(_, done)| !*done)
.map(|(stage, _)| *stage)
}
pub fn one_line_summary(&self) -> String {
self.stages
.iter()
.map(|(stage, done)| {
let status = if *done { "OK" } else { "--" };
format!("{} {}", stage.label(), status)
})
.collect::<Vec<_>>()
.join(" ")
}
}
impl Config {
pub fn profile_completeness(&self) -> ProfileCompleteness {
let opt_non_empty =
|opt: &Option<String>| opt.as_ref().is_some_and(|v| !v.trim().is_empty());
let voice = opt_non_empty(&self.business.brand_voice)
|| opt_non_empty(&self.business.reply_style)
|| opt_non_empty(&self.business.content_style);
let persona = !self.business.persona_opinions.is_empty()
|| !self.business.persona_experiences.is_empty()
|| !self.business.content_pillars.is_empty();
let targeting =
!self.targets.accounts.is_empty() || !self.business.competitor_keywords.is_empty();
ProfileCompleteness {
stages: vec![
(EnrichmentStage::Voice, voice),
(EnrichmentStage::Persona, persona),
(EnrichmentStage::Targeting, targeting),
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_config() -> Config {
Config::default()
}
#[test]
fn all_empty_has_zero_completeness() {
let config = empty_config();
let pc = config.profile_completeness();
assert_eq!(pc.completed_count(), 0);
assert_eq!(pc.total_count(), 3);
assert!(!pc.is_fully_enriched());
}
#[test]
fn voice_stage_complete_when_brand_voice_set() {
let mut config = empty_config();
config.business.brand_voice = Some("witty and concise".to_string());
let pc = config.profile_completeness();
assert_eq!(pc.completed_count(), 1);
assert!(pc.stages[0].1); }
#[test]
fn voice_stage_complete_when_reply_style_set() {
let mut config = empty_config();
config.business.reply_style = Some("helpful".to_string());
let pc = config.profile_completeness();
assert!(pc.stages[0].1);
}
#[test]
fn voice_stage_complete_when_content_style_set() {
let mut config = empty_config();
config.business.content_style = Some("educational".to_string());
let pc = config.profile_completeness();
assert!(pc.stages[0].1);
}
#[test]
fn persona_stage_complete_when_opinions_set() {
let mut config = empty_config();
config.business.persona_opinions = vec!["types are good".to_string()];
let pc = config.profile_completeness();
assert!(pc.stages[1].1); }
#[test]
fn targeting_stage_complete_when_accounts_set() {
let mut config = empty_config();
config.targets.accounts = vec!["elonmusk".to_string()];
let pc = config.profile_completeness();
assert!(pc.stages[2].1); }
#[test]
fn targeting_stage_complete_when_competitor_keywords_set() {
let mut config = empty_config();
config.business.competitor_keywords = vec!["rival_tool".to_string()];
let pc = config.profile_completeness();
assert!(pc.stages[2].1);
}
#[test]
fn empty_string_voice_not_counted() {
let mut config = empty_config();
config.business.brand_voice = Some("".to_string());
let pc = config.profile_completeness();
assert!(!pc.stages[0].1);
}
#[test]
fn whitespace_only_voice_not_counted() {
let mut config = empty_config();
config.business.brand_voice = Some(" ".to_string());
let pc = config.profile_completeness();
assert!(!pc.stages[0].1);
}
#[test]
fn fully_enriched_config() {
let mut config = empty_config();
config.business.brand_voice = Some("witty".to_string());
config.business.persona_opinions = vec!["opinion".to_string()];
config.targets.accounts = vec!["target".to_string()];
let pc = config.profile_completeness();
assert_eq!(pc.completed_count(), 3);
assert!(pc.is_fully_enriched());
assert!(pc.next_incomplete().is_none());
}
#[test]
fn next_incomplete_returns_first_missing() {
let mut config = empty_config();
config.business.brand_voice = Some("witty".to_string());
let pc = config.profile_completeness();
assert_eq!(pc.next_incomplete(), Some(EnrichmentStage::Persona));
}
#[test]
fn one_line_summary_format() {
let mut config = empty_config();
config.business.brand_voice = Some("witty".to_string());
config.targets.accounts = vec!["target".to_string()];
let pc = config.profile_completeness();
assert_eq!(pc.one_line_summary(), "Voice OK Persona -- Targeting OK");
}
#[test]
fn enrichment_stage_all_returns_three() {
assert_eq!(EnrichmentStage::all().len(), 3);
}
#[test]
fn enrichment_stage_display() {
assert_eq!(format!("{}", EnrichmentStage::Voice), "Voice");
assert_eq!(format!("{}", EnrichmentStage::Persona), "Persona");
assert_eq!(format!("{}", EnrichmentStage::Targeting), "Targeting");
}
#[test]
fn enrichment_stage_labels() {
assert_eq!(EnrichmentStage::Voice.label(), "Voice");
assert_eq!(EnrichmentStage::Persona.label(), "Persona");
assert_eq!(EnrichmentStage::Targeting.label(), "Targeting");
}
#[test]
fn enrichment_stage_descriptions_non_empty() {
for stage in EnrichmentStage::all() {
assert!(!stage.description().is_empty());
}
}
#[test]
fn enrichment_stage_all_order() {
let all = EnrichmentStage::all();
assert_eq!(all[0], EnrichmentStage::Voice);
assert_eq!(all[1], EnrichmentStage::Persona);
assert_eq!(all[2], EnrichmentStage::Targeting);
}
#[test]
fn enrichment_stage_debug_and_clone() {
let stage = EnrichmentStage::Voice;
let cloned = stage;
assert_eq!(stage, cloned);
let debug = format!("{:?}", stage);
assert!(debug.contains("Voice"));
}
#[test]
fn enrichment_stage_copy_trait() {
let a = EnrichmentStage::Persona;
let b = a; assert_eq!(a, b);
}
#[test]
fn profile_completeness_total_count() {
let config = empty_config();
let pc = config.profile_completeness();
assert_eq!(pc.total_count(), 3);
}
#[test]
fn persona_stage_complete_when_experiences_set() {
let mut config = empty_config();
config.business.persona_experiences = vec!["Built CLI tools".to_string()];
let pc = config.profile_completeness();
assert!(pc.stages[1].1);
}
#[test]
fn persona_stage_complete_when_pillars_set() {
let mut config = empty_config();
config.business.content_pillars = vec!["DevOps".to_string()];
let pc = config.profile_completeness();
assert!(pc.stages[1].1);
}
#[test]
fn next_incomplete_skips_completed() {
let mut config = empty_config();
config.business.brand_voice = Some("witty".to_string());
config.business.persona_opinions = vec!["types are good".to_string()];
let pc = config.profile_completeness();
assert_eq!(pc.next_incomplete(), Some(EnrichmentStage::Targeting));
}
#[test]
fn one_line_summary_all_incomplete() {
let config = empty_config();
let pc = config.profile_completeness();
assert_eq!(pc.one_line_summary(), "Voice -- Persona -- Targeting --");
}
#[test]
fn one_line_summary_all_complete() {
let mut config = empty_config();
config.business.brand_voice = Some("witty".to_string());
config.business.persona_opinions = vec!["opinion".to_string()];
config.targets.accounts = vec!["target".to_string()];
let pc = config.profile_completeness();
assert_eq!(pc.one_line_summary(), "Voice OK Persona OK Targeting OK");
}
#[test]
fn completed_count_partial() {
let mut config = empty_config();
config.business.brand_voice = Some("witty".to_string());
let pc = config.profile_completeness();
assert_eq!(pc.completed_count(), 1);
assert!(!pc.is_fully_enriched());
}
#[test]
fn voice_with_only_content_style() {
let mut config = empty_config();
config.business.content_style = Some("Technical".to_string());
let pc = config.profile_completeness();
assert!(pc.stages[0].1);
assert_eq!(pc.completed_count(), 1);
}
#[test]
fn targeting_both_accounts_and_competitors() {
let mut config = empty_config();
config.targets.accounts = vec!["target".to_string()];
config.business.competitor_keywords = vec!["rival".to_string()];
let pc = config.profile_completeness();
assert!(pc.stages[2].1);
}
#[test]
fn enrichment_stage_description_voice() {
let desc = EnrichmentStage::Voice.description();
assert!(desc.contains("LLM"));
}
#[test]
fn enrichment_stage_description_persona() {
let desc = EnrichmentStage::Persona.description();
assert!(desc.contains("authentic"));
}
#[test]
fn enrichment_stage_description_targeting() {
let desc = EnrichmentStage::Targeting.description();
assert!(desc.contains("discovery"));
}
}