#![cfg(feature = "slow-tests")]
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn sgr_cmd() -> Command {
let mock_dir = common::mock_llm_path();
let mut c = Command::cargo_bin("sqlite-graphrag").expect("sqlite-graphrag binary not found");
c.env("PATH", common::prepend_path(&mock_dir));
c
}
#[path = "common/mod.rs"]
mod common;
fn cfg_dir(tmp: &TempDir) -> std::path::PathBuf {
tmp.path().join("config")
}
fn cache_dir(tmp: &TempDir) -> std::path::PathBuf {
tmp.path().join("cache")
}
fn base_cmd(tmp: &TempDir) -> Command {
let mut c = sgr_cmd();
c.env_remove("LC_ALL");
c.env_remove("LC_MESSAGES");
c.env_remove("LANG");
c.arg("--config-dir").arg(cfg_dir(tmp));
c.arg("--cache-dir").arg(cache_dir(tmp));
c
}
fn cmd_lang(tmp: &TempDir, lang: &str) -> Command {
let mut c = base_cmd(tmp);
c.arg("--lang").arg(lang);
c
}
fn cmd_xdg_lang(tmp: &TempDir, lang_val: &str) -> Command {
base_cmd(tmp)
.args(["config", "set", "i18n.lang", lang_val])
.assert()
.success();
base_cmd(tmp)
}
fn cmd_no_lang(tmp: &TempDir) -> Command {
base_cmd(tmp)
}
fn init_db(tmp: &TempDir) {
base_cmd(tmp)
.args(["config", "set", "db.path"])
.arg(tmp.path().join("test.sqlite"))
.assert()
.success();
base_cmd(tmp).arg("init").assert().success();
}
#[test]
fn paridade_localized_message_todas_variantes_apperror() {
use sqlite_graphrag::errors::AppError;
use sqlite_graphrag::i18n::Language;
use std::io;
let variantes: Vec<AppError> = vec![
AppError::Validation("campo x".into()),
AppError::Duplicate("ns/mem".into()),
AppError::Conflict("ts mudou".into()),
AppError::NotFound("mem-x".into()),
AppError::NamespaceError("sem marcador".into()),
AppError::LimitExceeded("corpo enorme".into()),
AppError::Embedding("dim errada".into()),
AppError::VecExtension("extensao failed".into()),
AppError::DbBusy("retries esgotados".into()),
AppError::BatchPartialFailure {
total: 10,
failed: 3,
},
AppError::Io(io::Error::new(io::ErrorKind::NotFound, "arquivo ausente")),
AppError::LockBusy("outra instancia ativa".into()),
AppError::AllSlotsFull {
max: 4,
waited_secs: 60,
},
AppError::LowMemory {
available_mb: 100,
required_mb: 500,
},
];
for variante in &variantes {
let msg_en = variante.localized_message_for(Language::English);
let msg_pt = variante.localized_message_for(Language::Portuguese);
assert!(
!msg_en.is_empty(),
"mensagem EN vazia para variante: {variante:?}"
);
assert!(
!msg_pt.is_empty(),
"mensagem PT vazia para variante: {variante:?}"
);
assert_ne!(
msg_en, msg_pt,
"mensagem EN e PT identicas para variante {variante:?}: '{msg_en}'"
);
}
}
#[test]
fn localized_message_en_cada_variante_contem_termo_ingles() {
use sqlite_graphrag::errors::AppError;
use sqlite_graphrag::i18n::Language;
let casos: Vec<(AppError, &str)> = vec![
(AppError::Validation("campo".into()), "validation error"),
(AppError::Duplicate("ns/m".into()), "duplicate detected"),
(AppError::Conflict("ts".into()), "conflict"),
(AppError::NotFound("m".into()), "not found"),
(
AppError::NamespaceError("ns".into()),
"namespace not resolved",
),
(AppError::LimitExceeded("l".into()), "limit exceeded"),
(AppError::Embedding("e".into()), "embedding error"),
(
AppError::VecExtension("v".into()),
"sqlite-vec extension failed",
),
(AppError::DbBusy("d".into()), "database busy"),
(AppError::LockBusy("l".into()), "lock busy"),
];
for (variante, esperado) in &casos {
let msg = variante.localized_message_for(Language::English);
assert!(
msg.contains(esperado),
"EN: esperado '{esperado}' em '{msg}' (variante: {variante:?})"
);
}
}
#[test]
fn localized_message_pt_cada_variante_contem_termo_portugues() {
use sqlite_graphrag::errors::AppError;
use sqlite_graphrag::i18n::Language;
let casos: Vec<(AppError, &str)> = vec![
(AppError::Validation("campo".into()), "erro de validação"),
(AppError::Duplicate("ns/m".into()), "duplicata detectada"),
(AppError::Conflict("ts".into()), "conflito"),
(AppError::NotFound("m".into()), "não encontrado"),
(
AppError::NamespaceError("ns".into()),
"namespace não resolvido",
),
(AppError::LimitExceeded("l".into()), "limite excedido"),
(AppError::Embedding("e".into()), "erro de embedding"),
(
AppError::VecExtension("v".into()),
"extensão sqlite-vec falhou",
),
(AppError::DbBusy("d".into()), "banco ocupado"),
(AppError::LockBusy("l".into()), "lock ocupado"),
];
for (variante, esperado) in &casos {
let msg = variante.localized_message_for(Language::Portuguese);
assert!(
msg.contains(esperado),
"PT: esperado '{esperado}' em '{msg}' (variante: {variante:?})"
);
}
}
#[test]
fn lang_pt_remember_invalid_name_stderr_portuguese() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_lang(&tmp, "pt")
.args([
"remember",
"--name",
"___",
"--type",
"user",
"--description",
"descricao de teste",
"--body",
"conteudo",
])
.assert()
.failure()
.stderr(
predicate::str::contains("erro de validação").or(predicate::str::contains("empty")),
);
}
#[test]
fn lang_en_mesmo_cenario_stderr_ingles() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_lang(&tmp, "en")
.args([
"remember",
"--name",
"___",
"--type",
"user",
"--description",
"test description",
"--body",
"conteudo",
])
.assert()
.failure()
.stderr(predicate::str::contains("validation error").or(predicate::str::contains("empty")));
}
#[test]
fn lang_pt_not_found_stderr_portugues() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_lang(&tmp, "pt")
.args(["read", "--name", "memoria-que-nao-existe"])
.assert()
.failure()
.stderr(predicate::str::contains("não encontrado"));
}
#[test]
fn lang_en_not_found_stderr_ingles() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_lang(&tmp, "en")
.args(["read", "--name", "memoria-que-nao-existe"])
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn lang_pt_body_excede_limite_stderr_portugues() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let corpo_enorme = "x".repeat(512_001);
let body_path = tmp.path().join("body-grande-pt.txt");
std::fs::write(&body_path, corpo_enorme).unwrap();
cmd_lang(&tmp, "pt")
.args([
"remember",
"--name",
"mem-grande",
"--type",
"user",
"--description",
"descricao de teste",
"--body-file",
body_path.to_str().unwrap(),
])
.assert()
.failure()
.stderr(
predicate::str::contains("corpo excede")
.or(predicate::str::contains("limite excedido")),
);
}
#[test]
fn lang_en_body_excede_limite_stderr_ingles() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let corpo_enorme = "x".repeat(512_001);
let body_path = tmp.path().join("body-grande-en.txt");
std::fs::write(&body_path, corpo_enorme).unwrap();
cmd_lang(&tmp, "en")
.args([
"remember",
"--name",
"mem-grande",
"--type",
"user",
"--description",
"test description",
"--body-file",
body_path.to_str().unwrap(),
])
.assert()
.failure()
.stderr(
predicate::str::contains("body exceeds").or(predicate::str::contains("limit exceeded")),
);
}
#[test]
fn xdg_i18n_lang_pt_aplica_portugues() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_xdg_lang(&tmp, "pt")
.args(["read", "--name", "inexistente"])
.assert()
.failure()
.stderr(predicate::str::contains("não encontrado"));
}
#[test]
fn xdg_i18n_lang_en_aplica_ingles() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_xdg_lang(&tmp, "en")
.args(["read", "--name", "inexistente"])
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn xdg_i18n_lang_pt_br_aplica_portugues() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_xdg_lang(&tmp, "pt-BR")
.args(["read", "--name", "inexistente"])
.assert()
.failure()
.stderr(predicate::str::contains("não encontrado"));
}
#[test]
fn flag_lang_en_overrides_xdg_lang_pt() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let mut c = cmd_xdg_lang(&tmp, "pt");
c.arg("--lang").arg("en");
c.args(["read", "--name", "inexistente"]);
c.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn flag_lang_pt_overrides_xdg_lang_en() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let mut c = cmd_xdg_lang(&tmp, "en");
c.arg("--lang").arg("pt");
c.args(["read", "--name", "inexistente"]);
c.assert()
.failure()
.stderr(predicate::str::contains("não encontrado"));
}
#[test]
fn default_without_flag_without_xdg_without_locale_returns_english() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_no_lang(&tmp)
.args(["read", "--name", "inexistente"])
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn locale_ptbr_without_flag_without_xdg_applies_portuguese() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let mut c = base_cmd(&tmp);
c.env("LC_ALL", "pt_BR.UTF-8");
c.args(["read", "--name", "inexistente"]);
c.assert()
.failure()
.stderr(predicate::str::contains("não encontrado"));
}
#[test]
fn json_stdout_identico_em_en_e_pt() {
let tmp_en = TempDir::new().unwrap();
let tmp_pt = TempDir::new().unwrap();
init_db(&tmp_en);
init_db(&tmp_pt);
let saida_en = cmd_lang(&tmp_en, "en")
.arg("health")
.assert()
.success()
.get_output()
.stdout
.clone();
let saida_pt = cmd_lang(&tmp_pt, "pt")
.arg("health")
.assert()
.success()
.get_output()
.stdout
.clone();
let json_en: serde_json::Value = serde_json::from_slice(&saida_en).unwrap();
let json_pt: serde_json::Value = serde_json::from_slice(&saida_pt).unwrap();
assert_eq!(
json_en["status"], json_pt["status"],
"campo status difere entre EN e PT"
);
assert_eq!(
json_en["integrity"], json_pt["integrity"],
"campo integrity difere entre EN e PT"
);
}
#[test]
fn alias_english_accepted_by_cli() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let saida = cmd_lang(&tmp, "en")
.arg("health")
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&saida).unwrap();
assert_eq!(json["status"], "ok");
}
#[test]
fn alias_pt_br_accepted_by_cli() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let saida = cmd_lang(&tmp, "pt")
.arg("health")
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&saida).unwrap();
assert_eq!(json["status"], "ok");
}