Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .gitignore
Binary file not shown.
122 changes: 122 additions & 0 deletions src/knowledge_engine/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,3 +473,125 @@ fn save_repo_index_state(
)?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::registry::RepoEntry;
use crate::registry::test_helpers::WorkspaceRegistry;
use std::path::Path;

fn init_git_repo(path: &Path) -> git2::Repository {
let repo = git2::Repository::init(path).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
let sig = repo.signature().unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[]).unwrap();
drop(tree);
repo
}

#[test]
fn test_prepare_repos_empty_path_returns_all() -> anyhow::Result<()> {
let mut conn = WorkspaceRegistry::init_in_memory()?;
let _ = WorkspaceRegistry::seed_test_repo(&mut conn, "repo1")?;
let _ = WorkspaceRegistry::seed_test_repo(&mut conn, "repo2")?;

let repos = prepare_repos(&mut conn, "")?;
assert_eq!(repos.len(), 2);
assert!(repos.iter().any(|r| r.id == "repo1"));
assert!(repos.iter().any(|r| r.id == "repo2"));
Ok(())
}

#[test]
fn test_prepare_repos_matching_path_returns_one() -> anyhow::Result<()> {
let mut conn = WorkspaceRegistry::init_in_memory()?;
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("myrepo");
std::fs::create_dir(&path)?;

let repo = RepoEntry {
id: "myrepo".to_string(),
local_path: path.clone(),
tags: vec![],
language: Some("rust".to_string()),
discovered_at: chrono::Utc::now(),
workspace_type: "git".to_string(),
data_tier: "private".to_string(),
last_synced_at: None,
stars: None,
remotes: vec![],
};
crate::registry::repo::save_repo(&mut conn, &repo)?;

let repos = prepare_repos(&mut conn, path.to_str().unwrap())?;
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].id, "myrepo");
Ok(())
}

#[test]
fn test_prepare_repos_nonexistent_path_errors() -> anyhow::Result<()> {
let mut conn = WorkspaceRegistry::init_in_memory()?;
let result = prepare_repos(&mut conn, "/nonexistent/path/12345");
assert!(result.is_err());
Ok(())
}

#[test]
fn test_prepare_repos_unregistered_existing_path_auto_registers() -> anyhow::Result<()> {
let mut conn = WorkspaceRegistry::init_in_memory()?;
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("unregistered");
std::fs::create_dir(&path)?;
let _ = init_git_repo(&path);

let repos = prepare_repos(&mut conn, path.to_str().unwrap())?;
assert_eq!(repos.len(), 1);
// Use file_name comparison to avoid Windows short-name (8.3) path mismatches
assert_eq!(repos[0].local_path.file_name(), path.file_name());
assert!(repos[0].local_path.exists());
// Verify it was saved to registry
let all = crate::registry::repo::list_repos(&conn)?;
assert_eq!(all.len(), 1);
Ok(())
}

#[test]
fn test_save_and_get_repo_index_state() -> anyhow::Result<()> {
let mut conn = WorkspaceRegistry::init_in_memory()?;
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("gitrepo");
std::fs::create_dir(&path)?;
let repo = git2::Repository::init(&path)?;
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
let sig = repo.signature().unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
let oid = repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])?;

save_repo_index_state(&mut conn, "test-repo", &oid.to_string())?;

let hash: Option<String> = conn
.query_row(
"SELECT last_commit_hash FROM repo_index_state WHERE repo_id = ?1",
["test-repo"],
|row| row.get(0),
)
.unwrap_or(None);
assert_eq!(hash, Some(oid.to_string()));
Ok(())
}
}
196 changes: 196 additions & 0 deletions src/knowledge_engine/index_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,200 @@ mod tests {
assert!(json.contains("\"state\":\"unknown\""));
assert!(json.contains("\"reason\":\"x\""));
}

#[test]
fn test_index_state_is_fresh_and_changed_count() {
assert!(IndexState::Fresh.is_fresh());
assert!(!IndexState::Missing.is_fresh());
assert!(!IndexState::Unknown { reason: "err".into() }.is_fresh());

let stale = IndexState::Stale {
added: vec!["a.rs".into()],
modified: vec!["b.rs".into(), "c.rs".into()],
deleted: vec![],
};
assert_eq!(stale.changed_files_count(), 3);
assert_eq!(IndexState::Fresh.changed_files_count(), 0);
assert_eq!(IndexState::Missing.changed_files_count(), 0);
}

#[test]
fn test_get_repo_index_state_missing() -> anyhow::Result<()> {
use crate::registry::test_helpers::WorkspaceRegistry;
let conn = WorkspaceRegistry::init_in_memory()?;
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("repo");
std::fs::create_dir(&path)?;
let repo = git2::Repository::init(&path)?;
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
let sig = repo.signature().unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])?;

let entry = RepoEntry {
id: "missing-repo".to_string(),
local_path: path,
tags: vec![],
language: Some("rust".to_string()),
discovered_at: chrono::Utc::now(),
workspace_type: "git".to_string(),
data_tier: "private".to_string(),
last_synced_at: None,
stars: None,
remotes: vec![],
};

let state = get_repo_index_state(&conn, &entry);
assert!(matches!(state, IndexState::Missing));
Ok(())
}

#[test]
fn test_get_repo_index_state_fresh() -> anyhow::Result<()> {
use crate::registry::test_helpers::WorkspaceRegistry;
let conn = WorkspaceRegistry::init_in_memory()?;
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("repo");
std::fs::create_dir(&path)?;
let repo = git2::Repository::init(&path)?;
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
let sig = repo.signature().unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
let oid = repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])?;

conn.execute(
"INSERT INTO repo_index_state (repo_id, last_commit_hash, indexed_at) VALUES (?1, ?2, datetime('now'))",
["fresh-repo", &oid.to_string()],
)?;

let entry = RepoEntry {
id: "fresh-repo".to_string(),
local_path: path,
tags: vec![],
language: Some("rust".to_string()),
discovered_at: chrono::Utc::now(),
workspace_type: "git".to_string(),
data_tier: "private".to_string(),
last_synced_at: None,
stars: None,
remotes: vec![],
};

let state = get_repo_index_state(&conn, &entry);
assert!(matches!(state, IndexState::Fresh));
Ok(())
}

#[test]
fn test_get_repo_index_state_stale() -> anyhow::Result<()> {
use crate::registry::test_helpers::WorkspaceRegistry;
let conn = WorkspaceRegistry::init_in_memory()?;
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("repo");
std::fs::create_dir(&path)?;
let repo = git2::Repository::init(&path)?;
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
let sig = repo.signature().unwrap();

// First commit
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
let old_oid = repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])?;
drop(tree);

// Save the first commit hash as last indexed
conn.execute(
"INSERT INTO repo_index_state (repo_id, last_commit_hash, indexed_at) VALUES (?1, ?2, datetime('now'))",
["stale-repo", &old_oid.to_string()],
)?;

// Second commit so HEAD moves forward and diff_since detects changes
let tree_id2 = {
let mut index = repo.index().unwrap();
// Add a dummy file to create a new tree
let blob_oid = repo.blob(b"hello")?;
index.add_frombuffer(
&git2::IndexEntry {
ctime: git2::IndexTime::new(0, 0),
mtime: git2::IndexTime::new(0, 0),
dev: 0,
ino: 0,
mode: 0o100644,
uid: 0,
gid: 0,
file_size: 5,
id: blob_oid,
flags: 0,
flags_extended: 0,
path: b"file.txt".to_vec(),
},
b"hello",
)?;
index.write_tree().unwrap()
};
let parent = repo.find_commit(old_oid)?;
let tree2 = repo.find_tree(tree_id2).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "second", &tree2, &[&parent])?;
drop(tree2);

let entry = RepoEntry {
id: "stale-repo".to_string(),
local_path: path,
tags: vec![],
language: Some("rust".to_string()),
discovered_at: chrono::Utc::now(),
workspace_type: "git".to_string(),
data_tier: "private".to_string(),
last_synced_at: None,
stars: None,
remotes: vec![],
};

let state = get_repo_index_state(&conn, &entry);
assert!(matches!(state, IndexState::Stale { .. }), "expected Stale, got {:?}", state);
Ok(())
}

#[test]
fn test_get_repo_index_state_unknown_not_git() -> anyhow::Result<()> {
use crate::registry::test_helpers::WorkspaceRegistry;
let conn = WorkspaceRegistry::init_in_memory()?;
let tmp = tempfile::tempdir()?;
let path = tmp.path().join("not-git");
std::fs::create_dir(&path)?;

let entry = RepoEntry {
id: "unknown-repo".to_string(),
local_path: path,
tags: vec![],
language: Some("rust".to_string()),
discovered_at: chrono::Utc::now(),
workspace_type: "git".to_string(),
data_tier: "private".to_string(),
last_synced_at: None,
stars: None,
remotes: vec![],
};

let state = get_repo_index_state(&conn, &entry);
assert!(matches!(state, IndexState::Unknown { .. }), "expected Unknown, got {:?}", state);
Ok(())
}
}
2 changes: 1 addition & 1 deletion src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ impl crate::clients::RegistryClient for crate::storage::AppContext {
}

#[cfg(test)]
mod test_helpers;
pub mod test_helpers;

#[cfg(test)]
mod tests;
7 changes: 7 additions & 0 deletions src/registry/test_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 juice094
pub use super::WorkspaceRegistry;
use super::*;

#[cfg(test)]
Expand Down Expand Up @@ -355,6 +356,12 @@ CREATE TABLE IF NOT EXISTS orphan_tantivy_docs (
repo_id TEXT PRIMARY KEY,
detected_at DATETIME DEFAULT current_timestamp
);

CREATE TABLE IF NOT EXISTS repo_index_state (
repo_id TEXT PRIMARY KEY,
last_commit_hash TEXT,
indexed_at DATETIME DEFAULT current_timestamp
);
"#;

#[cfg(test)]
Expand Down
Loading