Skip to content

Commit e4d7cc7

Browse files
committed
test(coverage): batch 5–8 — Rust unit tests toward 80% for 20 modules (tinyhumansai#530)
Add 244 new unit tests across 20 modules to push line coverage from 75.06% → 75.70% overall. Modules improved: - api/socket: 77% → 100% - rpc/dispatch: 75% → 100% - config/schema/channels: 77% → 100% - composio/gmail/sync: 78% → 97% - composio/notion/sync: 77% → 96% - webhooks/router: 78% → 96% - agent/hooks: 78% → 92% - config/schema/proxy: 76% → 90% - local_ai/device: 79% → 89% - text_input/schemas: 73% → 89% - agent/harness/interrupt: 76% → 88% - billing/schemas: 79% → 87% - screen_intelligence/schemas: 78% → 81% - about_app/types, health/schemas, app_state/schemas, core/types, config/schema/identity_cost, cron/scheduler Tests cover: schema catalog integrity, param deserialization, serde roundtrips, helper functions, edge cases, error paths. All 3974 tests pass.
1 parent a78506f commit e4d7cc7

21 files changed

Lines changed: 2272 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/socket.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,53 @@ pub fn websocket_url(http_or_https_base: &str) -> String {
1212
};
1313
format!("{}/socket.io/?EIO=4&transport=websocket", ws_base)
1414
}
15+
16+
#[cfg(test)]
17+
mod tests {
18+
use super::*;
19+
20+
#[test]
21+
fn converts_https_to_wss() {
22+
let url = websocket_url("https://api.tinyhumans.ai");
23+
assert_eq!(
24+
url,
25+
"wss://api.tinyhumans.ai/socket.io/?EIO=4&transport=websocket"
26+
);
27+
}
28+
29+
#[test]
30+
fn converts_http_to_ws() {
31+
let url = websocket_url("http://localhost:3000");
32+
assert_eq!(
33+
url,
34+
"ws://localhost:3000/socket.io/?EIO=4&transport=websocket"
35+
);
36+
}
37+
38+
#[test]
39+
fn passes_through_unknown_scheme() {
40+
let url = websocket_url("ftp://example.com");
41+
assert_eq!(
42+
url,
43+
"ftp://example.com/socket.io/?EIO=4&transport=websocket"
44+
);
45+
}
46+
47+
#[test]
48+
fn strips_trailing_slash() {
49+
let url = websocket_url("https://api.tinyhumans.ai/");
50+
assert_eq!(
51+
url,
52+
"wss://api.tinyhumans.ai/socket.io/?EIO=4&transport=websocket"
53+
);
54+
}
55+
56+
#[test]
57+
fn strips_multiple_trailing_slashes() {
58+
let url = websocket_url("https://api.tinyhumans.ai///");
59+
assert_eq!(
60+
url,
61+
"wss://api.tinyhumans.ai/socket.io/?EIO=4&transport=websocket"
62+
);
63+
}
64+
}

src/core/types.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,126 @@ pub struct AppState {
140140
/// The current version of the OpenHuman core binary, usually from `CARGO_PKG_VERSION`.
141141
pub core_version: String,
142142
}
143+
144+
#[cfg(test)]
145+
mod tests {
146+
use super::*;
147+
use serde_json::json;
148+
149+
#[test]
150+
fn invocation_result_ok_serializes_value() {
151+
let result = InvocationResult::ok(json!({"key": "value"})).unwrap();
152+
assert_eq!(result.value, json!({"key": "value"}));
153+
assert!(result.logs.is_empty());
154+
}
155+
156+
#[test]
157+
fn invocation_result_with_logs() {
158+
let result =
159+
InvocationResult::with_logs(json!(42), vec!["log1".into(), "log2".into()]).unwrap();
160+
assert_eq!(result.value, json!(42));
161+
assert_eq!(result.logs.len(), 2);
162+
}
163+
164+
#[test]
165+
fn invocation_to_rpc_json_no_logs_returns_value_directly() {
166+
let inv = InvocationResult {
167+
value: json!({"data": true}),
168+
logs: vec![],
169+
};
170+
let json = invocation_to_rpc_json(inv);
171+
assert_eq!(json, json!({"data": true}));
172+
}
173+
174+
#[test]
175+
fn invocation_to_rpc_json_with_logs_wraps_in_envelope() {
176+
let inv = InvocationResult {
177+
value: json!({"data": true}),
178+
logs: vec!["info".into()],
179+
};
180+
let json = invocation_to_rpc_json(inv);
181+
assert!(json.get("result").is_some());
182+
assert!(json.get("logs").is_some());
183+
assert_eq!(json["result"], json!({"data": true}));
184+
assert_eq!(json["logs"][0], "info");
185+
}
186+
187+
#[test]
188+
fn command_response_serde_roundtrip() {
189+
let resp = CommandResponse {
190+
result: "ok".to_string(),
191+
logs: vec!["log1".into()],
192+
};
193+
let json = serde_json::to_string(&resp).unwrap();
194+
let back: CommandResponse<String> = serde_json::from_str(&json).unwrap();
195+
assert_eq!(back.result, "ok");
196+
assert_eq!(back.logs.len(), 1);
197+
}
198+
199+
#[test]
200+
fn rpc_request_deserializes() {
201+
let json = r#"{"jsonrpc":"2.0","id":1,"method":"test","params":{}}"#;
202+
let req: RpcRequest = serde_json::from_str(json).unwrap();
203+
assert_eq!(req.method, "test");
204+
assert_eq!(req.id, json!(1));
205+
}
206+
207+
#[test]
208+
fn rpc_request_params_default_to_null() {
209+
let json = r#"{"jsonrpc":"2.0","id":"abc","method":"foo"}"#;
210+
let req: RpcRequest = serde_json::from_str(json).unwrap();
211+
assert!(req.params.is_null());
212+
}
213+
214+
#[test]
215+
fn rpc_success_serializes() {
216+
let resp = RpcSuccess {
217+
jsonrpc: "2.0",
218+
id: json!(42),
219+
result: json!({"ok": true}),
220+
};
221+
let json = serde_json::to_string(&resp).unwrap();
222+
assert!(json.contains("\"jsonrpc\":\"2.0\""));
223+
assert!(json.contains("\"id\":42"));
224+
}
225+
226+
#[test]
227+
fn rpc_failure_serializes() {
228+
let resp = RpcFailure {
229+
jsonrpc: "2.0",
230+
id: json!("req-1"),
231+
error: RpcError {
232+
code: -32601,
233+
message: "Method not found".into(),
234+
data: Some(json!({"detail": "unknown"})),
235+
},
236+
};
237+
let json = serde_json::to_string(&resp).unwrap();
238+
assert!(json.contains("-32601"));
239+
assert!(json.contains("Method not found"));
240+
}
241+
242+
#[test]
243+
fn rpc_failure_serializes_without_data() {
244+
let resp = RpcFailure {
245+
jsonrpc: "2.0",
246+
id: json!(null),
247+
error: RpcError {
248+
code: -32700,
249+
message: "Parse error".into(),
250+
data: None,
251+
},
252+
};
253+
let json = serde_json::to_string(&resp).unwrap();
254+
assert!(json.contains("-32700"));
255+
}
256+
257+
#[test]
258+
fn app_state_clone() {
259+
let state = AppState {
260+
core_version: "0.1.0".into(),
261+
};
262+
let cloned = state.clone();
263+
assert_eq!(cloned.core_version, "0.1.0");
264+
}
265+
}

src/openhuman/about_app/types.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,92 @@ mod tests {
144144
"\"coming_soon\""
145145
);
146146
}
147+
148+
#[test]
149+
fn category_all_has_10_variants() {
150+
assert_eq!(CapabilityCategory::ALL.len(), 10);
151+
}
152+
153+
#[test]
154+
fn category_as_str_roundtrips_through_from_str() {
155+
for cat in CapabilityCategory::ALL {
156+
let s = cat.as_str();
157+
let parsed: CapabilityCategory = s.parse().unwrap();
158+
assert_eq!(parsed, cat);
159+
}
160+
}
161+
162+
#[test]
163+
fn category_from_str_accepts_aliases() {
164+
assert_eq!(
165+
"local-ai".parse::<CapabilityCategory>().unwrap(),
166+
CapabilityCategory::LocalAI
167+
);
168+
assert_eq!(
169+
"local ai".parse::<CapabilityCategory>().unwrap(),
170+
CapabilityCategory::LocalAI
171+
);
172+
assert_eq!(
173+
"localai".parse::<CapabilityCategory>().unwrap(),
174+
CapabilityCategory::LocalAI
175+
);
176+
assert_eq!(
177+
"screen-intelligence".parse::<CapabilityCategory>().unwrap(),
178+
CapabilityCategory::ScreenIntelligence
179+
);
180+
assert_eq!(
181+
"screen intelligence".parse::<CapabilityCategory>().unwrap(),
182+
CapabilityCategory::ScreenIntelligence
183+
);
184+
}
185+
186+
#[test]
187+
fn category_from_str_is_case_insensitive() {
188+
assert_eq!(
189+
"CONVERSATION".parse::<CapabilityCategory>().unwrap(),
190+
CapabilityCategory::Conversation
191+
);
192+
assert_eq!(
193+
" Team ".parse::<CapabilityCategory>().unwrap(),
194+
CapabilityCategory::Team
195+
);
196+
}
197+
198+
#[test]
199+
fn category_from_str_rejects_unknown() {
200+
let err = "bogus".parse::<CapabilityCategory>().unwrap_err();
201+
assert!(err.contains("unknown capability category"));
202+
assert!(err.contains("bogus"));
203+
}
204+
205+
#[test]
206+
fn status_as_str_covers_all_variants() {
207+
assert_eq!(CapabilityStatus::Stable.as_str(), "stable");
208+
assert_eq!(CapabilityStatus::Beta.as_str(), "beta");
209+
assert_eq!(CapabilityStatus::ComingSoon.as_str(), "coming_soon");
210+
assert_eq!(CapabilityStatus::Deprecated.as_str(), "deprecated");
211+
}
212+
213+
#[test]
214+
fn status_serde_roundtrip() {
215+
for status in [
216+
CapabilityStatus::Stable,
217+
CapabilityStatus::Beta,
218+
CapabilityStatus::ComingSoon,
219+
CapabilityStatus::Deprecated,
220+
] {
221+
let json = serde_json::to_string(&status).unwrap();
222+
let back: CapabilityStatus = serde_json::from_str(&json).unwrap();
223+
assert_eq!(back, status);
224+
}
225+
}
226+
227+
#[test]
228+
fn category_serde_roundtrip_all() {
229+
for cat in CapabilityCategory::ALL {
230+
let json = serde_json::to_string(&cat).unwrap();
231+
let back: CapabilityCategory = serde_json::from_str(&json).unwrap();
232+
assert_eq!(back, cat);
233+
}
234+
}
147235
}

src/openhuman/agent/harness/interrupt.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,72 @@ pub fn check_interrupt(fence: &InterruptFence) -> Result<(), InterruptedError> {
9595
Ok(())
9696
}
9797
}
98+
99+
#[cfg(test)]
100+
mod tests {
101+
use super::*;
102+
103+
#[test]
104+
fn new_fence_is_not_interrupted() {
105+
let fence = InterruptFence::new();
106+
assert!(!fence.is_interrupted());
107+
}
108+
109+
#[test]
110+
fn trigger_sets_interrupted() {
111+
let fence = InterruptFence::new();
112+
fence.trigger();
113+
assert!(fence.is_interrupted());
114+
}
115+
116+
#[test]
117+
fn reset_clears_interrupted() {
118+
let fence = InterruptFence::new();
119+
fence.trigger();
120+
assert!(fence.is_interrupted());
121+
fence.reset();
122+
assert!(!fence.is_interrupted());
123+
}
124+
125+
#[test]
126+
fn flag_handle_shares_state() {
127+
let fence = InterruptFence::new();
128+
let handle = fence.flag_handle();
129+
handle.store(true, std::sync::atomic::Ordering::Relaxed);
130+
assert!(fence.is_interrupted());
131+
}
132+
133+
#[test]
134+
fn clone_shares_state() {
135+
let fence = InterruptFence::new();
136+
let clone = fence.clone();
137+
fence.trigger();
138+
assert!(clone.is_interrupted());
139+
}
140+
141+
#[test]
142+
fn default_is_not_interrupted() {
143+
let fence = InterruptFence::default();
144+
assert!(!fence.is_interrupted());
145+
}
146+
147+
#[test]
148+
fn check_interrupt_ok_when_not_triggered() {
149+
let fence = InterruptFence::new();
150+
assert!(check_interrupt(&fence).is_ok());
151+
}
152+
153+
#[test]
154+
fn check_interrupt_err_when_triggered() {
155+
let fence = InterruptFence::new();
156+
fence.trigger();
157+
let err = check_interrupt(&fence).unwrap_err();
158+
assert_eq!(err.to_string(), "operation interrupted by user");
159+
}
160+
161+
#[test]
162+
fn interrupted_error_display() {
163+
let err = InterruptedError;
164+
assert_eq!(format!("{err}"), "operation interrupted by user");
165+
}
166+
}

0 commit comments

Comments
 (0)