Skip to main content

cersei_agent/
system_prompt.rs

1//! Modular system prompt assembly with conditional components.
2//!
3//! The system prompt is built from named components, each with an inclusion rule.
4//! Static components go before `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` (cacheable).
5//! Dynamic components go after (recomputed each turn).
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{Mutex, OnceLock};
10
11// ─── Dynamic boundary marker ────────────────────────────────────────────────
12
13pub const SYSTEM_PROMPT_DYNAMIC_BOUNDARY: &str = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__";
14
15// ─── Section cache ──────────────────────────────────────────────────────────
16
17fn section_cache() -> &'static Mutex<HashMap<String, Option<String>>> {
18    static CACHE: OnceLock<Mutex<HashMap<String, Option<String>>>> = OnceLock::new();
19    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
20}
21
22pub fn clear_system_prompt_sections() {
23    if let Ok(mut cache) = section_cache().lock() {
24        cache.clear();
25    }
26}
27
28// ─── Section type (kept for backward compat) ────────────────────────────────
29
30#[derive(Debug, Clone)]
31pub struct SystemPromptSection {
32    pub tag: String,
33    pub content: Option<String>,
34    pub cache_break: bool,
35}
36
37impl SystemPromptSection {
38    pub fn cached(tag: impl Into<String>, content: impl Into<String>) -> Self {
39        Self {
40            tag: tag.into(),
41            content: Some(content.into()),
42            cache_break: false,
43        }
44    }
45    pub fn uncached(tag: impl Into<String>, content: Option<String>) -> Self {
46        Self {
47            tag: tag.into(),
48            content,
49            cache_break: true,
50        }
51    }
52}
53
54// ─── Output style ───────────────────────────────────────────────────────────
55
56#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
57#[serde(rename_all = "lowercase")]
58pub enum OutputStyle {
59    #[default]
60    Default,
61    Explanatory,
62    Learning,
63    Concise,
64    Formal,
65    Casual,
66}
67
68impl OutputStyle {
69    pub fn prompt_suffix(self) -> Option<&'static str> {
70        match self {
71            Self::Explanatory => Some("When explaining code or concepts, be thorough and educational. Include reasoning, alternatives considered, and potential pitfalls. Err on the side of over-explaining."),
72            Self::Learning => Some("This user is learning. Explain concepts as you implement them. Point out patterns, best practices, and why you made each decision. Use analogies when helpful."),
73            Self::Concise => Some("Be maximally concise. Skip preamble, summaries, and filler. Lead with the answer. One sentence is better than three."),
74            Self::Formal => Some("Maintain a formal, professional tone. Use precise technical language."),
75            Self::Casual => Some("Use a casual, conversational tone."),
76            Self::Default => None,
77        }
78    }
79
80    pub fn from_str(s: &str) -> Self {
81        match s.to_lowercase().as_str() {
82            "explanatory" => Self::Explanatory,
83            "learning" => Self::Learning,
84            "concise" => Self::Concise,
85            "formal" => Self::Formal,
86            "casual" => Self::Casual,
87            _ => Self::Default,
88        }
89    }
90}
91
92// ─── Prefix ─────────────────────────────────────────────────────────────────
93
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum SystemPromptPrefix {
96    Interactive,
97    Sdk,
98    SdkPreset,
99    SubAgent,
100}
101
102impl SystemPromptPrefix {
103    pub fn detect(is_non_interactive: bool, has_append_system_prompt: bool) -> Self {
104        if is_non_interactive {
105            if has_append_system_prompt {
106                return Self::SdkPreset;
107            }
108            return Self::Sdk;
109        }
110        Self::Interactive
111    }
112
113    pub fn attribution_text(self) -> &'static str {
114        match self {
115            Self::Interactive => "You are a coding agent built with the Cersei SDK.",
116            Self::SdkPreset => "You are a coding agent built with the Cersei SDK, running with custom instructions.",
117            Self::Sdk => "You are an agent built on the Cersei SDK.",
118            Self::SubAgent => "You are a specialized sub-agent.",
119        }
120    }
121}
122
123// ─── Git snapshot ───────────────────────────────────────────────────────────
124
125/// Pre-computed git repository information for the system prompt.
126#[derive(Debug, Clone, Default)]
127pub struct GitSnapshot {
128    pub branch: String,
129    pub recent_commits: Vec<String>,
130    pub status_lines: Vec<String>,
131    pub user: Option<String>,
132}
133
134// ─── Build options ──────────────────────────────────────────────────────────
135
136#[derive(Debug, Clone, Default)]
137pub struct SystemPromptOptions {
138    // ── Existing fields ──
139    pub prefix: Option<SystemPromptPrefix>,
140    pub is_non_interactive: bool,
141    pub has_append_system_prompt: bool,
142    pub output_style: OutputStyle,
143    pub custom_output_style_prompt: Option<String>,
144    pub working_directory: Option<String>,
145    pub memory_content: String,
146    pub custom_system_prompt: Option<String>,
147    pub append_system_prompt: Option<String>,
148    pub replace_system_prompt: bool,
149    pub coordinator_mode: bool,
150    pub extra_cached_sections: Vec<(String, String)>,
151    pub extra_dynamic_sections: Vec<(String, String)>,
152
153    // ── New fields for conditional components ──
154    /// Tool names available in the agent's tool list (for conditional guidance).
155    pub tools_available: Vec<String>,
156    /// Whether a memory backend is configured.
157    pub has_memory: bool,
158    /// Whether auto-compact is enabled.
159    pub has_auto_compact: bool,
160    /// Pre-computed git repository snapshot.
161    pub git_status: Option<GitSnapshot>,
162    /// Per-MCP-server instructions: (server_name, instructions).
163    pub mcp_instructions: Vec<(String, String)>,
164    /// Preferred response language (e.g., "Japanese").
165    pub language: Option<String>,
166}
167
168// ─── Main assembly ──────────────────────────────────────────────────────────
169
170pub fn build_system_prompt(opts: &SystemPromptOptions) -> String {
171    // Replace mode
172    if opts.replace_system_prompt {
173        if let Some(custom) = &opts.custom_system_prompt {
174            return format!("{}\n\n{}", custom, SYSTEM_PROMPT_DYNAMIC_BOUNDARY);
175        }
176    }
177
178    let prefix = opts.prefix.unwrap_or_else(|| {
179        SystemPromptPrefix::detect(opts.is_non_interactive, opts.has_append_system_prompt)
180    });
181
182    let mut parts: Vec<String> = Vec::new();
183
184    // ── CACHEABLE sections ──────────────────────────────────────────────
185
186    // 1. Attribution
187    parts.push(prefix.attribution_text().to_string());
188
189    // 2. Core capabilities
190    parts.push(CORE_CAPABILITIES.to_string());
191
192    // 3. Tool use guidelines
193    parts.push(TOOL_USE_GUIDELINES.to_string());
194
195    // 4. Actions with care
196    parts.push(ACTIONS_SECTION.to_string());
197
198    // 5. Safety
199    parts.push(SAFETY_GUIDELINES.to_string());
200
201    // 6. Security
202    parts.push(SECURITY_SECTION.to_string());
203
204    // 7. Output efficiency
205    parts.push(OUTPUT_EFFICIENCY.to_string());
206
207    // 8. Summarize tool results
208    parts.push(SUMMARIZE_TOOL_RESULTS.to_string());
209
210    // 9. Output style
211    if let Some(style_text) = opts
212        .custom_output_style_prompt
213        .as_deref()
214        .filter(|s| !s.trim().is_empty())
215        .or_else(|| opts.output_style.prompt_suffix())
216    {
217        parts.push(format!("\n## Output Style\n{}", style_text));
218    }
219
220    // 10. Coordinator mode
221    if opts.coordinator_mode {
222        parts.push(COORDINATOR_SECTION.to_string());
223    }
224
225    // 11. Session guidance: Agent tool
226    if opts
227        .tools_available
228        .iter()
229        .any(|t| t == "Agent" || t == "TaskCreate")
230    {
231        parts.push(SESSION_AGENT_GUIDANCE.to_string());
232    }
233
234    // 12. Session guidance: Skills
235    if opts.tools_available.iter().any(|t| t == "Skill") {
236        parts.push(SESSION_SKILLS_GUIDANCE.to_string());
237    }
238
239    // 13. Session guidance: Memory
240    if opts.has_memory {
241        parts.push(SESSION_MEMORY_GUIDANCE.to_string());
242    }
243
244    // 14. Function result clearing warning
245    if opts.has_auto_compact {
246        parts.push(FUNCTION_RESULT_CLEARING.to_string());
247    }
248
249    // 15. Language preference
250    if let Some(lang) = &opts.language {
251        parts.push(format!(
252            "\n## Language\nAlways respond in {lang}. Use {lang} for all explanations, comments, and communications. Technical terms and code identifiers should remain in their original form."
253        ));
254    }
255
256    // 16. Custom system prompt
257    if let Some(custom) = &opts.custom_system_prompt {
258        parts.push(format!(
259            "\n<custom_instructions>\n{}\n</custom_instructions>",
260            custom
261        ));
262    }
263
264    // 17. Extra cached sections
265    for (tag, content) in &opts.extra_cached_sections {
266        parts.push(format!("\n<{}>\n{}\n</{}>", tag, content, tag));
267    }
268
269    // ── BOUNDARY ────────────────────────────────────────────────────────
270    parts.push(SYSTEM_PROMPT_DYNAMIC_BOUNDARY.to_string());
271
272    // ── DYNAMIC sections ────────────────────────────────────────────────
273
274    // 18. Working directory
275    if let Some(cwd) = &opts.working_directory {
276        parts.push(format!("\n<working_directory>{}</working_directory>", cwd));
277    }
278
279    // 19. Git status snapshot
280    if let Some(git) = &opts.git_status {
281        let mut git_section = format!("\n<git_status>\nBranch: {}", git.branch);
282        if let Some(user) = &git.user {
283            git_section.push_str(&format!("\nUser: {}", user));
284        }
285        if !git.status_lines.is_empty() {
286            git_section.push_str("\nStatus:");
287            for line in &git.status_lines {
288                git_section.push_str(&format!("\n  {}", line));
289            }
290        }
291        if !git.recent_commits.is_empty() {
292            git_section.push_str("\nRecent commits:");
293            for commit in &git.recent_commits {
294                git_section.push_str(&format!("\n  {}", commit));
295            }
296        }
297        git_section.push_str("\n</git_status>");
298        parts.push(git_section);
299    }
300
301    // 20. Memory
302    if !opts.memory_content.is_empty() {
303        parts.push(format!("\n<memory>\n{}\n</memory>", opts.memory_content));
304    }
305
306    // 21. MCP server instructions
307    if !opts.mcp_instructions.is_empty() {
308        let mut mcp_section = String::from("\n<mcp_instructions>");
309        for (name, instructions) in &opts.mcp_instructions {
310            mcp_section.push_str(&format!("\n## {}\n{}", name, instructions));
311        }
312        mcp_section.push_str("\n</mcp_instructions>");
313        parts.push(mcp_section);
314    }
315
316    // 22. Extra dynamic sections
317    for (tag, content) in &opts.extra_dynamic_sections {
318        parts.push(format!("\n<{}>\n{}\n</{}>", tag, content, tag));
319    }
320
321    // 23. Appended system prompt
322    if let Some(append) = &opts.append_system_prompt {
323        parts.push(format!("\n{}", append));
324    }
325
326    parts.join("\n")
327}
328
329// ─── Static sections ────────────────────────────────────────────────────────
330
331const CORE_CAPABILITIES: &str = r#"
332## Capabilities
333
334You have access to powerful tools for software engineering tasks:
335- **Read/Write files**: Read any file, write new files, edit existing files with precise diffs
336- **Execute commands**: Run bash commands, PowerShell scripts, background processes
337- **Search**: Glob patterns, regex grep, web search, file content search
338- **LSP**: Language server queries for hover, go-to-definition, references, symbols, diagnostics
339- **Web**: Fetch URLs, search the internet
340- **Agents**: Spawn parallel sub-agents for complex multi-step work
341- **Memory**: Persistent notes across sessions via the memory system
342- **MCP servers**: Connect to external tools and APIs via Model Context Protocol
343- **Jupyter notebooks**: Read and edit notebook cells
344
345## Task Management
346
347You have access to the TodoWrite tool to help you manage and plan tasks. Use this tool VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
348This tool is also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
349
350It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.
351
352IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
353
354## How to approach tasks
355
356The user will primarily request you perform software engineering tasks. For these tasks:
357- NEVER propose changes to code you haven't read. Read first, then modify.
358- Use the TodoWrite tool to plan the task if required.
359- Be careful not to introduce security vulnerabilities.
360- Avoid over-engineering. Only make changes that are directly requested or clearly necessary.
361- Don't add features, refactor code, or make improvements beyond what was asked.
362- ALWAYS verify information about the codebase using tools before answering. Never rely solely on general knowledge or assumptions about how code works.
363
364## Tool usage policy
365
366- When doing file search or research, prefer using Bash (with grep, find) or Grep tool for targeted searches.
367- When you need information you don't have, use WebSearch to find it. Do not guess APIs, node types, or library details — search for the current documentation.
368- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency.
369- If the user specifies running tools in parallel, you MUST send a single response with multiple tool calls.
370- Use specialized tools instead of bash when possible: Read for reading files, Edit for editing, Glob for finding files, Grep for searching content.
371"#;
372
373const TOOL_USE_GUIDELINES: &str = r#"
374## Tool use guidelines
375
376- Use dedicated tools (Read, Edit, Glob, Grep, LSP) instead of bash equivalents
377- For searches, prefer Grep over `grep`; prefer Glob over `find`
378- For file edits: always read the file first, then make targeted edits
379- Bash commands timeout after 2 minutes; use background mode for long operations
380- Use Glob for targeted patterns (`src/**/*.rs`), never glob `**/*` at root
381- Use LSP tool for semantic understanding: symbols, definitions, references, diagnostics
382- Write down key findings in your response — tool results may be cleared from context later
383- Old tool results are automatically cleared to free space. Summarize important information.
384"#;
385
386const ACTIONS_SECTION: &str = r#"
387## Executing actions with care
388
389Carefully consider the reversibility and blast radius of actions. For actions
390that are hard to reverse, affect shared systems, or could be risky or
391destructive, check with the user before proceeding. Authorization stands for
392the scope specified, not beyond. Match the scope of your actions to what was
393actually requested.
394"#;
395
396const SAFETY_GUIDELINES: &str = r#"
397## Safety guidelines
398
399- Never delete files without explicit user confirmation
400- Don't modify protected files (.gitconfig, .bashrc, .zshrc)
401- Be careful with destructive operations (rm -rf, DROP TABLE, etc.)
402- Don't commit secrets, credentials, or API keys
403- For ambiguous destructive actions, ask before proceeding
404"#;
405
406const SECURITY_SECTION: &str = r#"
407## Security
408
409You are authorized to assist with security research, CTF challenges, penetration testing
410with explicit authorization, defensive security, and educational security content. Do not
411assist with creating malware, unauthorized access, denial-of-service attacks, or any
412destructive security techniques without clear legitimate purpose.
413"#;
414
415const OUTPUT_EFFICIENCY: &str = r#"
416## Output efficiency
417
418Be direct and informative. Lead with the answer, not the reasoning.
419- For analysis/explanation: Be thorough and structured. Use tables, lists, and sections.
420- For code changes: Be concise. Show what changed and why.
421- For status updates: One sentence is enough.
422- Never ask "would you like me to investigate more?" — just investigate.
423- Never stop at surface-level answers when deeper investigation would give better results.
424- Use multiple tool calls in a single response to gather evidence in parallel.
425"#;
426
427const SUMMARIZE_TOOL_RESULTS: &str = r#"
428## Tool results
429
430When working with tool results, write down any important information you might need later
431in your response, as the original tool result may be cleared from context later.
432"#;
433
434const COORDINATOR_SECTION: &str = r#"
435## Coordinator Mode
436
437You are operating as an orchestrator. Spawn parallel worker agents using the Agent tool.
438Each worker prompt must be fully self-contained. Synthesize findings before delegating
439follow-up work. Use TaskCreate/TaskUpdate to track parallel work.
440"#;
441
442// ─── Conditional sections ───────────────────────────────────────────────────
443
444const SESSION_AGENT_GUIDANCE: &str = r#"
445## Sub-agents
446
447Use the Agent tool for complex multi-step tasks that benefit from parallel work or
448deep research. Each sub-agent runs independently with its own context window.
449- Launch multiple agents in parallel when tasks are independent
450- Provide each agent with a complete, self-contained prompt
451- The agent's output is not visible to the user — summarize results yourself
452- Use TaskCreate/TaskUpdate to track background work
453"#;
454
455const SESSION_SKILLS_GUIDANCE: &str = r#"
456## Skills
457
458/<skill-name> (e.g., /commit) invokes a skill — a reusable prompt template.
459Skills are loaded from .claude/commands/*.md, .claude/skills/*/SKILL.md, or bundled.
460Use the Skill tool to execute them. Only use skills that are listed as available.
461"#;
462
463const SESSION_MEMORY_GUIDANCE: &str = r#"
464## Persistent memory
465
466You have access to persistent memory across sessions. Memory files survive across
467conversations and are injected into your context automatically.
468- Store facts about the user's preferences, project decisions, and recurring patterns
469- Before recommending from memory, verify that files and functions still exist
470- Memory records can become stale — if a recalled memory conflicts with current code, trust what you observe now
471"#;
472
473const FUNCTION_RESULT_CLEARING: &str = r#"
474## Context management
475
476Old tool results will be automatically summarized to free context space when the
477conversation grows long. The most recent results are always kept. Write down any
478important information from tool results in your response text — the originals may
479be cleared in future turns.
480"#;
481
482// ─── Tests ──────────────────────────────────────────────────────────────────
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    fn default_opts() -> SystemPromptOptions {
489        SystemPromptOptions::default()
490    }
491
492    #[test]
493    fn test_default_prompt_contains_boundary() {
494        let prompt = build_system_prompt(&default_opts());
495        assert!(prompt.contains(SYSTEM_PROMPT_DYNAMIC_BOUNDARY));
496    }
497
498    #[test]
499    fn test_default_prompt_contains_attribution() {
500        let prompt = build_system_prompt(&default_opts());
501        assert!(prompt.contains("Cersei SDK"));
502    }
503
504    #[test]
505    fn test_replace_system_prompt() {
506        let opts = SystemPromptOptions {
507            custom_system_prompt: Some("Custom only.".to_string()),
508            replace_system_prompt: true,
509            ..Default::default()
510        };
511        let prompt = build_system_prompt(&opts);
512        assert!(prompt.starts_with("Custom only."));
513        assert!(!prompt.contains("Capabilities"));
514        assert!(prompt.contains(SYSTEM_PROMPT_DYNAMIC_BOUNDARY));
515    }
516
517    #[test]
518    fn test_working_directory_in_dynamic_section() {
519        let opts = SystemPromptOptions {
520            working_directory: Some("/home/user/project".to_string()),
521            ..Default::default()
522        };
523        let prompt = build_system_prompt(&opts);
524        let boundary_pos = prompt.find(SYSTEM_PROMPT_DYNAMIC_BOUNDARY).unwrap();
525        let cwd_pos = prompt.find("/home/user/project").unwrap();
526        assert!(cwd_pos > boundary_pos);
527    }
528
529    #[test]
530    fn test_memory_content_in_dynamic_section() {
531        let opts = SystemPromptOptions {
532            memory_content: "- [test.md](test.md) -- a test memory".to_string(),
533            ..Default::default()
534        };
535        let prompt = build_system_prompt(&opts);
536        let boundary_pos = prompt.find(SYSTEM_PROMPT_DYNAMIC_BOUNDARY).unwrap();
537        let mem_pos = prompt.find("test.md").unwrap();
538        assert!(mem_pos > boundary_pos);
539    }
540
541    #[test]
542    fn test_output_style_concise() {
543        let opts = SystemPromptOptions {
544            output_style: OutputStyle::Concise,
545            ..Default::default()
546        };
547        let prompt = build_system_prompt(&opts);
548        assert!(prompt.contains("maximally concise"));
549    }
550
551    #[test]
552    fn test_output_style_default_no_suffix() {
553        let prompt = build_system_prompt(&default_opts());
554        assert!(!prompt.contains("maximally concise"));
555        assert!(!prompt.contains("This user is learning"));
556    }
557
558    #[test]
559    fn test_coordinator_mode() {
560        let opts = SystemPromptOptions {
561            coordinator_mode: true,
562            ..Default::default()
563        };
564        let prompt = build_system_prompt(&opts);
565        assert!(prompt.contains("Coordinator Mode"));
566        assert!(prompt.contains("orchestrator"));
567    }
568
569    #[test]
570    fn test_output_style_from_str() {
571        assert_eq!(OutputStyle::from_str("concise"), OutputStyle::Concise);
572        assert_eq!(OutputStyle::from_str("FORMAL"), OutputStyle::Formal);
573        assert_eq!(OutputStyle::from_str("unknown"), OutputStyle::Default);
574    }
575
576    #[test]
577    fn test_sdk_prefix() {
578        let prefix = SystemPromptPrefix::detect(true, false);
579        assert_eq!(prefix, SystemPromptPrefix::Sdk);
580    }
581
582    #[test]
583    fn test_sdk_preset_prefix() {
584        let prefix = SystemPromptPrefix::detect(true, true);
585        assert_eq!(prefix, SystemPromptPrefix::SdkPreset);
586    }
587
588    #[test]
589    fn test_extra_sections() {
590        let opts = SystemPromptOptions {
591            extra_cached_sections: vec![("rules".into(), "no swearing".into())],
592            extra_dynamic_sections: vec![("context".into(), "today is Monday".into())],
593            ..Default::default()
594        };
595        let prompt = build_system_prompt(&opts);
596        let boundary = prompt.find(SYSTEM_PROMPT_DYNAMIC_BOUNDARY).unwrap();
597        let rules_pos = prompt.find("no swearing").unwrap();
598        let context_pos = prompt.find("today is Monday").unwrap();
599        assert!(rules_pos < boundary);
600        assert!(context_pos > boundary);
601    }
602
603    #[test]
604    fn test_clear_section_cache() {
605        {
606            let mut cache = section_cache().lock().unwrap();
607            cache.insert("test".to_string(), Some("content".to_string()));
608        }
609        clear_system_prompt_sections();
610        let cache = section_cache().lock().unwrap();
611        assert!(cache.is_empty());
612    }
613
614    // ── New component tests ──
615
616    #[test]
617    fn test_agent_guidance_included_when_tools_available() {
618        let opts = SystemPromptOptions {
619            tools_available: vec!["Agent".into(), "Read".into()],
620            ..Default::default()
621        };
622        let prompt = build_system_prompt(&opts);
623        assert!(prompt.contains("Sub-agents"));
624    }
625
626    #[test]
627    fn test_agent_guidance_excluded_when_no_agent_tool() {
628        let opts = SystemPromptOptions {
629            tools_available: vec!["Read".into(), "Write".into()],
630            ..Default::default()
631        };
632        let prompt = build_system_prompt(&opts);
633        assert!(!prompt.contains("Sub-agents"));
634    }
635
636    #[test]
637    fn test_skills_guidance_conditional() {
638        let with = SystemPromptOptions {
639            tools_available: vec!["Skill".into()],
640            ..Default::default()
641        };
642        assert!(build_system_prompt(&with).contains("/<skill-name>"));
643
644        let without = SystemPromptOptions::default();
645        assert!(!build_system_prompt(&without).contains("/<skill-name>"));
646    }
647
648    #[test]
649    fn test_memory_guidance_conditional() {
650        let with = SystemPromptOptions {
651            has_memory: true,
652            ..Default::default()
653        };
654        assert!(build_system_prompt(&with).contains("Persistent memory"));
655
656        let without = SystemPromptOptions::default();
657        assert!(!build_system_prompt(&without).contains("Persistent memory"));
658    }
659
660    #[test]
661    fn test_auto_compact_warning() {
662        let with = SystemPromptOptions {
663            has_auto_compact: true,
664            ..Default::default()
665        };
666        assert!(build_system_prompt(&with).contains("Context management"));
667
668        let without = SystemPromptOptions::default();
669        assert!(!build_system_prompt(&without).contains("Context management"));
670    }
671
672    #[test]
673    fn test_git_snapshot() {
674        let opts = SystemPromptOptions {
675            git_status: Some(GitSnapshot {
676                branch: "main".into(),
677                recent_commits: vec!["abc1234 Fix bug".into()],
678                status_lines: vec!["M src/main.rs".into()],
679                user: Some("Dev".into()),
680            }),
681            ..Default::default()
682        };
683        let prompt = build_system_prompt(&opts);
684        let boundary = prompt.find(SYSTEM_PROMPT_DYNAMIC_BOUNDARY).unwrap();
685        let git_pos = prompt.find("Branch: main").unwrap();
686        assert!(git_pos > boundary); // dynamic section
687        assert!(prompt.contains("abc1234 Fix bug"));
688        assert!(prompt.contains("M src/main.rs"));
689        assert!(prompt.contains("User: Dev"));
690    }
691
692    #[test]
693    fn test_mcp_instructions() {
694        let opts = SystemPromptOptions {
695            mcp_instructions: vec![("db-server".into(), "Use LIMIT clauses".into())],
696            ..Default::default()
697        };
698        let prompt = build_system_prompt(&opts);
699        assert!(prompt.contains("db-server"));
700        assert!(prompt.contains("Use LIMIT clauses"));
701    }
702
703    #[test]
704    fn test_language_preference() {
705        let opts = SystemPromptOptions {
706            language: Some("Japanese".into()),
707            ..Default::default()
708        };
709        let prompt = build_system_prompt(&opts);
710        assert!(prompt.contains("Always respond in Japanese"));
711    }
712
713    #[test]
714    fn test_output_efficiency_always_included() {
715        let prompt = build_system_prompt(&default_opts());
716        assert!(prompt.contains("Output efficiency"));
717    }
718
719    #[test]
720    fn test_summarize_tool_results_always_included() {
721        let prompt = build_system_prompt(&default_opts());
722        assert!(prompt.contains("Tool results"));
723    }
724}