1use std::collections::BTreeMap;
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::config::InteractionLevel;
10use super::item::Capability;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct Project {
14 pub name: String,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub acronym: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub description: Option<String>,
19 #[serde(default = "default_language")]
20 pub language: String,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub forge: Option<String>,
23 #[serde(default, skip_serializing_if = "Docs::is_empty")]
24 pub docs: Docs,
25 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
26 pub members: BTreeMap<String, Member>,
27 pub created: DateTime<Utc>,
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
34pub struct Docs {
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub architecture: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub vision: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub contributing: Option<String>,
41}
42
43impl Docs {
44 pub const DEFAULT_ARCHITECTURE: &'static str = "docs/dev/architecture/README.md";
45 pub const DEFAULT_VISION: &'static str = "docs/dev/vision/README.md";
46 pub const DEFAULT_CONTRIBUTING: &'static str = "CONTRIBUTING.md";
47
48 pub fn is_empty(&self) -> bool {
49 self.architecture.is_none() && self.vision.is_none() && self.contributing.is_none()
50 }
51
52 pub fn architecture_or_default(&self) -> &str {
54 self.architecture
55 .as_deref()
56 .unwrap_or(Self::DEFAULT_ARCHITECTURE)
57 }
58
59 pub fn vision_or_default(&self) -> &str {
61 self.vision.as_deref().unwrap_or(Self::DEFAULT_VISION)
62 }
63
64 pub fn contributing_or_default(&self) -> &str {
66 self.contributing
67 .as_deref()
68 .unwrap_or(Self::DEFAULT_CONTRIBUTING)
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct Member {
74 pub capabilities: MemberCapabilities,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub public_key: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub salt: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub otp_hash: Option<String>,
81 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
82 pub ai_delegations: BTreeMap<String, AiDelegationEntry>,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub struct AiDelegationEntry {
90 pub delegation_key: String,
92 pub created: chrono::DateTime<chrono::Utc>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub rotated: Option<chrono::DateTime<chrono::Utc>>,
97}
98
99#[derive(Debug, Clone, PartialEq)]
100pub enum MemberCapabilities {
101 All,
102 Specific(BTreeMap<Capability, CapabilityConfig>),
103}
104
105#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
106pub struct CapabilityConfig {
107 #[serde(rename = "max-mode", default, skip_serializing_if = "Option::is_none")]
108 pub max_mode: Option<InteractionLevel>,
109 #[serde(
110 rename = "max-cost-per-job",
111 default,
112 skip_serializing_if = "Option::is_none"
113 )]
114 pub max_cost_per_job: Option<f64>,
115}
116
117#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
124pub struct ModeDefaults {
125 #[serde(default)]
127 pub default: InteractionLevel,
128 #[serde(flatten, default)]
130 pub capabilities: BTreeMap<Capability, InteractionLevel>,
131}
132
133#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
136pub struct AiDefaults {
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
138 pub capabilities: Vec<Capability>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum ModeSource {
144 Default,
146 Project,
148 Personal,
150 Item,
152 ProjectMax,
154}
155
156impl std::fmt::Display for ModeSource {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 Self::Default => write!(f, "default"),
160 Self::Project => write!(f, "project"),
161 Self::Personal => write!(f, "personal"),
162 Self::Item => write!(f, "item"),
163 Self::ProjectMax => write!(f, "project max"),
164 }
165 }
166}
167
168pub fn resolve_mode(
177 capability: &Capability,
178 raw_defaults: &ModeDefaults,
179 effective_defaults: &ModeDefaults,
180 personal_mode: Option<InteractionLevel>,
181 member_cap_config: Option<&CapabilityConfig>,
182) -> (InteractionLevel, ModeSource) {
183 let mut mode = effective_defaults.default;
185 let mut source = if effective_defaults.default != raw_defaults.default {
186 ModeSource::Project
187 } else {
188 ModeSource::Default
189 };
190
191 if let Some(&cap_mode) = effective_defaults.capabilities.get(capability) {
193 mode = cap_mode;
194 let from_raw = raw_defaults.capabilities.get(capability) == Some(&cap_mode);
195 source = if from_raw {
196 ModeSource::Default
197 } else {
198 ModeSource::Project
199 };
200 }
201
202 if let Some(personal) = personal_mode {
204 mode = personal;
205 source = ModeSource::Personal;
206 }
207
208 if let Some(cap_config) = member_cap_config {
210 if let Some(max) = cap_config.max_mode {
211 if mode < max {
212 mode = max;
213 source = ModeSource::ProjectMax;
214 }
215 }
216 }
217
218 (mode, source)
219}
220
221impl Serialize for MemberCapabilities {
223 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
224 match self {
225 MemberCapabilities::All => serializer.serialize_str("all"),
226 MemberCapabilities::Specific(map) => map.serialize(serializer),
227 }
228 }
229}
230
231impl<'de> Deserialize<'de> for MemberCapabilities {
232 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
233 let value = serde_yaml_ng::Value::deserialize(deserializer)?;
234 match &value {
235 serde_yaml_ng::Value::String(s) if s == "all" => Ok(MemberCapabilities::All),
236 serde_yaml_ng::Value::Mapping(_) => {
237 let map: BTreeMap<Capability, CapabilityConfig> =
238 serde_yaml_ng::from_value(value).map_err(serde::de::Error::custom)?;
239 Ok(MemberCapabilities::Specific(map))
240 }
241 _ => Err(serde::de::Error::custom(
242 "expected \"all\" or a map of capabilities",
243 )),
244 }
245 }
246}
247
248impl Member {
249 pub fn new(capabilities: MemberCapabilities) -> Self {
251 Self {
252 capabilities,
253 public_key: None,
254 salt: None,
255 otp_hash: None,
256 ai_delegations: BTreeMap::new(),
257 }
258 }
259
260 pub fn has_capability(&self, cap: &Capability) -> bool {
262 match &self.capabilities {
263 MemberCapabilities::All => true,
264 MemberCapabilities::Specific(map) => map.contains_key(cap),
265 }
266 }
267}
268
269pub fn is_ai_member(id: &str) -> bool {
271 id.starts_with("ai:")
272}
273
274fn default_language() -> String {
275 "en".to_string()
276}
277
278impl Project {
279 pub fn new(name: String, acronym: Option<String>) -> Self {
280 Self {
281 name,
282 acronym,
283 description: None,
284 language: default_language(),
285 forge: None,
286 docs: Docs::default(),
287 members: BTreeMap::new(),
288 created: Utc::now(),
289 }
290 }
291}
292
293pub fn derive_acronym(name: &str) -> String {
297 let words: Vec<&str> = name.split_whitespace().collect();
298 if words.len() == 1 {
299 words[0]
300 .chars()
301 .filter(|c| c.is_alphanumeric())
302 .take(3)
303 .collect::<String>()
304 .to_uppercase()
305 } else {
306 words
307 .iter()
308 .filter_map(|w| w.chars().next())
309 .filter(|c| c.is_alphanumeric())
310 .take(4)
311 .collect::<String>()
312 .to_uppercase()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn project_roundtrip() {
322 let project = Project::new("Test Project".into(), Some("TP".into()));
323 let yaml = serde_yaml_ng::to_string(&project).unwrap();
324 let parsed: Project = serde_yaml_ng::from_str(&yaml).unwrap();
325 assert_eq!(project, parsed);
326 }
327
328 #[test]
333 fn ai_delegations_omitted_when_empty() {
334 let mut m = Member::new(MemberCapabilities::All);
335 assert!(m.ai_delegations.is_empty());
336 let yaml = serde_yaml_ng::to_string(&m).unwrap();
337 assert!(
338 !yaml.contains("ai_delegations"),
339 "empty ai_delegations should be skipped, got: {yaml}"
340 );
341 m.public_key = Some("aa".repeat(32));
343 let yaml = serde_yaml_ng::to_string(&m).unwrap();
344 let parsed: Member = serde_yaml_ng::from_str(&yaml).unwrap();
345 assert_eq!(m, parsed);
346 }
347
348 #[test]
349 fn ai_delegations_yaml_roundtrip() {
350 let mut m = Member::new(MemberCapabilities::All);
351 m.public_key = Some("aa".repeat(32));
352 m.salt = Some("bb".repeat(32));
353 m.ai_delegations.insert(
354 "ai:claude@joy".into(),
355 AiDelegationEntry {
356 delegation_key: "cc".repeat(32),
357 created: chrono::DateTime::parse_from_rfc3339("2026-04-15T10:00:00Z")
358 .unwrap()
359 .with_timezone(&chrono::Utc),
360 rotated: None,
361 },
362 );
363 let yaml = serde_yaml_ng::to_string(&m).unwrap();
364 assert!(yaml.contains("ai_delegations:"));
365 assert!(yaml.contains("ai:claude@joy:"));
366 assert!(yaml.contains("delegation_key:"));
367 assert!(
368 !yaml.contains("rotated:"),
369 "unset rotated should be skipped"
370 );
371
372 let parsed: Member = serde_yaml_ng::from_str(&yaml).unwrap();
373 assert_eq!(m, parsed);
374 }
375
376 #[test]
377 fn ai_delegations_with_rotated_roundtrips() {
378 let mut m = Member::new(MemberCapabilities::All);
379 let created = chrono::DateTime::parse_from_rfc3339("2026-04-01T10:00:00Z")
380 .unwrap()
381 .with_timezone(&chrono::Utc);
382 let rotated = chrono::DateTime::parse_from_rfc3339("2026-04-15T12:30:00Z")
383 .unwrap()
384 .with_timezone(&chrono::Utc);
385 m.ai_delegations.insert(
386 "ai:claude@joy".into(),
387 AiDelegationEntry {
388 delegation_key: "dd".repeat(32),
389 created,
390 rotated: Some(rotated),
391 },
392 );
393 let yaml = serde_yaml_ng::to_string(&m).unwrap();
394 assert!(yaml.contains("rotated:"));
395 let parsed: Member = serde_yaml_ng::from_str(&yaml).unwrap();
396 assert_eq!(m.ai_delegations["ai:claude@joy"].rotated, Some(rotated));
397 assert_eq!(parsed, m);
398 }
399
400 #[test]
401 fn unknown_fields_from_legacy_yaml_are_ignored() {
402 let yaml = r#"
406capabilities: all
407public_key: aa
408salt: bb
409ai_tokens:
410 ai:claude@joy:
411 token_key: oldkey
412 created: "2026-03-28T22:00:00Z"
413ai_delegations:
414 ai:claude@joy:
415 delegation_key: newkey
416 created: "2026-04-15T10:00:00Z"
417"#;
418 let parsed: Member = serde_yaml_ng::from_str(yaml).unwrap();
419 assert_eq!(
420 parsed.ai_delegations["ai:claude@joy"].delegation_key,
421 "newkey"
422 );
423 }
424
425 #[test]
430 fn docs_defaults_when_unset() {
431 let docs = Docs::default();
432 assert_eq!(docs.architecture_or_default(), Docs::DEFAULT_ARCHITECTURE);
433 assert_eq!(docs.vision_or_default(), Docs::DEFAULT_VISION);
434 assert_eq!(docs.contributing_or_default(), Docs::DEFAULT_CONTRIBUTING);
435 }
436
437 #[test]
438 fn docs_returns_configured_value() {
439 let docs = Docs {
440 architecture: Some("ARCHITECTURE.md".into()),
441 vision: Some("docs/product/vision.md".into()),
442 contributing: None,
443 };
444 assert_eq!(docs.architecture_or_default(), "ARCHITECTURE.md");
445 assert_eq!(docs.vision_or_default(), "docs/product/vision.md");
446 assert_eq!(docs.contributing_or_default(), Docs::DEFAULT_CONTRIBUTING);
447 }
448
449 #[test]
450 fn docs_omitted_from_yaml_when_empty() {
451 let project = Project::new("X".into(), None);
452 let yaml = serde_yaml_ng::to_string(&project).unwrap();
453 assert!(
454 !yaml.contains("docs:"),
455 "empty docs should be skipped, got: {yaml}"
456 );
457 }
458
459 #[test]
460 fn docs_present_in_yaml_when_set() {
461 let mut project = Project::new("X".into(), None);
462 project.docs.architecture = Some("ARCHITECTURE.md".into());
463 let yaml = serde_yaml_ng::to_string(&project).unwrap();
464 assert!(yaml.contains("docs:"), "docs block expected: {yaml}");
465 assert!(yaml.contains("architecture: ARCHITECTURE.md"));
466 assert!(!yaml.contains("vision:"), "unset fields should be skipped");
467 }
468
469 #[test]
470 fn docs_yaml_roundtrip_with_overrides() {
471 let yaml = r#"
472name: Existing
473language: en
474docs:
475 architecture: ARCHITECTURE.md
476 contributing: docs/CONTRIBUTING.md
477created: 2026-01-01T00:00:00Z
478"#;
479 let parsed: Project = serde_yaml_ng::from_str(yaml).unwrap();
480 assert_eq!(parsed.docs.architecture.as_deref(), Some("ARCHITECTURE.md"));
481 assert_eq!(parsed.docs.vision, None);
482 assert_eq!(
483 parsed.docs.contributing.as_deref(),
484 Some("docs/CONTRIBUTING.md")
485 );
486 assert_eq!(parsed.docs.vision_or_default(), Docs::DEFAULT_VISION);
487 }
488
489 #[test]
490 fn derive_acronym_multi_word() {
491 assert_eq!(derive_acronym("My Cool Project"), "MCP");
492 }
493
494 #[test]
495 fn derive_acronym_single_word() {
496 assert_eq!(derive_acronym("Joy"), "JOY");
497 }
498
499 #[test]
500 fn derive_acronym_long_name() {
501 assert_eq!(derive_acronym("A Very Long Project Name"), "AVLP");
502 }
503
504 #[test]
505 fn derive_acronym_single_long_word() {
506 assert_eq!(derive_acronym("Platform"), "PLA");
507 }
508
509 #[test]
514 fn mode_defaults_flat_yaml_roundtrip() {
515 let yaml = r#"
516default: interactive
517implement: collaborative
518review: pairing
519"#;
520 let parsed: ModeDefaults = serde_yaml_ng::from_str(yaml).unwrap();
521 assert_eq!(parsed.default, InteractionLevel::Interactive);
522 assert_eq!(
523 parsed.capabilities[&Capability::Implement],
524 InteractionLevel::Collaborative
525 );
526 assert_eq!(
527 parsed.capabilities[&Capability::Review],
528 InteractionLevel::Pairing
529 );
530 }
531
532 #[test]
533 fn mode_defaults_empty_yaml() {
534 let yaml = "{}";
535 let parsed: ModeDefaults = serde_yaml_ng::from_str(yaml).unwrap();
536 assert_eq!(parsed.default, InteractionLevel::Collaborative);
537 assert!(parsed.capabilities.is_empty());
538 }
539
540 #[test]
541 fn mode_defaults_only_default() {
542 let yaml = "default: pairing";
543 let parsed: ModeDefaults = serde_yaml_ng::from_str(yaml).unwrap();
544 assert_eq!(parsed.default, InteractionLevel::Pairing);
545 assert!(parsed.capabilities.is_empty());
546 }
547
548 #[test]
549 fn ai_defaults_yaml_roundtrip() {
550 let yaml = r#"
551capabilities:
552 - implement
553 - review
554"#;
555 let parsed: AiDefaults = serde_yaml_ng::from_str(yaml).unwrap();
556 assert_eq!(parsed.capabilities.len(), 2);
557 assert_eq!(parsed.capabilities[0], Capability::Implement);
558 }
559
560 fn defaults_with_mode(mode: InteractionLevel) -> ModeDefaults {
565 ModeDefaults {
566 default: mode,
567 ..Default::default()
568 }
569 }
570
571 fn defaults_with_cap_mode(cap: Capability, mode: InteractionLevel) -> ModeDefaults {
572 let mut d = ModeDefaults::default();
573 d.capabilities.insert(cap, mode);
574 d
575 }
576
577 #[test]
578 fn resolve_mode_uses_global_default() {
579 let raw = defaults_with_mode(InteractionLevel::Collaborative);
580 let effective = raw.clone();
581 let (mode, source) = resolve_mode(&Capability::Implement, &raw, &effective, None, None);
582 assert_eq!(mode, InteractionLevel::Collaborative);
583 assert_eq!(source, ModeSource::Default);
584 }
585
586 #[test]
587 fn resolve_mode_uses_per_capability_default() {
588 let raw = defaults_with_cap_mode(Capability::Review, InteractionLevel::Interactive);
589 let effective = raw.clone();
590 let (mode, source) = resolve_mode(&Capability::Review, &raw, &effective, None, None);
591 assert_eq!(mode, InteractionLevel::Interactive);
592 assert_eq!(source, ModeSource::Default);
593 }
594
595 #[test]
596 fn resolve_mode_project_override_detected() {
597 let raw = defaults_with_cap_mode(Capability::Implement, InteractionLevel::Collaborative);
598 let effective =
599 defaults_with_cap_mode(Capability::Implement, InteractionLevel::Interactive);
600 let (mode, source) = resolve_mode(&Capability::Implement, &raw, &effective, None, None);
601 assert_eq!(mode, InteractionLevel::Interactive);
602 assert_eq!(source, ModeSource::Project);
603 }
604
605 #[test]
606 fn resolve_mode_personal_overrides_default() {
607 let raw = defaults_with_mode(InteractionLevel::Collaborative);
608 let effective = raw.clone();
609 let (mode, source) = resolve_mode(
610 &Capability::Implement,
611 &raw,
612 &effective,
613 Some(InteractionLevel::Pairing),
614 None,
615 );
616 assert_eq!(mode, InteractionLevel::Pairing);
617 assert_eq!(source, ModeSource::Personal);
618 }
619
620 #[test]
621 fn resolve_mode_max_mode_clamps_upward() {
622 let raw = defaults_with_mode(InteractionLevel::Autonomous);
623 let effective = raw.clone();
624 let cap_config = CapabilityConfig {
625 max_mode: Some(InteractionLevel::Supervised),
626 ..Default::default()
627 };
628 let (mode, source) = resolve_mode(
629 &Capability::Implement,
630 &raw,
631 &effective,
632 None,
633 Some(&cap_config),
634 );
635 assert_eq!(mode, InteractionLevel::Supervised);
636 assert_eq!(source, ModeSource::ProjectMax);
637 }
638
639 #[test]
640 fn resolve_mode_max_mode_does_not_lower() {
641 let raw = defaults_with_mode(InteractionLevel::Pairing);
642 let effective = raw.clone();
643 let cap_config = CapabilityConfig {
644 max_mode: Some(InteractionLevel::Supervised),
645 ..Default::default()
646 };
647 let (mode, source) = resolve_mode(
648 &Capability::Implement,
649 &raw,
650 &effective,
651 None,
652 Some(&cap_config),
653 );
654 assert_eq!(mode, InteractionLevel::Pairing);
656 assert_eq!(source, ModeSource::Default);
657 }
658
659 #[test]
660 fn resolve_mode_personal_clamped_by_max() {
661 let raw = defaults_with_mode(InteractionLevel::Collaborative);
662 let effective = raw.clone();
663 let cap_config = CapabilityConfig {
664 max_mode: Some(InteractionLevel::Interactive),
665 ..Default::default()
666 };
667 let (mode, source) = resolve_mode(
668 &Capability::Implement,
669 &raw,
670 &effective,
671 Some(InteractionLevel::Autonomous),
672 Some(&cap_config),
673 );
674 assert_eq!(mode, InteractionLevel::Interactive);
676 assert_eq!(source, ModeSource::ProjectMax);
677 }
678
679 #[test]
684 fn item_mode_field_roundtrip() {
685 use crate::model::item::{Item, ItemType, Priority};
686
687 let mut item = Item::new(
688 "TST-0001".into(),
689 "Test".into(),
690 ItemType::Task,
691 Priority::Medium,
692 vec![],
693 );
694 item.mode = Some(InteractionLevel::Pairing);
695
696 let yaml = serde_yaml_ng::to_string(&item).unwrap();
697 assert!(yaml.contains("mode: pairing"), "mode field not serialized");
698
699 let parsed: Item = serde_yaml_ng::from_str(&yaml).unwrap();
700 assert_eq!(parsed.mode, Some(InteractionLevel::Pairing));
701 }
702
703 #[test]
704 fn item_mode_field_absent_when_none() {
705 use crate::model::item::{Item, ItemType, Priority};
706
707 let item = Item::new(
708 "TST-0002".into(),
709 "Test".into(),
710 ItemType::Task,
711 Priority::Medium,
712 vec![],
713 );
714 assert_eq!(item.mode, None);
715
716 let yaml = serde_yaml_ng::to_string(&item).unwrap();
717 assert!(
718 !yaml.contains("mode:"),
719 "mode field should not appear when None"
720 );
721 }
722
723 #[test]
724 fn item_mode_deserialized_from_existing_yaml() {
725 let yaml = r#"
726id: TST-0003
727title: Test
728type: task
729status: new
730priority: medium
731mode: interactive
732created: "2026-01-01T00:00:00+00:00"
733updated: "2026-01-01T00:00:00+00:00"
734"#;
735 let item: crate::model::item::Item = serde_yaml_ng::from_str(yaml).unwrap();
736 assert_eq!(item.mode, Some(InteractionLevel::Interactive));
737 }
738
739 #[test]
744 fn resolve_mode_full_scenario() {
745 let raw = defaults_with_cap_mode(Capability::Implement, InteractionLevel::Collaborative);
747 let effective =
749 defaults_with_cap_mode(Capability::Implement, InteractionLevel::Interactive);
750 let personal = Some(InteractionLevel::Autonomous);
752 let cap_config = CapabilityConfig {
754 max_mode: Some(InteractionLevel::Supervised),
755 ..Default::default()
756 };
757
758 let (mode, source) = resolve_mode(
759 &Capability::Implement,
760 &raw,
761 &effective,
762 personal,
763 Some(&cap_config),
764 );
765
766 assert_eq!(mode, InteractionLevel::Supervised);
768 assert_eq!(source, ModeSource::ProjectMax);
769 }
770
771 #[test]
772 fn resolve_mode_all_layers_no_clamping() {
773 let raw = defaults_with_cap_mode(Capability::Implement, InteractionLevel::Collaborative);
775 let effective =
777 defaults_with_cap_mode(Capability::Implement, InteractionLevel::Interactive);
778 let personal = Some(InteractionLevel::Pairing);
780 let cap_config = CapabilityConfig::default();
782
783 let (mode, source) = resolve_mode(
784 &Capability::Implement,
785 &raw,
786 &effective,
787 personal,
788 Some(&cap_config),
789 );
790
791 assert_eq!(mode, InteractionLevel::Pairing);
793 assert_eq!(source, ModeSource::Personal);
794 }
795}