1use std::collections::BTreeMap;
47
48use schemars::JsonSchema;
49use serde::{Deserialize, Serialize};
50
51use super::journal::StreamLiveness;
52use super::state::{ClaimState, CooperationState, ReviewState, SessionState};
53
54pub const SUMMARY_LIST: usize = 24;
56
57pub const SUMMARY_TEXT: usize = 160;
59
60#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71#[serde(rename_all = "snake_case")]
72#[schemars(rename = "MeshView")]
73pub enum View {
74 #[default]
77 Summary,
78 Full,
80}
81
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "snake_case")]
93#[schemars(rename = "MeshClaimFilter")]
94pub enum ClaimFilter {
95 #[default]
97 All,
98 Live,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
112#[serde(rename_all = "snake_case")]
113#[schemars(rename = "MeshKind")]
114pub enum Kind {
115 Sessions,
117 Claims,
119 Overlaps,
121 Handovers,
123 Reviews,
125}
126
127#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
139#[serde(deny_unknown_fields)]
140#[schemars(rename = "MeshQuery")]
141pub struct Query {
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub view: Option<View>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub claims: Option<ClaimFilter>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub after: Option<u64>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub session: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub runtime: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub kind: Option<Kind>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub limit: Option<usize>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub open: Option<bool>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub matching: Option<String>,
174}
175
176impl crate::capability::benchmark::BenchmarkCases for Query {
177 fn benchmark_cases(
178 _: &crate::capability::benchmark::CaseContext<'_>,
179 ) -> Vec<crate::capability::benchmark::NamedCase<Self>> {
180 use crate::capability::benchmark::NamedCase;
181 vec![
182 NamedCase::new("summary", Query::default()),
183 NamedCase::new(
184 "full",
185 Query {
186 view: Some(View::Full),
187 ..Query::default()
188 },
189 ),
190 ]
191 }
192}
193
194impl Query {
195 pub fn view(&self) -> View {
206 self.view.unwrap_or_default()
207 }
208
209 pub(crate) fn admits_session(&self, key: &str) -> bool {
211 self.session.as_deref().is_none_or(|wanted| {
212 key == wanted || key.rsplit_once('/').is_some_and(|(_, id)| id == wanted)
213 })
214 }
215
216 pub(crate) fn admits_runtime(&self, runtime: &str) -> bool {
218 self.runtime
219 .as_deref()
220 .is_none_or(|wanted| runtime == wanted || runtime.split('-').next() == Some(wanted))
221 }
222
223 fn admits_lamport(&self, lamport: u64) -> bool {
224 self.after.is_none_or(|after| lamport > after)
225 }
226
227 fn admits_kind(&self, kind: Kind) -> bool {
228 self.kind.is_none_or(|k| k == kind)
229 }
230
231 fn open_only(&self) -> bool {
232 self.open == Some(true)
233 }
234
235 fn admits_text<'a>(&self, texts: impl IntoIterator<Item = Option<&'a str>>) -> bool {
237 let Some(wanted) = self.matching.as_deref().map(str::to_lowercase) else {
238 return true;
239 };
240 texts
241 .into_iter()
242 .flatten()
243 .any(|text| text.to_lowercase().contains(&wanted))
244 }
245}
246
247fn newest<T>(items: Vec<T>, limit: Option<usize>, stamp: impl Fn(&T) -> u64) -> Vec<T> {
249 let Some(limit) = limit else {
250 return items;
251 };
252 let mut stamps: std::collections::BinaryHeap<u64> = items.iter().map(&stamp).collect();
254 if stamps.len() <= limit {
255 return items;
256 }
257 let floor = (0..limit.max(1))
258 .filter_map(|_| stamps.pop())
259 .last()
260 .unwrap_or(u64::MAX);
261 let mut kept = 0;
264 items
265 .into_iter()
266 .filter(|item| {
267 let keep = stamp(item) >= floor && kept < limit;
268 kept += usize::from(keep);
269 keep
270 })
271 .collect()
272}
273
274pub fn narrow(state: &CooperationState, query: &Query) -> CooperationState {
301 let sessions: Vec<_> = state
302 .sessions
303 .iter()
304 .filter(|s| query.admits_session(&s.key) && query.admits_runtime(&s.runtime))
305 .filter(|s| query.admits_lamport(s.updated_lamport))
306 .filter(|s| !query.open_only() || s.state == SessionState::Active)
307 .filter(|s| {
308 let i = &s.info;
309 query.admits_text([
310 Some(s.key.as_str()),
311 i.intent.as_deref(),
312 i.issue.as_deref(),
313 i.milestone.as_deref(),
314 i.branch.as_deref(),
315 i.worker.as_deref(),
316 ])
317 })
318 .cloned()
319 .collect();
320 let claims: Vec<_> = state
321 .claims
322 .iter()
323 .filter(|c| query.admits_session(&c.session) && query.admits_runtime(&c.runtime))
324 .filter(|c| query.admits_lamport(c.acquired_lamport))
325 .filter(|c| query.claims != Some(ClaimFilter::Live) || c.state.is_live())
326 .filter(|c| !query.open_only() || c.state.is_live())
327 .filter(|c| {
328 query.admits_text(
329 c.scope
330 .iter()
331 .map(|p| Some(p.as_str()))
332 .chain([c.intent.as_deref(), c.issue.as_deref()]),
333 )
334 })
335 .cloned()
336 .collect();
337 let kept: std::collections::BTreeSet<&str> = claims.iter().map(|c| c.key.as_str()).collect();
338 let overlaps: Vec<_> = state
339 .overlaps
340 .iter()
341 .filter(|o| kept.contains(o.first.as_str()) || kept.contains(o.second.as_str()))
342 .cloned()
343 .collect();
344 let handovers: Vec<_> = state
345 .handovers
346 .iter()
347 .filter(|h| query.admits_runtime(&h.runtime))
348 .filter(|h| query.admits_lamport(h.published_lamport))
349 .filter(|h| {
350 query.session.is_none() || h.consumed_by.iter().any(|s| query.admits_session(s))
351 })
352 .filter(|h| !query.open_only() || h.consumed_by.is_empty())
353 .filter(|h| {
354 let b = &h.handover;
355 query.admits_text([
356 b.branch.as_deref(),
357 b.issue.as_deref(),
358 b.milestone.as_deref(),
359 b.name.as_deref(),
360 ])
361 })
362 .cloned()
363 .collect();
364 let reviews: Vec<_> = state
365 .reviews
366 .iter()
367 .filter(|r| query.admits_session(&r.session))
368 .filter(|r| query.admits_runtime(runtime_of(&r.session)))
369 .filter(|r| query.after.is_none() || sessions.iter().any(|s| s.key == r.session))
370 .filter(|r| !query.open_only() || r.state == ReviewState::Open)
371 .filter(|r| {
372 query.admits_text(
373 r.scope
374 .iter()
375 .map(|p| Some(p.as_str()))
376 .chain([Some(r.subject.as_str()), r.issue.as_deref()]),
377 )
378 })
379 .cloned()
380 .collect();
381 let only = |kind: Kind| query.admits_kind(kind);
382 CooperationState {
383 sessions: newest(sessions, query.limit, |s| s.updated_lamport)
384 .into_iter()
385 .filter(|_| only(Kind::Sessions))
386 .collect(),
387 claims: newest(claims, query.limit, |c| c.acquired_lamport)
388 .into_iter()
389 .filter(|_| only(Kind::Claims))
390 .collect(),
391 overlaps: newest(overlaps, query.limit, |_| 0)
392 .into_iter()
393 .filter(|_| only(Kind::Overlaps))
394 .collect(),
395 handovers: newest(handovers, query.limit, |h| h.published_lamport)
396 .into_iter()
397 .filter(|_| only(Kind::Handovers))
398 .collect(),
399 reviews: newest(reviews, query.limit, |_| 0)
400 .into_iter()
401 .filter(|_| only(Kind::Reviews))
402 .collect(),
403 opaque_events: state.opaque_events,
404 digest: state.digest.clone(),
405 }
406}
407
408fn runtime_of(key: &str) -> &str {
410 key.get(..49).unwrap_or(key)
411}
412
413pub(crate) fn cut(text: &str) -> String {
415 match text.char_indices().nth(SUMMARY_TEXT) {
416 Some((at, _)) => format!("{}…", &text[..at]),
417 None => text.to_string(),
418 }
419}
420
421#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
432#[schemars(rename = "MeshSessionCounts")]
433pub struct SessionCounts {
434 pub active: usize,
436 pub closed: usize,
438 pub expired: usize,
440}
441
442impl SessionCounts {
443 fn add(&mut self, state: SessionState) {
444 match state {
445 SessionState::Active => self.active += 1,
446 SessionState::Closed => self.closed += 1,
447 SessionState::Expired => self.expired += 1,
448 }
449 }
450}
451
452#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
462#[schemars(rename = "MeshClaimCounts")]
463pub struct ClaimCounts {
464 pub held: usize,
466 pub conflicted: usize,
468 pub released: usize,
470 pub expired: usize,
472}
473
474impl ClaimCounts {
475 fn add(&mut self, state: &ClaimState) {
476 match state {
477 ClaimState::Held => self.held += 1,
478 ClaimState::Conflicted(_) => self.conflicted += 1,
479 ClaimState::Released => self.released += 1,
480 ClaimState::Expired(_) => self.expired += 1,
481 }
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
512#[schemars(rename = "MeshClaimBrief")]
513pub struct ClaimBrief {
514 pub key: String,
516 pub session: String,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub issue: Option<String>,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub intent: Option<String>,
524 pub paths: usize,
526 pub mode: super::journal::ClaimMode,
528 pub acquired_lamport: u64,
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
559#[schemars(rename = "MeshRuntimeSummary")]
560pub struct RuntimeSummary {
561 pub runtime: String,
563 pub liveness: StreamLiveness,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
567 pub last_beat_ms: Option<u64>,
568 pub sessions: SessionCounts,
570 pub claims: ClaimCounts,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
574 pub newest_claim: Option<ClaimBrief>,
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
588#[schemars(rename = "MeshReviewBrief")]
589pub struct ReviewBrief {
590 pub key: String,
592 pub subject: String,
594 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub issue: Option<String>,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub reviewer: Option<String>,
600}
601
602#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
613#[schemars(rename = "MeshHandoverBrief")]
614pub struct HandoverBrief {
615 pub id: String,
617 pub runtime: String,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub branch: Option<String>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub issue: Option<String>,
625 pub published_lamport: u64,
627}
628
629#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
656#[schemars(rename = "MeshSummary")]
657pub struct Summary {
658 pub digest: String,
660 pub sessions: SessionCounts,
662 pub claims: ClaimCounts,
664 pub overlaps: usize,
666 pub runtimes: Vec<RuntimeSummary>,
668 pub runtimes_not_listed: usize,
670 pub open_reviews: Vec<ReviewBrief>,
672 pub open_reviews_not_listed: usize,
674 pub untaken_handovers: Vec<HandoverBrief>,
676 pub untaken_handovers_not_listed: usize,
678}
679
680fn rank(liveness: StreamLiveness) -> u8 {
681 match liveness {
682 StreamLiveness::Own => 0,
683 StreamLiveness::Live => 1,
684 StreamLiveness::Expired => 2,
685 }
686}
687
688pub fn summarize(
717 state: &CooperationState,
718 streams: &[(String, StreamLiveness, Option<u64>)],
719) -> Summary {
720 let mut runtimes: BTreeMap<String, RuntimeSummary> = BTreeMap::new();
721 let entry = |key: &str| -> RuntimeSummary {
722 RuntimeSummary {
723 runtime: key.to_string(),
724 liveness: StreamLiveness::Expired,
725 last_beat_ms: None,
726 sessions: SessionCounts::default(),
727 claims: ClaimCounts::default(),
728 newest_claim: None,
729 }
730 };
731 for (runtime, liveness, age) in streams {
732 let node = runtimes
733 .entry(runtime.clone())
734 .or_insert_with(|| entry(runtime));
735 if rank(*liveness) < rank(node.liveness) {
736 node.liveness = *liveness;
737 }
738 if let Some(age) = age {
739 node.last_beat_ms = Some(node.last_beat_ms.map_or(*age, |b| b.min(*age)));
740 }
741 }
742 let mut summary = Summary {
743 digest: state.digest.clone(),
744 overlaps: state.overlaps.len(),
745 ..Summary::default()
746 };
747 for session in &state.sessions {
748 summary.sessions.add(session.state);
749 runtimes
750 .entry(session.runtime.clone())
751 .or_insert_with(|| entry(&session.runtime))
752 .sessions
753 .add(session.state);
754 }
755 for claim in &state.claims {
756 summary.claims.add(&claim.state);
757 let node = runtimes
758 .entry(claim.runtime.clone())
759 .or_insert_with(|| entry(&claim.runtime));
760 node.claims.add(&claim.state);
761 let newer = node
762 .newest_claim
763 .as_ref()
764 .is_none_or(|b| claim.acquired_lamport > b.acquired_lamport);
765 if claim.state.is_live() && newer {
766 node.newest_claim = Some(ClaimBrief {
767 key: claim.key.clone(),
768 session: claim.session.clone(),
769 issue: claim.issue.clone(),
770 intent: claim.intent.as_deref().map(cut),
771 paths: claim.scope.len(),
772 mode: claim.mode,
773 acquired_lamport: claim.acquired_lamport,
774 });
775 }
776 }
777 let ranked: BTreeMap<(u8, String), RuntimeSummary> = runtimes
779 .into_values()
780 .map(|r| ((rank(r.liveness), r.runtime.clone()), r))
781 .collect();
782 let all: Vec<RuntimeSummary> = ranked.into_values().collect();
783 summary.runtimes_not_listed = all.len().saturating_sub(SUMMARY_LIST);
784 summary.runtimes = all.into_iter().take(SUMMARY_LIST).collect();
785
786 let open: Vec<ReviewBrief> = state
787 .reviews
788 .iter()
789 .filter(|r| r.state == ReviewState::Open)
790 .map(|r| ReviewBrief {
791 key: r.key.clone(),
792 subject: cut(&r.subject),
793 issue: r.issue.clone(),
794 reviewer: r.reviewer.clone(),
795 })
796 .collect();
797 summary.open_reviews_not_listed = open.len().saturating_sub(SUMMARY_LIST);
798 summary.open_reviews = open.into_iter().take(SUMMARY_LIST).collect();
799
800 let untaken: Vec<HandoverBrief> = state
801 .handovers
802 .iter()
803 .filter(|h| h.consumed_by.is_empty())
804 .map(|h| HandoverBrief {
805 id: h.id.clone(),
806 runtime: h.runtime.clone(),
807 branch: h.handover.branch.clone(),
808 issue: h.handover.issue.clone(),
809 published_lamport: h.published_lamport,
810 })
811 .collect();
812 summary.untaken_handovers_not_listed = untaken.len().saturating_sub(SUMMARY_LIST);
813 summary.untaken_handovers = untaken.into_iter().take(SUMMARY_LIST).collect();
814 summary
815}
816
817#[cfg(test)]
818mod tests {
819 use std::sync::Arc;
820
821 use super::*;
822 use crate::mesh::identity::NodeIdentity;
823 use crate::mesh::journal::{
824 ClaimMode, EventBody, HandoverBody, Journal, SessionInfo, StreamLiveness,
825 };
826 use crate::mesh::state::fold;
827
828 fn journal() -> Journal {
829 Journal::open(
830 Arc::new(NodeIdentity::ephemeral().unwrap()),
831 "0000000000000001",
832 "repo".into(),
833 None,
834 )
835 .unwrap()
836 }
837
838 fn claim(j: &Journal, claim: &str, session: &str, intent: &str) {
839 j.append_own(EventBody::ClaimAcquired {
840 claim: claim.into(),
841 session: session.into(),
842 scope: vec![format!("apps/{claim}")],
843 intent: Some(intent.into()),
844 mode: ClaimMode::Exclusive,
845 issue: Some(format!("#{claim}")),
846 })
847 .unwrap();
848 }
849
850 fn history() -> (Journal, CooperationState) {
853 let j = journal();
854 for n in 0..40 {
855 let session = format!("s{n}");
856 j.append_own(EventBody::SessionOpened {
857 info: SessionInfo::named(&session, "claude-code"),
858 })
859 .unwrap();
860 claim(&j, &format!("c{n}"), &session, &"a long intent ".repeat(30));
861 if n < 38 {
862 j.append_own(EventBody::ClaimReleased {
863 claim: format!("c{n}"),
864 })
865 .unwrap();
866 j.append_own(EventBody::SessionClosed {
867 session: session.clone(),
868 })
869 .unwrap();
870 }
871 }
872 for (review, answered) in [("r1", true), ("r2", false)] {
873 j.append_own(EventBody::ReviewRequested {
874 review: review.into(),
875 session: "s39".into(),
876 subject: format!("feature/{review}"),
877 scope: vec![],
878 issue: None,
879 reviewer: None,
880 body: None,
881 })
882 .unwrap();
883 if answered {
884 let key = fold(&j.events(), &|_| StreamLiveness::Own)
885 .reviews
886 .iter()
887 .find(|r| r.subject.ends_with(review))
888 .unwrap()
889 .key
890 .clone();
891 j.append_own(EventBody::ReviewAnswered {
892 request: key,
893 session: "s38".into(),
894 verdict: "approved".into(),
895 note: None,
896 })
897 .unwrap();
898 }
899 }
900 for (n, taken) in [(1, true), (2, false)] {
901 let body = format!("# Objective\nhandover {n}\n");
902 let handover = HandoverBody {
903 id: HandoverBody::digest_of(&body),
904 task: None,
905 issue: Some(format!("I{n}")),
906 milestone: None,
907 branch: Some(format!("feature/h{n}")),
908 head: None,
909 created_at: None,
910 name: None,
911 body,
912 };
913 let id = handover.id.clone();
914 j.append_own(EventBody::HandoverPublished { handover })
915 .unwrap();
916 if taken {
917 j.append_own(EventBody::HandoverConsumed {
918 handover: id,
919 session: "s39".into(),
920 })
921 .unwrap();
922 }
923 }
924 let state = fold(&j.events(), &|_| StreamLiveness::Own);
925 (j, state)
926 }
927
928 fn streams(state: &CooperationState) -> Vec<(String, StreamLiveness, Option<u64>)> {
929 vec![(
930 state.sessions[0].runtime.clone(),
931 StreamLiveness::Own,
932 Some(5),
933 )]
934 }
935
936 fn overlapping() -> CooperationState {
939 let j = journal();
940 for (claim, session, path, mode) in [
941 ("c1", "s1", "apps", ClaimMode::Advisory),
942 ("c2", "s2", "apps/x", ClaimMode::Exclusive),
943 ("c3", "s3", "apps/x/y", ClaimMode::Exclusive),
944 ] {
945 j.append_own(EventBody::ClaimAcquired {
946 claim: claim.into(),
947 session: session.into(),
948 scope: vec![path.into()],
949 intent: None,
950 mode,
951 issue: None,
952 })
953 .unwrap();
954 }
955 j.append_own(EventBody::ReviewRequested {
956 review: "r1".into(),
957 session: "s1".into(),
958 subject: "feature/r1".into(),
959 scope: vec!["apps/x".into()],
960 issue: None,
961 reviewer: None,
962 body: None,
963 })
964 .unwrap();
965 fold(&j.events(), &|_| StreamLiveness::Own)
966 }
967
968 #[test]
969 fn a_conflict_and_a_runtime_known_only_by_its_claims_are_counted() {
970 let state = overlapping();
971 let summary = summarize(&state, &[]);
972 assert_eq!(
973 summary.runtimes.len(),
974 1,
975 "a runtime heard of only through claims"
976 );
977 assert_eq!(summary.runtimes[0].claims.held, 2);
978 assert_eq!(summary.claims.conflicted, 1);
979 assert_eq!(summary.runtimes[0].claims.conflicted, 1);
980 assert!(
981 summary.overlaps > 0,
982 "the advisory claim meets the exclusive ones"
983 );
984 }
985
986 #[test]
987 fn an_expired_stream_counts_its_sessions_and_claims_as_expired() {
988 let (j, _) = history();
989 let gone = fold(&j.events(), &|_| StreamLiveness::Expired);
990 let summary = summarize(&gone, &[]);
991 assert_eq!(summary.sessions.expired, 2, "the two sessions still open");
992 assert_eq!(summary.claims.expired, 2, "the two claims still held");
993 assert_eq!(summary.claims.released, 38, "a release stays a release");
994 }
995
996 #[test]
997 fn overlaps_follow_the_matched_claims_and_a_review_matches_by_its_scope() {
998 let state = overlapping();
999 let matched = narrow(
1000 &state,
1001 &Query {
1002 matching: Some("apps/x".into()),
1003 ..Query::default()
1004 },
1005 );
1006 assert_eq!(
1007 matched.reviews.len(),
1008 1,
1009 "the review's scope names the path"
1010 );
1011 assert!(!matched.overlaps.is_empty());
1012 assert!(matched.overlaps.iter().all(|o| matched
1013 .claims
1014 .iter()
1015 .any(|c| c.key == o.first || c.key == o.second)));
1016 let overlaps = narrow(
1017 &state,
1018 &Query {
1019 kind: Some(Kind::Overlaps),
1020 ..Query::default()
1021 },
1022 );
1023 assert_eq!(overlaps.overlaps.len(), state.overlaps.len());
1024 assert!(overlaps.claims.is_empty() && overlaps.reviews.is_empty());
1025 let (_j, history) = history();
1027 let after = narrow(
1028 &history,
1029 &Query {
1030 after: Some(0),
1031 ..Query::default()
1032 },
1033 );
1034 assert_eq!(after.reviews.len(), history.reviews.len());
1035 }
1036
1037 #[test]
1038 fn the_summary_counts_the_history_and_lists_only_what_is_live_or_waiting() {
1039 let (_j, state) = history();
1040 let summary = summarize(&state, &streams(&state));
1041 assert_eq!(
1042 summary.digest, state.digest,
1043 "the whole fold's digest, not a page's"
1044 );
1045 assert_eq!(
1046 summary.sessions,
1047 SessionCounts {
1048 active: 2,
1049 closed: 38,
1050 expired: 0
1051 }
1052 );
1053 assert_eq!((summary.claims.held, summary.claims.released), (2, 38));
1054 assert_eq!(summary.runtimes.len(), 1);
1055 let runtime = &summary.runtimes[0];
1056 assert_eq!(runtime.liveness, StreamLiveness::Own);
1057 assert_eq!(runtime.last_beat_ms, Some(5));
1058 let newest = runtime.newest_claim.as_ref().expect("a live claim");
1059 assert_eq!(
1060 newest.issue.as_deref(),
1061 Some("#c39"),
1062 "the newest live claim"
1063 );
1064 assert!(
1065 newest.intent.as_ref().unwrap().ends_with('…'),
1066 "the intent is cut"
1067 );
1068 assert_eq!(summary.open_reviews.len(), 1);
1069 assert_eq!(summary.open_reviews[0].subject, "feature/r2");
1070 assert_eq!(summary.untaken_handovers.len(), 1);
1071 assert_eq!(
1072 summary.untaken_handovers[0].branch.as_deref(),
1073 Some("feature/h2")
1074 );
1075 }
1076
1077 #[test]
1078 fn the_summary_stays_small_however_long_the_history() {
1079 let (_j, state) = history();
1080 let full = serde_json::to_string(&state).unwrap().len();
1081 let summary = serde_json::to_string(&summarize(&state, &streams(&state)))
1082 .unwrap()
1083 .len();
1084 assert!(summary < 16 * 1024, "{summary} bytes");
1085 assert!(
1086 summary * 10 < full,
1087 "summary {summary} against the full {full}"
1088 );
1089 }
1090
1091 #[test]
1092 fn a_list_past_its_bound_is_counted_not_dropped() {
1093 let (_j, mut state) = history();
1094 let template = state.reviews[0].clone();
1095 state.reviews = (0..SUMMARY_LIST + 3)
1096 .map(|n| {
1097 let mut r = template.clone();
1098 r.key = format!("{}-{n}", template.key);
1099 r.state = ReviewState::Open;
1100 r
1101 })
1102 .collect();
1103 let summary = summarize(&state, &[]);
1104 assert_eq!(summary.open_reviews.len(), SUMMARY_LIST);
1105 assert_eq!(summary.open_reviews_not_listed, 3);
1106 }
1107
1108 #[test]
1109 fn many_runtimes_are_listed_living_first_and_the_rest_counted() {
1110 let (_j, state) = history();
1111 let own = state.sessions[0].runtime.clone();
1112 let mut streams: Vec<_> = (0..SUMMARY_LIST + 2)
1113 .map(|n| (format!("{n:0>49}"), StreamLiveness::Expired, None))
1114 .collect();
1115 streams.push((own.clone(), StreamLiveness::Live, Some(9)));
1116 streams.push((own.clone(), StreamLiveness::Own, Some(3)));
1117 let summary = summarize(&state, &streams);
1118 assert_eq!(summary.runtimes[0].runtime, own, "the living first");
1119 assert_eq!(
1120 summary.runtimes[0].liveness,
1121 StreamLiveness::Own,
1122 "the best verdict wins"
1123 );
1124 assert_eq!(
1125 summary.runtimes[0].last_beat_ms,
1126 Some(3),
1127 "and the freshest beat"
1128 );
1129 assert_eq!(summary.runtimes.len(), SUMMARY_LIST);
1130 assert_eq!(summary.runtimes_not_listed, 3);
1131 }
1132
1133 #[test]
1134 fn live_claims_only_drops_the_ended_ones_and_their_overlaps_follow() {
1135 let (_j, state) = history();
1136 let live = narrow(
1137 &state,
1138 &Query {
1139 claims: Some(ClaimFilter::Live),
1140 ..Query::default()
1141 },
1142 );
1143 assert_eq!(live.claims.len(), 2);
1144 assert!(live.claims.iter().all(|c| c.state.is_live()));
1145 assert_eq!(
1146 live.sessions.len(),
1147 state.sessions.len(),
1148 "sessions are not claims"
1149 );
1150 assert_eq!(live.digest, state.digest);
1151 assert!(live.overlaps.iter().all(|o| live
1152 .claims
1153 .iter()
1154 .any(|c| c.key == o.first || c.key == o.second)));
1155 }
1156
1157 #[test]
1158 fn a_session_is_asked_for_by_its_key_or_its_bare_id() {
1159 let (_j, state) = history();
1160 let by_id = narrow(
1161 &state,
1162 &Query {
1163 session: Some("s39".into()),
1164 ..Query::default()
1165 },
1166 );
1167 assert_eq!(by_id.sessions.len(), 1);
1168 assert_eq!(by_id.claims.len(), 1);
1169 assert_eq!(by_id.reviews.len(), 2, "both requests it opened");
1170 assert_eq!(by_id.handovers.len(), 1, "the one it took");
1171 let key = by_id.sessions[0].key.clone();
1172 let by_key = narrow(
1173 &state,
1174 &Query {
1175 session: Some(key),
1176 ..Query::default()
1177 },
1178 );
1179 assert_eq!(by_key, by_id);
1180 let nobody = narrow(
1181 &state,
1182 &Query {
1183 session: Some("s99".into()),
1184 ..Query::default()
1185 },
1186 );
1187 assert!(nobody.sessions.is_empty() && nobody.claims.is_empty());
1188 }
1189
1190 #[test]
1191 fn a_runtime_is_asked_for_by_its_key_or_its_node() {
1192 let (_j, state) = history();
1193 let runtime = state.sessions[0].runtime.clone();
1194 let node = runtime.split('-').next().unwrap().to_string();
1195 let mine = narrow(
1196 &state,
1197 &Query {
1198 runtime: Some(runtime),
1199 ..Query::default()
1200 },
1201 );
1202 assert_eq!(mine.sessions.len(), state.sessions.len());
1203 let machine = narrow(
1204 &state,
1205 &Query {
1206 runtime: Some(node),
1207 ..Query::default()
1208 },
1209 );
1210 assert_eq!(machine, mine);
1211 let elsewhere = narrow(
1212 &state,
1213 &Query {
1214 runtime: Some("f".repeat(49)),
1215 ..Query::default()
1216 },
1217 );
1218 assert!(elsewhere.sessions.is_empty() && elsewhere.reviews.is_empty());
1219 assert!(elsewhere.handovers.is_empty());
1220 }
1221
1222 #[test]
1223 fn after_keeps_only_what_moved_since_the_stamp() {
1224 let (_j, state) = history();
1225 let stamp = state
1226 .claims
1227 .iter()
1228 .map(|c| c.acquired_lamport)
1229 .max()
1230 .unwrap();
1231 let later = narrow(
1232 &state,
1233 &Query {
1234 after: Some(stamp - 1),
1235 ..Query::default()
1236 },
1237 );
1238 assert_eq!(later.claims.len(), 1, "only the newest acquisition");
1239 assert!(later.sessions.iter().all(|s| s.updated_lamport > stamp - 1));
1240 assert!(later
1241 .reviews
1242 .iter()
1243 .all(|r| later.sessions.iter().any(|s| s.key == r.session)));
1244 let nothing = narrow(
1245 &state,
1246 &Query {
1247 after: Some(u64::MAX),
1248 ..Query::default()
1249 },
1250 );
1251 assert!(nothing.sessions.is_empty() && nothing.claims.is_empty());
1252 assert!(nothing.handovers.is_empty() && nothing.reviews.is_empty());
1253 }
1254
1255 #[test]
1256 fn a_kind_answers_one_collection_and_a_limit_the_newest_of_each() {
1257 let (_j, state) = history();
1258 let claims = narrow(
1259 &state,
1260 &Query {
1261 kind: Some(Kind::Claims),
1262 ..Query::default()
1263 },
1264 );
1265 assert_eq!(claims.claims.len(), state.claims.len());
1266 assert!(claims.sessions.is_empty() && claims.reviews.is_empty());
1267 assert!(claims.handovers.is_empty() && claims.overlaps.is_empty());
1268 assert_eq!(claims.digest, state.digest);
1269
1270 let three = narrow(
1271 &state,
1272 &Query {
1273 limit: Some(3),
1274 ..Query::default()
1275 },
1276 );
1277 assert_eq!(three.claims.len(), 3);
1278 let newest = state
1279 .claims
1280 .iter()
1281 .map(|c| c.acquired_lamport)
1282 .max()
1283 .unwrap();
1284 assert!(three.claims.iter().any(|c| c.acquired_lamport == newest));
1285 let in_fold: Vec<_> = state
1286 .claims
1287 .iter()
1288 .filter(|c| three.claims.contains(c))
1289 .cloned()
1290 .collect();
1291 assert_eq!(three.claims, in_fold, "kept in the fold's own order");
1292 assert!(three.sessions.len() <= 3 && three.reviews.len() <= 3);
1293 let none = narrow(
1294 &state,
1295 &Query {
1296 limit: Some(0),
1297 ..Query::default()
1298 },
1299 );
1300 assert!(none.claims.is_empty() && none.sessions.is_empty());
1301 let all = narrow(
1302 &state,
1303 &Query {
1304 limit: Some(10_000),
1305 ..Query::default()
1306 },
1307 );
1308 assert_eq!(all, state);
1309 }
1310
1311 #[test]
1312 fn a_limit_keeps_ties_in_order_until_it_is_reached() {
1313 let items = vec![("a", 5), ("b", 7), ("c", 7), ("d", 1)];
1314 assert_eq!(newest(items.clone(), Some(1), |i| i.1), [("b", 7)]);
1315 assert_eq!(
1316 newest(items.clone(), Some(2), |i| i.1),
1317 [("b", 7), ("c", 7)]
1318 );
1319 assert_eq!(
1320 newest(items.clone(), Some(3), |i| i.1),
1321 [("a", 5), ("b", 7), ("c", 7)]
1322 );
1323 assert_eq!(newest(items.clone(), None, |i| i.1), items);
1324 }
1325
1326 #[test]
1327 fn open_keeps_only_what_is_still_open_in_every_collection() {
1328 let (_j, state) = history();
1329 let open = narrow(
1330 &state,
1331 &Query {
1332 open: Some(true),
1333 ..Query::default()
1334 },
1335 );
1336 assert_eq!(open.sessions.len(), 2);
1337 assert!(open
1338 .sessions
1339 .iter()
1340 .all(|s| s.state == SessionState::Active));
1341 assert_eq!(open.claims.len(), 2);
1342 assert_eq!(open.reviews.len(), 1);
1343 assert_eq!(open.reviews[0].subject, "feature/r2");
1344 assert_eq!(open.handovers.len(), 1);
1345 assert_eq!(
1346 open.handovers[0].handover.branch.as_deref(),
1347 Some("feature/h2")
1348 );
1349 let closed = narrow(
1350 &state,
1351 &Query {
1352 open: Some(false),
1353 ..Query::default()
1354 },
1355 );
1356 assert_eq!(closed, state, "open=false asks for nothing narrower");
1357 }
1358
1359 #[test]
1360 fn matching_finds_a_path_a_branch_an_issue_or_a_subject_ignoring_case() {
1361 let (_j, state) = history();
1362 let path = narrow(
1363 &state,
1364 &Query {
1365 matching: Some("APPS/C7".into()),
1366 ..Query::default()
1367 },
1368 );
1369 assert_eq!(path.claims.len(), 1, "apps/c7 and nothing else");
1370 assert_eq!(path.claims[0].scope, ["apps/c7"]);
1371 assert!(path.handovers.is_empty() && path.reviews.is_empty());
1372 let issue = narrow(
1373 &state,
1374 &Query {
1375 matching: Some("#c39".into()),
1376 ..Query::default()
1377 },
1378 );
1379 assert_eq!(issue.claims.len(), 1);
1380 let branch = narrow(
1381 &state,
1382 &Query {
1383 matching: Some("feature/h2".into()),
1384 ..Query::default()
1385 },
1386 );
1387 assert_eq!(branch.handovers.len(), 1);
1388 let subject = narrow(
1389 &state,
1390 &Query {
1391 matching: Some("feature/r1".into()),
1392 ..Query::default()
1393 },
1394 );
1395 assert_eq!(subject.reviews.len(), 1);
1396 let session = narrow(
1397 &state,
1398 &Query {
1399 matching: Some("/s39".into()),
1400 ..Query::default()
1401 },
1402 );
1403 assert_eq!(session.sessions.len(), 1, "a session's key is text too");
1404 let nothing = narrow(
1405 &state,
1406 &Query {
1407 matching: Some("no such".into()),
1408 ..Query::default()
1409 },
1410 );
1411 assert!(nothing.claims.is_empty() && nothing.sessions.is_empty());
1412 assert_eq!(nothing.digest, state.digest);
1413 }
1414
1415 #[test]
1416 fn twenty_thousand_events_summarise_under_sixteen_kilobytes() {
1417 let j = journal();
1419 let mut events = 0;
1420 let mut n = 0;
1421 while events < 20_000 {
1422 let session = format!("s{n}");
1423 j.append_own(EventBody::SessionOpened {
1424 info: SessionInfo::named(&session, "claude-code"),
1425 })
1426 .unwrap();
1427 claim(&j, &format!("c{n}"), &session, &"an intent ".repeat(40));
1428 j.append_own(EventBody::ClaimReleased {
1429 claim: format!("c{n}"),
1430 })
1431 .unwrap();
1432 j.append_own(EventBody::SessionClosed { session }).unwrap();
1433 events += 4;
1434 n += 1;
1435 }
1436 let state = fold(&j.events(), &|_| StreamLiveness::Own);
1437 assert_eq!(state.claims.len(), n);
1438 let summary = serde_json::to_string(&summarize(&state, &streams(&state))).unwrap();
1439 assert!(summary.len() < 16 * 1024, "{} bytes", summary.len());
1440 let full = serde_json::to_string(&state).unwrap();
1441 assert!(
1442 full.len() > 1024 * 1024,
1443 "the whole is what the summary spares: {}",
1444 full.len()
1445 );
1446 }
1447
1448 #[test]
1449 fn a_text_is_cut_on_a_character_and_says_so() {
1450 assert_eq!(cut("short"), "short");
1451 let long = "\u{10d}".repeat(SUMMARY_TEXT + 1);
1453 let cut = cut(&long);
1454 assert!(cut.ends_with('…'));
1455 assert_eq!(cut.chars().count(), SUMMARY_TEXT + 1);
1456 }
1457
1458 #[test]
1459 fn the_view_defaults_to_the_summary() {
1460 assert_eq!(Query::default().view(), View::Summary);
1461 let full: Query = serde_json::from_str(r#"{"view":"full","claims":"live"}"#).unwrap();
1462 assert_eq!(full.view(), View::Full);
1463 assert!(serde_json::from_str::<Query>(r#"{"bogus":1}"#).is_err());
1464 }
1465}