//! System prompt template source and constants. //! //! Templates are XOR-obfuscated by `scripts/encrypt_templates.py` (obfuscation, //! not security — seeds live in-repo) so they don't appear as obvious plaintext //! in `strings` output. They are decrypted on demand and the returned //! `Zeroizing` wipes the plaintext from memory on drop. use zeroize::Zeroizing; // Encrypted template bytes (pre-generated by scripts/encrypt_templates.py). #[path = "prompt_encrypted.rs"] mod prompt_encrypted; use prompt_encrypted::*; /// Decrypt XOR-obfuscated template data (mirrors `scripts/encrypt_templates.py::xor_encrypt`). /// Obfuscation only — not a security boundary. fn decrypt(data: &[u8], seed: u8) -> Zeroizing { let bytes: Vec = data .iter() .enumerate() .map(|(i, &b)| b ^ seed.wrapping_add(i as u8)) .collect(); Zeroizing::new(String::from_utf8(bytes).expect( "prompt template decryption produced invalid UTF-8 — \ prompt_encrypted.rs is likely stale; run: \ python3 scripts/encrypt_templates.py", )) } /// The base prompt template (decrypted fresh; zeroed on drop). pub(crate) fn base_template() -> Zeroizing { decrypt(BASE_PROMPT_ENC, PROMPT_SEEDS[0]) } /// The base prompt template source, exposed for `kigi prompt --section template`. pub fn base_template_source() -> Zeroizing { base_template() } pub(crate) fn apply_patch_template() -> Zeroizing { decrypt(CODEX_PROMPT_ENC, PROMPT_SEEDS[1]) } /// Apply-patch prompt template source, exposed for `kigi prompt --section apply-patch-template`. pub fn apply_patch_template_source() -> Zeroizing { apply_patch_template() } /// The subagent-specific base template (decrypted fresh; zeroed on drop). pub(crate) fn subagent_template() -> Zeroizing { decrypt(SUBAGENT_PROMPT_ENC, PROMPT_SEEDS[2]) } /// The compact system prompt used after conversation compaction. pub const COMPACT_SYSTEM_PROMPT: &str = "You are an AI coding agent. You operate in a workspace with a provided codebase.\n\n\ Your main goal is to complete the user's request, denoted within the tag."; #[cfg(test)] mod tests { use super::*; use kigi_tools::types::template_renderer::TemplateRenderer; use kigi_tools::types::tool::ToolKind; use std::collections::HashMap; /// Verify the pre-generated encrypted file matches the current template sources. /// If this fails, run: `python3 scripts/encrypt_templates.py` #[test] fn test_encrypted_templates_not_stale() { fn xor_encrypt(data: &[u8], seed: u8) -> Vec { data.iter() .enumerate() .map(|(i, &b)| b ^ seed.wrapping_add(i as u8)) .collect() } let base_raw = include_bytes!("../../templates/prompt.md"); let apply_patch_raw = include_bytes!("../../templates/apply_patch_prompt.md"); let subagent_raw = include_bytes!("../../templates/subagent_prompt.md"); assert_eq!( BASE_PROMPT_ENC, &xor_encrypt(base_raw, PROMPT_SEEDS[0]), "prompt.md encrypted bytes are stale — run scripts/encrypt_templates.py" ); assert_eq!( CODEX_PROMPT_ENC, &xor_encrypt(apply_patch_raw, PROMPT_SEEDS[1]), "apply_patch_prompt.md encrypted bytes are stale — run scripts/encrypt_templates.py" ); assert_eq!( SUBAGENT_PROMPT_ENC, &xor_encrypt(subagent_raw, PROMPT_SEEDS[2]), "subagent_prompt.md encrypted bytes are stale — run scripts/encrypt_templates.py" ); } /// Build a TemplateRenderer with the standard kigi tool kinds. fn default_renderer() -> TemplateRenderer { let tools: HashMap = [ (ToolKind::Read, "read_file"), (ToolKind::Edit, "search_replace"), (ToolKind::Execute, "run_terminal_command"), (ToolKind::Search, "grep"), (ToolKind::List, "list_dir"), (ToolKind::Plan, "todo_write"), (ToolKind::Skill, "skill"), ( ToolKind::BackgroundTaskAction, "get_command_or_subagent_output", ), (ToolKind::KillTaskAction, "kill_command_or_subagent"), (ToolKind::WebSearch, "web_search"), ] .into_iter() .map(|(k, v)| (k, v.to_string())) .collect(); TemplateRenderer::new(tools, HashMap::new()) } fn default_placeholders() -> serde_json::Value { serde_json::json!({ "os_name": "macos", "shell_path": "/bin/zsh", "working_directory": "/tmp/test", "current_date": "2025-01-15", "memory_enabled": false, "is_non_interactive": false, "system_prompt_label": crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL, }) } fn render_base(renderer: &TemplateRenderer, placeholders: &serde_json::Value) -> String { let tmpl = base_template(); renderer .render_with_extra(&tmpl, placeholders) .expect("base template render failed") } fn render_subagent(renderer: &TemplateRenderer, placeholders: &serde_json::Value) -> String { let tmpl = subagent_template(); renderer .render_with_extra(&tmpl, placeholders) .expect("subagent template render failed") } fn render_apply_patch(renderer: &TemplateRenderer, placeholders: &serde_json::Value) -> String { let tmpl = apply_patch_template(); renderer .render_with_extra(&tmpl, placeholders) .expect("codex template render failed") } // Variable substitution #[test] fn test_variable_substitution_tool_kind() { let r = default_renderer(); let p = default_placeholders(); let result = r .render_with_extra("Use ${{ tools.by_kind.read }} to read files.", &p) .unwrap(); assert_eq!(result, "Use read_file to read files."); } #[test] fn test_variable_substitution_agent_fields() { let r = default_renderer(); let p = default_placeholders(); let result = r .render_with_extra("OS: ${{ os_name }}, Shell: ${{ shell_path }}", &p) .unwrap(); assert_eq!(result, "OS: macos, Shell: /bin/zsh"); } // Conditionals #[test] fn test_conditional_tool_present() { let r = default_renderer(); let p = default_placeholders(); let result = r .render_with_extra("${%- if tools.by_kind.plan %}show${%- endif %}", &p) .unwrap(); assert_eq!(result, "show"); } #[test] fn test_conditional_tool_absent() { // Renderer without plan tool let tools: HashMap = [(ToolKind::Read, "read_file".to_string())].into(); let r = TemplateRenderer::new(tools, HashMap::new()); let p = default_placeholders(); let result = r .render_with_extra("${%- if tools.by_kind.plan %}show${%- endif %}", &p) .unwrap(); assert_eq!(result, ""); } #[test] fn test_literal_braces_pass_through() { let r = default_renderer(); let p = default_placeholders(); let result = r .render_with_extra("Use {{ literal_braces }} in prose.", &p) .unwrap(); assert_eq!(result, "Use {{ literal_braces }} in prose."); } // Tool name overrides #[test] fn test_tool_name_override() { let tools: HashMap = [ (ToolKind::Read, "view_file".to_string()), (ToolKind::Edit, "Edit".to_string()), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let p = default_placeholders(); let result = r .render_with_extra( "Use ${{ tools.by_kind.read }} and ${{ tools.by_kind.edit }}.", &p, ) .unwrap(); assert_eq!(result, "Use view_file and Edit."); } // Base template rendering #[test] fn test_base_template_renders() { let prompt = render_base(&default_renderer(), &default_placeholders()); assert!(prompt.contains(crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL)); assert!(prompt.contains("user_query")); } #[test] fn test_base_template_contains_resolved_tool_names() { let prompt = render_base(&default_renderer(), &default_placeholders()); // The minimal prompt only resolves the read/edit tool names, inside // . (todo_write / run_terminal_command lived in sections // that the trimmed prompt no longer renders.) assert!(prompt.contains("read_file"), "Should contain 'read_file'"); assert!( prompt.contains("search_replace"), "Should contain 'search_replace'" ); assert!(!prompt.contains("${{"), "No unresolved template variables"); assert!(!prompt.contains("${%"), "No unresolved template blocks"); } #[test] fn test_base_template_with_overridden_tool_names() { let tools: HashMap = [ (ToolKind::Read, "view_file".to_string()), (ToolKind::Edit, "edit".to_string()), (ToolKind::Execute, "run_terminal_cmd".to_string()), (ToolKind::Search, "grep".to_string()), (ToolKind::Plan, "todo_write".to_string()), ( ToolKind::BackgroundTaskAction, "get_task_output".to_string(), ), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_base(&r, &default_placeholders()); assert!( prompt.contains("`view_file`"), "Should use overridden 'view_file'" ); assert!(prompt.contains("`edit`"), "Should use overridden 'edit'"); assert!( !prompt.contains("`read_file`"), "Should NOT contain canonical 'read_file'" ); } #[test] fn test_base_template_plan_absent_omits_task_management() { // Renderer without Plan tool let tools: HashMap = [ (ToolKind::Read, "read_file".to_string()), (ToolKind::Execute, "run_terminal_cmd".to_string()), ( ToolKind::BackgroundTaskAction, "get_task_output".to_string(), ), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_base(&r, &default_placeholders()); assert!( !prompt.contains("Task Management"), "Task Management section should be omitted" ); } #[test] fn test_base_template_execute_absent_omits_background_tasks() { // Renderer without Execute tool let tools: HashMap = [(ToolKind::Plan, "todo_write".to_string())].into(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_base(&r, &default_placeholders()); assert!( !prompt.contains("background_tasks"), "background_tasks section should be omitted" ); } #[test] fn test_monitor_tool_renders_watch_section() { let tools: HashMap = [ (ToolKind::Execute, "run_command".to_string()), (ToolKind::BackgroundTaskAction, "get_output".to_string()), (ToolKind::KillTaskAction, "kill_task".to_string()), (ToolKind::Monitor, "monitor".to_string()), ] .into_iter() .collect(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_base(&r, &default_placeholders()); assert!( prompt.contains("For watch processes"), "monitor section should render when Monitor tool is present" ); assert!( prompt.contains("streams each stdout line back as a chat notification"), "monitor section should describe streaming stdout as notifications" ); assert!( prompt.contains("Use the `monitor` tool"), "monitor section should resolve the Monitor tool name" ); } #[test] fn test_no_monitor_tool_omits_watch_section() { let tools: HashMap = [ (ToolKind::Execute, "run_command".to_string()), (ToolKind::BackgroundTaskAction, "get_output".to_string()), (ToolKind::KillTaskAction, "kill_task".to_string()), ] .into_iter() .collect(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_base(&r, &default_placeholders()); assert!( !prompt.contains("For watch processes"), "monitor section should NOT render without Monitor tool" ); assert!( !prompt.contains(""), "background_tasks section is gated on the Monitor tool and is omitted without it" ); } // Required sections regression #[test] fn test_base_template_contains_required_sections() { let p = default_placeholders(); let prompt = render_base(&default_renderer(), &p); assert!( prompt.contains(crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL), "Must contain agent identity" ); assert!( prompt.contains("user_query"), "Must reference user_query tag" ); } #[test] fn test_compact_prompt_matches_expected() { assert_eq!( COMPACT_SYSTEM_PROMPT, "You are an AI coding agent. You operate in a workspace with a provided codebase.\n\n\ Your main goal is to complete the user's request, denoted within the tag.", ); } // Mid-session mode switching #[test] fn test_mid_session_switch_concise_to_full() { let compact = COMPACT_SYSTEM_PROMPT; assert!(!compact.contains("read_file"), "Compact has no tool names"); assert!( !compact.contains(""), "Compact has no tool section" ); let full = render_base(&default_renderer(), &default_placeholders()); assert!( full.contains(""), "Full prompt has tool section" ); assert!(full.contains("read_file"), "Full prompt has read_file"); assert!( full.contains("search_replace"), "Full prompt has search_replace" ); } #[test] fn test_mid_session_switch_preserves_tool_overrides() { let tools: HashMap = [ (ToolKind::Read, "view".to_string()), (ToolKind::Edit, "edit".to_string()), (ToolKind::Execute, "run_terminal_cmd".to_string()), (ToolKind::Plan, "todo_write".to_string()), ( ToolKind::BackgroundTaskAction, "get_task_output".to_string(), ), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_base(&r, &default_placeholders()); assert!(prompt.contains("`edit`"), "Should use overridden 'edit'"); assert!(prompt.contains("`view`"), "Should use overridden 'view'"); assert!( !prompt.contains("`read_file`"), "Should not contain original 'read_file'" ); assert!( !prompt.contains("`search_replace`"), "Should not contain original 'search_replace'" ); } // Determinism #[test] fn test_prompt_deterministic_across_renders() { let r = default_renderer(); let p = default_placeholders(); let a = render_base(&r, &p); let b = render_base(&r, &p); assert_eq!(a, b, "Prompt rendering must be deterministic"); } #[test] fn test_full_mode_deterministic() { let r = default_renderer(); let p = default_placeholders(); let body = "Agent: ${{ tools.by_kind.read }}, OS: ${{ os_name }}"; let a = r.render_with_extra(body, &p).unwrap(); let b = r.render_with_extra(body, &p).unwrap(); assert_eq!(a, b, "Full mode rendering must be deterministic"); } // Disabled tools #[test] fn test_disabled_tools_omit_sections() { // No plan, no execute let tools: HashMap = [(ToolKind::Read, "read_file".to_string())].into(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_base(&r, &default_placeholders()); assert!( !prompt.contains("Task Management"), "Task Management must be omitted" ); assert!( !prompt.contains("background_tasks"), "background_tasks must be omitted" ); } // Memory section #[test] fn test_memory_enabled_does_not_render_memory_section() { // The section is absent from the minimal base prompt. // Even when the memory tools are registered AND memory_enabled=true, // the trimmed template must not render a memory section. (Complements // test_memory_disabled_omits_memory_section, which covers the default.) let tools: HashMap = [ (ToolKind::Read, "read_file".to_string()), (ToolKind::MemorySearch, "memory_search".to_string()), (ToolKind::MemoryGet, "memory_get".to_string()), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let mut p = default_placeholders(); p["memory_enabled"] = serde_json::json!(true); let prompt = render_base(&r, &p); assert!( !prompt.contains(""), "Memory section was removed from the minimal prompt" ); assert!( !prompt.contains("### Memory Management"), "Memory Management section was removed from the minimal prompt" ); assert!( !prompt.contains("memory_search"), "memory tool names must not appear once the memory section is gone" ); assert!( !prompt.contains("memory_get"), "memory tool names must not appear once the memory section is gone" ); } #[test] fn test_memory_disabled_omits_memory_section() { let prompt = render_base(&default_renderer(), &default_placeholders()); assert!( !prompt.contains(""), "Memory section must be omitted" ); } // Web search disabled #[test] fn test_web_search_disabled_renders_without_crash() { // No Fetch tool let tools: HashMap = [ (ToolKind::Read, "read_file".to_string()), (ToolKind::Plan, "todo_write".to_string()), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let tmpl = base_template(); let result = r.render_with_extra(&tmpl, &default_placeholders()); assert!( result.is_ok(), "Must render without crash: {:?}", result.err() ); } // Apply-patch template rendering #[test] fn test_apply_patch_template_renders() { let prompt = render_apply_patch(&default_renderer(), &default_placeholders()); assert!(prompt.contains("coding agent")); } #[test] fn test_apply_patch_template_contains_resolved_tool_names() { let prompt = render_apply_patch(&default_renderer(), &default_placeholders()); assert!(prompt.contains("todo_write"), "Should contain 'todo_write'"); // apply_patch is hardcoded, not resolved via ${{ tools.by_kind.edit }} assert!( prompt.contains("apply_patch"), "Should contain hardcoded 'apply_patch'" ); assert!(!prompt.contains("${{"), "No unresolved template variables"); assert!(!prompt.contains("${%"), "No unresolved template blocks"); } #[test] fn test_apply_patch_template_plan_absent_omits_planning() { // Renderer without Plan tool let tools: HashMap = [ (ToolKind::Read, "read_file".to_string()), (ToolKind::Edit, "search_replace".to_string()), (ToolKind::Execute, "run_terminal_cmd".to_string()), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_apply_patch(&r, &default_placeholders()); assert!( !prompt.contains("## Planning"), "Planning section should be omitted when plan tool absent" ); assert!( !prompt.contains("update_plan"), "update_plan references should be omitted" ); } #[test] fn test_apply_patch_template_plan_present_includes_planning() { let prompt = render_apply_patch(&default_renderer(), &default_placeholders()); assert!( prompt.contains("## Planning"), "Planning section should be present when plan tool exists" ); } #[test] fn test_apply_patch_template_with_overridden_tool_names() { let tools: HashMap = [ (ToolKind::Read, "view_file".to_string()), (ToolKind::Edit, "some_other_edit".to_string()), (ToolKind::Execute, "run_terminal_cmd".to_string()), (ToolKind::Plan, "update_plan".to_string()), ( ToolKind::BackgroundTaskAction, "get_task_output".to_string(), ), ] .into(); let r = TemplateRenderer::new(tools, HashMap::new()); let prompt = render_apply_patch(&r, &default_placeholders()); // apply_patch is hardcoded — NOT affected by Edit tool override assert!( prompt.contains("`apply_patch`"), "apply_patch must remain hardcoded regardless of edit override" ); assert!( !prompt.contains("some_other_edit"), "Edit override must NOT leak into apply-patch prompt" ); // Plan tool IS resolved via template assert!( prompt.contains("`update_plan`"), "Should use overridden 'update_plan'" ); } #[test] fn test_apply_patch_template_deterministic_across_renders() { let r = default_renderer(); let p = default_placeholders(); let a = render_apply_patch(&r, &p); let b = render_apply_patch(&r, &p); assert_eq!(a, b, "Apply-patch template rendering must be deterministic"); } #[test] fn test_subagent_template_deterministic_across_renders() { let r = default_renderer(); let p = default_placeholders(); let a = render_subagent(&r, &p); let b = render_subagent(&r, &p); assert_eq!(a, b, "Subagent template rendering must be deterministic"); } // Task completion discipline // // The `` block is absent from both // base and subagent templates. These tests pin the deletion so the // block doesn't accidentally come back, and so the runtime TodoGate // doesn't start firing reminders that reference a non-existent // block. #[test] fn task_completion_discipline_block_is_not_rendered() { let prompt = render_base(&default_renderer(), &default_placeholders()); assert!( !prompt.contains(""), "discipline block was removed from the base template" ); let subagent = render_subagent(&default_renderer(), &default_placeholders()); assert!( !subagent.contains(""), "discipline block was removed from the subagent template" ); } /// Soft byte ceiling shared by both prompt-size budget tests. /// Forward-budget guard against runaway growth, not a tight target. const PROMPT_SIZE_SOFT_CEILING_BYTES: usize = 16384; fn assert_template_size_under(prompt: &str, label: &str) { assert!( prompt.len() < PROMPT_SIZE_SOFT_CEILING_BYTES, "{label} prompt is {} bytes, exceeding soft ceiling of {} bytes", prompt.len(), PROMPT_SIZE_SOFT_CEILING_BYTES, ); } #[test] fn test_base_template_size_budget() { let prompt = render_base(&default_renderer(), &default_placeholders()); assert_template_size_under(&prompt, "base"); } #[test] fn test_subagent_template_size_budget() { let prompt = render_subagent(&default_renderer(), &default_placeholders()); assert_template_size_under(&prompt, "subagent"); } // Guard invariant // Every `${{ tools.by_kind.X }}` must sit inside a `${%- if ... %}` // whose condition requires X (contains `tools.by_kind.X` at a word // boundary, with no top-level ` or `). If violated, X could render // as empty string at runtime. fn word_bounded(hay: &str, needle: &str) -> bool { let mut s = 0; while let Some(i) = hay[s..].find(needle) { let end = s + i + needle.len(); match hay[end..].chars().next() { None => return true, Some(c) if !(c.is_alphanumeric() || c == '_') => return true, _ => s += i + 1, } } false } fn guarantees(cond: &str, kind: &str) -> bool { if word_bounded(cond, &format!("tools.by_kind.{kind}")) && !cond.contains(" or ") { return true; } false } fn assert_guards(template: &str, label: &str) { let bytes = template.as_bytes(); let mut stack: Vec = Vec::new(); let mut errors: Vec = Vec::new(); let mut i = 0; while i + 2 < bytes.len() { let three = &bytes[i..i + 3]; if three == b"${%" { let end = bytes[i + 3..] .windows(2) .position(|w| w == b"%}") .map(|e| i + 3 + e + 2) .unwrap_or(bytes.len()); let body = std::str::from_utf8(&bytes[i + 3..end - 2]) .unwrap() .trim_matches(['-', ' ']); if let Some(c) = body.strip_prefix("if ") { stack.push(c.trim().into()); } else if let Some(c) = body.strip_prefix("elif ") { stack.pop(); stack.push(c.trim().into()); } else if body == "else" { stack.pop(); stack.push("".into()); } else if body == "endif" { stack.pop(); } i = end; } else if three == b"${{" { let end = bytes[i + 3..] .windows(2) .position(|w| w == b"}}") .map(|e| i + 3 + e + 2) .unwrap_or(bytes.len()); let body = std::str::from_utf8(&bytes[i + 3..end - 2]).unwrap().trim(); // search_tool and use_tool are always built-in, so they // never need a guard. const ALWAYS_BUILTIN: &[&str] = &["search_tool", "use_tool"]; if let Some(kind) = body.strip_prefix("tools.by_kind.") && kind.chars().all(|c| c.is_alphanumeric() || c == '_') && !ALWAYS_BUILTIN.contains(&kind) && !stack.iter().any(|c| guarantees(c, kind)) { let line = template[..i].lines().count() + 1; errors.push(format!( "{label}:{line}: unguarded `${{{{ tools.by_kind.{kind} }}}}` (stack: {stack:?})" )); } i = end; } else { i += 1; } } assert!(errors.is_empty(), "\n {}", errors.join("\n ")); } #[test] fn test_template_vars_are_always_guarded() { assert_guards(&base_template(), "prompt.md"); assert_guards(&subagent_template(), "subagent_prompt.md"); assert_guards(&apply_patch_template(), "apply_patch_prompt.md"); } // Combination sweep // Belt-and-braces: renders the base template across tool-kind subsets // and asserts no raw template tokens leak. The static guard test above // is the authoritative check; this one just catches syntax drift. // is_non_interactive gating // Headless / SDK / stdio / generic-ACP sessions have no human typing // into a TUI prompt, so the `! ` shell-prefix tip and the // `` TUI pointer are noise. Those sections must drop out // when `is_non_interactive=true` and remain when it's false. #[test] fn interactive_renders_shell_prefix_tip_and_user_guide() { // The `! ` shell-prefix tip is absent from the minimal // prompt. The block still renders for interactive // sessions only, so that's what we assert here. let mut p = default_placeholders(); p["is_non_interactive"] = serde_json::json!(false); let prompt = render_base(&default_renderer(), &p); assert!( prompt.contains(""), "interactive prompt must keep the block" ); assert!( prompt.contains("interactive CLI tool"), "interactive prompt must declare interactive mode in the header" ); assert!( !prompt.contains("autonomous agent"), "interactive prompt must NOT advertise non-interactive (autonomous) mode" ); } #[test] fn non_interactive_suppresses_shell_prefix_tip_and_user_guide() { let mut p = default_placeholders(); p["is_non_interactive"] = serde_json::json!(true); let prompt = render_base(&default_renderer(), &p); assert!( !prompt.contains("`! `"), "non-interactive prompt must suppress the shell-prefix tip" ); assert!( !prompt.contains(""), "non-interactive prompt must suppress the block" ); assert!( prompt.contains("autonomous agent"), "non-interactive prompt must declare autonomous mode in the header" ); assert!( !prompt.contains("interactive CLI tool"), "non-interactive prompt must NOT claim to be the interactive CLI" ); // Sanity: rest of the template still renders. assert!(prompt.contains(crate::prompt::context::DEFAULT_SYSTEM_PROMPT_LABEL)); assert!(prompt.contains("user_query")); } #[test] fn test_combination_sweep_no_unresolved_variables() { let optional = [ ToolKind::Read, ToolKind::Edit, ToolKind::Execute, ToolKind::Search, ToolKind::List, ToolKind::Plan, ToolKind::Skill, ToolKind::Task, ToolKind::AskUser, ToolKind::EnterPlan, ToolKind::ExitPlan, ToolKind::BackgroundTaskAction, ToolKind::Monitor, ToolKind::MemorySearch, ToolKind::MemoryGet, ]; let mut subsets: Vec> = vec![vec![], optional.to_vec()]; for i in 0..optional.len() { subsets.push(vec![optional[i]]); for j in (i + 1)..optional.len() { subsets.push(vec![optional[i], optional[j]]); } } for memory_enabled in [false, true] { for subset in &subsets { let tools: HashMap = subset .iter() .map(|k| (*k, format!("{k:?}").to_lowercase())) .collect(); let r = TemplateRenderer::new(tools, HashMap::new()); let mut p = default_placeholders(); p["memory_enabled"] = serde_json::json!(memory_enabled); let rendered = r .render_with_extra(&base_template(), &p) .unwrap_or_else(|e| { panic!("render failed: {subset:?} mem={memory_enabled}: {e:?}") }); assert!( !rendered.contains("${{") && !rendered.contains("${%"), "unresolved token in render: {subset:?} mem={memory_enabled}", ); } } } }