vtcode 0.173.2

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use super::*;

fn test_context() -> SystemPromptContext {
    SystemPromptContext {
        full_auto: false,
        planning_active: false,
        request_user_input_enabled: true,
        discovered_skills: Vec::new(),
        active_instruction_directory: None,
        instruction_context_paths: Vec::new(),
    }
}

#[tokio::test]
async fn interactive_planning_contract_survives_tool_density_replacements() {
    use vtcode_core::config::types::{ResolvedShellPromptProfile, ShellPromptProfile, SystemPromptMode};
    use vtcode_core::config::{
        VTCodeConfig,
        constants::{models, tools},
    };
    use vtcode_core::core::agent::harness_kernel::SessionToolCatalogSnapshot;
    use vtcode_core::llm::{provider::ToolDefinition, providers::OpenAIProvider};
    use vtcode_core::prompts::system::*;
    use vtcode_core::prompts::{PromptContext, append_runtime_tool_prompt_sections_for_model};

    let workspace = tempfile::TempDir::new().expect("workspace");
    let provider = OpenAIProvider::new("offline-fixture".into());
    for mode in [
        SystemPromptMode::Default,
        SystemPromptMode::Minimal,
        SystemPromptMode::Lightweight,
        SystemPromptMode::Specialized,
    ] {
        for environment in [false, true] {
            for request_user_input_enabled in [false, true] {
                let mut names = vec![tools::EXEC_COMMAND, tools::CODE_SEARCH, tools::TASK_TRACKER];
                if request_user_input_enabled {
                    names.push(tools::REQUEST_USER_INPUT);
                }
                let snapshot = SessionToolCatalogSnapshot::new(
                    7,
                    9,
                    true,
                    request_user_input_enabled,
                    Some(Arc::new(
                        names
                            .iter()
                            .map(|name| {
                                ToolDefinition::function(
                                    (*name).to_string(),
                                    "Fixture tool".to_string(),
                                    serde_json::json!({"type": "object"}),
                                )
                            })
                            .collect(),
                    )),
                    false,
                );
                let mut config = VTCodeConfig::default();
                config.agent.system_prompt_mode = mode;
                config.agent.include_temporal_context = false;
                config.agent.include_working_directory = environment;
                config.agent.instruction_max_bytes = 0;
                config.agent.shell_prompt_profile = ShellPromptProfile::UnixLike;
                let mut context = PromptContext {
                    available_tools: names.iter().map(|name| (*name).to_string()).collect(),
                    ..Default::default()
                };
                context.set_current_directory(PathBuf::from("/workspace"));
                let base = compose_system_instruction_text(workspace.path(), Some(&config), Some(&context)).await;
                let mut runtime_context = test_context();
                runtime_context.planning_active = true;
                runtime_context.request_user_input_enabled = request_user_input_enabled;
                let builder = IncrementalSystemPrompt::new();
                let initial = builder
                    .get_system_prompt(
                        &base,
                        hash_base_system_prompt(&base),
                        runtime_context.hash(),
                        &runtime_context,
                        None,
                    )
                    .await;
                for budget in [100_000, 1] {
                    config.agent.max_system_prompt_tokens = budget;
                    let mut prompt = initial.clone();
                    let replace = |prompt: &mut String| {
                        append_runtime_tool_prompt_sections_for_model(
                            prompt,
                            &snapshot,
                            true,
                            ResolvedShellPromptProfile::UnixLike,
                            &provider,
                            models::openai::DEFAULT_MODEL,
                            Some(&config),
                        )
                    };
                    replace(&mut prompt);
                    let once = prompt.clone();
                    replace(&mut prompt);
                    assert_eq!(
                        prompt, once,
                        "{mode:?}, environment={environment}, input={request_user_input_enabled}, budget={budget}"
                    );
                    for line in [
                        PLANNING_WORKFLOW_PLAN_PERSISTENCE_POLICY_LINE,
                        PLANNING_WORKFLOW_PLAN_QUALITY_LINE,
                        PLANNING_WORKFLOW_RESEARCH_SCOPE_LINE,
                        PLANNING_WORKFLOW_PLAN_POLICY_LINE,
                    ] {
                        assert_eq!(prompt.matches(line).count(), 1, "missing or repeated canonical planning line");
                    }
                    assert_eq!(prompt.matches("## Active Tools").count(), 1);
                    assert_eq!(prompt.matches("## Environment").count(), usize::from(environment));
                    assert_eq!(
                        prompt.contains(PLANNING_WORKFLOW_NO_REQUEST_USER_INPUT_POLICY_LINE),
                        !request_user_input_enabled
                    );
                    for duplicate in [
                        "Monitor the available planning tool-loop budget",
                        "Every implementation step in the final plan must",
                        "emit only one `<proposed_plan>` block",
                        "Stop research when the plan is specified or the budget is near",
                    ] {
                        assert!(!prompt.contains(duplicate), "repeated planning guidance: {duplicate}");
                    }
                    assert!(prompt.contains("index 0 is invalid while planning"));
                    assert!(prompt.contains("omit unused filters"));
                    assert_eq!(prompt.contains("- Planning is read-only."), budget == 1);
                }
            }
        }
    }
}

#[tokio::test]
async fn test_incremental_prompt_caching() {
    let prompt_builder = IncrementalSystemPrompt::new();
    let base_prompt = "Test system prompt";
    let context = test_context();

    let prompt1 = prompt_builder
        .get_system_prompt(base_prompt, 1, context.hash(), &context, None)
        .await;
    let prompt2 = prompt_builder
        .get_system_prompt(base_prompt, 1, context.hash(), &context, None)
        .await;

    assert_eq!(prompt1, prompt2);
    assert!(prompt1.contains("Test system prompt"));
    assert!(!prompt1.contains("[Context]"));
    assert!(!prompt1.contains("[Runtime Context]"));

    let (is_cached, size) = prompt_builder.cache_stats().await;
    assert!(is_cached);
    assert!(size >= base_prompt.len());
}

#[tokio::test]
async fn test_base_prompt_hash_is_stable() {
    assert_eq!(hash_base_system_prompt("Test"), hash_base_system_prompt("Test"));
}

#[tokio::test]
async fn test_instruction_appendix_uses_explicit_directory() {
    let prompt_builder = IncrementalSystemPrompt::new();
    let workspace = tempfile::TempDir::new().expect("workspace");
    std::fs::write(workspace.path().join(".git"), "gitdir: /tmp/git").expect("write git");
    std::fs::write(workspace.path().join("AGENTS.md"), "root rule").expect("write root");
    let nested = workspace.path().join("nested/sub");
    std::fs::create_dir_all(&nested).expect("create nested");
    std::fs::write(nested.join("AGENTS.md"), "nested rule").expect("write nested");

    let config = vtcode_config::core::AgentConfig {
        user_instructions: Some("be brief".to_string()),
        instruction_max_bytes: 4096,
        include_temporal_context: true,
        temporal_context_use_utc: true,
        ..Default::default()
    };

    let context = SystemPromptContext {
        active_instruction_directory: Some(nested.clone()),
        instruction_context_paths: vec![nested.join("file.rs")],
        ..test_context()
    };

    let prompt = prompt_builder
        .get_system_prompt("Stable base prompt", 1, context.hash(), &context, Some(&config))
        .await;

    assert!(prompt.contains("be brief"));
    assert!(prompt.contains("### Instruction map"));
    assert!(prompt.contains("AGENTS.md (workspace AGENTS)"));
    assert!(prompt.contains("nested/sub/AGENTS.md (workspace AGENTS)"));
    assert!(prompt.contains("root rule"));
    assert!(prompt.contains("nested rule"));
    assert!(!prompt.contains("[Runtime Context]"));
    assert!(!prompt.contains("Time (UTC):"));
}

#[tokio::test]
async fn test_prompt_omits_runtime_context_sections() {
    let prompt_builder = IncrementalSystemPrompt::new();
    let agent_config = vtcode_config::core::AgentConfig {
        include_temporal_context: true,
        temporal_context_use_utc: true,
        ..Default::default()
    };
    let context = test_context();
    let prompt = prompt_builder
        .get_system_prompt("You are a helpful assistant.", 1, context.hash(), &context, Some(&agent_config))
        .await;

    assert!(!prompt.contains("[Context]"));
    assert!(!prompt.contains("[Runtime Context]"));
    assert!(!prompt.contains("Retry #"));
    assert!(!prompt.contains("task_tracker"));
    assert!(!prompt.contains("Time (UTC):"));
    assert!(!prompt.contains("<budget:token_budget>"));
    assert!(!prompt.contains("<system_warning>"));
    assert!(!prompt.contains("token_usage:"));
}

#[tokio::test]
async fn test_planning_workflow_notice_appended() {
    let prompt_builder = IncrementalSystemPrompt::new();
    let context = SystemPromptContext { planning_active: true, ..test_context() };

    let prompt = prompt_builder
        .get_system_prompt("You are a helpful assistant.", 1, context.hash(), &context, None)
        .await;

    assert!(prompt.contains(vtcode_core::prompts::system::PLANNING_WORKFLOW_READ_ONLY_HEADER));
    assert!(prompt.contains(vtcode_core::prompts::system::PLANNING_WORKFLOW_EXIT_INSTRUCTION_LINE));
    assert!(prompt.contains(vtcode_core::prompts::system::PLANNING_WORKFLOW_PLAN_QUALITY_LINE));
    assert!(prompt.contains("<proposed_plan>"));
    assert!(prompt.contains("Next open decision"));
    assert!(!prompt.contains("Scope checkpoint"));
    assert!(prompt.contains(vtcode_core::prompts::system::PLANNING_WORKFLOW_NO_AUTO_EXIT_LINE));
    assert!(prompt.contains(vtcode_core::prompts::system::PLANNING_WORKFLOW_TASK_TRACKER_LINE));
    assert!(!prompt.contains("[Context]"));
}

#[tokio::test]
async fn test_planning_workflow_uses_plan_notice_unconditionally() {
    for request_user_input_enabled in [true, false] {
        let prompt_builder = IncrementalSystemPrompt::new();
        let context = SystemPromptContext {
            planning_active: true,
            request_user_input_enabled,
            ..test_context()
        };

        let prompt = prompt_builder
            .get_system_prompt("You are a helpful assistant.", 1, context.hash(), &context, None)
            .await;

        assert!(prompt.contains(vtcode_core::prompts::system::PLANNING_WORKFLOW_PLAN_POLICY_LINE));
    }
}

#[tokio::test]
async fn test_full_auto_is_constrained_in_planning_workflow() {
    let prompt_builder = IncrementalSystemPrompt::new();
    let context = SystemPromptContext {
        full_auto: true,
        planning_active: true,
        ..test_context()
    };

    let prompt = prompt_builder
        .get_system_prompt("You are a helpful assistant.", 1, context.hash(), &context, None)
        .await;

    assert!(
        prompt.contains("# FULL-AUTO (PLANNING WORKFLOW): Work autonomously within planning workflow constraints.")
    );
    assert!(!prompt.contains("# FULL-AUTO: Complete task autonomously until done or blocked."));
}

#[tokio::test]
async fn test_mode_changes_invalidate_cached_prompt() {
    let prompt_builder = IncrementalSystemPrompt::new();
    let base_prompt = "Base prompt";
    let base_context = test_context();
    let full_auto_context = SystemPromptContext { full_auto: true, ..test_context() };

    let base = prompt_builder
        .get_system_prompt(base_prompt, 1, base_context.hash(), &base_context, None)
        .await;
    let full_auto = prompt_builder
        .get_system_prompt(base_prompt, 1, full_auto_context.hash(), &full_auto_context, None)
        .await;

    assert_ne!(base, full_auto);
    assert!(!base.contains("# FULL-AUTO:"));
    assert!(full_auto.contains("# FULL-AUTO: Complete task autonomously until done or blocked."));
}