difflore_core/team/
mod.rs1mod api;
2mod cloud_id;
3mod types;
4
5pub use api::{
6 invite, members, publish_rule, remove_member, resolve_known_cloud_rule_id, review_inbox,
7 skills, unpublish_rule, update_role,
8};
9pub use types::{
10 ReviewInboxItem, TeamContextInput, TeamInviteInput, TeamInviteResult, TeamMemberIdInput,
11 TeamMemberRecord, TeamMembersResult, TeamRulePublishInput, TeamRuleUnpublishInput,
12 TeamSkillsResult, TeamUpdateRoleInput,
13};
14
15#[cfg(test)]
16mod tests {
17 use super::cloud_id::{
18 build_rule_create_body, resolve_cloud_rule_id_for_unpublish, rule_cloud_mapping_key,
19 };
20 use super::types::{LocalRuleUploadRow, TeamRulePublishInput};
21 use sqlx::SqlitePool;
22 use uuid::Uuid;
23
24 async fn setup_migrated_pool() -> SqlitePool {
25 let pool = sqlx::sqlite::SqlitePoolOptions::new()
26 .max_connections(1)
27 .connect("sqlite::memory:")
28 .await
29 .expect("open in-memory pool");
30 sqlx::migrate!("./migrations")
31 .run(&pool)
32 .await
33 .expect("apply migrations");
34 pool
35 }
36
37 #[test]
38 fn rule_create_body_preserves_local_origin() {
39 let row = LocalRuleUploadRow {
40 name: "Prefer structured logs".into(),
41 rule_type: "review_standard".into(),
42 description: "Use logger.info instead of println.".into(),
43 version: "1.0.0".into(),
44 engines_json: r#"["claude"]"#.into(),
45 tags_json: r#"["conversation"]"#.into(),
46 trigger: None,
47 check_prompt: Some("Check logging calls".into()),
48 file_patterns_json: Some(r#"["**/*.rs"]"#.into()),
49 origin: "conversation".into(),
50 source_repo: Some("acme/widgets".into()),
51 };
52
53 let body = build_rule_create_body(&row);
54 assert_eq!(body["origin"].as_str(), Some("conversation"));
55 assert_eq!(body["content"].as_str(), Some(row.description.as_str()));
56 assert_eq!(body["visibility"].as_str(), Some("team"));
57 assert_eq!(body["filePatterns"][0].as_str(), Some("**/*.rs"));
58 assert_eq!(body["sourceRepo"].as_str(), Some("acme/widgets"));
59 }
60
61 #[test]
62 fn rule_create_body_falls_back_to_name_for_empty_content() {
63 let row = LocalRuleUploadRow {
64 name: "Name only".into(),
65 rule_type: "skill".into(),
66 description: " ".into(),
67 version: "1.0.0".into(),
68 engines_json: "[]".into(),
69 tags_json: "[]".into(),
70 trigger: None,
71 check_prompt: None,
72 file_patterns_json: None,
73 origin: "manual".into(),
74 source_repo: None,
75 };
76
77 let body = build_rule_create_body(&row);
78 assert_eq!(body["content"].as_str(), Some("Name only"));
79 assert_eq!(body["origin"].as_str(), Some("manual"));
80 }
81
82 #[tokio::test]
83 async fn unpublish_resolves_slug_from_auth_mapping() {
84 let pool = setup_migrated_pool().await;
85 let cloud_id = Uuid::new_v4().to_string();
86 let key = rule_cloud_mapping_key("conv-example-12345678");
87 sqlx::query!(
88 "INSERT INTO auth (key, value) VALUES (?1, ?2)",
89 key,
90 cloud_id
91 )
92 .execute(&pool)
93 .await
94 .expect("seed auth mapping");
95
96 let resolved = resolve_cloud_rule_id_for_unpublish(&pool, "conv-example-12345678")
97 .await
98 .expect("resolve cloud rule id");
99 assert_eq!(resolved, cloud_id);
100 }
101
102 #[tokio::test]
103 async fn unpublish_resolves_slug_from_cloud_id_column_when_present() {
104 let pool = setup_migrated_pool().await;
105 let cloud_id = Uuid::new_v4().to_string();
106 sqlx::query!(
107 "INSERT INTO skills (id, name, source, directory, version, cloud_id) \
108 VALUES (?1, 'n', 's', 'd', '1.0.0', ?2)",
109 "local-example",
110 cloud_id
111 )
112 .execute(&pool)
113 .await
114 .expect("seed skill row");
115
116 let resolved = resolve_cloud_rule_id_for_unpublish(&pool, "local-example")
117 .await
118 .expect("resolve via cloud_id column");
119 assert_eq!(resolved, cloud_id);
120 }
121
122 #[tokio::test]
123 async fn unpublish_missing_slug_mapping_is_not_found() {
124 let pool = setup_migrated_pool().await;
125
126 let err = resolve_cloud_rule_id_for_unpublish(&pool, "conv-missing-12345678")
127 .await
128 .expect_err("expected NotFound");
129 assert!(
130 err.to_string().contains("publish"),
131 "unexpected error: {err}"
132 );
133 }
134
135 #[test]
139 fn team_rule_publish_input_includes_origin_on_wire() {
140 let input = TeamRulePublishInput {
141 rule_id: "rule-1".into(),
142 enforcement: Some("required".into()),
143 team_id: Some("t1".into()),
144 origin: Some("conversation".into()),
145 };
146 let json = serde_json::to_value(&input).unwrap();
147 assert_eq!(json.get("ruleId").and_then(|v| v.as_str()), Some("rule-1"));
148 assert_eq!(
149 json.get("origin").and_then(|v| v.as_str()),
150 Some("conversation")
151 );
152 }
153}