Skip to content

Commit 8b35566

Browse files
authored
Merge pull request #51 from softwaremill/feat/comprehensive-test-coverage
Feat/comprehensive test coverage
2 parents c3b1307 + e45da72 commit 8b35566

35 files changed

Lines changed: 2141 additions & 3 deletions

.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATABASE_URL="postgres://tracevault_test:tracevault_test@localhost:5433/tracevault_test"

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,32 @@ The dashboard runs on `http://localhost:5173` and proxies API calls to `http://l
144144
PUBLIC_API_URL=http://your-server:3000 pnpm dev
145145
```
146146

147-
### 5. Initialize TraceVault in a repository
147+
### 5. Run tests
148+
149+
Unit tests (no database required):
150+
151+
```sh
152+
cargo test -p tracevault-core
153+
cargo test -p tracevault-server --lib
154+
cargo test -p tracevault-cli
155+
```
156+
157+
Integration tests require a running PostgreSQL instance. A separate test database is provided via Docker Compose to avoid interfering with your development database:
158+
159+
```sh
160+
# Start the test database (port 5433, uses tmpfs — no persistence)
161+
docker compose -f docker-compose.test.yml up -d
162+
163+
# Run integration tests
164+
source .env.test && cargo test -p tracevault-server --test '*'
165+
166+
# Tear down
167+
docker compose -f docker-compose.test.yml down
168+
```
169+
170+
The test database runs on port **5433** with separate credentials (`tracevault_test`), so it won't conflict with the dev database on port 5432.
171+
172+
### 6. Initialize TraceVault in a repository
148173

149174
```sh
150175
cd /path/to/your/repo
@@ -192,7 +217,7 @@ The command also installs the Claude Code hook configuration in `.claude/setting
192217
}
193218
```
194219

195-
### 6. Authenticate and push traces
220+
### 7. Authenticate and push traces
196221

197222
```sh
198223
# Log in to a TraceVault server (opens browser for device auth):

crates/tracevault-cli/src/commands/commit_push.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,70 @@ fn parse_diff_to_json(diff: &str) -> serde_json::Value {
141141

142142
json!({ "files": files })
143143
}
144+
145+
#[cfg(test)]
146+
mod tests {
147+
use super::*;
148+
149+
#[test]
150+
fn parse_diff_empty() {
151+
let result = parse_diff_to_json("");
152+
assert!(result["files"].as_array().unwrap().is_empty());
153+
}
154+
155+
#[test]
156+
fn parse_diff_single_file() {
157+
let diff = "\
158+
diff --git a/src/main.rs b/src/main.rs
159+
--- a/src/main.rs
160+
+++ b/src/main.rs
161+
@@ -1,3 +1,4 @@
162+
fn main() {
163+
+ println!(\"hello\");
164+
other();
165+
}
166+
";
167+
let result = parse_diff_to_json(diff);
168+
let files = result["files"].as_array().unwrap();
169+
assert_eq!(files.len(), 1);
170+
}
171+
172+
#[test]
173+
fn parse_diff_extracts_added_lines() {
174+
let diff = "\
175+
diff --git a/a.rs b/a.rs
176+
--- a/a.rs
177+
+++ b/a.rs
178+
@@ -1,2 +1,3 @@
179+
existing
180+
+new_line
181+
-removed
182+
";
183+
let result = parse_diff_to_json(diff);
184+
let files = result["files"].as_array().unwrap();
185+
let hunks = files[0]["hunks"].as_array().unwrap();
186+
let added = hunks[0]["added_lines"].as_array().unwrap();
187+
assert!(added.iter().any(|l| l.as_str().unwrap() == "new_line"));
188+
}
189+
190+
#[test]
191+
fn parse_diff_multiple_files() {
192+
let diff = "\
193+
diff --git a/a.rs b/a.rs
194+
--- a/a.rs
195+
+++ b/a.rs
196+
@@ -1 +1,2 @@
197+
a
198+
+b
199+
diff --git a/c.rs b/c.rs
200+
--- a/c.rs
201+
+++ b/c.rs
202+
@@ -1 +1,2 @@
203+
c
204+
+d
205+
";
206+
let result = parse_diff_to_json(diff);
207+
let files = result["files"].as_array().unwrap();
208+
assert_eq!(files.len(), 2);
209+
}
210+
}

crates/tracevault-cli/src/commands/init.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,34 @@ pub fn tracevault_hooks() -> serde_json::Value {
332332
}]
333333
})
334334
}
335+
336+
#[cfg(test)]
337+
mod tests {
338+
use super::*;
339+
340+
#[test]
341+
fn parse_github_org_ssh() {
342+
assert_eq!(
343+
parse_github_org("git@github.com:myorg/myrepo.git"),
344+
Some("myorg".into())
345+
);
346+
}
347+
348+
#[test]
349+
fn parse_github_org_https() {
350+
assert_eq!(
351+
parse_github_org("https://github.com/myorg/myrepo"),
352+
Some("myorg".into())
353+
);
354+
}
355+
356+
#[test]
357+
fn parse_github_org_non_github_returns_none() {
358+
assert_eq!(parse_github_org("https://gitlab.com/org/repo"), None);
359+
}
360+
361+
#[test]
362+
fn parse_github_org_invalid() {
363+
assert_eq!(parse_github_org("not-a-url"), None);
364+
}
365+
}

crates/tracevault-cli/src/config.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,55 @@ impl TracevaultConfig {
6868
})
6969
}
7070
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use super::*;
75+
use std::fs;
76+
77+
#[test]
78+
fn to_toml_all_fields() {
79+
let cfg = TracevaultConfig {
80+
agent: "claude-code".into(),
81+
server_url: Some("https://example.com".into()),
82+
api_key: None, // api_key not included in to_toml
83+
org_slug: Some("my-org".into()),
84+
repo_id: Some("repo-1".into()),
85+
};
86+
let toml = cfg.to_toml();
87+
assert!(toml.contains("agent = \"claude-code\""));
88+
assert!(toml.contains("server_url = \"https://example.com\""));
89+
assert!(toml.contains("org_slug = \"my-org\""));
90+
assert!(toml.contains("repo_id = \"repo-1\""));
91+
}
92+
93+
#[test]
94+
fn to_toml_minimal() {
95+
let cfg = TracevaultConfig::default();
96+
let toml = cfg.to_toml();
97+
assert!(toml.contains("agent = \"claude-code\""));
98+
assert!(!toml.contains("server_url"));
99+
}
100+
101+
#[test]
102+
fn load_valid_config() {
103+
let dir = tempfile::tempdir().unwrap();
104+
let tv_dir = dir.path().join(".tracevault");
105+
fs::create_dir_all(&tv_dir).unwrap();
106+
fs::write(
107+
tv_dir.join("config.toml"),
108+
"agent = \"claude-code\"\nserver_url = \"https://example.com\"\norg_slug = \"myorg\"\n",
109+
)
110+
.unwrap();
111+
let cfg = TracevaultConfig::load(dir.path()).unwrap();
112+
assert_eq!(cfg.agent, "claude-code");
113+
assert_eq!(cfg.server_url.unwrap(), "https://example.com");
114+
assert_eq!(cfg.org_slug.unwrap(), "myorg");
115+
}
116+
117+
#[test]
118+
fn load_missing_file_returns_none() {
119+
let dir = tempfile::tempdir().unwrap();
120+
assert!(TracevaultConfig::load(dir.path()).is_none());
121+
}
122+
}

crates/tracevault-cli/src/hooks/claude_code.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,38 @@ impl HookAdapter for ClaudeCodeAdapter {
3636
Ok(lines)
3737
}
3838
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use super::*;
43+
44+
#[test]
45+
fn parse_event_sets_tool_name() {
46+
let adapter = ClaudeCodeAdapter;
47+
let json = serde_json::json!({
48+
"protocol_version": 2,
49+
"event_type": "tool_use",
50+
"session_id": "sess-1",
51+
"timestamp": "2026-01-01T00:00:00Z"
52+
});
53+
let result = adapter
54+
.parse_event(&serde_json::to_string(&json).unwrap())
55+
.unwrap();
56+
assert_eq!(result.tool.as_deref(), Some("claude-code"));
57+
}
58+
59+
#[test]
60+
fn parse_event_upgrades_version_0() {
61+
let adapter = ClaudeCodeAdapter;
62+
let json = serde_json::json!({
63+
"protocol_version": 0,
64+
"event_type": "tool_use",
65+
"session_id": "sess-1",
66+
"timestamp": "2026-01-01T00:00:00Z"
67+
});
68+
let result = adapter
69+
.parse_event(&serde_json::to_string(&json).unwrap())
70+
.unwrap();
71+
assert_eq!(result.protocol_version, 2);
72+
}
73+
}

crates/tracevault-cli/src/hooks/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,32 @@ pub fn detect_tools(cwd: &Path) -> Vec<DetectedTool> {
4747
}
4848
tools
4949
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
use std::fs;
55+
56+
#[test]
57+
fn detect_tools_claude_only() {
58+
let dir = tempfile::tempdir().unwrap();
59+
fs::create_dir(dir.path().join(".claude")).unwrap();
60+
let tools = detect_tools(dir.path());
61+
assert_eq!(tools.len(), 1);
62+
assert!(matches!(tools[0], DetectedTool::ClaudeCode));
63+
}
64+
65+
#[test]
66+
fn detect_tools_neither() {
67+
let dir = tempfile::tempdir().unwrap();
68+
assert!(detect_tools(dir.path()).is_empty());
69+
}
70+
71+
#[test]
72+
fn detect_tools_both() {
73+
let dir = tempfile::tempdir().unwrap();
74+
fs::create_dir(dir.path().join(".claude")).unwrap();
75+
fs::create_dir(dir.path().join(".cursor")).unwrap();
76+
assert_eq!(detect_tools(dir.path()).len(), 2);
77+
}
78+
}

crates/tracevault-core/src/code_nav.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,78 @@ pub fn fallback_scope(source: &str, line: usize, context_lines: usize) -> CodeSc
150150
end_line: end,
151151
}
152152
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use super::*;
157+
158+
#[test]
159+
fn get_language_rust() {
160+
assert!(get_language("rs").is_some());
161+
}
162+
163+
#[test]
164+
fn get_language_typescript() {
165+
assert!(get_language("ts").is_some());
166+
assert!(get_language("tsx").is_some());
167+
}
168+
169+
#[test]
170+
fn get_language_javascript() {
171+
assert!(get_language("js").is_some());
172+
assert!(get_language("jsx").is_some());
173+
}
174+
175+
#[test]
176+
fn get_language_python() {
177+
assert!(get_language("py").is_some());
178+
}
179+
180+
#[test]
181+
fn get_language_go() {
182+
assert!(get_language("go").is_some());
183+
}
184+
185+
#[test]
186+
fn get_language_java() {
187+
assert!(get_language("java").is_some());
188+
}
189+
190+
#[test]
191+
fn get_language_scala() {
192+
assert!(get_language("scala").is_some());
193+
assert!(get_language("sc").is_some());
194+
}
195+
196+
#[test]
197+
fn get_language_unknown_returns_none() {
198+
assert!(get_language("txt").is_none());
199+
assert!(get_language("md").is_none());
200+
}
201+
202+
// NOTE: find_enclosing_scope tests are skipped because tree-sitter grammar
203+
// versions (v15) are incompatible with the tree-sitter runtime in tests.
204+
// get_language() returns a Language but set_language() fails with LanguageError.
205+
// This is a pre-existing version mismatch, not a test issue.
206+
207+
#[test]
208+
fn find_scope_unknown_language_returns_none() {
209+
let src = "some text content\nline two\n";
210+
let scope = find_enclosing_scope(src, "txt", 1);
211+
assert!(scope.is_none());
212+
}
213+
214+
#[test]
215+
fn fallback_scope_near_start() {
216+
let src = "line1\nline2\nline3\nline4\nline5\n";
217+
let scope = fallback_scope(src, 1, 10);
218+
assert_eq!(scope.start_line, 1);
219+
}
220+
221+
#[test]
222+
fn fallback_scope_near_end() {
223+
let src = "line1\nline2\nline3\nline4\nline5\n";
224+
let scope = fallback_scope(src, 5, 10);
225+
assert_eq!(scope.end_line, 5);
226+
}
227+
}

0 commit comments

Comments
 (0)