1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// E2E concurrency limit tests for the sqlite-graphrag slot semaphore.
//
// ISOLATION: `XDG_CACHE_HOME` points to a `TempDir` unique per test.
//
// GAP-SG-101: these tests used to set `SQLITE_GRAPHRAG_CACHE_DIR`, a retired
// product env that `lock::cache_dir` never reads — it resolves the XDG key
// `paths.cache` and then `ProjectDirs::cache_dir()`. The locks therefore landed
// in a temp directory the binary never inspected, while the binary itself
// competed for slots in the developer's REAL cache directory. That made
// `all_slots_busy_return_75` pass or fail depending on leftover
// `cli-slot-*.lock` files from earlier runs. `XDG_CACHE_HOME` is honoured by
// `ProjectDirs` and is an OS env, so it is a legitimate isolation channel.
//
// `#[serial]` is required in all tests to avoid filesystem races between tests
// that share the same compiled binary.
//
// `--skip-memory-guard` is used in all tests so that the available RAM check
// does not abort before the semaphore is exercised.
use assert_cmd::Command;
use serial_test::serial;
use tempfile::TempDir;
/// Builds a fresh `Command` with the mock LLM PATH prepended.
///
/// v1.0.76 spawns `claude` or `codex` on every `remember` / `ingest` /
/// `edit`. The bundled mocks under `tests/mock-llm/` return a fixed
/// 64-dim zero vector so the binary finishes without a real OAuth
/// login. The mock directory is leaked (no TempDir cleanup) so the
/// spawned subprocess always finds the mocks.
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;
// ---------------------------------------------------------------------------
// Test 1 — concurrency limit is respected under 10-process load
// ---------------------------------------------------------------------------
// Spawns 10 parallel invocations with --max-concurrency 4 and --wait-lock 30.
// Verifies that ALL complete successfully (the 6 that cannot acquire a slot
// keep polling until one of the initial 4 finishes and releases its slot).
#[test]
#[serial]
fn concurrency_limit_respected_under_load() {
let tmp = TempDir::new().expect("TempDir deve ser criado");
let bin = assert_cmd::cargo::cargo_bin("sqlite-graphrag");
// Spawn 10 invocations in parallel using std::process::Command for
// direct PID control (assert_cmd does not expose spawn).
let handles: Vec<_> = (0..10)
.map(|_| {
std::process::Command::new(&bin)
.env("XDG_CACHE_HOME", tmp.path())
.args([
"--skip-memory-guard",
"--max-concurrency",
"4",
"--wait-lock",
"30",
"namespace-detect",
])
.spawn()
.expect("failure ao spawnar invocação paralela")
})
.collect();
// Waits for all of them and collects exit codes.
let results: Vec<_> = handles
.into_iter()
.map(|h| h.wait_with_output().expect("wait failed"))
.collect();
let successes = results.iter().filter(|r| r.status.success()).count();
let failures = results.iter().filter(|r| !r.status.success()).count();
// All 10 invocations must complete successfully when --wait-lock=30.
assert_eq!(
successes, 10,
"all 10 invocations must complete successfully (--wait-lock 30), \
got {successes} successes and {failures} failures"
);
}
// ---------------------------------------------------------------------------
// Test 2 — --max-concurrency 0 is rejected with exit 2
// ---------------------------------------------------------------------------
// Validates that the validation guard in `Cli::validate_flags` rejects N=0
// before trying to acquire any slot.
#[test]
#[serial]
fn max_concurrency_zero_rejected_with_exit_2() {
let tmp = TempDir::new().expect("TempDir deve ser criado");
sgr_cmd()
.env("XDG_CACHE_HOME", tmp.path())
.args([
"--skip-memory-guard",
"--max-concurrency",
"0",
"namespace-detect",
])
.assert()
.failure()
.code(2);
}
// ---------------------------------------------------------------------------
// Test 3 — all slots occupied return exit 75 without waiting
// ---------------------------------------------------------------------------
// Occupy N slots directly via fs4 and verify that invocation with --wait-lock 0
// returns exit 75 (AllSlotsFull) immediately without a timeout.
#[test]
#[serial]
fn all_slots_busy_return_75() {
use fs4::fs_std::FileExt;
use std::fs::OpenOptions;
let tmp = TempDir::new().expect("TempDir deve ser criado");
let max: usize = 4;
// `ProjectDirs::cache_dir()` appends the application directory under
// `XDG_CACHE_HOME`, so the locks must live one level down to land where
// `lock::cli_slot_path` will look for them.
let slots_dir = tmp.path().join("sqlite-graphrag");
std::fs::create_dir_all(&slots_dir).expect("slot dir deve ser criado");
// Lock all slots directly to simulate 4 active instances.
let mut handles = Vec::new();
for slot in 1..=max {
let path = slots_dir.join(format!("cli-slot-{slot}.lock"));
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("criação de lock file deve funcionar");
file.try_lock_exclusive()
.unwrap_or_else(|_| panic!("slot {slot} deve estar livre antes do teste"));
handles.push(file);
}
// Invocation with --wait-lock 0 must fail immediately with exit 75.
sgr_cmd()
.env("XDG_CACHE_HOME", tmp.path())
.args([
"--skip-memory-guard",
"--max-concurrency",
"4",
"--wait-lock",
"0",
"namespace-detect",
])
.assert()
.failure()
.code(75);
// Releases the locks before drop(tmp).
drop(handles);
}
// ---------------------------------------------------------------------------
// Test 4 — --skip-memory-guard bypasses the available-memory check
// ---------------------------------------------------------------------------
// Verifies that `--skip-memory-guard` allows the command to run without going
// through the RAM check. Without the flag, the command could fail with
// exit 77 in CI environments with limited available memory. With the flag, it
// should complete normally regardless of available RAM.
#[test]
#[serial]
fn skip_memory_guard_bypasses_ram_check() {
let tmp = TempDir::new().expect("TempDir deve ser criado");
// With --skip-memory-guard, the command must complete successfully even in
// environments where available RAM could cause exit 77.
sgr_cmd()
.env("XDG_CACHE_HOME", tmp.path())
.args(["--skip-memory-guard", "namespace-detect"])
.assert()
.success();
}