use assert_cmd::Command;
use serial_test::serial;
use tempfile::TempDir;
fn slot_path(tmp: &TempDir, slot: usize) -> std::path::PathBuf {
tmp.path().join(format!("cli-slot-{slot}.lock"))
}
#[test]
#[serial]
fn slot_liberado_apos_processo_sair() {
let tmp = TempDir::new().expect("TempDir deve ser criado");
Command::cargo_bin("sqlite-graphrag")
.expect("binário sqlite-graphrag não encontrado")
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "namespace-detect"])
.assert()
.success();
Command::cargo_bin("sqlite-graphrag")
.expect("binário sqlite-graphrag não encontrado")
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "namespace-detect"])
.assert()
.success();
}
#[test]
#[serial]
fn arquivo_slot_criado_em_cache_dir() {
let tmp = TempDir::new().expect("TempDir deve ser criado");
Command::cargo_bin("sqlite-graphrag")
.expect("binário sqlite-graphrag não encontrado")
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args(["--skip-memory-guard", "namespace-detect"])
.assert()
.success();
assert!(
slot_path(&tmp, 1).exists(),
"cli-slot-1.lock deve existir em {:?} após invocação do binário",
tmp.path()
);
}
#[test]
#[serial]
fn wait_lock_zero_retorna_75_quando_slots_ocupados() {
use fs4::fs_std::FileExt;
use std::fs::OpenOptions;
let tmp = TempDir::new().expect("TempDir deve ser criado");
let max = 4;
let mut handles = Vec::new();
for slot in 1..=max {
let path = slot_path(&tmp, slot);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("criação do lock file deve funcionar");
file.try_lock_exclusive()
.unwrap_or_else(|_| panic!("slot {slot} deve estar livre para testes"));
handles.push(file);
}
Command::cargo_bin("sqlite-graphrag")
.expect("binário sqlite-graphrag não encontrado")
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args([
"--skip-memory-guard",
"--max-concurrency",
"4",
"--wait-lock",
"0",
"namespace-detect",
])
.assert()
.failure()
.code(75);
drop(handles);
}
#[test]
#[serial]
#[ignore = "flaky — depende de timing de subprocessos — rodar manualmente com: cargo test -- --ignored"]
fn slot_bloqueia_segunda_instancia_com_exit_75() {
use fs4::fs_std::FileExt;
use std::fs::OpenOptions;
let tmp = TempDir::new().expect("TempDir deve ser criado");
let mut handles = Vec::new();
for slot in 1..=4 {
let path = slot_path(&tmp, slot);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("criação do lock file deve funcionar");
file.try_lock_exclusive().expect("slot deve estar livre");
handles.push(file);
}
std::thread::sleep(std::time::Duration::from_millis(100));
Command::cargo_bin("sqlite-graphrag")
.expect("binário sqlite-graphrag não encontrado")
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args([
"--skip-memory-guard",
"--max-concurrency",
"4",
"--wait-lock",
"0",
"namespace-detect",
])
.assert()
.failure()
.code(75);
drop(handles);
}
#[test]
#[serial]
#[ignore = "flaky — depende de timing de subprocessos — rodar manualmente com: cargo test -- --ignored"]
fn wait_lock_espera_e_adquire_slot() {
use fs4::fs_std::FileExt;
use std::fs::OpenOptions;
let tmp = TempDir::new().expect("TempDir deve ser criado");
let mut handles = Vec::new();
for slot in 1..=4 {
let path = slot_path(&tmp, slot);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("criação do lock file deve funcionar");
file.try_lock_exclusive().expect("slot deve estar livre");
handles.push(file);
}
let tmp_path = tmp.path().to_path_buf();
let _ = tmp_path; std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1000));
drop(handles);
});
Command::cargo_bin("sqlite-graphrag")
.expect("binário sqlite-graphrag não encontrado")
.env("SQLITE_GRAPHRAG_CACHE_DIR", tmp.path())
.args([
"--skip-memory-guard",
"--max-concurrency",
"4",
"--wait-lock",
"10",
"namespace-detect",
])
.assert()
.success();
}