Skip to content

Commit d1e5e24

Browse files
authored
Merge pull request #4 from rtk-ai/test/comprehensive-tests
test: add 47 comprehensive tests across all modules
2 parents ea3748c + a4a76dc commit d1e5e24

7 files changed

Lines changed: 926 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ colored = "2"
4343
aws-config = { version = "1", features = ["behavior-version-latest"] }
4444
aws-sdk-s3 = "1"
4545
urlencoding = "2"
46+
47+
[dev-dependencies]
48+
tempfile = "3"

src/cli/mod.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,3 +933,83 @@ fn cmd_config_show(repo: &str) -> Result<()> {
933933

934934
Ok(())
935935
}
936+
937+
#[cfg(test)]
938+
mod tests {
939+
use super::*;
940+
use crate::db::lock_store::LockEntry;
941+
942+
// ── validate_identifier tests ──
943+
944+
#[test]
945+
fn test_validate_identifier_valid() {
946+
assert!(validate_identifier("agent-1", "id").is_ok());
947+
assert!(validate_identifier("my_agent", "id").is_ok());
948+
assert!(validate_identifier("agent.v2", "id").is_ok());
949+
assert!(validate_identifier("abc123", "id").is_ok());
950+
}
951+
952+
#[test]
953+
fn test_validate_identifier_empty() {
954+
assert!(validate_identifier("", "id").is_err());
955+
}
956+
957+
#[test]
958+
fn test_validate_identifier_path_traversal() {
959+
assert!(validate_identifier("..", "id").is_err());
960+
}
961+
962+
#[test]
963+
fn test_validate_identifier_slash() {
964+
assert!(validate_identifier("foo/bar", "id").is_err());
965+
}
966+
967+
#[test]
968+
fn test_validate_identifier_backslash() {
969+
assert!(validate_identifier("foo\\bar", "id").is_err());
970+
}
971+
972+
#[test]
973+
fn test_validate_identifier_starts_with_dash() {
974+
assert!(validate_identifier("-agent", "id").is_err());
975+
}
976+
977+
#[test]
978+
fn test_validate_identifier_special_chars() {
979+
assert!(validate_identifier("foo@bar", "id").is_err());
980+
assert!(validate_identifier("foo bar", "id").is_err());
981+
assert!(validate_identifier("foo;rm", "id").is_err());
982+
}
983+
984+
// ── is_entry_expired_local tests ──
985+
986+
fn make_entry(locked_at: &str, ttl: u64) -> LockEntry {
987+
LockEntry {
988+
symbol_id: "test::sym".to_string(),
989+
agent_id: "agent-1".to_string(),
990+
intent: "testing".to_string(),
991+
locked_at: locked_at.to_string(),
992+
ttl_seconds: ttl,
993+
}
994+
}
995+
996+
#[test]
997+
fn test_is_entry_expired_local_fresh() {
998+
let now = chrono::Utc::now().to_rfc3339();
999+
let entry = make_entry(&now, 600);
1000+
assert!(!is_entry_expired_local(&entry));
1001+
}
1002+
1003+
#[test]
1004+
fn test_is_entry_expired_local_expired() {
1005+
let one_hour_ago = (chrono::Utc::now() - chrono::Duration::hours(1)).to_rfc3339();
1006+
let entry = make_entry(&one_hour_ago, 60);
1007+
assert!(is_entry_expired_local(&entry));
1008+
}
1009+
1010+
#[test]
1011+
fn test_is_entry_expired_local_bad_timestamp() {
1012+
let entry = make_entry("not-a-timestamp", 600);
1013+
assert!(is_entry_expired_local(&entry));
1014+
}
1015+
}

src/config.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,71 @@ impl GritConfig {
5050
Ok(())
5151
}
5252
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use super::*;
57+
use crate::db::s3_store::S3Config;
58+
use tempfile::TempDir;
59+
60+
#[test]
61+
fn test_default_config() {
62+
let config = GritConfig::default();
63+
assert_eq!(config.backend, "local");
64+
assert!(config.s3.is_none());
65+
}
66+
67+
#[test]
68+
fn test_save_and_load() {
69+
let tmp = TempDir::new().unwrap();
70+
let config = GritConfig {
71+
backend: "local".to_string(),
72+
s3: None,
73+
};
74+
config.save(tmp.path()).unwrap();
75+
let loaded = GritConfig::load(tmp.path()).unwrap();
76+
assert_eq!(loaded.backend, "local");
77+
assert!(loaded.s3.is_none());
78+
}
79+
80+
#[test]
81+
fn test_load_missing_file() {
82+
let tmp = TempDir::new().unwrap();
83+
// No config.json written — should return default
84+
let config = GritConfig::load(tmp.path()).unwrap();
85+
assert_eq!(config.backend, "local");
86+
assert!(config.s3.is_none());
87+
}
88+
89+
#[test]
90+
fn test_load_malformed_json() {
91+
let tmp = TempDir::new().unwrap();
92+
let path = tmp.path().join("config.json");
93+
std::fs::write(&path, "not valid json {{{").unwrap();
94+
let config = GritConfig::load(tmp.path()).unwrap();
95+
assert_eq!(config.backend, "local");
96+
assert!(config.s3.is_none());
97+
}
98+
99+
#[test]
100+
fn test_s3_config_roundtrip() {
101+
let tmp = TempDir::new().unwrap();
102+
let config = GritConfig {
103+
backend: "s3".to_string(),
104+
s3: Some(S3Config {
105+
bucket: "my-bucket".to_string(),
106+
prefix: Some("grit/locks/".to_string()),
107+
region: Some("us-east-1".to_string()),
108+
endpoint: Some("https://custom.endpoint.com".to_string()),
109+
}),
110+
};
111+
config.save(tmp.path()).unwrap();
112+
let loaded = GritConfig::load(tmp.path()).unwrap();
113+
assert_eq!(loaded.backend, "s3");
114+
let s3 = loaded.s3.unwrap();
115+
assert_eq!(s3.bucket, "my-bucket");
116+
assert_eq!(s3.prefix.unwrap(), "grit/locks/");
117+
assert_eq!(s3.region.unwrap(), "us-east-1");
118+
assert_eq!(s3.endpoint.unwrap(), "https://custom.endpoint.com");
119+
}
120+
}

src/db/mod.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,173 @@ impl Database {
251251
}
252252

253253
}
254+
255+
#[cfg(test)]
256+
mod tests {
257+
use super::*;
258+
use crate::parser::Symbol;
259+
use tempfile::TempDir;
260+
261+
fn make_symbol(id: &str, file: &str, name: &str, kind: &str) -> Symbol {
262+
Symbol {
263+
id: id.to_string(),
264+
file: file.to_string(),
265+
name: name.to_string(),
266+
kind: kind.to_string(),
267+
start_line: 1,
268+
end_line: 10,
269+
hash: "abc123".to_string(),
270+
}
271+
}
272+
273+
fn setup_db() -> (TempDir, Database) {
274+
let tmp = TempDir::new().unwrap();
275+
let db_path = tmp.path().join("test.db");
276+
let db = Database::open(&db_path).unwrap();
277+
db.init_schema().unwrap();
278+
(tmp, db)
279+
}
280+
281+
#[test]
282+
fn test_open_and_init_schema() {
283+
let tmp = TempDir::new().unwrap();
284+
let db_path = tmp.path().join("test.db");
285+
let db = Database::open(&db_path).unwrap();
286+
assert!(db.init_schema().is_ok());
287+
}
288+
289+
#[test]
290+
fn test_upsert_and_count_symbols() {
291+
let (_tmp, db) = setup_db();
292+
let symbols: Vec<Symbol> = (0..5)
293+
.map(|i| make_symbol(&format!("file.rs::fn{}", i), "file.rs", &format!("fn{}", i), "function"))
294+
.collect();
295+
db.upsert_symbols(&symbols).unwrap();
296+
assert_eq!(db.count_symbols().unwrap(), 5);
297+
}
298+
299+
#[test]
300+
fn test_upsert_updates_existing() {
301+
let (_tmp, db) = setup_db();
302+
let sym = make_symbol("file.rs::foo", "file.rs", "foo", "function");
303+
db.upsert_symbols(&[sym]).unwrap();
304+
assert_eq!(db.count_symbols().unwrap(), 1);
305+
306+
// Update same symbol with different hash
307+
let updated = Symbol {
308+
id: "file.rs::foo".to_string(),
309+
file: "file.rs".to_string(),
310+
name: "foo".to_string(),
311+
kind: "function".to_string(),
312+
start_line: 5,
313+
end_line: 20,
314+
hash: "new_hash".to_string(),
315+
};
316+
db.upsert_symbols(&[updated]).unwrap();
317+
assert_eq!(db.count_symbols().unwrap(), 1);
318+
}
319+
320+
#[test]
321+
fn test_list_symbols_no_filter() {
322+
let (_tmp, db) = setup_db();
323+
let symbols = vec![
324+
make_symbol("a.rs::fn1", "a.rs", "fn1", "function"),
325+
make_symbol("a.rs::fn2", "a.rs", "fn2", "function"),
326+
make_symbol("b.rs::fn3", "b.rs", "fn3", "function"),
327+
];
328+
db.upsert_symbols(&symbols).unwrap();
329+
let all = db.list_symbols(None).unwrap();
330+
assert_eq!(all.len(), 3);
331+
}
332+
333+
#[test]
334+
fn test_list_symbols_with_filter() {
335+
let (_tmp, db) = setup_db();
336+
let symbols = vec![
337+
make_symbol("src/a.rs::fn1", "src/a.rs", "fn1", "function"),
338+
make_symbol("src/a.rs::fn2", "src/a.rs", "fn2", "function"),
339+
make_symbol("src/b.rs::fn3", "src/b.rs", "fn3", "function"),
340+
];
341+
db.upsert_symbols(&symbols).unwrap();
342+
let filtered = db.list_symbols(Some("a.rs")).unwrap();
343+
assert_eq!(filtered.len(), 2);
344+
for row in &filtered {
345+
assert!(row.1.contains("a.rs"));
346+
}
347+
}
348+
349+
#[test]
350+
fn test_search_symbols() {
351+
let (_tmp, db) = setup_db();
352+
let symbols = vec![
353+
make_symbol("src/auth.rs::login", "src/auth.rs", "login", "function"),
354+
make_symbol("src/auth.rs::logout", "src/auth.rs", "logout", "function"),
355+
make_symbol("src/db.rs::connect", "src/db.rs", "connect", "function"),
356+
];
357+
db.upsert_symbols(&symbols).unwrap();
358+
let results = db.search_symbols(&["login"]).unwrap();
359+
assert_eq!(results.len(), 1);
360+
assert_eq!(results[0].2, "login");
361+
}
362+
363+
#[test]
364+
fn test_available_symbols_in_files() {
365+
let (_tmp, db) = setup_db();
366+
let symbols = vec![
367+
make_symbol("f.rs::a", "f.rs", "a", "function"),
368+
make_symbol("f.rs::b", "f.rs", "b", "function"),
369+
make_symbol("f.rs::c", "f.rs", "c", "function"),
370+
];
371+
db.upsert_symbols(&symbols).unwrap();
372+
373+
// Lock symbol "f.rs::b"
374+
db.conn.execute(
375+
"INSERT INTO locks (symbol_id, agent_id, intent) VALUES (?1, ?2, ?3)",
376+
params!["f.rs::b", "agent-1", "editing"],
377+
).unwrap();
378+
379+
let available = db.available_symbols_in_files(&["f.rs"]).unwrap();
380+
assert_eq!(available.len(), 2);
381+
assert!(available.contains(&"f.rs::a".to_string()));
382+
assert!(available.contains(&"f.rs::c".to_string()));
383+
assert!(!available.contains(&"f.rs::b".to_string()));
384+
}
385+
386+
#[test]
387+
fn test_session_lifecycle() {
388+
let (_tmp, db) = setup_db();
389+
db.create_session("sess1", "feature/x", "main").unwrap();
390+
391+
let active = db.get_active_session().unwrap();
392+
assert!(active.is_some());
393+
let (name, branch, base) = active.unwrap();
394+
assert_eq!(name, "sess1");
395+
assert_eq!(branch, "feature/x");
396+
assert_eq!(base, "main");
397+
398+
db.close_session("sess1").unwrap();
399+
let active = db.get_active_session().unwrap();
400+
assert!(active.is_none());
401+
}
402+
403+
#[test]
404+
fn test_no_active_session() {
405+
let (_tmp, db) = setup_db();
406+
let active = db.get_active_session().unwrap();
407+
assert!(active.is_none());
408+
}
409+
410+
#[test]
411+
fn test_integrity_check_on_open() {
412+
let tmp = TempDir::new().unwrap();
413+
let db_path = tmp.path().join("test.db");
414+
// First open creates the file
415+
{
416+
let db = Database::open(&db_path).unwrap();
417+
db.init_schema().unwrap();
418+
}
419+
// Second open runs integrity check on existing DB
420+
let result = Database::open(&db_path);
421+
assert!(result.is_ok());
422+
}
423+
}

0 commit comments

Comments
 (0)