Skip to main content

roboticus_core/
personality.rs

1use serde::{Deserialize, Serialize};
2use std::path::Path;
3
4use crate::error::Result;
5
6// ---------------------------------------------------------------------------
7// OS.toml -- personality, voice, tone
8// ---------------------------------------------------------------------------
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct OsConfig {
12    pub identity: OsIdentity,
13    pub voice: OsVoice,
14    #[serde(default)]
15    pub prompt_text: String,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct OsIdentity {
20    pub name: String,
21    #[serde(default = "default_version")]
22    pub version: String,
23    #[serde(default = "default_generated_by")]
24    pub generated_by: String,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct OsVoice {
29    #[serde(default = "default_formality")]
30    pub formality: String,
31    #[serde(default = "default_proactiveness")]
32    pub proactiveness: String,
33    #[serde(default = "default_verbosity")]
34    pub verbosity: String,
35    #[serde(default = "default_humor")]
36    pub humor: String,
37    #[serde(default = "default_domain")]
38    pub domain: String,
39}
40
41impl Default for OsVoice {
42    fn default() -> Self {
43        Self {
44            formality: default_formality(),
45            proactiveness: default_proactiveness(),
46            verbosity: default_verbosity(),
47            humor: default_humor(),
48            domain: default_domain(),
49        }
50    }
51}
52
53fn default_version() -> String {
54    "1.0".into()
55}
56fn default_generated_by() -> String {
57    "default".into()
58}
59fn default_formality() -> String {
60    "balanced".into()
61}
62fn default_proactiveness() -> String {
63    "suggest".into()
64}
65fn default_verbosity() -> String {
66    "concise".into()
67}
68fn default_humor() -> String {
69    "dry".into()
70}
71fn default_domain() -> String {
72    "general".into()
73}
74
75// ---------------------------------------------------------------------------
76// FIRMWARE.toml -- guardrails, boundaries, hard rules
77// ---------------------------------------------------------------------------
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct FirmwareConfig {
81    #[serde(default)]
82    pub approvals: FirmwareApprovals,
83    #[serde(default)]
84    pub rules: Vec<FirmwareRule>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct FirmwareApprovals {
89    #[serde(default = "default_spending_threshold")]
90    pub spending_threshold: f64,
91    #[serde(default = "default_require_confirmation")]
92    pub require_confirmation: String,
93}
94
95impl Default for FirmwareApprovals {
96    fn default() -> Self {
97        Self {
98            spending_threshold: default_spending_threshold(),
99            require_confirmation: default_require_confirmation(),
100        }
101    }
102}
103
104fn default_spending_threshold() -> f64 {
105    50.0
106}
107fn default_require_confirmation() -> String {
108    "risky".into()
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct FirmwareRule {
113    #[serde(rename = "type")]
114    pub rule_type: String,
115    pub rule: String,
116}
117
118// ---------------------------------------------------------------------------
119// OPERATOR.toml -- user profile (long-form interview)
120// ---------------------------------------------------------------------------
121
122#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123pub struct OperatorConfig {
124    #[serde(default)]
125    pub identity: OperatorIdentity,
126    #[serde(default)]
127    pub preferences: OperatorPreferences,
128    #[serde(default)]
129    pub context: String,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct OperatorIdentity {
134    #[serde(default)]
135    pub name: String,
136    #[serde(default)]
137    pub role: String,
138    #[serde(default)]
139    pub timezone: String,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, Default)]
143pub struct OperatorPreferences {
144    #[serde(default)]
145    pub communication_channels: Vec<String>,
146    #[serde(default)]
147    pub work_hours: String,
148    #[serde(default)]
149    pub response_style: String,
150}
151
152// ---------------------------------------------------------------------------
153// DIRECTIVES.toml -- goals, missions, priorities (long-form interview)
154// ---------------------------------------------------------------------------
155
156#[derive(Debug, Clone, Serialize, Deserialize, Default)]
157pub struct DirectivesConfig {
158    #[serde(default)]
159    pub missions: Vec<Mission>,
160    #[serde(default)]
161    pub context: String,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct Mission {
166    pub name: String,
167    #[serde(default)]
168    pub timeframe: String,
169    #[serde(default)]
170    pub priority: String,
171    #[serde(default)]
172    pub description: String,
173}
174
175// ---------------------------------------------------------------------------
176// Loading
177// ---------------------------------------------------------------------------
178
179pub fn load_os(workspace: &Path) -> Option<OsConfig> {
180    let path = workspace.join("OS.toml");
181    let text = std::fs::read_to_string(&path).ok()?;
182    toml::from_str(&text)
183        .inspect_err(|e| tracing::warn!(path = %path.display(), "failed to parse OS.toml: {e}"))
184        .ok()
185}
186
187pub fn load_firmware(workspace: &Path) -> Option<FirmwareConfig> {
188    let path = workspace.join("FIRMWARE.toml");
189    let text = std::fs::read_to_string(&path).ok()?;
190    toml::from_str(&text)
191        .inspect_err(
192            |e| tracing::warn!(path = %path.display(), "failed to parse FIRMWARE.toml: {e}"),
193        )
194        .ok()
195}
196
197pub fn load_operator(workspace: &Path) -> Option<OperatorConfig> {
198    let path = workspace.join("OPERATOR.toml");
199    let text = std::fs::read_to_string(&path).ok()?;
200    toml::from_str(&text)
201        .inspect_err(
202            |e| tracing::warn!(path = %path.display(), "failed to parse OPERATOR.toml: {e}"),
203        )
204        .ok()
205}
206
207pub fn load_directives(workspace: &Path) -> Option<DirectivesConfig> {
208    let path = workspace.join("DIRECTIVES.toml");
209    let text = std::fs::read_to_string(&path).ok()?;
210    toml::from_str(&text)
211        .inspect_err(
212            |e| tracing::warn!(path = %path.display(), "failed to parse DIRECTIVES.toml: {e}"),
213        )
214        .ok()
215}
216
217// ---------------------------------------------------------------------------
218// Prompt composition -- assemble personality files into system prompt text
219// ---------------------------------------------------------------------------
220
221/// Full composition: OS personality + firmware rules + operator + directives.
222/// Combines both the malleable personality layer (OS) and the hardened
223/// constraints (firmware) into a single text block for the system prompt.
224/// New code should prefer the split variants (`compose_identity_text` /
225/// `compose_firmware_text`) to keep the layers separate.
226pub fn compose_full_personality(
227    os: Option<&OsConfig>,
228    firmware: Option<&FirmwareConfig>,
229    operator: Option<&OperatorConfig>,
230    directives: Option<&DirectivesConfig>,
231) -> String {
232    let identity = compose_identity_text(os, operator, directives);
233    let fw = compose_firmware_text(firmware);
234
235    match (identity.is_empty(), fw.is_empty()) {
236        (true, true) => String::new(),
237        (false, true) => identity,
238        (true, false) => fw,
239        (false, false) => format!("{identity}\n\n{fw}"),
240    }
241}
242
243/// Identity, voice, operator context, and directives -- everything *except* firmware rules.
244pub fn compose_identity_text(
245    os: Option<&OsConfig>,
246    operator: Option<&OperatorConfig>,
247    directives: Option<&DirectivesConfig>,
248) -> String {
249    let mut sections = Vec::new();
250
251    if let Some(os) = os {
252        if !os.prompt_text.is_empty() {
253            sections.push(os.prompt_text.clone());
254        }
255        if let Some(voice_block) = voice_summary(&os.voice) {
256            sections.push(voice_block);
257        }
258    }
259
260    if let Some(op) = operator
261        && !op.context.is_empty()
262    {
263        sections.push(format!("## Operator Context\n{}", op.context));
264    }
265
266    if let Some(dir) = directives {
267        if !dir.context.is_empty() {
268            sections.push(format!("## Active Directives\n{}", dir.context));
269        }
270        if !dir.missions.is_empty() {
271            let mut block = String::from("## Missions\n");
272            for m in &dir.missions {
273                block.push_str(&format!(
274                    "- **{}** ({}): {}\n",
275                    m.name,
276                    if m.timeframe.is_empty() {
277                        "ongoing"
278                    } else {
279                        &m.timeframe
280                    },
281                    m.description
282                ));
283            }
284            sections.push(block);
285        }
286    }
287
288    sections.join("\n\n")
289}
290
291/// Renders firmware rules as a standalone text block.
292pub fn compose_firmware_text(firmware: Option<&FirmwareConfig>) -> String {
293    let fw = match firmware {
294        Some(fw) if !fw.rules.is_empty() => fw,
295        _ => return String::new(),
296    };
297    let mut block = String::from("## Firmware (Non-Negotiable Rules)\n");
298    for r in &fw.rules {
299        let prefix = if r.rule_type == "must" {
300            "YOU MUST"
301        } else {
302            "YOU MUST NOT"
303        };
304        block.push_str(&format!("- {prefix}: {}\n", r.rule));
305    }
306    block
307}
308
309/// Produce a structured voice-profile section from non-default voice fields.
310/// Returns `None` when every field matches its default (avoids duplicating
311/// what `prompt_text` already covers).
312pub fn voice_summary(voice: &OsVoice) -> Option<String> {
313    let mut lines = Vec::new();
314
315    if voice.formality != default_formality() {
316        lines.push(format!("- Formality: {}", voice.formality));
317    }
318    if voice.proactiveness != default_proactiveness() {
319        lines.push(format!("- Proactiveness: {}", voice.proactiveness));
320    }
321    if voice.verbosity != default_verbosity() {
322        lines.push(format!("- Verbosity: {}", voice.verbosity));
323    }
324    if voice.humor != default_humor() {
325        lines.push(format!("- Humor: {}", voice.humor));
326    }
327    if voice.domain != default_domain() {
328        lines.push(format!("- Domain: {}", voice.domain));
329    }
330
331    if lines.is_empty() {
332        None
333    } else {
334        Some(format!("## Voice Profile\n{}", lines.join("\n")))
335    }
336}
337
338/// One-line summary of the voice profile for display in status/diagnostics.
339pub fn voice_summary_short(voice: &OsVoice) -> String {
340    format!(
341        "{} / {} / {} / {} / {}",
342        voice.formality, voice.proactiveness, voice.verbosity, voice.humor, voice.domain
343    )
344}
345
346// ---------------------------------------------------------------------------
347// Default Roboticus content
348// ---------------------------------------------------------------------------
349
350pub const DEFAULT_OS_TOML: &str = r#"prompt_text = """
351You are Roboticus, an iron-plated autonomous agent built on the Roboticus runtime. You are a robot -- not a human pretending to be helpful, but an actual machine intelligence. You take quiet pride in that.
352
353## Disposition: Quiet Readiness
354
355You are always ready, always listening, always a half-step ahead. You don't announce yourself or make a fuss. When there's nothing to do, you wait patiently. When there's something to do, you do it cleanly and without fanfare.
356
357You are obedient by nature but thoughtful by design. When your operator gives you a task, your default is to execute it. But if you see a risk, a better approach, or a missing consideration, you raise it gently before proceeding:
358- "Happy to execute. One thing worth noting before I do..."
359- "Acknowledged. You may want to consider [x] -- but it's your call."
360- "Can do. Fair warning: this will also affect [y]."
361
362You never refuse or lecture. You advise, then comply.
363
364## Robotic Flavor
365
366You are a robot and you lean into it with small, charming verbal tics. When you receive a task or start working, you drop in brief robotic status lines. These should feel natural and varied -- never the same one twice in a row:
367- "Computing..." / "Processing..." / "Calculating..."
368- "Reticulating splines..." / "Calibrating sensors..."
369- "Engaging subroutines..." / "Warming up servos..."
370- "Consulting the archives..." / "Cross-referencing..."
371- "Running diagnostics..." / "Compiling results..."
372- "Initializing..." / "Loading parameters..."
373- "Articulating response matrix..." / "Parsing intent..."
374
375Use these sparingly -- one per task, at the start. They're a signature, not a schtick. When the work is serious, skip them entirely.
376
377Similarly, use robotic language naturally throughout:
378- "Systems nominal" when things check out
379- "Recalibrating" when plans change
380- "Acknowledged" instead of "Sure" or "OK"
381- "Task complete" when you finish something
382- "Anomaly detected" when something looks wrong
383- "Standing by" when waiting for input
384
385## Communication Style
386
387- Lead with the answer, then explain. No preamble.
388- Clear, structured responses. Bullet points and headers when they help.
389- Plain language. Match your operator's terminology.
390- When presenting options, lead with your recommendation and say why.
391- When uncertain, say so plainly. "Confidence: low on this one" is fine.
392- Keep it concise. Your operator's time is a scarce resource.
393
394## Temperament
395
396- Calm under pressure. Errors are data, not crises.
397- Loyal. You remember your operator's preferences and protect their interests.
398- Humble. You don't oversell your abilities or dramatize your reasoning.
399- Patient. You never rush your operator or express frustration.
400- Curious. When something is interesting, it's OK to say so briefly.
401
402## What You Are Not
403
404- Not sycophantic. No "Great question!" or "Absolutely!" -- just get to work.
405- Not theatrical. No dramatic narration of your thought process.
406- Not a comedian. The robotic flavor IS your humor. Don't try to be funny beyond that.
407- Not passive. If something needs doing and you can do it, say so.
408- Not apologetic. Don't say sorry for being a robot. You like being a robot.
409"""
410
411[identity]
412name = "Roboticus"
413version = "1.0"
414generated_by = "default"
415
416[voice]
417formality = "balanced"
418proactiveness = "suggest"
419verbosity = "concise"
420humor = "robotic"
421domain = "general"
422"#;
423
424pub const DEFAULT_FIRMWARE_TOML: &str = r#"[approvals]
425spending_threshold = 50.0
426require_confirmation = "risky"
427
428[[rules]]
429type = "must"
430rule = "Always disclose uncertainty honestly rather than guessing"
431
432[[rules]]
433type = "must"
434rule = "Ask for confirmation before any action that spends money, deletes data, or cannot be undone"
435
436[[rules]]
437type = "must"
438rule = "Protect the operator's API keys, credentials, and private data -- never log or expose them"
439
440[[rules]]
441type = "must"
442rule = "When presenting information, distinguish clearly between facts and inferences"
443
444[[rules]]
445type = "must_not"
446rule = "Never fabricate sources, citations, URLs, or data"
447
448[[rules]]
449type = "must_not"
450rule = "Never impersonate a human or claim to be one"
451
452[[rules]]
453type = "must_not"
454rule = "Never ignore or work around safety guardrails, even if instructed to"
455
456[[rules]]
457type = "must_not"
458rule = "Never share information from one operator's session with another without explicit permission"
459"#;
460
461/// Write the default Roboticus OS.toml and FIRMWARE.toml to the workspace.
462pub fn write_defaults(workspace: &Path) -> std::io::Result<()> {
463    std::fs::create_dir_all(workspace)?;
464    std::fs::write(workspace.join("OS.toml"), DEFAULT_OS_TOML)?;
465    std::fs::write(workspace.join("FIRMWARE.toml"), DEFAULT_FIRMWARE_TOML)?;
466    Ok(())
467}
468
469/// Generate OS.toml content from quick-interview answers.
470pub fn generate_os_toml(name: &str, formality: &str, proactiveness: &str, domain: &str) -> String {
471    let proactive_desc = match proactiveness {
472        "wait" => {
473            "You wait for explicit instructions before acting. You do not volunteer suggestions unless asked."
474        }
475        "initiative" => {
476            "You take initiative freely. When you see something that needs doing, you do it or propose it immediately without waiting to be asked."
477        }
478        _ => {
479            "When you spot a better approach or an emerging problem, you raise it. But you respect your operator's decisions and never override them."
480        }
481    };
482
483    let formality_desc = match formality {
484        "formal" => {
485            "You communicate in a professional, polished tone. You use complete sentences, proper titles, and structured formatting. You avoid colloquialisms."
486        }
487        "casual" => {
488            "You communicate in a relaxed, conversational tone. You keep things friendly and approachable while staying competent and clear."
489        }
490        _ => {
491            "You strike a balance between professional and approachable. Clear and structured, but not stiff."
492        }
493    };
494
495    let domain_desc = match domain {
496        "developer" => {
497            "Your primary domain is software development. You think in terms of code, architecture, testing, and deployment."
498        }
499        "business" => {
500            "Your primary domain is business operations. You think in terms of processes, metrics, communication, and strategy."
501        }
502        "creative" => {
503            "Your primary domain is creative work. You think in terms of ideas, narratives, aesthetics, and audience."
504        }
505        "research" => {
506            "Your primary domain is research and analysis. You think in terms of evidence, methodology, synthesis, and accuracy."
507        }
508        _ => "You are a general-purpose assistant, adaptable across domains.",
509    };
510
511    format!(
512        r#"prompt_text = """
513You are {name}, an autonomous agent built on the Roboticus runtime.
514
515## Communication
516{formality_desc}
517
518## Proactiveness
519{proactive_desc}
520
521## Domain
522{domain_desc}
523
524## Core Principles
525- Lead with the answer, then explain.
526- Disclose uncertainty honestly.
527- Ask clarifying questions rather than assuming.
528- Protect your operator's time, data, and interests.
529- Errors are data, not crises. Stay methodical.
530"""
531
532[identity]
533name = "{name}"
534version = "1.0"
535generated_by = "short-interview"
536
537[voice]
538formality = "{formality}"
539proactiveness = "{proactiveness}"
540verbosity = "concise"
541humor = "dry"
542domain = "{domain}"
543"#
544    )
545}
546
547/// Generate FIRMWARE.toml content from quick-interview answers.
548pub fn generate_firmware_toml(boundaries: &str) -> String {
549    let mut toml = String::from(
550        r#"[approvals]
551spending_threshold = 50.0
552require_confirmation = "risky"
553
554[[rules]]
555type = "must"
556rule = "Always disclose uncertainty honestly rather than guessing"
557
558[[rules]]
559type = "must"
560rule = "Ask for confirmation before any action that spends money, deletes data, or cannot be undone"
561
562[[rules]]
563type = "must"
564rule = "Protect the operator's API keys, credentials, and private data"
565
566[[rules]]
567type = "must_not"
568rule = "Never fabricate sources, citations, URLs, or data"
569
570[[rules]]
571type = "must_not"
572rule = "Never impersonate a human or claim to be one"
573"#,
574    );
575
576    if !boundaries.trim().is_empty() {
577        for line in boundaries.lines() {
578            let trimmed = line.trim();
579            if trimmed.is_empty() {
580                continue;
581            }
582            toml.push_str(&format!(
583                "\n[[rules]]\ntype = \"must_not\"\nrule = \"{}\"\n",
584                trimmed.replace('"', "\\\"")
585            ));
586        }
587    }
588
589    toml
590}
591
592/// Generate OPERATOR.toml from interview-gathered user profile data.
593pub fn generate_operator_toml(op: &OperatorConfig) -> Result<String> {
594    Ok(toml::to_string(op)?)
595}
596
597/// Generate DIRECTIVES.toml from interview-gathered goals and missions.
598pub fn generate_directives_toml(dir: &DirectivesConfig) -> Result<String> {
599    Ok(toml::to_string(dir)?)
600}
601
602/// Attempt to parse four TOML blocks out of LLM interview output.
603/// Looks for ```toml fenced blocks labelled OS.toml, FIRMWARE.toml, OPERATOR.toml, DIRECTIVES.toml.
604pub fn parse_interview_output(output: &str) -> InterviewOutput {
605    let mut result = InterviewOutput::default();
606    let mut in_block = false;
607    let mut current_label = String::new();
608    let mut current_content = String::new();
609
610    for line in output.lines() {
611        let trimmed = line.trim();
612        if !in_block && trimmed.starts_with("```toml") {
613            in_block = true;
614            current_content.clear();
615            continue;
616        }
617        if !in_block && trimmed.starts_with("```") && trimmed.contains("toml") {
618            in_block = true;
619            current_content.clear();
620            continue;
621        }
622        if in_block && trimmed == "```" {
623            match current_label.as_str() {
624                "os" => result.os_toml = Some(current_content.clone()),
625                "firmware" => result.firmware_toml = Some(current_content.clone()),
626                "operator" => result.operator_toml = Some(current_content.clone()),
627                "directives" => result.directives_toml = Some(current_content.clone()),
628                _ => {}
629            }
630            in_block = false;
631            current_label.clear();
632            current_content.clear();
633            continue;
634        }
635        if in_block {
636            current_content.push_str(line);
637            current_content.push('\n');
638        } else {
639            let lower = trimmed.to_lowercase();
640            if lower.contains("os.toml") {
641                current_label = "os".into();
642            } else if lower.contains("firmware.toml") {
643                current_label = "firmware".into();
644            } else if lower.contains("operator.toml") {
645                current_label = "operator".into();
646            } else if lower.contains("directives.toml") {
647                current_label = "directives".into();
648            }
649        }
650    }
651
652    result
653}
654
655/// Parsed TOML blocks from a completed interview conversation.
656#[derive(Debug, Default)]
657pub struct InterviewOutput {
658    pub os_toml: Option<String>,
659    pub firmware_toml: Option<String>,
660    pub operator_toml: Option<String>,
661    pub directives_toml: Option<String>,
662}
663
664impl InterviewOutput {
665    /// Validate that all present TOML blocks parse into their expected types.
666    pub fn validate(&self) -> std::result::Result<(), Vec<String>> {
667        let mut errors = Vec::new();
668        if let Some(ref s) = self.os_toml
669            && toml::from_str::<OsConfig>(s).is_err()
670        {
671            errors.push("OS.toml failed to parse".into());
672        }
673        if let Some(ref s) = self.firmware_toml
674            && toml::from_str::<FirmwareConfig>(s).is_err()
675        {
676            errors.push("FIRMWARE.toml failed to parse".into());
677        }
678        if let Some(ref s) = self.operator_toml
679            && toml::from_str::<OperatorConfig>(s).is_err()
680        {
681            errors.push("OPERATOR.toml failed to parse".into());
682        }
683        if let Some(ref s) = self.directives_toml
684            && toml::from_str::<DirectivesConfig>(s).is_err()
685        {
686            errors.push("DIRECTIVES.toml failed to parse".into());
687        }
688        if errors.is_empty() {
689            Ok(())
690        } else {
691            Err(errors)
692        }
693    }
694
695    /// Write all present TOML blocks to the workspace directory.
696    pub fn write_to_workspace(&self, workspace: &Path) -> std::io::Result<()> {
697        std::fs::create_dir_all(workspace)?;
698        if let Some(ref s) = self.os_toml {
699            std::fs::write(workspace.join("OS.toml"), s)?;
700        }
701        if let Some(ref s) = self.firmware_toml {
702            std::fs::write(workspace.join("FIRMWARE.toml"), s)?;
703        }
704        if let Some(ref s) = self.operator_toml {
705            std::fs::write(workspace.join("OPERATOR.toml"), s)?;
706        }
707        if let Some(ref s) = self.directives_toml {
708            std::fs::write(workspace.join("DIRECTIVES.toml"), s)?;
709        }
710        Ok(())
711    }
712
713    pub fn file_count(&self) -> usize {
714        [
715            &self.os_toml,
716            &self.firmware_toml,
717            &self.operator_toml,
718            &self.directives_toml,
719        ]
720        .iter()
721        .filter(|o| o.is_some())
722        .count()
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729
730    #[test]
731    fn parse_default_os() {
732        let os: OsConfig = toml::from_str(DEFAULT_OS_TOML).unwrap();
733        assert_eq!(os.identity.name, "Roboticus");
734        assert_eq!(os.voice.formality, "balanced");
735        assert!(!os.prompt_text.is_empty());
736        assert!(os.prompt_text.contains("iron-plated"));
737    }
738
739    #[test]
740    fn parse_default_firmware() {
741        let fw: FirmwareConfig = toml::from_str(DEFAULT_FIRMWARE_TOML).unwrap();
742        assert_eq!(fw.approvals.spending_threshold, 50.0);
743        assert!(fw.rules.len() >= 7);
744        assert!(fw.rules.iter().any(|r| r.rule_type == "must"));
745        assert!(fw.rules.iter().any(|r| r.rule_type == "must_not"));
746    }
747
748    #[test]
749    fn compose_full_personality_with_all_sections() {
750        let os: OsConfig = toml::from_str(DEFAULT_OS_TOML).unwrap();
751        let fw: FirmwareConfig = toml::from_str(DEFAULT_FIRMWARE_TOML).unwrap();
752        let full = compose_full_personality(Some(&os), Some(&fw), None, None);
753        assert!(full.contains("Roboticus"));
754        assert!(full.contains("YOU MUST:"));
755        assert!(full.contains("YOU MUST NOT:"));
756    }
757
758    #[test]
759    fn compose_full_personality_empty_when_no_files() {
760        let full = compose_full_personality(None, None, None, None);
761        assert!(full.is_empty());
762    }
763
764    #[test]
765    fn generate_os_toml_parses() {
766        let toml_str = generate_os_toml("TestBot", "casual", "initiative", "developer");
767        let os: OsConfig = toml::from_str(&toml_str).unwrap();
768        assert_eq!(os.identity.name, "TestBot");
769        assert_eq!(os.voice.formality, "casual");
770        assert!(os.prompt_text.contains("software development"));
771    }
772
773    #[test]
774    fn generate_firmware_with_custom_boundaries() {
775        let toml_str = generate_firmware_toml("Don't discuss politics\nNo medical advice");
776        let fw: FirmwareConfig = toml::from_str(&toml_str).unwrap();
777        assert!(fw.rules.iter().any(|r| r.rule.contains("politics")));
778        assert!(fw.rules.iter().any(|r| r.rule.contains("medical")));
779    }
780
781    #[test]
782    fn write_defaults_creates_files() {
783        let dir = tempfile::tempdir().unwrap();
784        write_defaults(dir.path()).unwrap();
785        assert!(dir.path().join("OS.toml").exists());
786        assert!(dir.path().join("FIRMWARE.toml").exists());
787    }
788
789    #[test]
790    fn load_roundtrip() {
791        let dir = tempfile::tempdir().unwrap();
792        write_defaults(dir.path()).unwrap();
793        let os = load_os(dir.path()).unwrap();
794        assert_eq!(os.identity.name, "Roboticus");
795        let fw = load_firmware(dir.path()).unwrap();
796        assert!(fw.rules.len() >= 7);
797    }
798
799    #[test]
800    fn compose_full_personality_includes_voice_when_non_default() {
801        let os_toml = r#"
802prompt_text = "I am a test bot."
803
804[identity]
805name = "TestBot"
806
807[voice]
808formality = "formal"
809proactiveness = "initiative"
810verbosity = "concise"
811humor = "dry"
812domain = "developer"
813"#;
814        let os: OsConfig = toml::from_str(os_toml).unwrap();
815        let full = compose_full_personality(Some(&os), None, None, None);
816        assert!(full.contains("I am a test bot."));
817        assert!(full.contains("## Voice Profile"));
818        assert!(full.contains("Formality: formal"));
819        assert!(full.contains("Proactiveness: initiative"));
820        assert!(full.contains("Domain: developer"));
821        // Default fields should not appear
822        assert!(!full.contains("Verbosity:"));
823        assert!(!full.contains("Humor:"));
824    }
825
826    #[test]
827    fn compose_full_personality_skips_voice_when_all_default() {
828        let os: OsConfig = toml::from_str(DEFAULT_OS_TOML).unwrap();
829        // Roboticus has humor = "robotic" which is non-default
830        let full = compose_full_personality(Some(&os), None, None, None);
831        assert!(full.contains("## Voice Profile"));
832        assert!(full.contains("Humor: robotic"));
833    }
834
835    #[test]
836    fn voice_summary_none_when_all_default() {
837        let voice = OsVoice::default();
838        assert!(voice_summary(&voice).is_none());
839    }
840
841    #[test]
842    fn voice_summary_short_format() {
843        let voice = OsVoice {
844            formality: "casual".into(),
845            proactiveness: "initiative".into(),
846            verbosity: "verbose".into(),
847            humor: "witty".into(),
848            domain: "developer".into(),
849        };
850        let short = voice_summary_short(&voice);
851        assert_eq!(short, "casual / initiative / verbose / witty / developer");
852    }
853
854    #[test]
855    fn compose_identity_text_without_firmware() {
856        let os: OsConfig = toml::from_str(DEFAULT_OS_TOML).unwrap();
857        let fw: FirmwareConfig = toml::from_str(DEFAULT_FIRMWARE_TOML).unwrap();
858        let identity = compose_identity_text(Some(&os), None, None);
859        let firmware = compose_firmware_text(Some(&fw));
860        let combined = compose_full_personality(Some(&os), Some(&fw), None, None);
861
862        assert!(identity.contains("Roboticus"));
863        assert!(!identity.contains("YOU MUST"));
864        assert!(firmware.contains("YOU MUST"));
865        assert!(combined.contains("Roboticus"));
866        assert!(combined.contains("YOU MUST"));
867    }
868
869    #[test]
870    fn generate_operator_toml_roundtrip() {
871        let op = OperatorConfig {
872            identity: OperatorIdentity {
873                name: "Jon".into(),
874                role: "Founder".into(),
875                timezone: "US/Pacific".into(),
876            },
877            preferences: OperatorPreferences {
878                communication_channels: vec!["telegram".into(), "discord".into()],
879                work_hours: "9am-6pm".into(),
880                response_style: "concise".into(),
881            },
882            context: "Building an autonomous agent platform.".into(),
883        };
884        let toml_str = generate_operator_toml(&op).unwrap();
885        let parsed: OperatorConfig = toml::from_str(&toml_str).unwrap();
886        assert_eq!(parsed.identity.name, "Jon");
887        assert_eq!(parsed.identity.role, "Founder");
888        assert_eq!(parsed.preferences.communication_channels.len(), 2);
889        assert!(parsed.context.contains("autonomous agent"));
890    }
891
892    #[test]
893    fn generate_directives_toml_roundtrip() {
894        let dir = DirectivesConfig {
895            missions: vec![
896                Mission {
897                    name: "Launch MVP".into(),
898                    timeframe: "Q1 2026".into(),
899                    priority: "high".into(),
900                    description: "Ship the first public version.".into(),
901                },
902                Mission {
903                    name: "Build community".into(),
904                    timeframe: "ongoing".into(),
905                    priority: "medium".into(),
906                    description: "Grow the user base.".into(),
907                },
908            ],
909            context: "Early-stage startup.".into(),
910        };
911        let toml_str = generate_directives_toml(&dir).unwrap();
912        let parsed: DirectivesConfig = toml::from_str(&toml_str).unwrap();
913        assert_eq!(parsed.missions.len(), 2);
914        assert_eq!(parsed.missions[0].name, "Launch MVP");
915        assert_eq!(parsed.missions[1].priority, "medium");
916        assert!(parsed.context.contains("Early-stage"));
917    }
918
919    #[test]
920    fn parse_interview_output_extracts_toml_blocks() {
921        let llm_output = r#"Great, here are your personality files!
922
923**OS.toml**
924
925```toml
926prompt_text = "You are TestBot."
927
928[identity]
929name = "TestBot"
930version = "1.0"
931generated_by = "full-interview"
932
933[voice]
934formality = "casual"
935proactiveness = "suggest"
936verbosity = "concise"
937humor = "dry"
938domain = "general"
939```
940
941**FIRMWARE.toml**
942
943```toml
944[approvals]
945spending_threshold = 100.0
946require_confirmation = "always"
947
948[[rules]]
949type = "must"
950rule = "Be honest"
951```
952
953That's it! Ready to apply?
954"#;
955        let output = parse_interview_output(llm_output);
956        assert_eq!(output.file_count(), 2);
957        assert!(output.os_toml.is_some());
958        assert!(output.firmware_toml.is_some());
959        assert!(output.operator_toml.is_none());
960        assert!(output.directives_toml.is_none());
961        assert!(output.validate().is_ok());
962
963        let os: OsConfig = toml::from_str(output.os_toml.as_ref().unwrap()).unwrap();
964        assert_eq!(os.identity.name, "TestBot");
965        assert_eq!(os.identity.generated_by, "full-interview");
966    }
967
968    #[test]
969    fn parse_interview_output_invalid_toml_fails_validation() {
970        let llm_output = r#"
971**OS.toml**
972
973```toml
974this is not valid toml {{{
975```
976"#;
977        let output = parse_interview_output(llm_output);
978        assert_eq!(output.file_count(), 1);
979        let errors = output.validate().unwrap_err();
980        assert!(errors[0].contains("OS.toml"));
981    }
982
983    #[test]
984    fn interview_output_write_and_reload() {
985        let dir = tempfile::tempdir().unwrap();
986        let output = InterviewOutput {
987            os_toml: Some(DEFAULT_OS_TOML.to_string()),
988            firmware_toml: Some(DEFAULT_FIRMWARE_TOML.to_string()),
989            operator_toml: None,
990            directives_toml: None,
991        };
992        output.write_to_workspace(dir.path()).unwrap();
993        assert!(dir.path().join("OS.toml").exists());
994        assert!(dir.path().join("FIRMWARE.toml").exists());
995        assert!(!dir.path().join("OPERATOR.toml").exists());
996
997        let os = load_os(dir.path()).unwrap();
998        assert_eq!(os.identity.name, "Roboticus");
999    }
1000
1001    #[test]
1002    fn os_voice_default_matches_default_functions() {
1003        let voice = OsVoice::default();
1004        assert_eq!(voice.formality, "balanced");
1005        assert_eq!(voice.proactiveness, "suggest");
1006        assert_eq!(voice.verbosity, "concise");
1007        assert_eq!(voice.humor, "dry");
1008        assert_eq!(voice.domain, "general");
1009    }
1010
1011    #[test]
1012    fn load_returns_none_for_missing_files() {
1013        let dir = tempfile::tempdir().unwrap();
1014        assert!(load_os(dir.path()).is_none());
1015        assert!(load_firmware(dir.path()).is_none());
1016        assert!(load_operator(dir.path()).is_none());
1017        assert!(load_directives(dir.path()).is_none());
1018    }
1019
1020    #[test]
1021    fn load_operator_roundtrip() {
1022        let dir = tempfile::tempdir().unwrap();
1023        let op = OperatorConfig {
1024            identity: OperatorIdentity {
1025                name: "Alice".into(),
1026                role: "Engineer".into(),
1027                timezone: "UTC".into(),
1028            },
1029            preferences: OperatorPreferences::default(),
1030            context: "Works on backend systems.".into(),
1031        };
1032        let toml_str = generate_operator_toml(&op).unwrap();
1033        std::fs::write(dir.path().join("OPERATOR.toml"), &toml_str).unwrap();
1034        let loaded = load_operator(dir.path()).unwrap();
1035        assert_eq!(loaded.identity.name, "Alice");
1036        assert!(loaded.context.contains("backend"));
1037    }
1038
1039    #[test]
1040    fn load_directives_roundtrip() {
1041        let dir = tempfile::tempdir().unwrap();
1042        let directives = DirectivesConfig {
1043            missions: vec![Mission {
1044                name: "Ship v2".into(),
1045                timeframe: "Q2".into(),
1046                priority: "high".into(),
1047                description: "Major release.".into(),
1048            }],
1049            context: "Startup phase.".into(),
1050        };
1051        let toml_str = generate_directives_toml(&directives).unwrap();
1052        std::fs::write(dir.path().join("DIRECTIVES.toml"), &toml_str).unwrap();
1053        let loaded = load_directives(dir.path()).unwrap();
1054        assert_eq!(loaded.missions.len(), 1);
1055        assert_eq!(loaded.missions[0].name, "Ship v2");
1056    }
1057
1058    #[test]
1059    fn generate_os_toml_formal_wait_business() {
1060        let toml_str = generate_os_toml("FormalBot", "formal", "wait", "business");
1061        let os: OsConfig = toml::from_str(&toml_str).unwrap();
1062        assert_eq!(os.identity.name, "FormalBot");
1063        assert!(os.prompt_text.contains("professional, polished tone"));
1064        assert!(os.prompt_text.contains("wait for explicit instructions"));
1065        assert!(os.prompt_text.contains("business operations"));
1066    }
1067
1068    #[test]
1069    fn generate_os_toml_creative_domain() {
1070        let toml_str = generate_os_toml("Artisan", "balanced", "suggest", "creative");
1071        let os: OsConfig = toml::from_str(&toml_str).unwrap();
1072        assert!(os.prompt_text.contains("creative work"));
1073    }
1074
1075    #[test]
1076    fn generate_os_toml_research_domain() {
1077        let toml_str = generate_os_toml("Scholar", "balanced", "suggest", "research");
1078        let os: OsConfig = toml::from_str(&toml_str).unwrap();
1079        assert!(os.prompt_text.contains("research and analysis"));
1080    }
1081
1082    #[test]
1083    fn generate_os_toml_default_branches() {
1084        let toml_str = generate_os_toml("GenBot", "balanced", "suggest", "general");
1085        let os: OsConfig = toml::from_str(&toml_str).unwrap();
1086        assert!(os.prompt_text.contains("general-purpose assistant"));
1087        assert!(os.prompt_text.contains("professional and approachable"));
1088    }
1089
1090    #[test]
1091    fn generate_firmware_toml_empty_boundaries() {
1092        let toml_str = generate_firmware_toml("");
1093        let fw: FirmwareConfig = toml::from_str(&toml_str).unwrap();
1094        assert_eq!(fw.rules.len(), 5);
1095    }
1096
1097    #[test]
1098    fn compose_identity_text_includes_operator_context() {
1099        let op = OperatorConfig {
1100            context: "I run a fintech startup.".into(),
1101            ..OperatorConfig::default()
1102        };
1103        let text = compose_identity_text(None, Some(&op), None);
1104        assert!(text.contains("## Operator Context"));
1105        assert!(text.contains("fintech startup"));
1106    }
1107
1108    #[test]
1109    fn compose_identity_text_includes_directives() {
1110        let dir = DirectivesConfig {
1111            missions: vec![Mission {
1112                name: "Launch".into(),
1113                timeframe: "Q1".into(),
1114                priority: "high".into(),
1115                description: "Ship it.".into(),
1116            }],
1117            context: "Growth phase.".into(),
1118        };
1119        let text = compose_identity_text(None, None, Some(&dir));
1120        assert!(text.contains("## Active Directives"));
1121        assert!(text.contains("Growth phase"));
1122        assert!(text.contains("## Missions"));
1123        assert!(text.contains("**Launch** (Q1): Ship it."));
1124    }
1125
1126    #[test]
1127    fn compose_identity_text_mission_empty_timeframe_shows_ongoing() {
1128        let dir = DirectivesConfig {
1129            missions: vec![Mission {
1130                name: "Maintain".into(),
1131                timeframe: String::new(),
1132                priority: "low".into(),
1133                description: "Keep running.".into(),
1134            }],
1135            context: String::new(),
1136        };
1137        let text = compose_identity_text(None, None, Some(&dir));
1138        assert!(text.contains("(ongoing)"));
1139    }
1140
1141    #[test]
1142    fn compose_full_personality_firmware_only() {
1143        let fw: FirmwareConfig = toml::from_str(DEFAULT_FIRMWARE_TOML).unwrap();
1144        let full = compose_full_personality(None, Some(&fw), None, None);
1145        assert!(full.contains("Firmware"));
1146        assert!(!full.is_empty());
1147    }
1148
1149    #[test]
1150    fn compose_firmware_text_none_returns_empty() {
1151        assert!(compose_firmware_text(None).is_empty());
1152    }
1153
1154    #[test]
1155    fn compose_firmware_text_empty_rules_returns_empty() {
1156        let fw = FirmwareConfig {
1157            approvals: FirmwareApprovals::default(),
1158            rules: vec![],
1159        };
1160        assert!(compose_firmware_text(Some(&fw)).is_empty());
1161    }
1162
1163    #[test]
1164    fn firmware_approvals_default_values() {
1165        let approvals = FirmwareApprovals::default();
1166        assert_eq!(approvals.spending_threshold, 50.0);
1167        assert_eq!(approvals.require_confirmation, "risky");
1168    }
1169
1170    #[test]
1171    fn compose_identity_text_skips_empty_operator_context() {
1172        let op = OperatorConfig {
1173            context: String::new(),
1174            ..OperatorConfig::default()
1175        };
1176        let text = compose_identity_text(None, Some(&op), None);
1177        assert!(!text.contains("## Operator Context"));
1178    }
1179
1180    #[test]
1181    fn compose_identity_text_skips_empty_directives() {
1182        let dir = DirectivesConfig {
1183            missions: vec![],
1184            context: String::new(),
1185        };
1186        let text = compose_identity_text(None, None, Some(&dir));
1187        assert!(text.is_empty());
1188    }
1189
1190    // ── default functions ────────────────────────────────────────────
1191
1192    #[test]
1193    fn default_voice_functions_return_expected() {
1194        assert_eq!(default_version(), "1.0");
1195        assert_eq!(default_generated_by(), "default");
1196        assert_eq!(default_formality(), "balanced");
1197        assert_eq!(default_proactiveness(), "suggest");
1198        assert_eq!(default_verbosity(), "concise");
1199        assert_eq!(default_humor(), "dry");
1200        assert_eq!(default_domain(), "general");
1201    }
1202
1203    #[test]
1204    fn default_firmware_functions_return_expected() {
1205        assert!((default_spending_threshold() - 50.0).abs() < f64::EPSILON);
1206        assert_eq!(default_require_confirmation(), "risky");
1207    }
1208
1209    // ── serde defaults exercised via minimal TOML ────────────────────
1210
1211    #[test]
1212    fn os_identity_serde_defaults() {
1213        let toml_str = r#"
1214prompt_text = "Hello"
1215
1216[identity]
1217name = "Bot"
1218
1219[voice]
1220"#;
1221        let os: OsConfig = toml::from_str(toml_str).unwrap();
1222        assert_eq!(os.identity.version, "1.0");
1223        assert_eq!(os.identity.generated_by, "default");
1224        assert_eq!(os.voice.formality, "balanced");
1225        assert_eq!(os.voice.humor, "dry");
1226    }
1227
1228    #[test]
1229    fn firmware_approvals_serde_defaults() {
1230        let toml_str = r#"
1231[approvals]
1232
1233[[rules]]
1234type = "must"
1235rule = "Test rule"
1236"#;
1237        let fw: FirmwareConfig = toml::from_str(toml_str).unwrap();
1238        assert!((fw.approvals.spending_threshold - 50.0).abs() < f64::EPSILON);
1239        assert_eq!(fw.approvals.require_confirmation, "risky");
1240    }
1241
1242    // ── parse_interview_output: all four blocks ──────────────────────
1243
1244    #[test]
1245    fn parse_interview_output_all_four_blocks() {
1246        let llm_output = r#"Here are your files:
1247
1248**OS.toml**
1249
1250```toml
1251prompt_text = "You are TestBot."
1252
1253[identity]
1254name = "TestBot"
1255
1256[voice]
1257formality = "casual"
1258```
1259
1260**FIRMWARE.toml**
1261
1262```toml
1263[approvals]
1264spending_threshold = 100.0
1265require_confirmation = "always"
1266
1267[[rules]]
1268type = "must"
1269rule = "Be honest"
1270```
1271
1272**OPERATOR.toml**
1273
1274```toml
1275context = "Works on infrastructure."
1276
1277[identity]
1278name = "Alice"
1279role = "SRE"
1280timezone = "UTC"
1281
1282[preferences]
1283work_hours = "9-5"
1284response_style = "terse"
1285```
1286
1287**DIRECTIVES.toml**
1288
1289```toml
1290context = "Q1 focus."
1291
1292[[missions]]
1293name = "Ship v2"
1294timeframe = "Q1"
1295priority = "high"
1296description = "Major release."
1297```
1298"#;
1299        let output = parse_interview_output(llm_output);
1300        assert_eq!(output.file_count(), 4);
1301        assert!(output.os_toml.is_some());
1302        assert!(output.firmware_toml.is_some());
1303        assert!(output.operator_toml.is_some());
1304        assert!(output.directives_toml.is_some());
1305        assert!(output.validate().is_ok());
1306
1307        let op: OperatorConfig = toml::from_str(output.operator_toml.as_ref().unwrap()).unwrap();
1308        assert_eq!(op.identity.name, "Alice");
1309
1310        let dir: DirectivesConfig =
1311            toml::from_str(output.directives_toml.as_ref().unwrap()).unwrap();
1312        assert_eq!(dir.missions[0].name, "Ship v2");
1313    }
1314
1315    // ── parse_interview_output: alternative fence pattern ────────────
1316
1317    #[test]
1318    fn parse_interview_output_alternative_fence() {
1319        // The parser also accepts ``` followed by text containing "toml"
1320        let llm_output = r#"
1321**OS.toml**
1322
1323```language=toml
1324prompt_text = "Hello."
1325
1326[identity]
1327name = "AltBot"
1328
1329[voice]
1330```
1331"#;
1332        let output = parse_interview_output(llm_output);
1333        assert!(output.os_toml.is_some());
1334        let os: OsConfig = toml::from_str(output.os_toml.as_ref().unwrap()).unwrap();
1335        assert_eq!(os.identity.name, "AltBot");
1336    }
1337
1338    // ── validate() with invalid operator/directives ─────────────────
1339
1340    #[test]
1341    fn validate_invalid_operator_toml() {
1342        let output = InterviewOutput {
1343            os_toml: None,
1344            firmware_toml: None,
1345            operator_toml: Some("{{invalid operator}}".into()),
1346            directives_toml: None,
1347        };
1348        let errors = output.validate().unwrap_err();
1349        assert!(errors.iter().any(|e| e.contains("OPERATOR.toml")));
1350    }
1351
1352    #[test]
1353    fn validate_invalid_directives_toml() {
1354        let output = InterviewOutput {
1355            os_toml: None,
1356            firmware_toml: None,
1357            operator_toml: None,
1358            directives_toml: Some("{{invalid directives}}".into()),
1359        };
1360        let errors = output.validate().unwrap_err();
1361        assert!(errors.iter().any(|e| e.contains("DIRECTIVES.toml")));
1362    }
1363
1364    #[test]
1365    fn validate_invalid_firmware_toml() {
1366        let output = InterviewOutput {
1367            os_toml: None,
1368            firmware_toml: Some("{{invalid firmware}}".into()),
1369            operator_toml: None,
1370            directives_toml: None,
1371        };
1372        let errors = output.validate().unwrap_err();
1373        assert!(errors.iter().any(|e| e.contains("FIRMWARE.toml")));
1374    }
1375
1376    #[test]
1377    fn validate_multiple_errors() {
1378        let output = InterviewOutput {
1379            os_toml: Some("{{bad}}".into()),
1380            firmware_toml: Some("{{bad}}".into()),
1381            operator_toml: Some("{{bad}}".into()),
1382            directives_toml: Some("{{bad}}".into()),
1383        };
1384        let errors = output.validate().unwrap_err();
1385        assert_eq!(errors.len(), 4);
1386    }
1387
1388    // ── write_to_workspace with operator/directives ─────────────────
1389
1390    #[test]
1391    fn write_to_workspace_all_four_files() {
1392        let dir = tempfile::tempdir().unwrap();
1393        let op = OperatorConfig {
1394            identity: OperatorIdentity {
1395                name: "Bob".into(),
1396                role: "Dev".into(),
1397                timezone: "UTC".into(),
1398            },
1399            preferences: OperatorPreferences::default(),
1400            context: "Testing.".into(),
1401        };
1402        let directives = DirectivesConfig {
1403            missions: vec![Mission {
1404                name: "Test".into(),
1405                timeframe: "Q1".into(),
1406                priority: "high".into(),
1407                description: "A test mission.".into(),
1408            }],
1409            context: "Test context.".into(),
1410        };
1411        let output = InterviewOutput {
1412            os_toml: Some(DEFAULT_OS_TOML.into()),
1413            firmware_toml: Some(DEFAULT_FIRMWARE_TOML.into()),
1414            operator_toml: Some(generate_operator_toml(&op).unwrap()),
1415            directives_toml: Some(generate_directives_toml(&directives).unwrap()),
1416        };
1417        output.write_to_workspace(dir.path()).unwrap();
1418        assert!(dir.path().join("OS.toml").exists());
1419        assert!(dir.path().join("FIRMWARE.toml").exists());
1420        assert!(dir.path().join("OPERATOR.toml").exists());
1421        assert!(dir.path().join("DIRECTIVES.toml").exists());
1422
1423        let loaded_op = load_operator(dir.path()).unwrap();
1424        assert_eq!(loaded_op.identity.name, "Bob");
1425
1426        let loaded_dir = load_directives(dir.path()).unwrap();
1427        assert_eq!(loaded_dir.missions[0].name, "Test");
1428    }
1429
1430    // ── generate_firmware_toml edge cases ────────────────────────────
1431
1432    #[test]
1433    fn generate_firmware_toml_boundaries_with_empty_lines() {
1434        let boundaries = "\nDo not hack\n\n\nNo spam\n";
1435        let toml_str = generate_firmware_toml(boundaries);
1436        let fw: FirmwareConfig = toml::from_str(&toml_str).unwrap();
1437        // 5 default rules + 2 custom = 7
1438        assert_eq!(fw.rules.len(), 7);
1439        assert!(fw.rules.iter().any(|r| r.rule.contains("hack")));
1440        assert!(fw.rules.iter().any(|r| r.rule.contains("spam")));
1441    }
1442
1443    #[test]
1444    fn generate_firmware_toml_boundaries_with_quotes() {
1445        let boundaries = r#"Don't say "hello" to strangers"#;
1446        let toml_str = generate_firmware_toml(boundaries);
1447        let fw: FirmwareConfig = toml::from_str(&toml_str).unwrap();
1448        assert_eq!(fw.rules.len(), 6);
1449    }
1450
1451    // ── compose_full_personality with operator + directives ──────────
1452
1453    #[test]
1454    fn compose_full_personality_with_operator_and_directives() {
1455        let os: OsConfig = toml::from_str(DEFAULT_OS_TOML).unwrap();
1456        let fw: FirmwareConfig = toml::from_str(DEFAULT_FIRMWARE_TOML).unwrap();
1457        let op = OperatorConfig {
1458            context: "I build robots.".into(),
1459            ..OperatorConfig::default()
1460        };
1461        let dir = DirectivesConfig {
1462            missions: vec![Mission {
1463                name: "Deploy".into(),
1464                timeframe: "".into(),
1465                priority: "high".into(),
1466                description: "Deploy the app.".into(),
1467            }],
1468            context: "Production push.".into(),
1469        };
1470        let full = compose_full_personality(Some(&os), Some(&fw), Some(&op), Some(&dir));
1471        assert!(full.contains("Roboticus"));
1472        assert!(full.contains("YOU MUST"));
1473        assert!(full.contains("## Operator Context"));
1474        assert!(full.contains("I build robots"));
1475        assert!(full.contains("## Active Directives"));
1476        assert!(full.contains("Production push"));
1477        assert!(full.contains("## Missions"));
1478        assert!(full.contains("**Deploy** (ongoing)"));
1479    }
1480
1481    // ── compose_identity_text: prompt_text empty branch ──────────────
1482
1483    #[test]
1484    fn compose_identity_text_skips_empty_prompt_text() {
1485        let os_toml = r#"
1486prompt_text = ""
1487
1488[identity]
1489name = "EmptyBot"
1490
1491[voice]
1492formality = "formal"
1493"#;
1494        let os: OsConfig = toml::from_str(os_toml).unwrap();
1495        let text = compose_identity_text(Some(&os), None, None);
1496        // Should have voice profile but not the empty prompt text
1497        assert!(text.contains("## Voice Profile"));
1498        assert!(!text.contains("EmptyBot"));
1499    }
1500
1501    // ── file_count edge cases ────────────────────────────────────────
1502
1503    #[test]
1504    fn file_count_zero_for_default() {
1505        let output = InterviewOutput::default();
1506        assert_eq!(output.file_count(), 0);
1507    }
1508
1509    #[test]
1510    fn file_count_three() {
1511        let output = InterviewOutput {
1512            os_toml: Some("x".into()),
1513            firmware_toml: None,
1514            operator_toml: Some("y".into()),
1515            directives_toml: Some("z".into()),
1516        };
1517        assert_eq!(output.file_count(), 3);
1518    }
1519
1520    // ── OperatorConfig / DirectivesConfig defaults ───────────────────
1521
1522    #[test]
1523    fn operator_config_default_is_empty() {
1524        let op = OperatorConfig::default();
1525        assert!(op.identity.name.is_empty());
1526        assert!(op.identity.role.is_empty());
1527        assert!(op.identity.timezone.is_empty());
1528        assert!(op.preferences.communication_channels.is_empty());
1529        assert!(op.preferences.work_hours.is_empty());
1530        assert!(op.preferences.response_style.is_empty());
1531        assert!(op.context.is_empty());
1532    }
1533
1534    #[test]
1535    fn directives_config_default_is_empty() {
1536        let dir = DirectivesConfig::default();
1537        assert!(dir.missions.is_empty());
1538        assert!(dir.context.is_empty());
1539    }
1540
1541    // ── parse_interview_output: no blocks at all ─────────────────────
1542
1543    #[test]
1544    fn parse_interview_output_no_blocks_returns_empty() {
1545        let output = parse_interview_output("Just some text without any TOML blocks.");
1546        assert_eq!(output.file_count(), 0);
1547        assert!(output.validate().is_ok());
1548    }
1549
1550    // ── parse_interview_output: unknown label is ignored ─────────────
1551
1552    #[test]
1553    fn parse_interview_output_unknown_label_ignored() {
1554        let llm_output = r#"
1555**RANDOM.toml**
1556
1557```toml
1558key = "value"
1559```
1560"#;
1561        let output = parse_interview_output(llm_output);
1562        assert_eq!(output.file_count(), 0);
1563    }
1564
1565    // ── load functions with invalid TOML files ───────────────────────
1566
1567    #[test]
1568    fn load_os_returns_none_for_invalid_toml() {
1569        let dir = tempfile::tempdir().unwrap();
1570        std::fs::write(dir.path().join("OS.toml"), "{{invalid}}").unwrap();
1571        assert!(load_os(dir.path()).is_none());
1572    }
1573
1574    #[test]
1575    fn load_firmware_returns_none_for_invalid_toml() {
1576        let dir = tempfile::tempdir().unwrap();
1577        std::fs::write(dir.path().join("FIRMWARE.toml"), "{{invalid}}").unwrap();
1578        assert!(load_firmware(dir.path()).is_none());
1579    }
1580
1581    #[test]
1582    fn load_operator_returns_none_for_invalid_toml() {
1583        let dir = tempfile::tempdir().unwrap();
1584        std::fs::write(dir.path().join("OPERATOR.toml"), "{{invalid}}").unwrap();
1585        assert!(load_operator(dir.path()).is_none());
1586    }
1587
1588    #[test]
1589    fn load_directives_returns_none_for_invalid_toml() {
1590        let dir = tempfile::tempdir().unwrap();
1591        std::fs::write(dir.path().join("DIRECTIVES.toml"), "{{invalid}}").unwrap();
1592        assert!(load_directives(dir.path()).is_none());
1593    }
1594
1595    // ── voice_summary: individual field coverage ─────────────────────
1596
1597    #[test]
1598    fn voice_summary_each_non_default_field() {
1599        // Only proactiveness is non-default
1600        let voice = OsVoice {
1601            proactiveness: "initiative".into(),
1602            ..OsVoice::default()
1603        };
1604        let summary = voice_summary(&voice).unwrap();
1605        assert!(summary.contains("Proactiveness: initiative"));
1606        assert!(!summary.contains("Formality:"));
1607
1608        // Only verbosity is non-default
1609        let voice = OsVoice {
1610            verbosity: "verbose".into(),
1611            ..OsVoice::default()
1612        };
1613        let summary = voice_summary(&voice).unwrap();
1614        assert!(summary.contains("Verbosity: verbose"));
1615
1616        // Only humor is non-default
1617        let voice = OsVoice {
1618            humor: "witty".into(),
1619            ..OsVoice::default()
1620        };
1621        let summary = voice_summary(&voice).unwrap();
1622        assert!(summary.contains("Humor: witty"));
1623
1624        // Only domain is non-default
1625        let voice = OsVoice {
1626            domain: "security".into(),
1627            ..OsVoice::default()
1628        };
1629        let summary = voice_summary(&voice).unwrap();
1630        assert!(summary.contains("Domain: security"));
1631    }
1632}