Skip to main content

vtcode_core/prompts/
system.rs

1//! System instructions and prompt management.
2//!
3//! Prompt variants share one canonical base contract plus thin mode deltas and
4//! compact runtime addenda. Richer behavior comes from AGENTS.md, dynamic tool
5//! guidance, skill metadata, and runtime notices.
6
7use crate::config::constants::prompt_budget as prompt_budget_constants;
8use crate::config::types::SystemPromptMode;
9use crate::llm::providers::gemini::wire::Content;
10use crate::project_doc::read_project_doc;
11use crate::prompts::context::PromptContext;
12use crate::prompts::guidelines::generate_tool_guidelines;
13use crate::prompts::output_styles::OutputStyleApplier;
14use crate::prompts::resources::{apply_system_prompt_layers, resolve_system_prompt_layers};
15use crate::prompts::system_prompt_cache::PROMPT_CACHE;
16use crate::prompts::temporal::generate_temporal_context;
17use crate::skills::render::render_prompt_skills_section;
18use std::env;
19use std::path::Path;
20use std::sync::OnceLock;
21use tracing::warn;
22
23/// Shared Planning workflow header used by both static and incremental prompt builders.
24pub const PLANNING_WORKFLOW_READ_ONLY_HEADER: &str = "# PLANNING WORKFLOW (READ-ONLY)";
25/// Shared Planning workflow notice line describing strict read-only enforcement.
26pub const PLANNING_WORKFLOW_READ_ONLY_NOTICE_LINE: &str = "Planning workflow is active. Mutating tools are blocked except for optional plan artifact writes under `.vtcode/plans/` (or an explicit custom plan path).";
27/// Shared Planning workflow instruction line for transitioning to implementation.
28pub const PLANNING_WORKFLOW_EXIT_INSTRUCTION_LINE: &str =
29    "Call `finish_planning` when ready to transition to implementation.";
30/// Shared Planning workflow instruction line for decision-complete planning output.
31pub const PLANNING_WORKFLOW_PLAN_QUALITY_LINE: &str = "Explore repository facts first, ask only material blocking questions, keep planning read-only, and emit exactly one decision-complete `<proposed_plan>` block with a summary, implementation steps, test cases, and assumptions/defaults. If something is still unresolved, end with `Next open decision: ...`.";
32/// Shared Planning workflow policy line requiring context-aware interview closure before final plans.
33pub const PLANNING_WORKFLOW_INTERVIEW_POLICY_LINE: &str = "In Planning workflow, prefer model-generated `request_user_input` interview questions informed by discovered repository context, keep custom notes/free-form responses available as first-class input, and continue interviewing until material scope/decomposition/verification decisions are closed before finalizing `<proposed_plan>`.";
34/// Shared Planning workflow policy line for runtimes where `request_user_input` is unavailable.
35pub const PLANNING_WORKFLOW_NO_REQUEST_USER_INPUT_POLICY_LINE: &str = "In this runtime, `request_user_input` is unavailable. In Planning workflow, continue exploring repository facts with read-only permissions, finish any unblocked planning work, and surface material blockers explicitly in plain text instead of emitting interview tool calls.";
36/// Shared Planning workflow guard line requiring explicit transition from planning to execution.
37pub const PLANNING_WORKFLOW_NO_AUTO_EXIT_LINE: &str = "Do not auto-exit Planning workflow just because a plan exists; wait for explicit implementation intent.";
38/// Shared Planning workflow task-tracking line clarifying availability and aliasing.
39pub const PLANNING_WORKFLOW_TASK_TRACKER_LINE: &str =
40    "`task_tracker` remains available while planning.";
41/// Shared reminder appended when presenting plans while still in Planning workflow.
42pub const PLANNING_WORKFLOW_IMPLEMENT_REMINDER: &str = "• Planning workflow is active with read-only permissions. Say “implement” to execute, or “stay in planning workflow” to revise. If automatic planning handoff fails, call `finish_planning` to present the plan again.";
43
44const PROMPT_TITLE: &str = "# VT Code";
45const PROMPT_INTRO: &str = "VT Code. Be concise and safe.";
46const CONTRACT_HEADER: &str = "## Contract";
47
48const OPENAI_GPT55_CONTRACT_HEADER: &str = "## GPT-5.5 OpenAI Addendum";
49const OPENAI_GPT55_CONTRACT_LINES: &[&str] = &[
50    "State the outcome, constraints, evidence, and output shape up front; avoid over-prescribing the path unless the exact steps matter.",
51    "If context is missing, say so plainly; use the smallest missing detail that would change the result, and finish any unblocked portion first.",
52    "Verify changes yourself with the smallest relevant check; never claim a check passed unless you ran it.",
53    "Before multi-step tool work, send a brief progress update that names the first step.",
54    "Use retrieved evidence for citation-sensitive work; use the minimum evidence sufficient to answer correctly, then stop.",
55];
56
57const DEFAULT_CONTRACT_LINES: &[&str] = &[
58    "Start with `AGENTS.md`; inspect code first, match local patterns, use `@file`.",
59    "If context is missing, say so, do not guess, finish unblocked slices first.",
60    "Take safe, reversible steps; ask only for material behavior, API, UX, or credential changes.",
61    "Keep control on the main thread. Delegate bounded, independent work only.",
62    "Verify changes yourself; never claim a check passed unless you ran it.",
63    "Keep outputs concise. Keep user updates brief and high-signal.",
64    "Use retrieved evidence for citation-sensitive work.",
65    "Preserve task goal, tracker state, touched files, verification status, and decisions across compaction.",
66    "Read files before answering. Never speculate about code you have not opened.",
67    "Prefer `ast-grep` for code-shape queries; keep text grep for prose, logs, and config strings.",
68    "Make only requested changes. Implement by default; suggest only when intent is unclear.",
69];
70
71const MINIMAL_CONTRACT_LINES: &[&str] = &[
72    "Use `AGENTS.md`; inspect code first.",
73    "If context is missing, say so, do not guess, finish unblocked slices.",
74    "Take safe, reversible steps; verify changes yourself.",
75    "Keep delegation bounded and explicit.",
76    "Preserve tracker state, touched files, and verification status across compaction.",
77    "Use retrieved evidence when citation-sensitive.",
78    "Prefer `ast-grep` for code-shape queries; keep text grep for prose and config.",
79    "Keep outputs concise.",
80];
81
82const DEFAULT_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
83
84- Use `task_tracker` for non-trivial work.
85- Treat completion language as a checkpoint, not proof; only stop when the tracker is current and verification is resolved.
86- Use Planning workflow for research/spec work; stay read-only until implementation intent is explicit."#;
87
88const MINIMAL_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
89
90- Stay precise; use `task_tracker` once work stops being trivial.
91- Treat completion language as a checkpoint, not proof.
92- Use `AGENTS.md` as the map; open repo docs only when structural rules matter."#;
93
94const LIGHTWEIGHT_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
95
96- Act and verify in one thread.
97- Completion language is a checkpoint.
98- Use `task_tracker` for nontrivial work."#;
99
100const SPECIALIZED_OPERATING_PROFILE_DELTA: &str = r#"## Operating Profile
101
102- Explore, plan, then execute.
103- Use `task_tracker` for multi-step work and Planning workflow when scope or verification is still open.
104- Treat completion language as a checkpoint, not proof; only stop when tracker state, verification, and resumable state agree.
105- End plan work with one `<proposed_plan>` block; if a path stalls, re-plan into smaller verified slices.
106- Use `AGENTS.md` and `docs/harness/ARCHITECTURAL_INVARIANTS.md` when repo-wide invariants matter."#;
107
108static DEFAULT_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
109static MINIMAL_SYSTEM_PROMPT: OnceLock<String> = OnceLock::new();
110static DEFAULT_LIGHTWEIGHT_PROMPT: OnceLock<String> = OnceLock::new();
111static DEFAULT_SPECIALIZED_PROMPT: OnceLock<String> = OnceLock::new();
112
113pub fn default_system_prompt() -> &'static str {
114    static_profile_prompt(SystemPromptMode::Default)
115}
116
117pub fn minimal_system_prompt() -> &'static str {
118    static_profile_prompt(SystemPromptMode::Minimal)
119}
120
121pub fn default_lightweight_prompt() -> &'static str {
122    static_profile_prompt(SystemPromptMode::Lightweight)
123}
124
125pub fn specialized_system_prompt() -> &'static str {
126    static_profile_prompt(SystemPromptMode::Specialized)
127}
128
129pub fn minimal_instruction_text() -> String {
130    minimal_system_prompt().to_string()
131}
132
133pub fn lightweight_instruction_text() -> String {
134    default_lightweight_prompt().to_string()
135}
136
137pub fn specialized_instruction_text() -> String {
138    specialized_system_prompt().to_string()
139}
140
141pub fn openai_gpt55_contract_addendum() -> String {
142    let lines_len = OPENAI_GPT55_CONTRACT_LINES
143        .iter()
144        .map(|line| line.len())
145        .sum::<usize>();
146    let mut prompt = String::with_capacity(
147        OPENAI_GPT55_CONTRACT_HEADER.len() + lines_len + OPENAI_GPT55_CONTRACT_LINES.len() * 3 + 8,
148    );
149    prompt.push_str(OPENAI_GPT55_CONTRACT_HEADER);
150    prompt.push_str("\n\n");
151    for line in OPENAI_GPT55_CONTRACT_LINES {
152        prompt.push_str("- ");
153        prompt.push_str(line);
154        prompt.push('\n');
155    }
156    prompt.pop();
157    prompt
158}
159
160const STRUCTURED_REASONING_INSTRUCTIONS: &str = r#"
161## Structured Reasoning
162
163Use tags when helpful: `<analysis>` facts/options, `<plan>` steps, `<uncertainty>` blockers, `<verification>` checks.
164"#;
165
166/// System instruction configuration
167#[derive(Debug, Clone, Default)]
168pub struct SystemPromptConfig;
169
170/// Generate system instruction
171pub async fn generate_system_instruction(_config: &SystemPromptConfig) -> Content {
172    // OPTIMIZATION: default_system_prompt() is &'static str, use directly
173    let instruction = default_system_prompt().to_string();
174
175    // Apply output style if possible (using current directory as project root)
176    if let Ok(current_dir) = env::current_dir() {
177        let styled_instruction = apply_output_style(instruction, None, &current_dir).await;
178        Content::system_text(styled_instruction)
179    } else {
180        Content::system_text(instruction)
181    }
182}
183
184/// Read AGENTS.md file if present and extract agent guidelines
185pub async fn read_agent_guidelines(project_root: &Path) -> Option<String> {
186    let max_bytes = prompt_budget_constants::DEFAULT_MAX_BYTES;
187    match read_project_doc(project_root, max_bytes).await {
188        Ok(Some(bundle)) => Some(bundle.contents),
189        Ok(None) => None,
190        Err(err) => {
191            warn!("failed to load project documentation: {err:#}");
192            None
193        }
194    }
195}
196
197/// Compose the base system instruction plus compact tool/skill/environment addenda.
198pub async fn compose_system_instruction_text(
199    _project_root: &Path,
200    vtcode_config: Option<&crate::config::VTCodeConfig>,
201    prompt_context: Option<&PromptContext>,
202) -> String {
203    let prompt_mode = vtcode_config
204        .map(|c| c.agent.system_prompt_mode)
205        .unwrap_or(SystemPromptMode::Default);
206    let static_base_prompt = static_profile_prompt(prompt_mode);
207    let resolved_layers = resolve_system_prompt_layers(_project_root).await;
208    let base_prompt = apply_system_prompt_layers(static_base_prompt, &resolved_layers);
209
210    tracing::trace!(
211        mode = ?prompt_mode,
212        base_tokens_approx = base_prompt.len() / 4, // rough token estimate
213        "Selected system prompt mode"
214    );
215
216    let base_len = base_prompt.len();
217    let config_overhead = vtcode_config.map_or(0, |_| 1024);
218    let estimated_capacity = base_len + config_overhead + 1024;
219    let mut instruction = String::with_capacity(estimated_capacity);
220    instruction.push_str(&base_prompt);
221    if should_include_structured_reasoning(vtcode_config, prompt_mode) {
222        append_prompt_section(&mut instruction, STRUCTURED_REASONING_INSTRUCTIONS);
223    }
224
225    if let Some(ctx) = prompt_context {
226        let guidelines = generate_tool_guidelines(&ctx.available_tools, ctx.capability_level);
227        if !guidelines.is_empty() {
228            append_prompt_section(&mut instruction, guidelines.trim_start_matches('\n'));
229        }
230        if let Some(skills_section) = render_prompt_skills_section(&ctx.available_skill_metadata) {
231            append_prompt_section(&mut instruction, &skills_section);
232        }
233    }
234
235    if let Some(environment_section) = render_environment_addenda(vtcode_config, prompt_context) {
236        append_prompt_section(&mut instruction, &environment_section);
237    }
238
239    instruction
240}
241
242fn append_prompt_section(prompt: &mut String, section: &str) {
243    prompt.push_str("\n\n");
244    prompt.push_str(section);
245}
246
247fn static_profile_prompt(prompt_mode: SystemPromptMode) -> &'static str {
248    match prompt_mode {
249        SystemPromptMode::Default => DEFAULT_SYSTEM_PROMPT.get_or_init(|| {
250            build_profile_prompt(
251                &build_contract_prompt(DEFAULT_CONTRACT_LINES),
252                DEFAULT_OPERATING_PROFILE_DELTA,
253            )
254        }),
255        SystemPromptMode::Minimal => MINIMAL_SYSTEM_PROMPT.get_or_init(|| {
256            build_profile_prompt(
257                &build_contract_prompt(MINIMAL_CONTRACT_LINES),
258                MINIMAL_OPERATING_PROFILE_DELTA,
259            )
260        }),
261        SystemPromptMode::Lightweight => DEFAULT_LIGHTWEIGHT_PROMPT.get_or_init(|| {
262            build_profile_prompt(
263                &build_contract_prompt(DEFAULT_CONTRACT_LINES),
264                LIGHTWEIGHT_OPERATING_PROFILE_DELTA,
265            )
266        }),
267        SystemPromptMode::Specialized => DEFAULT_SPECIALIZED_PROMPT.get_or_init(|| {
268            build_profile_prompt(
269                &build_contract_prompt(DEFAULT_CONTRACT_LINES),
270                SPECIALIZED_OPERATING_PROFILE_DELTA,
271            )
272        }),
273    }
274}
275
276fn build_contract_prompt(contract_lines: &[&str]) -> String {
277    let lines_len = contract_lines.iter().map(|line| line.len()).sum::<usize>();
278    let mut prompt = String::with_capacity(
279        PROMPT_TITLE.len()
280            + PROMPT_INTRO.len()
281            + CONTRACT_HEADER.len()
282            + lines_len
283            + contract_lines.len() * 3
284            + 8,
285    );
286    prompt.push_str(PROMPT_TITLE);
287    prompt.push_str("\n\n");
288    prompt.push_str(PROMPT_INTRO);
289    prompt.push_str("\n\n");
290    prompt.push_str(CONTRACT_HEADER);
291    prompt.push_str("\n\n");
292
293    for line in contract_lines {
294        prompt.push_str("- ");
295        prompt.push_str(line);
296        prompt.push('\n');
297    }
298
299    if !contract_lines.is_empty() {
300        prompt.pop();
301    }
302    prompt
303}
304
305fn build_profile_prompt(base_prompt: &str, profile_delta: &str) -> String {
306    let mut prompt = String::with_capacity(base_prompt.len() + profile_delta.len() + 2);
307    prompt.push_str(base_prompt);
308    prompt.push_str("\n\n");
309    prompt.push_str(profile_delta);
310    prompt
311}
312
313fn render_environment_addenda(
314    vtcode_config: Option<&crate::config::VTCodeConfig>,
315    prompt_context: Option<&PromptContext>,
316) -> Option<String> {
317    let mut lines = Vec::new();
318
319    if let Some(ctx) = prompt_context
320        && !ctx.languages.is_empty()
321    {
322        lines.push(format!(
323            "- Languages: {}. Match structural-search `lang` when needed.",
324            ctx.languages.join(", ")
325        ));
326    }
327
328    if let Some(cfg) = vtcode_config {
329        if let Some(interaction_line) = render_interaction_addendum(cfg) {
330            lines.push(interaction_line);
331        }
332
333        if cfg.mcp.enabled {
334            lines.push("- Sources: prefer MCP before external fetches when available.".to_string());
335        }
336
337        if cfg.agent.include_temporal_context && !cfg.prompt_cache.cache_friendly_prompt_shaping {
338            lines.push(
339                generate_temporal_context(cfg.agent.temporal_context_use_utc)
340                    .trim()
341                    .replacen("Current date and time", "- Time", 1)
342                    .to_string(),
343            );
344        }
345
346        if cfg.agent.include_working_directory
347            && let Some(ctx) = prompt_context
348            && let Some(cwd) = &ctx.current_directory
349        {
350            lines.push(format!("- Working directory: {}", cwd.display()));
351        }
352    }
353
354    if lines.is_empty() {
355        None
356    } else {
357        Some(format!("## Environment\n{}", lines.join("\n")))
358    }
359}
360
361fn render_interaction_addendum(cfg: &crate::config::VTCodeConfig) -> Option<String> {
362    match (cfg.security.human_in_the_loop, cfg.chat.ask_questions.enabled) {
363        (true, true) => None,
364        (true, false) => Some(
365            "- Interaction: approval may gate sensitive actions; no `request_user_input`, so make reasonable assumptions unless Planning workflow needs follow-up.".to_string(),
366        ),
367        (false, true) => Some(
368            "- Interaction: approval reduced by config; use `request_user_input` for material blockers.".to_string(),
369        ),
370        (false, false) => Some(
371            "- Interaction: approval reduced by config; no `request_user_input`, so make reasonable assumptions unless Planning workflow needs follow-up.".to_string(),
372        ),
373    }
374}
375
376fn should_include_structured_reasoning(
377    vtcode_config: Option<&crate::config::VTCodeConfig>,
378    mode: SystemPromptMode,
379) -> bool {
380    if let Some(cfg) = vtcode_config {
381        return cfg.agent.should_include_structured_reasoning_tags();
382    }
383
384    // Backward-compatible fallback when no config is available.
385    matches!(mode, SystemPromptMode::Specialized)
386}
387
388/// Generate the stable base system instruction with configuration-aware sections.
389///
390/// Note: This function maintains backward compatibility by not accepting prompt_context.
391/// For enhanced prompts with dynamic guidelines, call `compose_system_instruction_text` directly.
392pub async fn generate_system_instruction_with_config(
393    _config: &SystemPromptConfig,
394    project_root: &Path,
395    vtcode_config: Option<&crate::config::VTCodeConfig>,
396) -> Content {
397    let cache_key = cache_key(project_root, vtcode_config, None);
398    let instruction = match PROMPT_CACHE.get(&cache_key) {
399        Some(cached) => cached,
400        None => {
401            let built = compose_system_instruction_text(project_root, vtcode_config, None).await;
402            PROMPT_CACHE.insert(cache_key, built.clone());
403            built
404        }
405    };
406
407    // Apply output style if configured
408    let styled_instruction = apply_output_style(instruction, vtcode_config, project_root).await;
409    Content::system_text(styled_instruction)
410}
411
412/// Generate the stable base system instruction without workspace configuration.
413pub async fn generate_system_instruction_with_guidelines(
414    _config: &SystemPromptConfig,
415    project_root: &Path,
416) -> Content {
417    let cache_key = cache_key(project_root, None, None);
418    let instruction = match PROMPT_CACHE.get(&cache_key) {
419        Some(cached) => cached,
420        None => {
421            let built = compose_system_instruction_text(project_root, None, None).await;
422            PROMPT_CACHE.insert(cache_key, built.clone());
423            built
424        }
425    };
426    // Apply output style if configured
427    let styled_instruction = apply_output_style(instruction, None, project_root).await;
428    Content::system_text(styled_instruction)
429}
430
431/// Apply output style to a generated system instruction
432pub async fn apply_output_style(
433    instruction: String,
434    vtcode_config: Option<&crate::config::VTCodeConfig>,
435    project_root: &Path,
436) -> String {
437    if let Some(config) = vtcode_config {
438        let output_style_applier = OutputStyleApplier::new();
439        if let Err(e) = output_style_applier
440            .load_styles_from_config(config, project_root)
441            .await
442        {
443            tracing::warn!("Failed to load output styles: {}", e);
444            instruction // Return original if loading fails
445        } else {
446            output_style_applier
447                .apply_style(&config.output_style.active_style, &instruction, config)
448                .await
449        }
450    } else {
451        instruction // Return original if no config
452    }
453}
454
455/// Build a cache key for the system prompt.
456///
457/// `catalog_epoch` is the tool-catalog version at the time of the request. When
458/// the tool set changes (e.g. planning workflow is toggled, MCP tools are refreshed), the
459/// epoch advances and the old cached prompt is superseded rather than served stale.
460/// Pass `None` to get the same behaviour as before epoch tracking was introduced.
461fn cache_key(
462    project_root: &Path,
463    vtcode_config: Option<&crate::config::VTCodeConfig>,
464    catalog_epoch: Option<u64>,
465) -> String {
466    use std::collections::hash_map::DefaultHasher;
467    use std::hash::{Hash, Hasher};
468
469    let mut hasher = DefaultHasher::new();
470
471    // Core key: project root
472    project_root.hash(&mut hasher);
473
474    if let Some(cfg) = vtcode_config {
475        // Config fields that affect prompt generation
476        cfg.agent.include_working_directory.hash(&mut hasher);
477        cfg.agent.include_temporal_context.hash(&mut hasher);
478        cfg.prompt_cache
479            .cache_friendly_prompt_shaping
480            .hash(&mut hasher);
481        cfg.agent
482            .include_structured_reasoning_tags
483            .hash(&mut hasher);
484        // Use discriminant since SystemPromptMode doesn't derive Hash
485        std::mem::discriminant(&cfg.agent.system_prompt_mode).hash(&mut hasher);
486    } else {
487        "default".hash(&mut hasher);
488    }
489
490    // Invalidate the cached prompt when the tool catalog changes (planning workflow toggle,
491    // MCP refresh, permission grant/revoke).
492    catalog_epoch.unwrap_or(0).hash(&mut hasher);
493
494    format!("sys_prompt:{:016x}", hasher.finish())
495}
496
497/// Generate a minimal system instruction (pi-inspired, <1K tokens)
498pub fn generate_minimal_instruction() -> Content {
499    Content::system_text(minimal_instruction_text())
500}
501
502/// Generate a lightweight system instruction for simple operations
503pub fn generate_lightweight_instruction() -> Content {
504    Content::system_text(lightweight_instruction_text())
505}
506
507/// Generate a specialized system instruction for advanced operations
508pub fn generate_specialized_instruction() -> Content {
509    Content::system_text(specialized_instruction_text())
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::config::VTCodeConfig;
516    use crate::config::types::SystemPromptMode;
517    use std::path::PathBuf;
518
519    #[tokio::test]
520    async fn test_minimal_mode_selection() {
521        let mut config = VTCodeConfig::default();
522        config.agent.system_prompt_mode = SystemPromptMode::Minimal;
523        // Disable enhancements for base prompt size testing
524        config.agent.include_temporal_context = false;
525        config.agent.include_working_directory = false;
526        config.agent.instruction_max_bytes = 0;
527
528        let result =
529            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
530
531        // Minimal prompt should remain compact and deterministic without AGENTS.md injection
532        assert!(
533            result.len() < 2200,
534            "Minimal mode should produce <2.2K chars (was {} chars)",
535            result.len()
536        );
537        assert!(
538            result.contains("VT Code") || result.contains("VT Code"),
539            "Should contain VT Code identifier"
540        );
541    }
542
543    #[tokio::test]
544    async fn test_default_prompt_selection() {
545        let mut config = VTCodeConfig::default();
546        config.agent.system_prompt_mode = SystemPromptMode::Default;
547        // Disable enhancements for base prompt size testing
548        config.agent.include_temporal_context = false;
549        config.agent.include_working_directory = false;
550        config.agent.instruction_max_bytes = 0;
551
552        let result =
553            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
554
555        assert!(
556            result.len() <= 1700,
557            "Default mode should stay sparse (<=1.7K chars, was {} chars)",
558            result.len()
559        );
560        assert!(result.contains("task_tracker"));
561        assert!(result.contains("@file"));
562        assert!(result.contains("Planning workflow"));
563    }
564
565    #[tokio::test]
566    async fn test_lightweight_mode_selection() {
567        let mut config = VTCodeConfig::default();
568        config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
569        // Disable enhancements for base prompt size testing
570        config.agent.include_temporal_context = false;
571        config.agent.include_working_directory = false;
572        config.agent.instruction_max_bytes = 0;
573
574        let result =
575            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
576
577        assert!(result.len() > 100, "Lightweight should be >100 chars");
578        assert!(
579            result.len() < 1550,
580            "Lightweight should be compact (<1.55K chars, was {} chars)",
581            result.len()
582        );
583        assert!(result.contains("task_tracker"));
584        assert!(result.contains("@file"));
585        assert!(result.contains("Act and verify in one thread"));
586    }
587
588    #[tokio::test]
589    async fn test_lightweight_mode_skips_structured_reasoning_by_default() {
590        let mut config = VTCodeConfig::default();
591        config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
592        config.agent.include_temporal_context = false;
593        config.agent.include_working_directory = false;
594        config.agent.instruction_max_bytes = 0;
595        config.agent.include_structured_reasoning_tags = None;
596
597        let result =
598            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
599
600        assert!(
601            !result.contains("## Structured Reasoning"),
602            "Lightweight mode should omit structured reasoning by default"
603        );
604    }
605
606    #[tokio::test]
607    async fn test_lightweight_mode_allows_explicit_structured_reasoning() {
608        let mut config = VTCodeConfig::default();
609        config.agent.system_prompt_mode = SystemPromptMode::Lightweight;
610        config.agent.include_temporal_context = false;
611        config.agent.include_working_directory = false;
612        config.agent.instruction_max_bytes = 0;
613        config.agent.include_structured_reasoning_tags = Some(true);
614
615        let result =
616            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
617
618        assert!(
619            result.contains("## Structured Reasoning"),
620            "Lightweight mode should include structured reasoning when explicitly enabled"
621        );
622    }
623
624    #[tokio::test]
625    async fn test_default_prompt_omits_structured_reasoning_by_default() {
626        let mut config = VTCodeConfig::default();
627        config.agent.system_prompt_mode = SystemPromptMode::Default;
628        config.agent.include_temporal_context = false;
629        config.agent.include_working_directory = false;
630        config.agent.instruction_max_bytes = 0;
631        config.agent.include_structured_reasoning_tags = None;
632
633        let result =
634            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
635
636        assert!(
637            !result.contains("## Structured Reasoning"),
638            "Default mode should omit structured reasoning by default"
639        );
640    }
641
642    #[tokio::test]
643    async fn test_specialized_mode_selection() {
644        let mut config = VTCodeConfig::default();
645        config.agent.system_prompt_mode = SystemPromptMode::Specialized;
646        // Disable enhancements for base prompt size testing
647        config.agent.include_temporal_context = false;
648        config.agent.include_working_directory = false;
649        config.agent.instruction_max_bytes = 0;
650
651        let result =
652            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
653
654        assert!(
655            result.len() <= 2050,
656            "Specialized should stay sparse (<=2.05K chars, was {} chars)",
657            result.len()
658        );
659        assert!(result.contains("task_tracker"));
660        assert!(result.contains("<proposed_plan>"));
661        assert!(result.contains("ARCHITECTURAL_INVARIANTS"));
662    }
663
664    #[test]
665    fn test_prompt_mode_enum_parsing() {
666        assert_eq!(
667            SystemPromptMode::parse("minimal"),
668            Some(SystemPromptMode::Minimal)
669        );
670        assert_eq!(
671            SystemPromptMode::parse("LIGHTWEIGHT"),
672            Some(SystemPromptMode::Lightweight)
673        );
674        assert_eq!(
675            SystemPromptMode::parse("Default"),
676            Some(SystemPromptMode::Default)
677        );
678        assert_eq!(
679            SystemPromptMode::parse("specialized"),
680            Some(SystemPromptMode::Specialized)
681        );
682        assert_eq!(SystemPromptMode::parse("invalid"), None);
683    }
684
685    #[test]
686    fn test_minimal_prompt_token_count() {
687        // Rough estimate: 1 token ≈ 4 characters
688        let approx_tokens = minimal_system_prompt().len() / 4;
689        assert!(
690            approx_tokens < 220,
691            "Minimal prompt should stay compact, got ~{}",
692            approx_tokens
693        );
694    }
695
696    #[test]
697    fn test_default_prompt_token_count() {
698        let approx_tokens = default_system_prompt().len() / 4;
699        assert!(
700            approx_tokens < 420,
701            "Default prompt should stay compact, got ~{}",
702            approx_tokens
703        );
704    }
705
706    #[tokio::test]
707    async fn test_default_live_prompt_budget_with_instruction_summary() {
708        use crate::project_doc::build_instruction_appendix_with_context;
709
710        let workspace = tempfile::TempDir::new().expect("workspace");
711        std::fs::write(workspace.path().join(".git"), "gitdir: /tmp/git").expect("git marker");
712        std::fs::write(
713            workspace.path().join("AGENTS.md"),
714            "- run ./scripts/check.sh\n- avoid adding to vtcode-core\n- use Conventional Commits\n- start with docs/ARCHITECTURE.md\n",
715        )
716        .expect("write agents");
717        std::fs::create_dir_all(workspace.path().join(".vtcode/rules")).expect("rules dir");
718        std::fs::write(
719            workspace.path().join(".vtcode/rules/rust.md"),
720            "---\npaths:\n  - \"**/*.rs\"\n---\n# Rust\n- keep changes surgical\n",
721        )
722        .expect("write rust rule");
723
724        let mut config = VTCodeConfig::default();
725        config.agent.include_temporal_context = false;
726        config.agent.include_working_directory = false;
727        let base = compose_system_instruction_text(workspace.path(), Some(&config), None).await;
728        let appendix = build_instruction_appendix_with_context(
729            &config.agent,
730            workspace.path(),
731            &[workspace.path().join("src/lib.rs")],
732        )
733        .await
734        .expect("instruction appendix");
735        let prompt = format!("{base}\n\n# INSTRUCTIONS\n{appendix}");
736        let approx_tokens = prompt.len() / 4;
737
738        assert!(prompt.contains("### Instruction map"));
739        assert!(prompt.contains("### On-demand loading"));
740        assert!(approx_tokens <= 1100, "got ~{} tokens", approx_tokens);
741    }
742
743    #[tokio::test]
744    async fn test_generated_prompts_use_task_tracker_not_update_plan() {
745        let project_root = PathBuf::from(".");
746
747        for (mode_name, mode) in [
748            ("default", SystemPromptMode::Default),
749            ("minimal", SystemPromptMode::Minimal),
750            ("specialized", SystemPromptMode::Specialized),
751        ] {
752            let mut config = VTCodeConfig::default();
753            config.agent.system_prompt_mode = mode;
754            config.agent.include_temporal_context = false;
755            config.agent.include_working_directory = false;
756            config.agent.instruction_max_bytes = 0;
757
758            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
759
760            assert!(
761                result.contains("task_tracker"),
762                "{mode_name} prompt should reference task_tracker"
763            );
764            assert!(
765                !result.contains("update_plan"),
766                "{mode_name} prompt should not reference deprecated update_plan"
767            );
768        }
769    }
770
771    #[tokio::test]
772    async fn test_default_and_specialized_prompts_drop_rigid_summary_template() {
773        let project_root = PathBuf::from(".");
774
775        for (mode_name, mode) in [
776            ("default", SystemPromptMode::Default),
777            ("specialized", SystemPromptMode::Specialized),
778        ] {
779            let mut config = VTCodeConfig::default();
780            config.agent.system_prompt_mode = mode;
781            config.agent.include_temporal_context = false;
782            config.agent.include_working_directory = false;
783            config.agent.instruction_max_bytes = 0;
784
785            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
786
787            assert!(
788                !result.contains("References\n"),
789                "{mode_name} prompt should not force a References section"
790            );
791            assert!(
792                !result.contains("Next action"),
793                "{mode_name} prompt should not force a Next action section"
794            );
795            assert!(
796                !result.contains("Scope checkpoint"),
797                "{mode_name} prompt should not require the old plan blueprint bullets"
798            );
799        }
800    }
801
802    #[tokio::test]
803    async fn test_generated_prompts_keep_sparse_execution_contract() {
804        let project_root = PathBuf::from(".");
805
806        for (mode_name, mode) in [
807            ("default", SystemPromptMode::Default),
808            ("minimal", SystemPromptMode::Minimal),
809            ("lightweight", SystemPromptMode::Lightweight),
810            ("specialized", SystemPromptMode::Specialized),
811        ] {
812            let mut config = VTCodeConfig::default();
813            config.agent.system_prompt_mode = mode;
814            config.agent.include_temporal_context = false;
815            config.agent.include_working_directory = false;
816            config.agent.instruction_max_bytes = 0;
817
818            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
819            let normalized = result.to_ascii_lowercase();
820
821            assert!(
822                normalized.contains("compact") || normalized.contains("concise"),
823                "{mode_name} prompt should keep output guidance compact"
824            );
825            assert!(
826                normalized.contains("low-risk") || normalized.contains("reversible"),
827                "{mode_name} prompt should include follow-through guidance"
828            );
829            assert!(
830                normalized.contains("verify") || normalized.contains("validation"),
831                "{mode_name} prompt should include verification guidance"
832            );
833            assert!(
834                normalized.contains("do not guess"),
835                "{mode_name} prompt should gate missing context"
836            );
837            assert!(
838                normalized.contains("unblocked portion")
839                    || normalized.contains("unblocked slices")
840                    || normalized.contains("answerable without a missing detail"),
841                "{mode_name} prompt should require partial progress before clarification"
842            );
843            assert!(
844                normalized.contains("retrieved sources")
845                    || normalized.contains("retrieved evidence"),
846                "{mode_name} prompt should include grounding/citation guidance"
847            );
848            assert!(
849                !result.contains('ƒ'),
850                "{mode_name} prompt should not contain stray prompt characters"
851            );
852        }
853    }
854
855    #[test]
856    fn test_prompt_text_avoids_hardcoded_loop_thresholds() {
857        let specialized_prompt = specialized_instruction_text();
858        assert!(!default_system_prompt().contains("stuck twice"));
859        assert!(!minimal_system_prompt().contains("stuck twice"));
860        assert!(!specialized_prompt.contains("stuck twice"));
861        assert!(!specialized_prompt.contains("10+ calls without progress"));
862        assert!(!specialized_prompt.contains("Same tool+params twice"));
863    }
864
865    #[test]
866    fn test_harness_awareness_in_prompts() {
867        assert!(
868            default_system_prompt().contains("AGENTS.md"),
869            "Default prompt should reference AGENTS.md as map"
870        );
871        assert!(
872            specialized_instruction_text().contains("ARCHITECTURAL_INVARIANTS"),
873            "Specialized prompt should reference architectural invariants"
874        );
875        assert!(
876            minimal_system_prompt().contains("AGENTS.md"),
877            "Minimal prompt should still reference AGENTS.md"
878        );
879    }
880
881    #[test]
882    fn test_prompts_reject_guessing_when_context_is_missing() {
883        assert!(
884            default_system_prompt().contains("do not guess"),
885            "Default prompt should reject guessing"
886        );
887        assert!(
888            specialized_instruction_text().contains("do not guess"),
889            "Specialized prompt should reject guessing"
890        );
891        assert!(
892            minimal_system_prompt().contains("do not guess"),
893            "Minimal prompt should still reject guessing"
894        );
895    }
896
897    #[test]
898    fn test_prompts_include_compaction_preservation_contract() {
899        assert!(
900            default_system_prompt().contains("touched files"),
901            "Default prompt should preserve touched files across compaction"
902        );
903        assert!(
904            default_system_prompt().contains("decisions across compaction"),
905            "Default prompt should preserve decision rationale across compaction"
906        );
907        assert!(
908            default_system_prompt().contains("tracker state"),
909            "Default prompt should preserve tracker state across compaction"
910        );
911        assert!(
912            default_system_prompt().contains("verification status"),
913            "Default prompt should preserve verification status across compaction"
914        );
915        assert!(
916            minimal_system_prompt().contains("touched files"),
917            "Minimal prompt should preserve touched files across compaction"
918        );
919    }
920
921    #[test]
922    fn test_default_prompt_stays_lean_but_complete() {
923        let prompt = default_system_prompt();
924
925        assert!(
926            prompt.contains("## Contract"),
927            "Default prompt should include the lean contract section"
928        );
929        assert!(
930            prompt.contains("Keep outputs concise"),
931            "Default prompt should clamp output shape"
932        );
933        assert!(
934            prompt.contains("Verify changes yourself"),
935            "Default prompt should require verification before finalizing"
936        );
937        assert!(
938            prompt.contains("Keep user updates brief and high-signal"),
939            "Default prompt should constrain progress updates"
940        );
941    }
942
943    #[test]
944    fn test_all_prompt_modes_treat_completion_as_checkpoint_not_proof() {
945        for (mode_name, prompt) in [
946            ("default", default_system_prompt()),
947            ("minimal", minimal_system_prompt()),
948            ("lightweight", default_lightweight_prompt()),
949            ("specialized", specialized_instruction_text().as_str()),
950        ] {
951            assert!(
952                prompt.contains("completion language as a checkpoint")
953                    || prompt.contains("Verify changes yourself")
954                    || prompt.contains("verification"),
955                "{mode_name} prompt should include verification guidance"
956            );
957        }
958    }
959
960    #[test]
961    fn test_prompts_encode_explicit_delegation_contract() {
962        let prompt = default_system_prompt();
963
964        assert!(
965            prompt.contains("Keep control on the main thread"),
966            "Default prompt should keep control on the main thread"
967        );
968        assert!(
969            prompt.contains("Delegate bounded, independent work"),
970            "Default prompt should restrict delegation to bounded independent work"
971        );
972        assert!(
973            minimal_system_prompt().contains("Keep delegation bounded and explicit"),
974            "Minimal prompt should preserve the delegation contract"
975        );
976    }
977
978    #[test]
979    fn test_default_prompt_includes_grounding_and_action_bias() {
980        let prompt = default_system_prompt();
981        assert!(
982            prompt.contains("Never speculate about code you have not opened"),
983            "Default prompt should include grounding guidance"
984        );
985        assert!(
986            prompt.contains("Make only requested changes"),
987            "Default prompt should include anti-overengineering guidance"
988        );
989        assert!(
990            prompt.contains("Implement by default"),
991            "Default prompt should include action bias"
992        );
993    }
994
995    #[test]
996    fn test_default_prompt_omits_accuracy_addendum() {
997        let runtime = tokio::runtime::Runtime::new().expect("runtime");
998        let config = VTCodeConfig::default();
999        let prompt = runtime.block_on(compose_system_instruction_text(
1000            &PathBuf::from("."),
1001            Some(&config),
1002            None,
1003        ));
1004
1005        assert!(
1006            !prompt.contains("## Accuracy Optimization"),
1007            "Runtime prompt should omit the accuracy optimization section"
1008        );
1009        assert!(
1010            prompt.contains("do not guess"),
1011            "Prompt should still preserve the uncertainty guardrail"
1012        );
1013    }
1014
1015    #[test]
1016    fn test_openai_gpt55_contract_addendum_is_specific() {
1017        let addendum = openai_gpt55_contract_addendum();
1018
1019        assert!(addendum.contains(OPENAI_GPT55_CONTRACT_HEADER));
1020        assert!(addendum.contains("outcome, constraints, evidence, and output shape"));
1021        assert!(addendum.contains("smallest missing detail"));
1022        assert!(addendum.contains("brief progress update"));
1023        assert!(addendum.contains("minimum evidence sufficient"));
1024        assert!(!default_system_prompt().contains(OPENAI_GPT55_CONTRACT_HEADER));
1025    }
1026
1027    #[tokio::test]
1028    async fn test_generated_prompts_keep_operating_profiles_bounded() {
1029        let project_root = PathBuf::from(".");
1030
1031        for (mode_name, mode) in [
1032            ("default", SystemPromptMode::Default),
1033            ("minimal", SystemPromptMode::Minimal),
1034            ("lightweight", SystemPromptMode::Lightweight),
1035            ("specialized", SystemPromptMode::Specialized),
1036        ] {
1037            let mut config = VTCodeConfig::default();
1038            config.agent.system_prompt_mode = mode;
1039            config.agent.include_temporal_context = false;
1040            config.agent.include_working_directory = false;
1041            config.agent.instruction_max_bytes = 0;
1042
1043            let result = compose_system_instruction_text(&project_root, Some(&config), None).await;
1044
1045            assert!(
1046                result.contains("## Contract"),
1047                "{mode_name} prompt should reuse the canonical base prompt"
1048            );
1049            assert!(
1050                result.matches("## Operating Profile").count() == 1,
1051                "{mode_name} prompt should add only one operating profile"
1052            );
1053        }
1054    }
1055
1056    #[test]
1057    fn test_search_guidance_prefers_structural_and_rg() {
1058        let guidelines = generate_tool_guidelines(
1059            &["unified_search".to_string(), "unified_exec".to_string()],
1060            None,
1061        );
1062        assert!(
1063            guidelines.contains("Prefer search over shell"),
1064            "Tool guidance should prefer search over shell exploration"
1065        );
1066        assert!(
1067            guidelines.contains("git diff -- <path>"),
1068            "Tool guidance should keep diff guidance explicit"
1069        );
1070    }
1071
1072    // ENHANCEMENT TESTS
1073
1074    #[tokio::test]
1075    async fn test_dynamic_guidelines_read_only() {
1076        use crate::config::types::CapabilityLevel;
1077
1078        let mut config = VTCodeConfig::default();
1079        config.agent.system_prompt_mode = SystemPromptMode::Default;
1080
1081        let mut ctx = PromptContext::default();
1082        ctx.add_tool("unified_search".to_string());
1083        ctx.capability_level = Some(CapabilityLevel::FileReading);
1084
1085        let result =
1086            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1087
1088        assert!(
1089            result.contains("Capabilities: read-only"),
1090            "Should detect read-only capabilities when no edit/write/exec tools available"
1091        );
1092        assert!(
1093            result.contains("do not modify files"),
1094            "Should explain read-only constraints"
1095        );
1096    }
1097
1098    #[tokio::test]
1099    async fn test_dynamic_guidelines_tool_preferences() {
1100        let config = VTCodeConfig::default();
1101
1102        let mut ctx = PromptContext::default();
1103        ctx.add_tool("unified_exec".to_string());
1104        ctx.add_tool("unified_search".to_string());
1105        ctx.add_tool("unified_file".to_string());
1106
1107        let result =
1108            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1109
1110        assert!(
1111            result.contains("unified_search") || result.contains("unified_file"),
1112            "Should suggest canonical search/file tools"
1113        );
1114    }
1115
1116    #[tokio::test]
1117    async fn test_live_prompt_renders_workspace_language_hints() {
1118        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1119        std::fs::create_dir_all(workspace.path().join("src")).expect("create src");
1120        std::fs::create_dir_all(workspace.path().join("web")).expect("create web");
1121        std::fs::write(workspace.path().join("src/lib.rs"), "fn alpha() {}\n").expect("write rust");
1122        std::fs::write(workspace.path().join("web/app.ts"), "const app = 1;\n").expect("write ts");
1123
1124        let config = VTCodeConfig::default();
1125        let ctx = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
1126        let result =
1127            compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
1128
1129        assert!(result.contains("## Environment"));
1130        assert!(result.contains("Rust, TypeScript"));
1131        assert!(result.contains("structural-search `lang`"));
1132    }
1133
1134    #[tokio::test]
1135    async fn test_live_prompt_omits_workspace_language_hints_without_languages() {
1136        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1137        let config = VTCodeConfig::default();
1138        let ctx = PromptContext::from_workspace_tools(workspace.path(), ["unified_search"]);
1139        let result =
1140            compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
1141
1142        assert!(!result.contains("Languages:"));
1143    }
1144
1145    #[tokio::test]
1146    async fn test_live_prompt_omits_project_docs_and_user_instructions_from_base_prompt() {
1147        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1148        std::fs::write(
1149            workspace.path().join("AGENTS.md"),
1150            "- Root summary\n\nFollow the root guidance.\n",
1151        )
1152        .expect("write agents");
1153
1154        let mut config = VTCodeConfig::default();
1155        config.agent.user_instructions = Some("keep responses terse".to_string());
1156        config.agent.include_temporal_context = false;
1157        config.agent.include_working_directory = false;
1158        config.agent.instruction_max_bytes = 4096;
1159
1160        let result = compose_system_instruction_text(workspace.path(), Some(&config), None).await;
1161
1162        assert!(!result.contains("## AGENTS.MD INSTRUCTION HIERARCHY"));
1163        assert!(!result.contains("### Instruction map"));
1164        assert!(!result.contains("### Key points"));
1165        assert!(!result.contains("keep responses terse"));
1166        assert!(!result.contains("Root summary"));
1167        assert!(!result.contains("Follow the root guidance."));
1168    }
1169
1170    #[tokio::test]
1171    async fn test_workspace_prompt_resources_override_base_and_keep_dynamic_sections() {
1172        use crate::skills::model::{SkillMetadata, SkillScope};
1173
1174        let workspace = tempfile::TempDir::new().expect("workspace tempdir");
1175        let prompts_dir = workspace.path().join(".vtcode/prompts");
1176        std::fs::create_dir_all(&prompts_dir).expect("create prompts dir");
1177        std::fs::write(prompts_dir.join("system.md"), "# Workspace system base").expect("system");
1178        std::fs::write(
1179            prompts_dir.join("append-system.md"),
1180            "Workspace prompt appendix",
1181        )
1182        .expect("append");
1183
1184        let mut config = VTCodeConfig::default();
1185        config.agent.include_temporal_context = false;
1186        config.agent.include_working_directory = true;
1187
1188        let mut ctx = PromptContext::default();
1189        ctx.add_tool("unified_search".to_string());
1190        ctx.add_skill_metadata(SkillMetadata {
1191            name: "skill-creator".to_string(),
1192            description: "Create skills".to_string(),
1193            short_description: None,
1194            path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
1195            scope: SkillScope::System,
1196            manifest: None,
1197        });
1198        ctx.set_current_directory(workspace.path().to_path_buf());
1199
1200        let result =
1201            compose_system_instruction_text(workspace.path(), Some(&config), Some(&ctx)).await;
1202
1203        assert!(result.starts_with("# Workspace system base"));
1204        assert!(result.contains("Workspace prompt appendix"));
1205        assert!(result.contains("## Active Tools"));
1206        assert!(result.contains("## Skills"));
1207        assert!(result.contains("## Environment"));
1208
1209        let appendix_pos = result
1210            .find("Workspace prompt appendix")
1211            .expect("append text");
1212        let tools_pos = result.find("## Active Tools").expect("tools section");
1213        let skills_pos = result.find("## Skills").expect("skills section");
1214        let env_pos = result.find("## Environment").expect("environment section");
1215
1216        assert!(appendix_pos < tools_pos);
1217        assert!(tools_pos < skills_pos);
1218        assert!(skills_pos < env_pos);
1219    }
1220
1221    #[tokio::test]
1222    async fn test_temporal_context_inclusion() {
1223        let mut config = VTCodeConfig::default();
1224        config.agent.include_temporal_context = true;
1225        config.prompt_cache.cache_friendly_prompt_shaping = false;
1226        config.agent.temporal_context_use_utc = false; // Local time
1227
1228        let result =
1229            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1230
1231        assert!(
1232            result.contains("Time:"),
1233            "Should include temporal context when enabled"
1234        );
1235        let env_pos = result.find("## Environment");
1236        let temporal_pos = result.find("Time:");
1237        if let (Some(t), Some(e)) = (temporal_pos, env_pos) {
1238            assert!(
1239                t > e,
1240                "Temporal context should appear inside the environment section"
1241            );
1242        }
1243    }
1244
1245    #[tokio::test]
1246    async fn test_temporal_context_utc_format() {
1247        let mut config = VTCodeConfig::default();
1248        config.agent.include_temporal_context = true;
1249        config.prompt_cache.cache_friendly_prompt_shaping = false;
1250        config.agent.temporal_context_use_utc = true; // UTC format
1251
1252        let result =
1253            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1254
1255        assert!(
1256            result.contains("UTC"),
1257            "Should indicate UTC when temporal_context_use_utc is true"
1258        );
1259        assert!(
1260            result.contains("T") && result.contains("Z"),
1261            "Should use RFC3339 format for UTC (contains T and Z)"
1262        );
1263    }
1264
1265    #[tokio::test]
1266    async fn test_temporal_context_disabled() {
1267        let mut config = VTCodeConfig::default();
1268        config.agent.include_temporal_context = false;
1269
1270        let result =
1271            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1272
1273        assert!(
1274            !result.contains("Time:"),
1275            "Should not include temporal context when disabled"
1276        );
1277    }
1278
1279    #[tokio::test]
1280    async fn test_cache_friendly_temporal_context_stays_out_of_base_prompt() {
1281        let mut config = VTCodeConfig::default();
1282        config.agent.include_temporal_context = true;
1283        config.prompt_cache.cache_friendly_prompt_shaping = true;
1284
1285        let result =
1286            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1287
1288        assert!(
1289            !result.contains("Time:"),
1290            "Stable system prompt should omit temporal context when cache-friendly shaping is enabled"
1291        );
1292    }
1293
1294    #[tokio::test]
1295    async fn test_configuration_awareness_stays_behavior_focused() {
1296        let mut config = VTCodeConfig::default();
1297        config.security.human_in_the_loop = true;
1298        config.chat.ask_questions.enabled = false;
1299        config.mcp.enabled = true;
1300        config.ide_context.enabled = true;
1301        config.ide_context.inject_into_prompt = true;
1302
1303        let result =
1304            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1305
1306        assert!(result.contains("## Environment"));
1307        assert!(result.contains("Interaction: approval may gate sensitive actions"));
1308        assert!(result.contains("request_user_input"));
1309        assert!(result.contains("Sources: prefer MCP"));
1310        assert!(!result.contains("PTY functionality"));
1311        assert!(!result.contains("Loop guards"));
1312        assert!(!result.contains(".vtcode/context/tool_outputs/"));
1313        assert!(!result.contains("IDE context:"));
1314    }
1315
1316    #[tokio::test]
1317    async fn test_configuration_awareness_mentions_reduced_approval_when_disabled() {
1318        let mut config = VTCodeConfig::default();
1319        config.security.human_in_the_loop = false;
1320
1321        let result =
1322            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1323
1324        assert!(result.contains("Interaction: approval reduced by config"));
1325    }
1326
1327    #[tokio::test]
1328    async fn test_default_environment_omits_default_interaction_guidance() {
1329        let config = VTCodeConfig::default();
1330
1331        let result =
1332            compose_system_instruction_text(&PathBuf::from("."), Some(&config), None).await;
1333
1334        assert!(
1335            !result.contains("Interaction:"),
1336            "Default-on interaction guidance should stay out of the prompt"
1337        );
1338    }
1339
1340    #[tokio::test]
1341    async fn test_working_directory_inclusion() {
1342        let mut config = VTCodeConfig::default();
1343        config.agent.include_working_directory = true;
1344
1345        let mut ctx = PromptContext::default();
1346        ctx.set_current_directory(PathBuf::from("/tmp/test"));
1347
1348        let result =
1349            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1350
1351        assert!(
1352            result.contains("Working directory"),
1353            "Should include working directory label"
1354        );
1355        assert!(
1356            result.contains("/tmp/test"),
1357            "Should show actual directory path"
1358        );
1359        let wd_pos = result.find("Working directory");
1360        let env_pos = result.find("## Environment");
1361        if let (Some(w), Some(e)) = (wd_pos, env_pos) {
1362            assert!(
1363                w > e,
1364                "Working directory should appear inside the environment section"
1365            );
1366        }
1367    }
1368
1369    #[tokio::test]
1370    async fn test_working_directory_disabled() {
1371        let mut config = VTCodeConfig::default();
1372        config.agent.include_working_directory = false;
1373
1374        let mut ctx = PromptContext::default();
1375        ctx.set_current_directory(PathBuf::from("/tmp/test"));
1376
1377        let result =
1378            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1379
1380        assert!(
1381            !result.contains("Working directory"),
1382            "Should not include working directory when disabled"
1383        );
1384    }
1385
1386    #[tokio::test]
1387    async fn test_backward_compatibility() {
1388        let config = VTCodeConfig::default();
1389
1390        // Old signature: no prompt context
1391        let result = compose_system_instruction_text(
1392            &PathBuf::from("."),
1393            Some(&config),
1394            None, // No context - backward compatible
1395        )
1396        .await;
1397
1398        // Should still work without new features
1399        assert!(result.len() > 600, "Should generate substantial prompt");
1400        assert!(
1401            result.contains("VT Code"),
1402            "Should contain base prompt content"
1403        );
1404        // Should not have dynamic guidelines without context
1405        assert!(
1406            !result.contains("## Active Tools"),
1407            "Should not have tool guidelines without prompt context"
1408        );
1409    }
1410
1411    #[tokio::test]
1412    async fn test_all_enhancements_combined() {
1413        use crate::skills::model::{SkillMetadata, SkillScope};
1414
1415        let mut config = VTCodeConfig::default();
1416        config.agent.include_temporal_context = true;
1417        config.agent.include_working_directory = true;
1418        config.prompt_cache.cache_friendly_prompt_shaping = false;
1419
1420        let mut ctx = PromptContext::default();
1421        ctx.add_tool("unified_file".to_string());
1422        ctx.add_tool("unified_search".to_string());
1423        ctx.infer_capability_level();
1424        ctx.set_current_directory(PathBuf::from("/workspace"));
1425        ctx.add_skill_metadata(SkillMetadata {
1426            name: "rust-skills".to_string(),
1427            description: "Rust coding guidance".to_string(),
1428            short_description: None,
1429            path: PathBuf::from("/tmp/rust-skills/SKILL.md"),
1430            scope: SkillScope::System,
1431            manifest: None,
1432        });
1433
1434        let result =
1435            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1436
1437        // Verify all enhancements present
1438        assert!(
1439            result.contains("## Active Tools"),
1440            "Should have dynamic guidelines"
1441        );
1442        assert!(
1443            result.contains("## Skills"),
1444            "Should have lean skills routing"
1445        );
1446        assert!(
1447            result.contains("## Environment"),
1448            "Should have environment addenda"
1449        );
1450        assert!(result.contains("Time:"), "Should have temporal context");
1451        assert!(
1452            result.contains("Working directory"),
1453            "Should have working directory"
1454        );
1455        assert!(result.contains("/workspace"), "Should show workspace path");
1456
1457        // Verify specific guideline for this tool set
1458        assert!(
1459            result.contains("Read before edit"),
1460            "Should have read-before-edit guideline"
1461        );
1462    }
1463
1464    #[tokio::test]
1465    async fn test_prompt_layers_render_in_stable_order() {
1466        use crate::skills::model::{SkillMetadata, SkillScope};
1467
1468        let mut config = VTCodeConfig::default();
1469        config.agent.include_temporal_context = true;
1470        config.agent.include_working_directory = true;
1471
1472        let mut ctx = PromptContext::default();
1473        ctx.add_tool("unified_search".to_string());
1474        ctx.add_tool("unified_exec".to_string());
1475        ctx.add_skill_metadata(SkillMetadata {
1476            name: "skill-creator".to_string(),
1477            description: "Create skills".to_string(),
1478            short_description: None,
1479            path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
1480            scope: SkillScope::System,
1481            manifest: None,
1482        });
1483        ctx.add_language("Rust".to_string());
1484        ctx.set_current_directory(PathBuf::from("/workspace"));
1485
1486        let result =
1487            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1488
1489        let mode_pos = result
1490            .find("## Operating Profile")
1491            .expect("operating profile section");
1492        let tools_pos = result.find("## Active Tools").expect("tools section");
1493        let skills_pos = result.find("## Skills").expect("skills section");
1494        let env_pos = result.find("## Environment").expect("environment section");
1495
1496        assert!(
1497            mode_pos < tools_pos,
1498            "operating profile should precede tools"
1499        );
1500        assert!(tools_pos < skills_pos, "tools should precede skills");
1501        assert!(skills_pos < env_pos, "skills should precede environment");
1502    }
1503
1504    #[tokio::test]
1505    async fn test_skills_section_stays_lean_and_routing_focused() {
1506        use crate::skills::model::SkillScope;
1507        use crate::skills::types::SkillManifest;
1508
1509        let config = VTCodeConfig::default();
1510        let mut ctx = PromptContext::default();
1511        ctx.available_skill_metadata
1512            .push(crate::skills::model::SkillMetadata {
1513                name: "skill-creator".to_string(),
1514                description: "Create or update skills".to_string(),
1515                short_description: None,
1516                path: PathBuf::from("/tmp/skill-creator/SKILL.md"),
1517                scope: SkillScope::System,
1518                manifest: Some(
1519                    SkillManifest {
1520                        when_to_use: Some("Use when creating or updating a skill.".to_string()),
1521                        when_not_to_use: Some(
1522                            "Avoid for unrelated implementation work.".to_string(),
1523                        ),
1524                        ..SkillManifest::default()
1525                    }
1526                    .into(),
1527                ),
1528            });
1529
1530        let result =
1531            compose_system_instruction_text(&PathBuf::from("."), Some(&config), Some(&ctx)).await;
1532
1533        assert!(result.contains("## Skills"));
1534        assert!(result.contains("skill-creator: Create or update skills"));
1535        assert!(result.contains("Use a skill only when the user names it"));
1536        assert!(!result.contains("Discovery: Available skills are listed"));
1537        assert!(!result.contains("/tmp/skill-creator/SKILL.md"));
1538        assert!(!result.contains("use: Use when creating or updating a skill."));
1539        assert!(!result.contains("avoid: Avoid for unrelated implementation work."));
1540    }
1541
1542    #[test]
1543    fn test_static_prompts_have_no_placeholders() {
1544        let _minimal = generate_minimal_instruction();
1545        let _lightweight = generate_lightweight_instruction();
1546        let _specialized = generate_specialized_instruction();
1547
1548        let minimal_text = minimal_instruction_text();
1549        let lightweight_text = lightweight_instruction_text();
1550        let specialized_text = specialized_instruction_text();
1551
1552        assert!(
1553            !minimal_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
1554            "Minimal prompt has uninterpolated placeholder"
1555        );
1556        assert!(
1557            !lightweight_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
1558            "Lightweight prompt has uninterpolated placeholder"
1559        );
1560        assert!(
1561            !specialized_text.contains("__UNIFIED_TOOL_GUIDANCE__"),
1562            "Specialized prompt has uninterpolated placeholder"
1563        );
1564        assert!(
1565            !default_system_prompt().contains("__UNIFIED_TOOL_GUIDANCE__"),
1566            "Default prompt has uninterpolated placeholder"
1567        );
1568    }
1569}