pi_agent_rust 0.6.0

Native AI coding agent CLI - Rust port of Pi Agent
#![allow(clippy::literal_string_with_formatting_args, clippy::format_collect)]

use super::*;

fn item(text: &str) -> Value {
    json!({"label":"Type","insertTextFormat":2,"insertText":text})
}

fn values(pairs: &[(&str, &str)]) -> Values {
    pairs
        .iter()
        .map(|(key, value)| (key.to_string(), value.to_string()))
        .collect()
}

fn expansion(text: &str, supplied: &[(&str, &str)]) -> Expanded {
    prepare(&item(text), Some(&values(supplied))).unwrap()
}

#[test]
fn numbered_defaults_mirrors_and_final_cursor_expand_without_markers() {
    let expanded = expansion("call(${1:argument}, $1)$0", &[]);
    assert_eq!(expanded.item["insertText"], "call(argument, argument)");
    assert_eq!(expanded.item["insertTextFormat"], 1);
    assert!(expanded.missing.is_empty());
    expanded.require_values().unwrap();
}

#[test]
fn substitutions_are_literal_including_snippet_and_shell_syntax() {
    let literal = "${2:no} \\ $CLIPBOARD $(touch nope) 😀\nline";
    let expanded = expansion("call(${1:default}, $1)$0", &[("1", literal)]);
    assert_eq!(
        expanded.item["insertText"],
        format!("call({literal}, {literal})")
    );
    assert!(expanded.missing.is_empty());
}

#[test]
fn forward_references_use_the_later_definition() {
    assert_eq!(
        expansion("$2 ${2:after} ${1:$2}", &[]).item["insertText"],
        "after after after"
    );
}

#[test]
fn nested_defaults_accept_inner_or_outer_overrides() {
    let text = "${1:outer ${2:inner}} / $2";
    assert_eq!(
        expansion(text, &[]).item["insertText"],
        "outer inner / inner"
    );
    assert_eq!(
        expansion(text, &[("2", "chosen")]).item["insertText"],
        "outer chosen / chosen"
    );
    assert_eq!(
        expansion(text, &[("1", "whole"), ("2", "chosen")]).item["insertText"],
        "whole / chosen"
    );
}

#[test]
fn suppressed_nested_fields_are_not_required_or_silently_consumed() {
    let text = "${1:outer $2}";
    let expanded = expansion(text, &[("1", "whole")]);
    assert!(expanded.missing.is_empty());
    assert_eq!(expanded.fields.as_array().unwrap().len(), 1);
    assert!(
        prepare(
            &item(text),
            Some(&values(&[("1", "whole"), ("2", "unused")]))
        )
        .is_err()
    );
}

#[test]
fn bare_positive_tabstops_require_explicit_values_but_zero_does_not() {
    let expanded = expansion("call($2, ${1})$0", &[]);
    assert_eq!(expanded.missing, vec![1, 2]);
    assert_eq!(expanded.item["insertText"], "call(, )");
    assert!(
        expanded
            .require_values()
            .unwrap_err()
            .to_string()
            .contains("VALUES_REQUIRED")
    );
    let filled = expansion("call($2, ${1})$0", &[("1", ""), ("2", "value")]);
    filled.require_values().unwrap();
    assert_eq!(filled.item["insertText"], "call(value, )");
}

#[test]
fn empty_defaults_and_final_placeholder_defaults_are_preserved() {
    let expanded = expansion("${1:} ${0:body}", &[]);
    assert!(expanded.missing.is_empty());
    assert_eq!(expanded.item["insertText"], " body");
    assert_eq!(
        expansion("${0:body}", &[("0", "changed")]).item["insertText"],
        "changed"
    );
}

#[test]
fn choices_expose_options_and_default_to_first_but_allow_literal_edits() {
    let expanded = expansion("${1|red,green,blue|}-$1", &[]);
    assert_eq!(expanded.item["insertText"], "red-red");
    assert_eq!(
        expanded.fields[0]["choices"],
        json!(["red", "green", "blue"])
    );
    assert_eq!(
        expansion("${1|red,green|}", &[("1", "custom")]).item["insertText"],
        "custom"
    );
}

#[test]
fn context_sensitive_escapes_preserve_unicode_and_literal_dollars() {
    assert_eq!(
        expansion(r"\$1 \} \\ 😀", &[]).item["insertText"],
        "$1 } \\ 😀"
    );
    let expanded = expansion(r"${1|a\,b,c\|d,e\\f,$0|}", &[]);
    assert_eq!(
        expanded.fields[0]["choices"],
        json!(["a,b", "c|d", "e\\f", "$0"])
    );
    assert_eq!(expanded.item["insertText"], "a,b");
}

#[test]
fn ordinary_code_braces_and_multiline_text_survive() {
    assert_eq!(
        expansion("fn x() {\r\n  ${1:body}\r\n}$0", &[]).item["insertText"],
        "fn x() {\r\n  body\r\n}"
    );
}

#[test]
fn contradictory_defaults_and_cycles_fail_closed() {
    for text in [
        "${1:a}${1:b}",
        "${1:$1}",
        "${1:$2}${2:$1}",
        "${1|a,b|}${1:a}",
    ] {
        assert!(prepare(&item(text), None).is_err(), "{text}");
    }
    assert_eq!(expansion("${1:a}${1:a}", &[]).item["insertText"], "aa");
}

#[test]
fn variables_and_transforms_are_rejected_even_in_overridden_defaults() {
    for text in [
        "$CLIPBOARD",
        "${TM_FILENAME:default}",
        "${1/(.*)/$1/}",
        "${1:${TM_FILEPATH}}",
    ] {
        let error = prepare(&item(text), Some(&values(&[("1", "override")])))
            .err()
            .unwrap();
        assert!(error.to_string().contains("UNSUPPORTED"), "{text}: {error}");
    }
}

#[test]
fn malformed_delimiters_and_invalid_escapes_are_not_inserted() {
    for text in [
        "${1:unfinished",
        "${1|a,b",
        "${1|a|x",
        "${1!}",
        r"\q",
        r"${1|\$|}",
        "$",
        "${}",
    ] {
        assert!(prepare(&item(text), None).is_err(), "{text}");
    }
}

#[test]
fn default_range_materialization_keeps_snippet_format_and_data() {
    let raw = json!({"label":"Type","textEditText":"Type(${1:arg})$0"});
    let range = json!({"start":{"line":0,"character":0},"end":{"line":0,"character":2}});
    let normalized = crate::lsp::completion::item::materialize(
        &raw,
        Some(&json!({
            "editRange":range,"insertTextFormat":2,"data":{"opaque":[1,"id"]}
        })),
    )
    .unwrap();
    let expanded = prepare(&normalized, None).unwrap();
    assert_eq!(expanded.item["textEdit"]["newText"], "Type(arg)");
    assert_eq!(expanded.item["textEdit"]["range"], range);
    assert_eq!(expanded.item["data"], normalized["data"]);
    assert_eq!(normalized["insertTextFormat"], 2);
}

#[test]
fn only_primary_text_is_expanded_and_the_cached_item_is_unchanged() {
    let raw = json!({"label":"Type","insertTextFormat":2,
        "textEdit":{"insert":{},"replace":{},"newText":"Type(${1:arg})$0"},
        "additionalTextEdits":[{"range":{},"newText":"// literal $0 ${1:keep}\n"}]});
    let expanded = prepare(&raw, Some(&values(&[("1", "value")]))).unwrap();
    assert_eq!(expanded.item["textEdit"]["newText"], "Type(value)");
    assert_eq!(
        expanded.item["additionalTextEdits"],
        raw["additionalTextEdits"]
    );
    assert_eq!(raw["textEdit"]["newText"], "Type(${1:arg})$0");
    assert_eq!(
        prepare(&raw, None).unwrap().item["textEdit"]["newText"],
        "Type(arg)"
    );
}

#[test]
fn caller_keys_and_sizes_are_validated_before_expansion() {
    for key in ["01", "-1", "1.0", "variable", "65536", ""] {
        assert!(validate_values(&values(&[(key, "x")])).is_err());
    }
    assert!(prepare(&item("$1"), Some(&values(&[("2", "unknown")]))).is_err());
    assert!(validate_values(&values(&[("1", &"x".repeat(16 * 1024 + 1))])).is_err());
    assert!(validate_values(&(0..65).map(|id| (id.to_string(), String::new())).collect()).is_err());
    assert!(
        validate_values(
            &(0..5)
                .map(|id| (id.to_string(), "x".repeat(16 * 1024)))
                .collect()
        )
        .is_err()
    );
}

#[test]
fn nesting_nodes_choices_and_expansion_are_all_bounded() {
    let nested = format!(
        "{}x{}",
        "${1:".repeat(MAX_DEPTH + 1),
        "}".repeat(MAX_DEPTH + 1)
    );
    let fields = (0..65).map(|id| format!("${id} ")).collect::<String>();
    let choices = format!("${{1|{}|}}", vec!["x"; 33].join(","));
    for text in [
        nested,
        fields,
        choices,
        "$0 ".repeat(MAX_NODES),
        "x".repeat(MAX_TEXT + 1),
    ] {
        assert!(prepare(&item(&text), None).is_err());
    }
    let text = "$1".repeat(5);
    assert!(
        prepare(
            &item(&text),
            Some(&values(&[("1", &"x".repeat(16 * 1024))]))
        )
        .is_err()
    );
}

#[test]
fn substitutions_cannot_bypass_command_or_indentation_rejection() {
    use crate::lsp::text::{Position, Range};
    let cursor = Position {
        line: 0,
        character: 0,
    };
    for extra in [
        json!({"command":{"command":"unsafe.run"}}),
        json!({"insertTextMode":2}),
    ] {
        let mut raw = item("${1:value}");
        raw.as_object_mut()
            .unwrap()
            .extend(extra.as_object().unwrap().clone());
        let expanded = prepare(&raw, None).unwrap();
        assert!(
            crate::lsp::completion::item::edits(
                &expanded.item,
                "",
                cursor,
                Some(Range {
                    start: cursor,
                    end: cursor
                })
            )
            .is_err()
        );
    }
}

#[test]
fn plain_items_reject_values_and_never_interpret_their_text() {
    let raw = json!({"label":"$CLIPBOARD", "insertText":"${1:literal}"});
    let expanded = prepare(&raw, None).unwrap();
    assert_eq!(expanded.item, raw);
    assert!(expanded.fields.is_null());
    assert!(prepare(&raw, Some(&Values::new())).is_err());
}