use assert_cmd::Command;
use rusqlite::Connection;
use serial_test::serial;
use std::path::PathBuf;
use tempfile::TempDir;
fn cmd_base(tmp: &TempDir) -> Command {
let mut c = Command::cargo_bin("sqlite-graphrag").unwrap();
c.env("SQLITE_GRAPHRAG_DB_PATH", tmp.path().join("test.sqlite"));
c.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path().join("cache"));
c.env("SQLITE_GRAPHRAG_LOG_LEVEL", "error");
c.env("SQLITE_GRAPHRAG_LANG", "en");
c.arg("--skip-memory-guard");
c
}
fn init_db(tmp: &TempDir) {
cmd_base(tmp).arg("init").assert().success();
}
fn remember_ok(tmp: &TempDir, name: &str, body: &str) {
cmd_base(tmp)
.args([
"remember",
"--name",
name,
"--type",
"user",
"--description",
"desc for prd test",
"--namespace",
"global",
"--body",
body,
"--skip-extraction",
])
.assert()
.success();
}
fn db_path(tmp: &TempDir) -> PathBuf {
tmp.path().join("test.sqlite")
}
#[test]
fn prd_name_double_underscore_rejected() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_base(&tmp)
.args([
"remember",
"--name",
"__reserved",
"--type",
"user",
"--description",
"deve falhar",
"--body",
"corpo",
])
.assert()
.failure()
.code(1);
}
#[test]
fn prd_cross_namespace_link_rejected() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "entidade-alpha", "corpo alpha");
cmd_base(&tmp)
.args([
"link",
"--from",
"entidade-alpha",
"--to",
"entidade-inexistente-beta",
"--relation",
"related",
"--namespace",
"global",
])
.assert()
.failure()
.code(4);
}
#[test]
fn prd_soft_delete_recall_nao_retorna_esquecida() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "memoria-apagavel", "conteudo apagavel importante");
cmd_base(&tmp)
.args([
"forget",
"--name",
"memoria-apagavel",
"--namespace",
"global",
])
.assert()
.success();
let conn = Connection::open(db_path(&tmp)).unwrap();
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM memories WHERE name='memoria-apagavel' AND deleted_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
count, 0,
"memória esquecida não deve aparecer sem deleted_at"
);
let deleted_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM memories WHERE name='memoria-apagavel' AND deleted_at IS NOT NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(deleted_count, 1, "soft-delete deve preencher deleted_at");
}
#[test]
fn prd_trg_fts_ad_idempotente_double_delete() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(
&tmp,
"memoria-dupla",
"conteudo para double delete fts test",
);
let conn = Connection::open(db_path(&tmp)).unwrap();
let memory_id: i64 = conn
.query_row(
"SELECT id FROM memories WHERE name='memoria-dupla'",
[],
|r| r.get(0),
)
.unwrap();
conn.execute(
"UPDATE memories SET deleted_at=strftime('%s','now') WHERE id=?1",
[memory_id],
)
.unwrap();
conn.execute("DELETE FROM fts_memories WHERE rowid=?1", [memory_id])
.unwrap_or(0);
let result =
conn.execute_batch("INSERT INTO fts_memories(fts_memories) VALUES('integrity-check')");
assert!(
result.is_ok(),
"fts_memories deve passar integrity-check após double-delete"
);
}
#[test]
fn prd_remember_duplicata_retorna_merged_into_memory_id() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-merge-alvo", "corpo original da memoria merge");
let output = cmd_base(&tmp)
.args([
"remember",
"--name",
"mem-merge-alvo",
"--type",
"user",
"--description",
"desc atualizada",
"--body",
"corpo novo do merge",
"--namespace",
"global",
"--force-merge",
"--skip-extraction",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert!(
json.get("merged_into_memory_id").is_some(),
"remember com --force-merge deve incluir campo merged_into_memory_id"
);
}
#[test]
fn prd_remember_json_contem_entities_e_relationships_persisted() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let output = cmd_base(&tmp)
.args([
"remember",
"--name",
"mem-fields-check",
"--type",
"user",
"--description",
"verificar campos de saida",
"--body",
"corpo para checar campos json",
"--namespace",
"global",
"--skip-extraction",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert!(
json.get("entities_persisted").is_some(),
"remember deve emitir entities_persisted"
);
assert!(
json.get("relationships_persisted").is_some(),
"remember deve emitir relationships_persisted"
);
}
#[test]
fn prd_fts5_unicode61_remove_diacritics() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let conn = Connection::open(db_path(&tmp)).unwrap();
let tokenize: String = conn
.query_row(
"SELECT tokenize FROM pragma_table_info('fts_memories') LIMIT 1",
[],
|r| r.get(0),
)
.unwrap_or_else(|_| {
conn.query_row(
"SELECT sql FROM sqlite_master WHERE name='fts_memories'",
[],
|r| r.get::<_, String>(0),
)
.unwrap_or_default()
});
assert!(
tokenize.contains("unicode61") || tokenize.contains("remove_diacritics"),
"fts_memories deve usar tokenize='unicode61 remove_diacritics 1', encontrado: {tokenize}"
);
}
#[test]
fn prd_vec_memories_distance_metric_cosine() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let conn = Connection::open(db_path(&tmp)).unwrap();
let sql: String = conn
.query_row(
"SELECT sql FROM sqlite_master WHERE name='vec_memories'",
[],
|r| r.get(0),
)
.unwrap();
assert!(
sql.contains("cosine"),
"vec_memories deve declarar distance_metric=cosine, sql: {sql}"
);
}
#[test]
fn prd_edit_expected_updated_at_stale_retorna_exit_3() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-edit-lock", "corpo para edit lock test");
cmd_base(&tmp)
.args([
"edit",
"--name",
"mem-edit-lock",
"--namespace",
"global",
"--body",
"novo corpo conflito",
"--expected-updated-at",
"0",
])
.assert()
.failure()
.code(3);
}
#[test]
#[serial]
fn prd_cinco_instancias_quinta_retorna_exit_75() {
use fs4::fs_std::FileExt;
use std::fs::OpenOptions;
let tmp = TempDir::new().unwrap();
let handles: Vec<std::fs::File> = (1..=4)
.map(|slot| {
let path = tmp.path().join(format!("cli-slot-{slot}.lock"));
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.unwrap();
file.try_lock_exclusive().unwrap();
file
})
.collect();
Command::cargo_bin("sqlite-graphrag")
.unwrap()
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.env("SQLITE_GRAPHRAG_LOG_LEVEL", "error")
.args([
"--skip-memory-guard",
"--max-concurrency",
"4",
"--wait-lock",
"0",
"namespace-detect",
])
.assert()
.failure()
.code(75);
drop(handles);
}
#[test]
fn prd_max_body_len_excedido_retorna_exit_6() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let corpo_gigante = "x".repeat(20_001);
cmd_base(&tmp)
.args([
"remember",
"--name",
"mem-body-limit",
"--type",
"user",
"--description",
"limite de corpo",
"--body",
&corpo_gigante,
])
.assert()
.failure()
.code(6);
}
#[test]
#[serial]
fn prd_sqlite_graphrag_namespace_env_funciona() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_base(&tmp)
.args([
"remember",
"--name",
"mem-via-env-ns",
"--type",
"user",
"--description",
"namespace via env",
"--namespace",
"ns-from-env",
"--body",
"corpo namespace env",
"--skip-extraction",
])
.assert()
.success();
let conn = Connection::open(db_path(&tmp)).unwrap();
let ns: String = conn
.query_row(
"SELECT namespace FROM memories WHERE name='mem-via-env-ns'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(ns, "ns-from-env", "namespace deve ser o fornecido via flag");
}
#[test]
fn prd_health_emite_integrity_ok_e_schema_ok() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let output = cmd_base(&tmp)
.arg("health")
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert!(
json.get("integrity_ok").is_some(),
"health deve emitir integrity_ok"
);
assert!(
json.get("schema_ok").is_some(),
"health deve emitir schema_ok"
);
}
#[test]
fn prd_history_inclui_created_at_iso() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-history-iso", "corpo para history test");
let output = cmd_base(&tmp)
.args([
"history",
"--name",
"mem-history-iso",
"--namespace",
"global",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
let versions = json["versions"].as_array().unwrap();
assert!(!versions.is_empty(), "deve haver ao menos uma versão");
assert!(
versions[0].get("created_at_iso").is_some(),
"versão deve conter campo created_at_iso"
);
}
#[test]
fn prd_link_cria_memory_relationships() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-link-src", "entidade alfa para link test");
remember_ok(&tmp, "mem-link-dst", "entidade beta para link test");
let output = cmd_base(&tmp)
.args([
"link",
"--from",
"mem-link-src",
"--to",
"mem-link-dst",
"--relation",
"related",
"--namespace",
"global",
])
.output()
.unwrap();
if output.status.success() {
let conn = Connection::open(db_path(&tmp)).unwrap();
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM memory_relationships", [], |r| {
r.get(0)
})
.unwrap_or(0);
let rel_count: i64 = conn
.query_row("SELECT COUNT(*) FROM relationships", [], |r| r.get(0))
.unwrap_or(0);
assert!(
count > 0 || rel_count > 0,
"link deve criar entrada em memory_relationships ou relationships"
);
} else {
assert_eq!(
output.status.code(),
Some(4),
"sem entidades, link deve retornar exit 4"
);
}
}
#[test]
fn prd_unlink_remove_apenas_relacao_especifica() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let conn = Connection::open(db_path(&tmp)).unwrap();
conn.execute_batch(
"INSERT INTO entities (name, type, namespace) VALUES ('ent-a', 'concept', 'global');
INSERT INTO entities (name, type, namespace) VALUES ('ent-b', 'concept', 'global');
INSERT INTO entities (name, type, namespace) VALUES ('ent-c', 'concept', 'global');",
)
.unwrap();
let id_a: i64 = conn
.query_row("SELECT id FROM entities WHERE name='ent-a'", [], |r| {
r.get(0)
})
.unwrap();
let id_b: i64 = conn
.query_row("SELECT id FROM entities WHERE name='ent-b'", [], |r| {
r.get(0)
})
.unwrap();
let id_c: i64 = conn
.query_row("SELECT id FROM entities WHERE name='ent-c'", [], |r| {
r.get(0)
})
.unwrap();
conn.execute(
"INSERT INTO relationships (source_id, target_id, relation, weight, namespace) VALUES (?1, ?2, 'related', 1.0, 'global')",
[id_a, id_b],
)
.unwrap();
conn.execute(
"INSERT INTO relationships (source_id, target_id, relation, weight, namespace) VALUES (?1, ?2, 'related', 1.0, 'global')",
[id_a, id_c],
)
.unwrap();
drop(conn);
cmd_base(&tmp)
.args([
"unlink",
"--from",
"ent-a",
"--to",
"ent-b",
"--relation",
"related",
"--namespace",
"global",
])
.assert()
.success();
let conn2 = Connection::open(db_path(&tmp)).unwrap();
let remaining: i64 = conn2
.query_row(
"SELECT COUNT(*) FROM relationships WHERE source_id=?1",
[id_a],
|r| r.get(0),
)
.unwrap();
assert_eq!(
remaining, 1,
"unlink deve remover apenas a relação específica A→B, preservando A→C"
);
}
#[test]
fn prd_graph_json_contem_nodes_e_edges() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let output = cmd_base(&tmp)
.args(["graph", "--format", "json", "--namespace", "global"])
.assert()
.success()
.get_output()
.stdout
.clone();
let text = String::from_utf8_lossy(&output);
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
assert!(
json.get("nodes").is_some(),
"graph JSON deve conter campo 'nodes'"
);
assert!(
json.get("edges").is_some(),
"graph JSON deve conter campo 'edges'"
);
}
#[test]
fn prd_graph_dot_e_digraph_valido() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let output = cmd_base(&tmp)
.args(["graph", "--format", "dot", "--namespace", "global"])
.assert()
.success()
.get_output()
.stdout
.clone();
let text = String::from_utf8_lossy(&output);
assert!(
text.contains("digraph sqlite-graphrag {"),
"graph DOT deve começar com 'digraph sqlite-graphrag {{', obtido: {text}"
);
}
#[test]
fn prd_graph_mermaid_comeca_com_graph_lr() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let output = cmd_base(&tmp)
.args(["graph", "--format", "mermaid", "--namespace", "global"])
.assert()
.success()
.get_output()
.stdout
.clone();
let text = String::from_utf8_lossy(&output);
assert!(
text.contains("graph LR"),
"graph Mermaid deve conter 'graph LR', obtido: {text}"
);
}
#[test]
fn prd_hybrid_search_rrf_k_default_60() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
cmd_base(&tmp)
.args([
"hybrid-search",
"query de teste prd",
"--rrf-k",
"60",
"--namespace",
"global",
])
.assert()
.success();
}
#[test]
fn prd_purge_retention_remove_deletados_antigos() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-purge-alvo", "corpo para purge test");
let conn = Connection::open(db_path(&tmp)).unwrap();
conn.execute(
"UPDATE memories SET deleted_at = strftime('%s','now') - 172800 WHERE name='mem-purge-alvo'",
[],
)
.unwrap();
drop(conn);
cmd_base(&tmp)
.args(["purge", "--retention-days", "1", "--yes"])
.assert()
.success();
let conn2 = Connection::open(db_path(&tmp)).unwrap();
let count: i64 = conn2
.query_row(
"SELECT COUNT(*) FROM memories WHERE name='mem-purge-alvo'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
count, 0,
"purge deve remover permanentemente memórias com deleted_at > retention"
);
}
#[test]
fn prd_optimize_executa_e_retorna_status_ok() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let output = cmd_base(&tmp)
.arg("optimize")
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(json["status"], "ok", "optimize deve retornar status 'ok'");
}
#[test]
fn prd_vacuum_retorna_size_before_e_size_after() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let output = cmd_base(&tmp)
.arg("vacuum")
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert!(
json.get("size_before_bytes").is_some(),
"vacuum deve emitir size_before_bytes"
);
assert!(
json.get("size_after_bytes").is_some(),
"vacuum deve emitir size_after_bytes"
);
}
#[test]
#[cfg(unix)]
fn prd_chmod_600_aplicado_apos_init() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let db = db_path(&tmp);
let perms = std::fs::metadata(&db).unwrap().permissions();
let mode = perms.mode() & 0o777;
assert_eq!(
mode, 0o600,
"database deve ter permissão 600 após init, atual: {mode:o}"
);
}
#[test]
#[serial]
fn prd_path_traversal_rejeitado_em_db_path() {
let tmp = TempDir::new().unwrap();
let mut c = Command::cargo_bin("sqlite-graphrag").unwrap();
c.env("SQLITE_GRAPHRAG_DB_PATH", "../../../etc/passwd");
c.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path().join("cache"));
c.env("SQLITE_GRAPHRAG_LOG_LEVEL", "error");
c.arg("--skip-memory-guard");
c.arg("init");
c.assert().failure();
}
#[test]
fn prd_stats_inclui_memories_entities_relationships() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-stats-check", "corpo para stats test");
let output = cmd_base(&tmp)
.arg("stats")
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert!(
json.get("memories").is_some(),
"stats deve ter campo 'memories'"
);
assert!(
json.get("entities").is_some(),
"stats deve ter campo 'entities'"
);
assert!(
json.get("relationships").is_some(),
"stats deve ter campo 'relationships'"
);
assert!(
json.get("memories_total").is_some() || json.get("memories").is_some(),
"stats deve ter memories_total ou memories"
);
}
#[test]
fn prd_list_respeita_limit() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
for i in 0..5 {
remember_ok(&tmp, &format!("mem-limit-{i}"), &format!("corpo {i}"));
}
let output = cmd_base(&tmp)
.args(["list", "--namespace", "global", "--limit", "2"])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
let items = json["items"].as_array().unwrap();
assert_eq!(
items.len(),
2,
"list com --limit 2 deve retornar exatamente 2 itens"
);
}
#[test]
fn prd_rename_atualiza_versao() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-rename-orig", "corpo para rename test");
let conn = Connection::open(db_path(&tmp)).unwrap();
let version_antes: i64 = conn
.query_row(
"SELECT MAX(version) FROM memory_versions mv \
JOIN memories m ON m.id = mv.memory_id WHERE m.name='mem-rename-orig'",
[],
|r| r.get(0),
)
.unwrap_or(0);
drop(conn);
cmd_base(&tmp)
.args([
"rename",
"--name",
"mem-rename-orig",
"--new-name",
"mem-rename-novo",
"--namespace",
"global",
])
.assert()
.success();
let conn2 = Connection::open(db_path(&tmp)).unwrap();
let count: i64 = conn2
.query_row(
"SELECT COUNT(*) FROM memories WHERE name='mem-rename-novo'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(count, 1, "memória deve existir com novo nome após rename");
let versions_count: i64 = conn2
.query_row(
"SELECT COUNT(*) FROM memory_versions WHERE name='mem-rename-novo' OR name='mem-rename-orig'",
[],
|r| r.get(0),
)
.unwrap();
assert!(
versions_count >= 1,
"rename deve registrar versão em memory_versions"
);
let _ = version_antes; }
#[test]
fn prd_restore_reverte_soft_delete() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-restore-test", "corpo original para restore");
cmd_base(&tmp)
.args([
"forget",
"--name",
"mem-restore-test",
"--namespace",
"global",
])
.assert()
.success();
let conn = Connection::open(db_path(&tmp)).unwrap();
let deleted: bool = conn
.query_row(
"SELECT deleted_at IS NOT NULL FROM memories WHERE name='mem-restore-test'",
[],
|r| r.get(0),
)
.unwrap();
assert!(deleted, "memória deve estar soft-deleted após forget");
let version: i64 = conn
.query_row(
"SELECT MAX(version) FROM memory_versions v JOIN memories m ON m.id=v.memory_id WHERE m.name='mem-restore-test'",
[],
|r| r.get(0),
)
.unwrap();
drop(conn);
cmd_base(&tmp)
.args([
"restore",
"--name",
"mem-restore-test",
"--namespace",
"global",
"--version",
&version.to_string(),
])
.assert()
.success();
let conn2 = Connection::open(db_path(&tmp)).unwrap();
let active: bool = conn2
.query_row(
"SELECT deleted_at IS NULL FROM memories WHERE name='mem-restore-test'",
[],
|r| r.get(0),
)
.unwrap();
assert!(
active,
"memória deve estar ativa (deleted_at NULL) após restore"
);
}
#[test]
fn prd_cleanup_orphans_remove_entities_sem_memorias() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
let conn = Connection::open(db_path(&tmp)).unwrap();
conn.execute(
"INSERT INTO entities (name, type, namespace) VALUES ('entidade-orfa', 'concept', 'global')",
[],
)
.unwrap();
drop(conn);
let conn2 = Connection::open(db_path(&tmp)).unwrap();
let antes: i64 = conn2
.query_row(
"SELECT COUNT(*) FROM entities WHERE name='entidade-orfa'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(antes, 1, "entidade órfã deve existir antes do cleanup");
drop(conn2);
let output = cmd_base(&tmp)
.args(["cleanup-orphans", "--yes"])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
let deleted = json["deleted"].as_u64().unwrap_or(0);
assert!(
deleted >= 1,
"cleanup-orphans deve reportar ao menos 1 deleted"
);
let conn3 = Connection::open(db_path(&tmp)).unwrap();
let depois: i64 = conn3
.query_row(
"SELECT COUNT(*) FROM entities WHERE name='entidade-orfa'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
depois, 0,
"entidade órfã deve ter sido removida pelo cleanup"
);
}
#[test]
fn prd_sync_safe_copy_gera_snapshot_coerente() {
let tmp = TempDir::new().unwrap();
init_db(&tmp);
remember_ok(&tmp, "mem-snapshot", "corpo para snapshot test");
let dest = tmp.path().join("snapshot.sqlite");
let output = cmd_base(&tmp)
.args(["sync-safe-copy", "--dest", dest.to_str().unwrap()])
.assert()
.success()
.get_output()
.stdout
.clone();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert!(
json.get("bytes_copied").is_some(),
"sync-safe-copy deve emitir bytes_copied"
);
assert!(
json["bytes_copied"].as_u64().unwrap_or(0) > 0,
"bytes_copied deve ser > 0"
);
assert_eq!(
json["status"], "ok",
"sync-safe-copy deve retornar status 'ok'"
);
assert!(dest.exists(), "arquivo de snapshot deve existir no destino");
}