138 lines
6.8 KiB
Rust
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"));
|
|
}
|
|
}
|