Files
notescraft/crates/engine/tests/assistant_connections.rs
2026-10-16 21:09:00 -03:00

138 lines
6.8 KiB
Rust

//! Connection migration, validation and whole-list settings edits.
use notescraft_engine::{AppSettings, Registry, Session, load_at, register_core, save_at};
use serde_json::{Value, json};
mod common;
fn connection(id: &str) -> Value {
json!({"id":id,"name":"Local","kind":"openai","command":"","env":{},"icon":"sparkles",
"base_url":"http://127.0.0.1:11434/v1","model":"local","api_key_env":"","system_prompt":""})
}
#[test]
fn legacy_command_migrates_only_when_loaded() {
let config = common::temp_config("legacy-connection");
std::fs::write(config.path().join("config.toml"), "schema_version = 1\n[assistant]\nagent = \"codex\"\ncommand = \"codex-acp\"\n").expect("fixture");
let (settings, _) = load_at(config.path());
let value = serde_json::to_value(&settings.assistant).expect("serialize");
assert_eq!(value["connections"][0]["command"], "codex-acp");
assert_eq!(value["connections"][0]["kind"], "acp");
assert_eq!(value["active"], value["connections"][0]["id"]);
save_at(config.path(), &settings).expect("persist migration");
assert_eq!(load_at(config.path()).0.assistant, settings.assistant);
let mut removed = settings;
removed.assistant.connections.clear();
removed.assistant.active.clear();
save_at(config.path(), &removed).expect("remove migrated connection");
assert_eq!(load_at(config.path()).0.assistant.connections, Vec::new());
let direct: AppSettings = serde_json::from_value(json!({"assistant":{"agent":"codex","command":"codex-acp"}})).expect("legacy shape");
assert_eq!(serde_json::to_value(direct).expect("serialize")["assistant"]["connections"], json!([]));
}
#[test]
fn connections_create_duplicate_edit_activate_and_remove_through_commands() {
let vault = common::temp_vault("connections-crud");
let config = common::temp_config("connections-crud");
let mut session = Session::open(vault.path()).expect("open");
session.use_settings_dir(config.path());
let mut registry = Registry::new();
register_core(&mut registry).expect("commands");
let first = connection("local");
let mut second = first.clone();
second["id"] = json!("copy");
second["model"] = json!("other");
for (key, value) in [
("assistant.connections", json!([first.clone()])),
("assistant.connections", json!([first, second.clone()])),
("assistant.active", json!("copy")),
("assistant.connections", json!([second])),
("assistant.active", json!("")),
("assistant.connections", json!([])),
] {
registry.run("settings.set", &mut session, &json!({"key":key,"value":value})).expect("edit");
}
let (loaded, _) = load_at(config.path());
let assistant = serde_json::to_value(loaded.assistant).expect("serialize");
assert_eq!(assistant["connections"], json!([]));
assert_eq!(assistant["active"], "");
}
#[test]
fn invalid_connections_are_rejected_without_changing_disk() {
let vault = common::temp_vault("connections-validation");
let config = common::temp_config("connections-validation");
let mut session = Session::open(vault.path()).expect("open");
session.use_settings_dir(config.path());
save_at(config.path(), &AppSettings::default()).expect("defaults");
let before = std::fs::read(config.path().join("config.toml")).expect("read");
let mut registry = Registry::new();
register_core(&mut registry).expect("commands");
let mut control = connection("bad");
control["name"] = json!("bad\nname");
let mut huge = connection("huge");
huge["model"] = json!("x".repeat(5000));
let mut url = connection("url");
url["base_url"] = json!("file:///tmp/model");
let mut environment = connection("env");
environment["env"] = json!({"BAD=NAME":"value"});
let mut missing = connection("empty-model");
missing["model"] = json!("");
let mut prompt = connection("prompt-control");
prompt["system_prompt"] = json!("line one\nline two");
for value in [
json!([connection("same"), connection("same")]),
json!([control]),
json!([huge]),
json!([url]),
json!([environment]),
json!([missing]),
json!([prompt]),
json!(vec![connection("many"); 65]),
] {
assert!(registry.run("settings.set", &mut session, &json!({"key":"assistant.connections","value":value})).is_err());
assert_eq!(std::fs::read(config.path().join("config.toml")).expect("read"), before);
}
assert!(registry.run("settings.set", &mut session, &json!({"key":"assistant.active","value":"missing"})).is_err());
}
#[test]
fn invalid_saved_connections_are_retained_and_checked_load_refuses_them() {
let config = common::temp_config("invalid-connections-load");
let path = config.path().join("config.toml");
let contents = "schema_version = 1\n[assistant]\nactive = \"broken\"\n[[assistant.connections]]\nid = \"broken\"\nname = \"Broken\"\nkind = \"openai\"\nbase_url = \"file:///tmp/no\"\nmodel = \"local\"\n";
std::fs::write(&path, contents).expect("fixture");
let (settings, notes) = load_at(config.path());
assert_eq!(settings.assistant.connections.len(), 1);
assert_eq!(settings.assistant.active, "broken");
assert!(notes.iter().any(|note| note.contains("invalid connections")));
assert!(notescraft_engine::load_checked_at(config.path()).is_err());
assert_eq!(std::fs::read_to_string(&path).expect("read"), contents);
let vault = common::temp_vault("invalid-connections-session");
let mut session = Session::open(vault.path()).expect("open");
session.use_settings_dir(config.path());
let before = session.config().clone();
let mut applying = settings.clone();
"catppuccin-latte".clone_into(&mut applying.session.theme);
assert!(session.apply_app_settings(&applying).is_err());
assert_eq!(session.config(), &before);
let mut registry = Registry::new();
register_core(&mut registry).expect("register");
assert!(registry.run("settings.get", &mut session, &json!({})).is_err());
assert!(registry.run("settings.set", &mut session, &json!({"key":"editor.vim","value":true})).is_err());
assert_eq!(std::fs::read_to_string(&path).expect("read"), contents);
let unknown_kind = contents.replace("kind = \"openai\"", "kind = \"unsupported\"");
std::fs::write(&path, &unknown_kind).expect("invalid kind fixture");
assert!(notescraft_engine::load_checked_at(config.path()).is_err());
assert_eq!(std::fs::read_to_string(&path).expect("read"), unknown_kind);
}
#[test]
fn active_identifier_rejects_oversized_and_control_values() {
for active in ["x".repeat(65), "\n".to_owned()] {
let settings: AppSettings = serde_json::from_value(json!({"assistant":{"active":active}})).expect("shape");
let error = settings.assistant.validate_connections().expect_err("invalid active identifier");
assert!(error.to_string().contains("exceeds 64 bytes or contains control characters"));
}
}