vv-agent 0.7.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::path::PathBuf;
use std::process::{Command, Output};

use serde_json::Value;
use sha2::{Digest, Sha256};
use vv_agent::cli::{
    build_cli_task_from_resolved, parse_cli_args_from_with_default_settings,
    parse_cli_command_from_with_default_settings, result_payload, CliCommand, DebugCliCommand,
};
use vv_agent::{AgentResult, AgentStatus, ResolvedModelConfig};

const CONTRACT_SOURCE: &str = include_str!("cli_contract_v1.json");
const CONTRACT_SHA256: &str = "029e2357351d7029932907bd84448dda92b0dc220eac5f215acd7eb69dee6d09";

fn contract() -> Value {
    assert_eq!(
        format!("{:x}", Sha256::digest(CONTRACT_SOURCE.as_bytes())),
        CONTRACT_SHA256
    );
    serde_json::from_str(CONTRACT_SOURCE).expect("CLI contract fixture")
}

fn resolved() -> ResolvedModelConfig {
    ResolvedModelConfig::new(
        "deepseek",
        "deepseek-v4-pro",
        "deepseek-v4-pro",
        "deepseek-v4-pro",
        vec![],
    )
    .with_token_limits(Some(1_000_000), Some(384_000))
    .with_capabilities(true, true, true)
}

fn result(status: AgentStatus, error: Option<&str>) -> AgentResult {
    AgentResult {
        status,
        completion_reason: match status {
            AgentStatus::Completed => Some(vv_agent::CompletionReason::ToolFinish),
            AgentStatus::WaitUser => Some(vv_agent::CompletionReason::WaitUser),
            AgentStatus::Failed => Some(vv_agent::CompletionReason::Failed),
            AgentStatus::MaxCycles => Some(vv_agent::CompletionReason::MaxCycles),
            AgentStatus::Pending | AgentStatus::Running | AgentStatus::ReconciliationRequired => {
                None
            }
        },
        completion_tool_name: None,
        partial_output: None,
        final_answer: (status == AgentStatus::Completed).then(|| "done".to_string()),
        wait_reason: None,
        error: error.map(str::to_string),
        messages: vec![],
        cycles: vec![],
        shared_state: Default::default(),
        token_usage: Default::default(),
        budget_usage: None,
        budget_exhaustion: None,
        checkpoint_key: None,
        resume_observation: None,
    }
}

fn run_with_settings_environment(
    args: &[&str],
    primary: Option<&str>,
    legacy: Option<&str>,
) -> Output {
    let mut command = Command::new(env!("CARGO_BIN_EXE_vv-agent"));
    command.args(args);
    command.env_remove("VV_AGENT_LOCAL_SETTINGS");
    command.env_remove("V_AGENT_LOCAL_SETTINGS");
    if let Some(value) = primary {
        command.env("VV_AGENT_LOCAL_SETTINGS", value);
    }
    if let Some(value) = legacy {
        command.env("V_AGENT_LOCAL_SETTINGS", value);
    }
    command.output().expect("run vv-agent")
}

#[test]
fn cli_contract_fixture_is_reviewable() {
    let value = contract();
    assert_eq!(value["contract"], "vv-agent-cli-v1");
    assert_eq!(value["scope"], "direct-task");
}

#[test]
fn multiword_prompt_model_settings_and_resolved_limits_project_to_task() {
    let contract = contract();
    let argv = contract["argument_projection"]["argv"]
        .as_array()
        .expect("argv")
        .iter()
        .map(|value| value.as_str().expect("string arg"))
        .collect::<Vec<_>>();
    let args = parse_cli_args_from_with_default_settings(
        std::iter::once("vv-agent").chain(argv),
        "local_settings.json",
    )
    .expect("parse contract args");

    let task = build_cli_task_from_resolved(&args, &resolved(), "task_fixed").expect("task");

    let expected = &contract["argument_projection"]["task"];
    assert_eq!(task.user_prompt, expected["user_prompt"]);
    assert_eq!(task.max_cycles, expected["max_cycles"]);
    assert_eq!(task.agent_type.as_deref(), expected["agent_type"].as_str());
    assert_eq!(
        serde_json::to_value(task.model_settings.expect("model settings")).expect("settings value"),
        expected["model_settings"]
    );
    let projection = &contract["resolved_model_projection"]["task"];
    assert_eq!(task.native_multimodal, projection["native_multimodal"]);
    for (key, value) in projection["metadata"].as_object().expect("metadata") {
        assert_eq!(task.metadata.get(key), Some(value));
    }
}

#[test]
fn explicit_settings_argument_wins_over_both_environment_names() {
    let explicit = "/definitely/missing-explicit-cli.json";
    let output = run_with_settings_environment(
        &["--prompt", "task", "--settings-file", explicit],
        Some("/definitely/missing-primary-cli.json"),
        Some("/definitely/missing-legacy-cli.json"),
    );

    assert_eq!(output.status.code(), Some(1));
    assert!(output.stdout.is_empty());
    assert!(String::from_utf8_lossy(&output.stderr).contains(explicit));
}

#[test]
fn primary_environment_wins_and_blank_primary_falls_back_to_legacy() {
    let primary = "/definitely/missing-primary-cli.json";
    let primary_output = run_with_settings_environment(
        &["--prompt", "task"],
        Some(primary),
        Some("/definitely/missing-legacy-cli.json"),
    );
    let legacy = "/definitely/missing-legacy-cli.json";
    let legacy_output =
        run_with_settings_environment(&["--prompt", "task"], Some("  "), Some(legacy));

    assert_eq!(primary_output.status.code(), Some(1));
    assert!(String::from_utf8_lossy(&primary_output.stderr).contains(primary));
    assert_eq!(legacy_output.status.code(), Some(1));
    assert!(String::from_utf8_lossy(&legacy_output.stderr).contains(legacy));
}

#[test]
fn result_json_covers_success_failure_and_cancellation() {
    let success = result_payload(&result(AgentStatus::Completed, None), &resolved());
    let failure = result_payload(
        &result(AgentStatus::Failed, Some("request failed")),
        &resolved(),
    );
    let cancellation = result_payload(
        &result(AgentStatus::Failed, Some("Operation was cancelled")),
        &resolved(),
    );

    assert_eq!(success["status"], "completed");
    assert_eq!(success["final_answer"], "done");
    assert_eq!(failure["status"], "failed");
    assert_eq!(failure["error"], "request failed");
    assert_eq!(cancellation["status"], "failed");
    assert_eq!(cancellation["error"], "Operation was cancelled");
}

#[test]
fn process_uses_contract_exit_codes_channels_and_multiword_prompt() {
    let usage = run_with_settings_environment(&[], None, None);
    let missing_path = "/definitely/missing-vv-agent-cli.json";
    let configuration = run_with_settings_environment(
        &[
            "--prompt",
            "inspect",
            "this",
            "repository",
            "--settings-file",
            missing_path,
        ],
        None,
        None,
    );
    let help = run_with_settings_environment(&["--help"], None, None);
    let outcomes = &contract()["process_outcomes"];

    assert_eq!(
        usage.status.code(),
        outcomes["usage_error"]["exit_code"]
            .as_i64()
            .map(|value| value as i32)
    );
    assert!(usage.stdout.is_empty());
    assert!(String::from_utf8_lossy(&usage.stderr).contains("--prompt"));
    assert_eq!(
        configuration.status.code(),
        outcomes["configuration_or_runtime_error"]["exit_code"]
            .as_i64()
            .map(|value| value as i32)
    );
    assert!(configuration.stdout.is_empty());
    let configuration_error = String::from_utf8_lossy(&configuration.stderr);
    assert!(configuration_error.contains(missing_path));
    assert!(!configuration_error.contains("unknown argument"));
    assert_eq!(help.status.code(), Some(0));
    assert!(!help.stdout.is_empty());
    assert!(help.stderr.is_empty());
}

#[test]
fn language_default_remains_json_for_rust() {
    let args = parse_cli_args_from_with_default_settings(
        ["vv-agent", "--prompt", "task"],
        contract()["settings_file_resolution"]["language_defaults"]["rust"]
            .as_str()
            .expect("rust default"),
    )
    .expect("parse args");

    assert_eq!(args.settings_file, PathBuf::from("local_settings.json"));
}

#[test]
fn debug_send_message_preserves_all_message_words() {
    let command = parse_cli_command_from_with_default_settings(
        [
            "vv-agent",
            "debug",
            "app-server",
            "send-message",
            "inspect",
            "this",
            "repository",
        ],
        "local_settings.json",
    )
    .expect("parse debug message");

    assert_eq!(
        command,
        CliCommand::Debug(DebugCliCommand::AppServerSendMessage {
            message: "inspect this repository".to_string(),
        })
    );
}