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