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