1use 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
23pub const PLAN_MODE_READ_ONLY_HEADER: &str = "# PLAN MODE (READ-ONLY)";
25pub 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).";
27pub const PLAN_MODE_EXIT_INSTRUCTION_LINE: &str =
29 "Call `exit_plan_mode` when ready to transition to implementation.";
30pub 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: ...`.";
32pub 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>`.";
34pub 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.";
36pub const PLAN_MODE_NO_AUTO_EXIT_LINE: &str = "Do not auto-exit Plan Mode just because a plan exists; wait for explicit implementation intent.";
38pub const PLAN_MODE_TASK_TRACKER_LINE: &str =
40 "`task_tracker` remains available in Plan Mode (`plan_task_tracker` is a compatibility alias).";
41pub 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#[derive(Debug, Clone, Default)]
166pub struct SystemPromptConfig;
167
168pub async fn generate_system_instruction(_config: &SystemPromptConfig) -> Content {
170 let instruction = default_system_prompt().to_string();
172
173 if let Ok(current_dir) = env::current_dir() {
175 let styled_instruction = apply_output_style(instruction, None, ¤t_dir).await;
176 Content::system_text(styled_instruction)
177 } else {
178 Content::system_text(instruction)
179 }
180}
181
182pub 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
195pub 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, "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 matches!(mode, SystemPromptMode::Specialized)
384}
385
386pub 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 let styled_instruction = apply_output_style(instruction, vtcode_config, project_root).await;
407 Content::system_text(styled_instruction)
408}
409
410pub 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 let styled_instruction = apply_output_style(instruction, None, project_root).await;
426 Content::system_text(styled_instruction)
427}
428
429pub 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 } else {
444 output_style_applier
445 .apply_style(&config.output_style.active_style, &instruction, config)
446 .await
447 }
448 } else {
449 instruction }
451}
452
453fn 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 project_root.hash(&mut hasher);
471
472 if let Some(cfg) = vtcode_config {
473 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 std::mem::discriminant(&cfg.agent.system_prompt_mode).hash(&mut hasher);
484 } else {
485 "default".hash(&mut hasher);
486 }
487
488 catalog_epoch.unwrap_or(0).hash(&mut hasher);
491
492 format!("sys_prompt:{:016x}", hasher.finish())
493}
494
495pub fn generate_minimal_instruction() -> Content {
497 Content::system_text(minimal_instruction_text())
498}
499
500pub fn generate_lightweight_instruction() -> Content {
502 Content::system_text(lightweight_instruction_text())
503}
504
505pub 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 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 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 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 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 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 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 #[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; 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; 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 let result = compose_system_instruction_text(
1390 &PathBuf::from("."),
1391 Some(&config),
1392 None, )
1394 .await;
1395
1396 assert!(result.len() > 600, "Should generate substantial prompt");
1398 assert!(
1399 result.contains("VT Code"),
1400 "Should contain base prompt content"
1401 );
1402 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 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 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}