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