1use heddle_object_model::object::{
3 CollaborationMention as Mention, CollaborationRecordKind as Kind, ContentHash, StateId,
4};
5use uuid::Uuid;
6
7use crate::{contract::*, transport::Error};
8
9fn spool(value: Option<&SpoolRef>) -> Result<Uuid, Error> {
10 let id = value
11 .ok_or(Error::Protocol("mention requires spool"))?
12 .id
13 .parse::<Uuid>()
14 .map_err(|_| Error::Protocol("mention spool must be UUID"))?;
15 if id.is_nil() {
16 return Err(Error::Protocol("mention spool cannot be nil"));
17 }
18 Ok(id)
19}
20fn key(value: &[u8]) -> Result<[u8; 32], Error> {
21 value
22 .try_into()
23 .map_err(|_| Error::Protocol("mention identity must contain 32 bytes"))
24}
25fn endpoint(value: Option<&EndpointRef>) -> Result<[u8; 32], Error> {
26 let endpoint = value.ok_or(Error::Protocol("device mention requires endpoint"))?;
27 if endpoint.kind != EndpointKind::Device as i32 {
28 return Err(Error::Protocol("device mention must target a device"));
29 }
30 key(&endpoint.public_key)
31}
32pub fn mention(value: &EntityRef) -> Result<Mention, Error> {
35 use entity_ref::Entity;
36 let result = match value
37 .entity
38 .as_ref()
39 .ok_or(Error::Protocol("empty mention"))?
40 {
41 Entity::Spool(r) => Mention::Spool {
42 spool: spool(Some(r))?,
43 },
44 Entity::Thread(r) => Mention::Thread {
45 spool: spool(r.spool.as_ref())?,
46 thread: ContentHash::from_bytes(key(&r
47 .id
48 .as_ref()
49 .ok_or(Error::Protocol("Thread mention requires ID"))?
50 .value)?),
51 },
52 Entity::Checkout(r) => Mention::Checkout {
53 spool: spool(r.spool.as_ref())?,
54 device: endpoint(r.device.as_ref())?,
55 id: r.id.clone(),
56 },
57 Entity::Revision(r) => match r
58 .revision
59 .as_ref()
60 .ok_or(Error::Protocol("revision mention requires exact revision"))?
61 {
62 revision_ref::Revision::State(id) => Mention::State {
63 spool: spool(r.spool.as_ref())?,
64 state: StateId::from_bytes(key(&id.value)?),
65 },
66 revision_ref::Revision::GitCommitOid(oid) => Mention::GitCommit {
67 spool: spool(r.spool.as_ref())?,
68 oid: oid.clone(),
69 },
70 },
71 Entity::Device(r) => Mention::Device {
72 key: endpoint(Some(r))?,
73 },
74 Entity::Discussion(r) => record(r, Kind::Discussion)?,
75 Entity::Context(r) => record(r, Kind::Context)?,
76 Entity::Operation(r) => record(r, Kind::Operation)?,
77 Entity::Run(r) => record(r, Kind::Run)?,
78 Entity::Policy(r) => record(r, Kind::Policy)?,
79 Entity::Analysis(r) => record(r, Kind::Analysis)?,
80 Entity::Invitation(r) => record(r, Kind::Invitation)?,
81 Entity::Grant(r) => record(r, Kind::Grant)?,
82 Entity::DiscussionTurn(r) => record(r, Kind::DiscussionTurn)?,
83 Entity::Review(r) => record(r, Kind::Review)?,
84 Entity::Notification(r) => record(r, Kind::Notification)?,
85 Entity::AttentionItem(r) => record(r, Kind::AttentionItem)?,
86 Entity::Member(r) => record(r, Kind::Member)?,
87 Entity::ApprovalGroup(r) => record(r, Kind::ApprovalGroup)?,
88 Entity::Session(r) => record(r, Kind::Session)?,
89 Entity::SignupInvitation(r) => record(r, Kind::SignupInvitation)?,
90 Entity::TimelineEvent(r) => record(r, Kind::TimelineEvent)?,
91 Entity::Artifact(r) => record(r, Kind::Artifact)?,
92 Entity::Mount(r) => record(r, Kind::Mount)?,
93 Entity::SupportAccess(r) => record(r, Kind::SupportAccess)?,
94 Entity::DeviceRecord(r) => record(r, Kind::DeviceRecord)?,
95 Entity::Delegation(r) => record(r, Kind::Delegation)?,
96 Entity::Recovery(r) => record(r, Kind::Recovery)?,
97 Entity::OwnerTransition(r) => record(r, Kind::OwnerTransition)?,
98 Entity::Billing(r) => record(r, Kind::Billing)?,
99 Entity::Evidence(r) => record(r, Kind::Evidence)?,
100 Entity::CheckAcknowledgement(r) => record(r, Kind::CheckAcknowledgement)?,
101 Entity::ProviderConnection(r) => record(r, Kind::ProviderConnection)?,
102 Entity::RemoteLink(r) => record(r, Kind::RemoteLink)?,
103 Entity::Bookmark(_) => {
104 return Err(Error::Protocol(
105 "bookmarks are account-private; mention their Spool or Thread instead",
106 ));
107 }
108 Entity::Passkey(_) => {
109 return Err(Error::Protocol(
110 "passkeys are account-private credentials and cannot be mentioned",
111 ));
112 }
113 Entity::Principal(r) => Mention::Principal {
114 principal_id: r
115 .id
116 .parse()
117 .map_err(|_| Error::Protocol("principal mention requires UUID"))?,
118 },
119 Entity::Agent(_) => {
120 return Err(Error::Protocol(
121 "agent references require an explicit mention contract",
122 ));
123 }
124 Entity::Source(r) => {
125 let target = super::annotation_source(&AnnotationSourceReference {
126 source: Some(SourceAnchor {
127 revision: r.revision.clone(),
128 path: r.path.clone(),
129 symbol_id: r.symbol_id.clone(),
130 start_line: r.start_line,
131 end_line: r.end_line,
132 thread: r.thread.clone(),
133 ..Default::default()
134 }),
135 })?;
136 Mention::Source {
137 target: Box::new(target),
138 }
139 }
140 };
141 result
142 .validate()
143 .map_err(|_| Error::Protocol("invalid collaboration mention"))?;
144 Ok(result)
145}
146fn record(r: &RecordRef, record_kind: Kind) -> Result<Mention, Error> {
147 Ok(Mention::Record {
148 spool: r.spool.as_ref().map(|r| spool(Some(r))).transpose()?,
149 record_kind,
150 id: r.id.clone(),
151 })
152}
153fn wire_spool(id: Uuid) -> Option<SpoolRef> {
154 Some(SpoolRef { id: id.to_string() })
155}
156fn wire_device(key: &[u8; 32]) -> EndpointRef {
157 EndpointRef {
158 kind: EndpointKind::Device as i32,
159 public_key: key.to_vec(),
160 }
161}
162pub fn mention_ref(value: &Mention) -> EntityRef {
163 use entity_ref::Entity;
164 let entity = match value {
165 Mention::Spool { spool } => Entity::Spool(SpoolRef {
166 id: spool.to_string(),
167 }),
168 Mention::Thread { spool, thread } => Entity::Thread(ThreadRef {
169 spool: wire_spool(*spool),
170 id: Some(ThreadId {
171 value: thread.as_bytes().to_vec(),
172 }),
173 }),
174 Mention::Checkout { spool, device, id } => Entity::Checkout(CheckoutRef {
175 spool: wire_spool(*spool),
176 device: Some(wire_device(device)),
177 id: id.clone(),
178 }),
179 Mention::State { spool, state } => Entity::Revision(RevisionRef {
180 spool: wire_spool(*spool),
181 revision: Some(revision_ref::Revision::State(
182 api::heddle::api::common::StateId {
183 value: state.as_bytes().to_vec(),
184 },
185 )),
186 }),
187 Mention::GitCommit { spool, oid } => Entity::Revision(RevisionRef {
188 spool: wire_spool(*spool),
189 revision: Some(revision_ref::Revision::GitCommitOid(oid.clone())),
190 }),
191 Mention::Device { key } => Entity::Device(wire_device(key)),
192 Mention::Principal { principal_id } => Entity::Principal(PrincipalRef {
193 id: principal_id.to_string(),
194 }),
195 Mention::Source { target } => {
196 let source = super::annotation_source_ref(target).source;
197 let source = source.unwrap_or_default();
199 Entity::Source(SourceMention {
200 revision: source.revision,
201 path: source.path,
202 symbol_id: source.symbol_id,
203 start_line: source.start_line,
204 end_line: source.end_line,
205 thread: source.thread,
206 })
207 }
208 Mention::Record {
209 spool,
210 record_kind,
211 id,
212 } => {
213 let r = RecordRef {
214 spool: spool.and_then(wire_spool),
215 id: id.clone(),
216 };
217 match record_kind {
218 Kind::Discussion => Entity::Discussion(r),
219 Kind::Context => Entity::Context(r),
220 Kind::Operation => Entity::Operation(r),
221 Kind::Run => Entity::Run(r),
222 Kind::Policy => Entity::Policy(r),
223 Kind::Analysis => Entity::Analysis(r),
224 Kind::Invitation => Entity::Invitation(r),
225 Kind::Grant => Entity::Grant(r),
226 Kind::DiscussionTurn => Entity::DiscussionTurn(r),
227 Kind::Review => Entity::Review(r),
228 Kind::Notification => Entity::Notification(r),
229 Kind::AttentionItem => Entity::AttentionItem(r),
230 Kind::Member => Entity::Member(r),
231 Kind::ApprovalGroup => Entity::ApprovalGroup(r),
232 Kind::Session => Entity::Session(r),
233 Kind::SignupInvitation => Entity::SignupInvitation(r),
234 Kind::TimelineEvent => Entity::TimelineEvent(r),
235 Kind::Artifact => Entity::Artifact(r),
236 Kind::Mount => Entity::Mount(r),
237 Kind::SupportAccess => Entity::SupportAccess(r),
238 Kind::DeviceRecord => Entity::DeviceRecord(r),
239 Kind::Delegation => Entity::Delegation(r),
240 Kind::Recovery => Entity::Recovery(r),
241 Kind::OwnerTransition => Entity::OwnerTransition(r),
242 Kind::Billing => Entity::Billing(r),
243 Kind::Evidence => Entity::Evidence(r),
244 Kind::CheckAcknowledgement => Entity::CheckAcknowledgement(r),
245 Kind::ProviderConnection => Entity::ProviderConnection(r),
246 Kind::RemoteLink => Entity::RemoteLink(r),
247 }
248 }
249 };
250 EntityRef {
251 entity: Some(entity),
252 }
253}
254
255pub(super) fn source_target(
256 value: &SourceTargetReference,
257) -> Result<heddle_object_model::object::source_target::SourceTargetReference, Error> {
258 use heddle_object_model::object::{
259 CollaborationRevision, CollaborationScope,
260 source_target::{SourceTargetBinding, SourceTargetReference as Target},
261 };
262 fn named(value: &ThreadRef) -> Result<CollaborationScope, Error> {
263 Ok(CollaborationScope {
264 spool: spool(value.spool.as_ref())?,
265 thread: Some(ContentHash::from_bytes(key(&value
266 .id
267 .as_ref()
268 .ok_or(Error::Protocol("target requires Thread ID"))?
269 .value)?)),
270 })
271 }
272 let binding = match value
273 .binding
274 .as_ref()
275 .ok_or(Error::Protocol("explicit source target binding required"))?
276 {
277 source_target_reference::Binding::ViewedThread(true) => SourceTargetBinding::ViewedThread,
278 source_target_reference::Binding::ViewedThread(false) => {
279 return Err(Error::Protocol("viewed Thread binding must be true"));
280 }
281 source_target_reference::Binding::NamedThread(value) => SourceTargetBinding::NamedThread {
282 scope: named(value)?,
283 },
284 source_target_reference::Binding::PinnedRevision(value) => {
285 let revision = value
286 .revision
287 .as_ref()
288 .ok_or(Error::Protocol("pinned target requires revision"))?;
289 let spool = spool(revision.spool.as_ref())?;
290 let scope = match &value.thread {
291 Some(thread) => {
292 let scope = named(thread)?;
293 if scope.spool != spool {
294 return Err(Error::Protocol(
295 "pinned target Thread belongs to another Spool",
296 ));
297 }
298 scope
299 }
300 None => CollaborationScope {
301 spool,
302 thread: None,
303 },
304 };
305 let revision = match revision
306 .revision
307 .as_ref()
308 .ok_or(Error::Protocol("pinned target requires exact revision"))?
309 {
310 revision_ref::Revision::State(id) => CollaborationRevision::State {
311 state_id: StateId::from_bytes(key(&id.value)?),
312 },
313 revision_ref::Revision::GitCommitOid(oid) => {
314 CollaborationRevision::GitCommit { oid: oid.clone() }
315 }
316 };
317 SourceTargetBinding::PinnedRevision { scope, revision }
318 }
319 };
320 let target = Target {
321 target: ContentHash::from_bytes(key(&value.target_id)?),
322 binding,
323 };
324 target
325 .validate()
326 .map_err(|_| Error::Protocol("invalid source target binding"))?;
327 Ok(target)
328}
329pub fn source_target_ref(
330 value: &heddle_object_model::object::source_target::SourceTargetReference,
331) -> SourceTargetReference {
332 use heddle_object_model::object::{
333 CollaborationRevision, CollaborationScope, source_target::SourceTargetBinding,
334 };
335 fn thread(scope: &CollaborationScope) -> Option<ThreadRef> {
336 scope.thread.map(|id| ThreadRef {
337 spool: wire_spool(scope.spool),
338 id: Some(ThreadId {
339 value: id.as_bytes().to_vec(),
340 }),
341 })
342 }
343 let binding = match &value.binding {
344 SourceTargetBinding::ViewedThread => source_target_reference::Binding::ViewedThread(true),
345 SourceTargetBinding::NamedThread { scope } => {
346 source_target_reference::Binding::NamedThread(ThreadRef {
347 spool: wire_spool(scope.spool),
348 id: scope.thread.map(|id| ThreadId {
349 value: id.as_bytes().to_vec(),
350 }),
351 })
352 }
353 SourceTargetBinding::PinnedRevision { scope, revision } => {
354 source_target_reference::Binding::PinnedRevision(PinnedSourceTargetRevision {
355 thread: thread(scope),
356 revision: Some(RevisionRef {
357 spool: wire_spool(scope.spool),
358 revision: Some(match revision {
359 CollaborationRevision::State { state_id } => {
360 revision_ref::Revision::State(api::heddle::api::common::StateId {
361 value: state_id.as_bytes().to_vec(),
362 })
363 }
364 CollaborationRevision::GitCommit { oid } => {
365 revision_ref::Revision::GitCommitOid(oid.clone())
366 }
367 }),
368 }),
369 })
370 }
371 };
372 SourceTargetReference {
373 target_id: value.target.as_bytes().to_vec(),
374 binding: Some(binding),
375 }
376}
377
378pub fn anchor(
380 value: &CollaborationAnchor,
381 scope: &heddle_object_model::object::CollaborationScope,
382) -> Result<heddle_object_model::object::CollaborationAnchor, Error> {
383 use heddle_object_model::object::{
384 CollaborationAnchor as Anchor, CollaborationRevision, CollaborationSourceAnchor,
385 };
386 fn thread(
387 value: &ThreadRef,
388 scope: &heddle_object_model::object::CollaborationScope,
389 ) -> Result<(), Error> {
390 if spool(value.spool.as_ref())? != scope.spool
391 || Some(ContentHash::from_bytes(key(&value
392 .id
393 .as_ref()
394 .ok_or(Error::Protocol("anchor Thread requires ID"))?
395 .value)?))
396 != scope.thread
397 {
398 return Err(Error::Protocol("anchor belongs to another Thread or spool"));
399 }
400 Ok(())
401 }
402 Ok(
403 match value
404 .target
405 .as_ref()
406 .ok_or(Error::Protocol("collaboration anchor required"))?
407 {
408 collaboration_anchor::Target::Thread(value) => {
409 thread(value, scope)?;
410 Anchor::Repository
411 }
412 collaboration_anchor::Target::Spool(value) => {
413 if scope.thread.is_some() || spool(Some(value))? != scope.spool {
414 return Err(Error::Protocol(
415 "spool anchor requires independent spool scope",
416 ));
417 }
418 Anchor::Repository
419 }
420 collaboration_anchor::Target::Source(value) => {
421 reject_path_kind(value)?;
422 thread(
423 value
424 .thread
425 .as_ref()
426 .ok_or(Error::Protocol("source anchor requires owning Thread"))?,
427 scope,
428 )?;
429 let revision = value
430 .revision
431 .as_ref()
432 .ok_or(Error::Protocol("source anchor requires exact revision"))?;
433 if spool(revision.spool.as_ref())? != scope.spool {
434 return Err(Error::Protocol("source revision belongs to another spool"));
435 }
436 let revision = match revision
437 .revision
438 .as_ref()
439 .ok_or(Error::Protocol("exact revision required"))?
440 {
441 revision_ref::Revision::State(id) => CollaborationRevision::State {
442 state_id: StateId::from_bytes(key(&id.value)?),
443 },
444 revision_ref::Revision::GitCommitOid(oid) => {
445 CollaborationRevision::GitCommit { oid: oid.clone() }
446 }
447 };
448 Anchor::Source {
449 source: CollaborationSourceAnchor {
450 revision,
451 path: value.path.clone(),
452 symbol_id: value.symbol_id.clone(),
453 start_line: value.start_line,
454 end_line: value.end_line,
455 target: value.target.as_ref().map(source_target).transpose()?,
456 },
457 }
458 }
459 },
460 )
461}
462
463pub(crate) fn reject_path_kind(value: &SourceAnchor) -> Result<(), Error> {
467 if value.path_kind != 0 || value.path_kind_source != 0 {
468 return Err(Error::Protocol(
469 "source anchor path_kind is not supported yet (heddle#1959)",
470 ));
471 }
472 Ok(())
473}
474
475pub fn visibility(
478 audience: i32,
479 label: &str,
480) -> Result<heddle_object_model::object::VisibilityTier, Error> {
481 use heddle_object_model::object::VisibilityTier as Tier;
482 match Audience::try_from(audience) {
483 Ok(Audience::Public) if label.is_empty() => Ok(Tier::Public),
484 Ok(Audience::Members) if label.is_empty() => Ok(Tier::Internal),
485 Ok(Audience::Private)
486 if !label.trim().is_empty()
487 && label.len() <= 512
488 && !label.chars().any(char::is_control) =>
489 {
490 Ok(Tier::Private {
491 scope_label: label.into(),
492 })
493 }
494 _ => Err(Error::Protocol(
495 "explicit audience and matching private label required",
496 )),
497 }
498}
499pub fn audience(tier: &heddle_object_model::object::VisibilityTier) -> Option<(Audience, String)> {
502 use heddle_object_model::object::VisibilityTier as Tier;
503 match tier {
504 Tier::Public => Some((Audience::Public, String::new())),
505 Tier::Internal => Some((Audience::Members, String::new())),
506 Tier::Private { scope_label } => Some((Audience::Private, scope_label.clone())),
507 Tier::TeamScoped { .. } | Tier::Restricted { .. } => None,
508 }
509}
510
511pub fn anchor_ref(
514 value: &heddle_object_model::object::CollaborationAnchor,
515 scope: &heddle_object_model::object::CollaborationScope,
516) -> Result<CollaborationAnchor, Error> {
517 use heddle_object_model::object::{
518 CollaborationAnchor as Anchor, CollaborationRevision as Revision,
519 };
520 let thread = scope.thread.map(|id| ThreadRef {
521 spool: wire_spool(scope.spool),
522 id: Some(ThreadId {
523 value: id.as_bytes().to_vec(),
524 }),
525 });
526 let source = match value {
527 Anchor::Repository => {
528 return Ok(CollaborationAnchor {
529 target: Some(match thread {
530 Some(thread) => collaboration_anchor::Target::Thread(thread),
531 None => collaboration_anchor::Target::Spool(SpoolRef {
532 id: scope.spool.to_string(),
533 }),
534 }),
535 });
536 }
537 Anchor::Source { source } => SourceAnchor {
538 revision: Some(RevisionRef {
539 spool: wire_spool(scope.spool),
540 revision: Some(match &source.revision {
541 Revision::State { state_id } => {
542 revision_ref::Revision::State(api::heddle::api::common::StateId {
543 value: state_id.as_bytes().to_vec(),
544 })
545 }
546 Revision::GitCommit { oid } => {
547 revision_ref::Revision::GitCommitOid(oid.clone())
548 }
549 }),
550 }),
551 path: source.path.clone(),
552 symbol_id: source.symbol_id.clone(),
553 start_line: source.start_line,
554 end_line: source.end_line,
555 target: source.target.as_ref().map(source_target_ref),
556 thread,
557 path_kind: api::heddle::api::v1alpha2::SourcePathKind::Unspecified as i32,
559 path_kind_source: api::heddle::api::v1alpha2::SourcePathKindSource::Unspecified as i32,
560 },
561 Anchor::State { state_id }
562 | Anchor::Path { state_id, .. }
563 | Anchor::Symbol { state_id, .. } => SourceAnchor {
564 revision: Some(RevisionRef {
565 spool: wire_spool(scope.spool),
566 revision: Some(revision_ref::Revision::State(
567 api::heddle::api::common::StateId {
568 value: state_id.as_bytes().to_vec(),
569 },
570 )),
571 }),
572 path: match value {
573 Anchor::Path { path, .. } | Anchor::Symbol { path, .. } => path.clone(),
574 _ => String::new(),
575 },
576 symbol_id: match value {
577 Anchor::Symbol { symbol, .. } => symbol.clone(),
578 _ => String::new(),
579 },
580 start_line: None,
581 end_line: None,
582 target: None,
583 thread,
584 path_kind: api::heddle::api::v1alpha2::SourcePathKind::Unspecified as i32,
585 path_kind_source: api::heddle::api::v1alpha2::SourcePathKindSource::Unspecified as i32,
586 },
587 Anchor::Change { .. } => {
588 return Err(Error::Protocol(
589 "change anchor requires exact source revision",
590 ));
591 }
592 };
593 Ok(CollaborationAnchor {
594 target: Some(collaboration_anchor::Target::Source(source)),
595 })
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 #[test]
602 fn explicit_target_bindings_round_trip_and_reject_incomplete_identity() {
603 use heddle_object_model::object::{
604 CollaborationRevision, CollaborationScope,
605 source_target::{SourceTargetBinding, SourceTargetReference as Target},
606 };
607 let scope = CollaborationScope {
608 spool: Uuid::from_u128(1),
609 thread: Some(ContentHash::from_bytes([2; 32])),
610 };
611 for binding in [
612 SourceTargetBinding::ViewedThread,
613 SourceTargetBinding::NamedThread {
614 scope: scope.clone(),
615 },
616 SourceTargetBinding::PinnedRevision {
617 scope: scope.clone(),
618 revision: CollaborationRevision::GitCommit {
619 oid: "a".repeat(40),
620 },
621 },
622 SourceTargetBinding::PinnedRevision {
623 scope: CollaborationScope {
624 thread: None,
625 ..scope.clone()
626 },
627 revision: CollaborationRevision::State {
628 state_id: StateId::from_bytes([5; 32]),
629 },
630 },
631 ] {
632 let target = Target {
633 target: ContentHash::from_bytes([6; 32]),
634 binding,
635 };
636 assert_eq!(
637 source_target(&source_target_ref(&target)).expect("lossless binding"),
638 target
639 );
640 let source = heddle_object_model::object::CollaborationSourceAnchor {
641 revision: CollaborationRevision::State {
642 state_id: StateId::from_bytes([5; 32]),
643 },
644 path: "src/main.rs".into(),
645 symbol_id: "run".into(),
646 start_line: Some(12),
647 end_line: Some(18),
648 target: Some(target),
649 };
650 let original = heddle_object_model::object::CollaborationAnchor::Source {
651 source: source.clone(),
652 };
653 assert_eq!(
654 anchor(&anchor_ref(&original, &scope).expect("wire anchor"), &scope)
655 .expect("canonical anchor"),
656 original
657 );
658 let reference = heddle_object_model::object::AnnotationSourceReference {
659 scope: scope.clone(),
660 source,
661 };
662 assert_eq!(
663 super::super::annotation_source(&super::super::annotation_source_ref(&reference))
664 .expect("canonical tag reference"),
665 reference
666 );
667 }
668 for binding in [
669 None,
670 Some(source_target_reference::Binding::ViewedThread(false)),
671 Some(source_target_reference::Binding::NamedThread(ThreadRef {
672 spool: wire_spool(Uuid::from_u128(1)),
673 id: None,
674 })),
675 Some(source_target_reference::Binding::PinnedRevision(
676 PinnedSourceTargetRevision {
677 revision: None,
678 thread: None,
679 },
680 )),
681 ] {
682 assert!(
683 source_target(&SourceTargetReference {
684 target_id: vec![6; 32],
685 binding
686 })
687 .is_err()
688 );
689 }
690 assert!(
691 source_target(&SourceTargetReference {
692 target_id: vec![6; 31],
693 binding: Some(source_target_reference::Binding::ViewedThread(true))
694 })
695 .is_err()
696 );
697 }
698 #[test]
699 fn every_native_mention_retains_kind_scope_and_identity() {
700 let spool = Uuid::from_u128(1);
701 let mut mentions = vec![
702 Mention::Spool { spool },
703 Mention::Thread {
704 spool,
705 thread: ContentHash::from_bytes([2; 32]),
706 },
707 Mention::State {
708 spool,
709 state: StateId::from_bytes([3; 32]),
710 },
711 Mention::GitCommit {
712 spool,
713 oid: "a".repeat(40),
714 },
715 Mention::Checkout {
716 spool,
717 device: [4; 32],
718 id: "checkout".into(),
719 },
720 Mention::Device { key: [5; 32] },
721 ];
722 for record_kind in [
723 Kind::Discussion,
724 Kind::Context,
725 Kind::Operation,
726 Kind::Run,
727 Kind::Policy,
728 Kind::Analysis,
729 Kind::Invitation,
730 Kind::Grant,
731 Kind::DiscussionTurn,
732 Kind::Review,
733 Kind::Notification,
734 Kind::AttentionItem,
735 Kind::Member,
736 Kind::ApprovalGroup,
737 Kind::Session,
738 Kind::SignupInvitation,
739 Kind::TimelineEvent,
740 Kind::Artifact,
741 Kind::Mount,
742 Kind::SupportAccess,
743 Kind::DeviceRecord,
744 Kind::Delegation,
745 Kind::Recovery,
746 Kind::OwnerTransition,
747 Kind::Billing,
748 Kind::Evidence,
749 Kind::CheckAcknowledgement,
750 Kind::ProviderConnection,
751 Kind::RemoteLink,
752 ] {
753 for spool in [None, Some(spool)] {
754 mentions.push(Mention::Record {
755 spool,
756 record_kind,
757 id: "record".into(),
758 });
759 }
760 }
761 assert_eq!(mentions.len(), 64);
762 for original in mentions {
763 assert_eq!(
764 mention(&mention_ref(&original)).expect("typed mention"),
765 original
766 );
767 }
768 assert!(
769 mention(&EntityRef {
770 entity: Some(entity_ref::Entity::Device(EndpointRef {
771 kind: EndpointKind::Weft as i32,
772 public_key: vec![5; 32]
773 }))
774 })
775 .is_err()
776 );
777 }
778 #[test]
779 fn source_anchor_binds_owning_thread_and_preserves_lines() {
780 use heddle_object_model::object::{CollaborationAnchor as Anchor, CollaborationScope};
781 let scope = CollaborationScope {
782 spool: Uuid::from_u128(1),
783 thread: Some(ContentHash::from_bytes([2; 32])),
784 };
785 let thread = ThreadRef {
786 spool: wire_spool(scope.spool),
787 id: Some(ThreadId { value: vec![2; 32] }),
788 };
789 let mut source = SourceAnchor {
790 revision: Some(RevisionRef {
791 spool: wire_spool(scope.spool),
792 revision: Some(revision_ref::Revision::GitCommitOid("a".repeat(40))),
793 }),
794 path: "src/main.rs".into(),
795 symbol_id: "run".into(),
796 start_line: Some(12),
797 end_line: Some(18),
798 target: None,
799 thread: Some(thread),
800 path_kind: 0,
801 path_kind_source: 0,
802 };
803 let value = CollaborationAnchor {
804 target: Some(collaboration_anchor::Target::Source(source.clone())),
805 };
806 let Anchor::Source { source: actual } =
807 anchor(&value, &scope).expect("bound source anchor")
808 else {
809 panic!("source");
810 };
811 assert_eq!(
812 (
813 actual.path.as_str(),
814 actual.symbol_id.as_str(),
815 actual.start_line,
816 actual.end_line
817 ),
818 ("src/main.rs", "run", Some(12), Some(18))
819 );
820 assert_eq!(
821 anchor_ref(&Anchor::Source { source: actual }, &scope).expect("source projection"),
822 value,
823 "source fields survive both directions"
824 );
825 source
826 .thread
827 .as_mut()
828 .expect("Thread")
829 .id
830 .as_mut()
831 .expect("ID")
832 .value[0] ^= 1;
833 assert!(
834 anchor(
835 &CollaborationAnchor {
836 target: Some(collaboration_anchor::Target::Source(source))
837 },
838 &scope
839 )
840 .is_err(),
841 "source must not escape the Thread"
842 );
843 }
844 #[test]
845 fn set_source_path_kind_is_rejected_not_dropped() {
846 use heddle_object_model::object::CollaborationScope;
847 let scope = CollaborationScope {
848 spool: Uuid::from_u128(1),
849 thread: Some(ContentHash::from_bytes([2; 32])),
850 };
851 let source = SourceAnchor {
852 revision: Some(RevisionRef {
853 spool: wire_spool(scope.spool),
854 revision: Some(revision_ref::Revision::GitCommitOid("a".repeat(40))),
855 }),
856 path: "src/main.rs".into(),
857 thread: Some(ThreadRef {
858 spool: wire_spool(scope.spool),
859 id: Some(ThreadId { value: vec![2; 32] }),
860 }),
861 ..Default::default()
862 };
863 let convert = |source: &SourceAnchor| {
864 (
865 anchor(
866 &CollaborationAnchor {
867 target: Some(collaboration_anchor::Target::Source(source.clone())),
868 },
869 &scope,
870 ),
871 super::super::tags::annotation_source(&AnnotationSourceReference {
872 source: Some(source.clone()),
873 }),
874 )
875 };
876 let (native, tag) = convert(&source);
877 native.expect("an unset kind converts");
878 tag.expect("an unset kind converts in a source tag");
879 for (kind, provenance) in [
880 (SourcePathKind::File, SourcePathKindSource::Recorded),
881 (SourcePathKind::Directory, SourcePathKindSource::Derived),
882 (SourcePathKind::File, SourcePathKindSource::Unspecified),
883 (SourcePathKind::Unspecified, SourcePathKindSource::Recorded),
884 ] {
885 let set = SourceAnchor {
886 path_kind: kind as i32,
887 path_kind_source: provenance as i32,
888 ..source.clone()
889 };
890 let (native, tag) = convert(&set);
891 for result in [native.map(|_| ()), tag.map(|_| ())] {
892 assert!(
893 matches!(result, Err(Error::Protocol(message)) if message.contains("heddle#1959")),
894 "{kind:?}/{provenance:?} must be refused, not dropped"
895 );
896 }
897 }
898 }
899 #[test]
900 fn audience_mapping_is_explicit_and_lossless() {
901 use heddle_object_model::object::VisibilityTier as Tier;
902 for (kind, label) in [
903 (Audience::Public, ""),
904 (Audience::Members, ""),
905 (Audience::Private, "review-team"),
906 ] {
907 let tier = visibility(kind as i32, label).expect("valid audience");
908 assert_eq!(audience(&tier), Some((kind, label.into())));
909 }
910 for (kind, label) in [
911 (Audience::Unspecified, ""),
912 (Audience::Private, ""),
913 (Audience::Public, "review-team"),
914 (Audience::Members, "review-team"),
915 ] {
916 assert!(
917 visibility(kind as i32, label).is_err(),
918 "must not change audience or fabricate label"
919 );
920 }
921 assert!(
922 audience(&Tier::TeamScoped {
923 team_id: "team".into()
924 })
925 .is_none()
926 );
927 assert!(
928 audience(&Tier::Restricted {
929 scope_label: "restricted".into()
930 })
931 .is_none()
932 );
933 }
934}