Skip to content

Commit df604fa

Browse files
committed
test(coverage): channels partial push (68.12% → 68.91%)
- channels/controllers/schemas.rs: every registered key resolves, required input coverage for describe/send_message/telegram_login_check. - channels/providers/web.rs: catalog parity, chat/cancel schema required inputs, unknown fallback, key_for, event_session_id_for stability, normalize_model_override trimming/empty, broadcast channel subscribe, field-builder helpers. - channels/providers/discord/api.rs: auth_header prefix, BotPermissionCheck serde with empty/full missing_permissions lists, permission bit flags are single-bit and distinct. Channels remains below the 80% issue target — the bulk of remaining uncovered code lives in telegram/channel.rs (574 lines), ops.rs (462), lark.rs (433) and runtime/startup.rs (350), which depend on live HTTP / socket / runtime bootstrap state that would require extensive mock infrastructure to exercise from unit tests.
1 parent a9eb565 commit df604fa

3 files changed

Lines changed: 270 additions & 1 deletion

File tree

src/openhuman/channels/controllers/schemas.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,4 +655,76 @@ mod tests {
655655
fns.dedup();
656656
assert_eq!(fns.len(), len, "duplicate function names found");
657657
}
658+
659+
#[test]
660+
fn every_known_key_resolves_to_non_unknown_schema() {
661+
let keys = [
662+
"list",
663+
"describe",
664+
"connect",
665+
"disconnect",
666+
"status",
667+
"test",
668+
"telegram_login_start",
669+
"telegram_login_check",
670+
"discord_list_guilds",
671+
"discord_list_channels",
672+
"discord_check_permissions",
673+
"send_message",
674+
"send_reaction",
675+
"create_thread",
676+
"update_thread",
677+
"list_threads",
678+
];
679+
for k in keys {
680+
let s = schemas(k);
681+
assert_eq!(s.namespace, "channels");
682+
assert_ne!(s.function, "unknown", "key `{k}` fell through");
683+
assert!(!s.description.is_empty(), "key `{k}` missing description");
684+
assert!(!s.outputs.is_empty(), "key `{k}` has no outputs");
685+
}
686+
}
687+
688+
#[test]
689+
fn unknown_function_returns_unknown_fallback() {
690+
let s = schemas("no_such_fn_123");
691+
assert_eq!(s.function, "unknown");
692+
assert_eq!(s.namespace, "channels");
693+
}
694+
695+
#[test]
696+
fn describe_schema_requires_channel() {
697+
let s = schemas("describe");
698+
let chan = s.inputs.iter().find(|f| f.name == "channel");
699+
assert!(chan.is_some_and(|f| f.required));
700+
}
701+
702+
#[test]
703+
fn send_message_requires_channel_and_message() {
704+
let s = schemas("send_message");
705+
let required: Vec<&str> = s
706+
.inputs
707+
.iter()
708+
.filter(|f| f.required)
709+
.map(|f| f.name)
710+
.collect();
711+
assert!(required.contains(&"channel"));
712+
// The rich-message body is carried in `message` (JSON).
713+
assert!(required.contains(&"message"));
714+
}
715+
716+
#[test]
717+
fn telegram_login_check_requires_session_id_or_token() {
718+
let s = schemas("telegram_login_check");
719+
// Should have at least one required input
720+
assert!(s.inputs.iter().any(|f| f.required));
721+
}
722+
723+
#[test]
724+
fn discord_list_guilds_schema_may_have_no_required_inputs() {
725+
let s = schemas("discord_list_guilds");
726+
// Either no inputs or all-optional inputs are acceptable — but the
727+
// schema must still exist with outputs.
728+
assert!(!s.outputs.is_empty());
729+
}
658730
}

src/openhuman/channels/providers/discord/api.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,69 @@ mod tests {
340340
assert_eq!(SEND_MESSAGES, 2048);
341341
assert_eq!(READ_MESSAGE_HISTORY, 65536);
342342
}
343+
344+
#[test]
345+
fn auth_header_has_bot_prefix() {
346+
assert_eq!(auth_header("abc"), "Bot abc");
347+
assert_eq!(auth_header(""), "Bot ");
348+
}
349+
350+
#[test]
351+
fn permission_check_lists_all_missing_permissions_when_bot_lacks_any() {
352+
let check = BotPermissionCheck {
353+
can_view_channel: false,
354+
can_send_messages: false,
355+
can_read_message_history: false,
356+
missing_permissions: vec![
357+
"VIEW_CHANNEL".into(),
358+
"SEND_MESSAGES".into(),
359+
"READ_MESSAGE_HISTORY".into(),
360+
],
361+
};
362+
let json = serde_json::to_string(&check).unwrap();
363+
assert!(json.contains("VIEW_CHANNEL"));
364+
assert!(json.contains("SEND_MESSAGES"));
365+
assert!(json.contains("READ_MESSAGE_HISTORY"));
366+
}
367+
368+
#[test]
369+
fn permission_check_with_all_granted_has_empty_missing_list() {
370+
let check = BotPermissionCheck {
371+
can_view_channel: true,
372+
can_send_messages: true,
373+
can_read_message_history: true,
374+
missing_permissions: vec![],
375+
};
376+
let json = serde_json::to_string(&check).unwrap();
377+
assert!(json.contains("\"missing_permissions\":[]"));
378+
}
379+
380+
#[test]
381+
fn text_channel_type_zero_is_standard_text() {
382+
let json = r#"{"id":"1","name":"general","type":0,"position":0,"parent_id":null}"#;
383+
let ch: DiscordTextChannel = serde_json::from_str(json).unwrap();
384+
assert_eq!(ch.channel_type, 0);
385+
}
386+
387+
#[test]
388+
fn guild_deserializes_with_full_payload() {
389+
let json = r#"{
390+
"id": "999",
391+
"name": "Full Guild",
392+
"icon": "hash"
393+
}"#;
394+
let g: DiscordGuild = serde_json::from_str(json).unwrap();
395+
assert_eq!(g.id, "999");
396+
assert_eq!(g.name, "Full Guild");
397+
}
398+
399+
#[test]
400+
fn permission_bit_flags_are_disjoint() {
401+
// Sanity: each permission is a single bit and distinct.
402+
assert_eq!(VIEW_CHANNEL.count_ones(), 1);
403+
assert_eq!(SEND_MESSAGES.count_ones(), 1);
404+
assert_eq!(READ_MESSAGE_HISTORY.count_ones(), 1);
405+
assert_ne!(VIEW_CHANNEL, SEND_MESSAGES);
406+
assert_ne!(SEND_MESSAGES, READ_MESSAGE_HISTORY);
407+
}
343408
}

src/openhuman/channels/providers/web.rs

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,12 @@ fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String>
696696

697697
#[cfg(test)]
698698
mod tests {
699-
use super::{cancel_chat, start_chat};
699+
use super::{
700+
all_web_channel_controller_schemas, all_web_channel_registered_controllers, cancel_chat,
701+
event_session_id_for, json_output, key_for, normalize_model_override, optional_f64,
702+
optional_string, required_string, schemas, start_chat, subscribe_web_channel_events,
703+
};
704+
use crate::core::TypeSchema;
700705

701706
#[tokio::test]
702707
async fn start_chat_validates_required_fields() {
@@ -728,4 +733,131 @@ mod tests {
728733
.expect_err("thread id should be required");
729734
assert!(err.contains("thread_id is required"));
730735
}
736+
737+
// ── Schema catalog ────────────────────────────────────────────
738+
739+
#[test]
740+
fn web_channel_catalog_has_chat_and_cancel() {
741+
let s = all_web_channel_controller_schemas();
742+
let c = all_web_channel_registered_controllers();
743+
assert_eq!(s.len(), c.len());
744+
assert_eq!(s.len(), 2);
745+
let fns: Vec<&str> = s.iter().map(|x| x.function).collect();
746+
assert!(fns.contains(&"web_chat"));
747+
assert!(fns.contains(&"web_cancel"));
748+
}
749+
750+
#[test]
751+
fn chat_schema_requires_client_thread_message() {
752+
let s = schemas("chat");
753+
let required: Vec<&str> = s
754+
.inputs
755+
.iter()
756+
.filter(|f| f.required)
757+
.map(|f| f.name)
758+
.collect();
759+
assert!(required.contains(&"client_id"));
760+
assert!(required.contains(&"thread_id"));
761+
assert!(required.contains(&"message"));
762+
// model_override and temperature must be optional.
763+
assert!(s
764+
.inputs
765+
.iter()
766+
.any(|f| f.name == "model_override" && !f.required));
767+
assert!(s
768+
.inputs
769+
.iter()
770+
.any(|f| f.name == "temperature" && !f.required));
771+
}
772+
773+
#[test]
774+
fn cancel_schema_requires_client_and_thread() {
775+
let s = schemas("cancel");
776+
let required: Vec<&str> = s
777+
.inputs
778+
.iter()
779+
.filter(|f| f.required)
780+
.map(|f| f.name)
781+
.collect();
782+
assert_eq!(required, vec!["client_id", "thread_id"]);
783+
}
784+
785+
#[test]
786+
fn unknown_schema_returns_unknown_fallback() {
787+
let s = schemas("no_such_fn");
788+
assert_eq!(s.function, "unknown");
789+
assert_eq!(s.namespace, "channel");
790+
assert_eq!(s.outputs.len(), 1);
791+
assert_eq!(s.outputs[0].name, "error");
792+
}
793+
794+
// ── Helpers ───────────────────────────────────────────────────
795+
796+
#[test]
797+
fn key_for_combines_client_id_and_thread_id() {
798+
assert_eq!(key_for("c1", "t1"), "c1::t1");
799+
assert_eq!(key_for("", ""), "::");
800+
}
801+
802+
#[test]
803+
fn event_session_id_for_is_stable() {
804+
// Two calls with the same args must produce the same id.
805+
let a = event_session_id_for("c1", "t1");
806+
let b = event_session_id_for("c1", "t1");
807+
assert_eq!(a, b);
808+
// Different args → different id.
809+
let c = event_session_id_for("c2", "t1");
810+
assert_ne!(a, c);
811+
}
812+
813+
#[test]
814+
fn normalize_model_override_returns_none_for_empty_or_whitespace() {
815+
assert!(normalize_model_override(None).is_none());
816+
assert!(normalize_model_override(Some("".into())).is_none());
817+
assert!(normalize_model_override(Some(" ".into())).is_none());
818+
}
819+
820+
#[test]
821+
fn normalize_model_override_trims_value() {
822+
assert_eq!(
823+
normalize_model_override(Some(" gpt-4 ".into())),
824+
Some("gpt-4".to_string())
825+
);
826+
}
827+
828+
// ── Broadcast events ──────────────────────────────────────────
829+
830+
#[test]
831+
fn subscribe_web_channel_events_returns_receiver() {
832+
// Just confirm we can subscribe without panic.
833+
let _rx = subscribe_web_channel_events();
834+
}
835+
836+
// ── Field builder helpers ─────────────────────────────────────
837+
838+
#[test]
839+
fn required_string_marks_field_required() {
840+
let f = required_string("client_id", "c");
841+
assert!(f.required);
842+
assert!(matches!(f.ty, TypeSchema::String));
843+
}
844+
845+
#[test]
846+
fn optional_string_marks_field_optional() {
847+
let f = optional_string("model", "c");
848+
assert!(!f.required);
849+
}
850+
851+
#[test]
852+
fn optional_f64_marks_field_optional() {
853+
let f = optional_f64("temperature", "c");
854+
assert!(!f.required);
855+
}
856+
857+
#[test]
858+
fn json_output_is_required_json_field() {
859+
let f = json_output("ack", "c");
860+
assert!(f.required);
861+
assert!(matches!(f.ty, TypeSchema::Json));
862+
}
731863
}

0 commit comments

Comments
 (0)