@@ -428,4 +428,169 @@ describe('[CTCT] Contact class', function () {
428428 assert . equal ( c . collectorClients . length , 1 ) ;
429429 } ) ;
430430 } ) ;
431+
432+ // ---- Plan 59 Phase 5a — CMC aggregator ---- //
433+
434+ describe ( '[CTCM] CMC Contact aggregator' , function ( ) {
435+ const SCOPE = ':_cmc:apps:hds-patient' ;
436+
437+ function counterpartyAccess ( overrides = { } ) {
438+ const cp = {
439+ username : 'drandy' ,
440+ host : 'demo.datasafe.dev' ,
441+ apiEndpoint : 'https://abctoken@drandy.demo.datasafe.dev/' ,
442+ remoteChatStreamId : ':_cmc:apps:hds-collector:foo:chats:pthdstest--demo-datasafe-dev' ,
443+ remoteCollectorStreamId : ':_cmc:apps:hds-collector:foo:collectors:pthdstest--demo-datasafe-dev' ,
444+ ...( overrides . counterpartyOverrides || { } )
445+ } ;
446+ const cmc = {
447+ role : 'counterparty' ,
448+ appCode : 'hds-collector' ,
449+ features : { chat : true , systemMessaging : true } ,
450+ counterparty : cp ,
451+ ...( overrides . cmcOverrides || { } )
452+ } ;
453+ return {
454+ id : overrides . id || 'acc-cp-1' ,
455+ apiEndpoint : 'irrelevant' ,
456+ permissions : overrides . permissions ?? [ { streamId : 'health' , level : 'read' } ] ,
457+ deleted : overrides . deleted ?? null ,
458+ clientData : { cmc }
459+ } ;
460+ }
461+
462+ function acceptEvent ( overrides = { } ) {
463+ return {
464+ acceptEventId : overrides . acceptEventId || 'evt-accept-1' ,
465+ counterparty : overrides . counterparty || { username : 'drandy' , host : 'demo.datasafe.dev' } ,
466+ appCode : overrides . appCode || 'hds-collector' ,
467+ scopeStreamId : SCOPE ,
468+ acceptedAt : overrides . acceptedAt ?? 1716000000 ,
469+ features : overrides . features || { chat : true , systemMessaging : true } ,
470+ backChannelAccessId : overrides . backChannelAccessId || 'acc-cp-1'
471+ } ;
472+ }
473+
474+ it ( '[CTM1] cmcDetectKind splits person vs service by hds-bridge- prefix' , ( ) => {
475+ assert . equal ( Contact . cmcDetectKind ( 'hds-collector' ) , 'person' ) ;
476+ assert . equal ( Contact . cmcDetectKind ( 'hds-patient' ) , 'person' ) ;
477+ assert . equal ( Contact . cmcDetectKind ( 'hds-bridge-mira' ) , 'service' ) ;
478+ assert . equal ( Contact . cmcDetectKind ( 'hds-bridge-athenahealth' ) , 'service' ) ;
479+ assert . equal ( Contact . cmcDetectKind ( null ) , 'unknown' ) ;
480+ assert . equal ( Contact . cmcDetectKind ( undefined ) , 'unknown' ) ;
481+ assert . equal ( Contact . cmcDetectKind ( '' ) , 'unknown' ) ;
482+ } ) ;
483+
484+ it ( '[CTM2] aggregateCmc returns empty when no counterparty accesses' , ( ) => {
485+ const out = Contact . aggregateCmc ( [ ] , [ ] , SCOPE ) ;
486+ assert . deepEqual ( out , [ ] ) ;
487+ } ) ;
488+
489+ it ( '[CTM3] aggregateCmc skips deleted accesses' , ( ) => {
490+ const a = counterpartyAccess ( { deleted : { reason : 'revoked' } } ) ;
491+ const out = Contact . aggregateCmc ( [ a ] , [ acceptEvent ( ) ] , SCOPE ) ;
492+ assert . deepEqual ( out , [ ] ) ;
493+ } ) ;
494+
495+ it ( '[CTM4] aggregateCmc skips accesses without cmc.role === counterparty' , ( ) => {
496+ const a = counterpartyAccess ( { cmcOverrides : { role : 'requester' } } ) ;
497+ const out = Contact . aggregateCmc ( [ a ] , [ ] , SCOPE ) ;
498+ assert . deepEqual ( out , [ ] ) ;
499+ } ) ;
500+
501+ it ( '[CTM5] aggregateCmc builds one Contact + one relationship from one access' , ( ) => {
502+ const a = counterpartyAccess ( ) ;
503+ const out = Contact . aggregateCmc ( [ a ] , [ acceptEvent ( ) ] , SCOPE ) ;
504+ assert . equal ( out . length , 1 ) ;
505+ const c = out [ 0 ] ;
506+ assert . equal ( c . counterparty . username , 'drandy' ) ;
507+ assert . equal ( c . counterparty . host , 'demo.datasafe.dev' ) ;
508+ assert . equal ( c . kind , 'person' ) ;
509+ assert . equal ( c . cmcRelationships . length , 1 ) ;
510+ const rel = c . cmcRelationships [ 0 ] ;
511+ assert . equal ( rel . accessId , 'acc-cp-1' ) ;
512+ assert . equal ( rel . acceptEventId , 'evt-accept-1' ) ;
513+ assert . equal ( rel . counterpartyApiEndpoint , 'https://abctoken@drandy.demo.datasafe.dev/' ) ;
514+ assert . equal ( rel . appCode , 'hds-collector' ) ;
515+ assert . deepEqual ( rel . features , { chat : true , systemMessaging : true } ) ;
516+ assert . equal ( rel . acceptedAt , 1716000000 ) ;
517+ // Local chat stream uses cmc.counterpartySlug (lowercase + host dots → hyphens, port stripped)
518+ assert . equal ( rel . localChatStreamId , ':_cmc:apps:hds-patient:chats:drandy--demo-datasafe-dev' ) ;
519+ } ) ;
520+
521+ it ( '[CTM6] aggregateCmc groups multiple accesses from the same counterparty into one Contact' , ( ) => {
522+ const a1 = counterpartyAccess ( { id : 'acc-cp-1' } ) ;
523+ const a2 = counterpartyAccess ( { id : 'acc-cp-2' , permissions : [ { streamId : 'sleep' , level : 'read' } ] } ) ;
524+ const out = Contact . aggregateCmc ( [ a1 , a2 ] , [ acceptEvent ( ) ] , SCOPE ) ;
525+ assert . equal ( out . length , 1 ) ;
526+ assert . equal ( out [ 0 ] . cmcRelationships . length , 2 ) ;
527+ } ) ;
528+
529+ it ( '[CTM7] aggregateCmc puts different counterparties into different Contacts' , ( ) => {
530+ const a1 = counterpartyAccess ( { id : 'acc-cp-1' } ) ;
531+ const a2 = counterpartyAccess ( {
532+ id : 'acc-cp-2' ,
533+ counterpartyOverrides : { username : 'drother' , host : 'demo.datasafe.dev' }
534+ } ) ;
535+ const out = Contact . aggregateCmc ( [ a1 , a2 ] , [ ] , SCOPE ) ;
536+ assert . equal ( out . length , 2 ) ;
537+ } ) ;
538+
539+ it ( '[CTM8] aggregateCmc marks bridge contacts as kind=service' , ( ) => {
540+ const a = counterpartyAccess ( {
541+ cmcOverrides : { appCode : 'hds-bridge-mira' , features : { chat : false } } ,
542+ counterpartyOverrides : { username : 'bridgemiratest' , host : 'demo.datasafe.dev' }
543+ } ) ;
544+ const out = Contact . aggregateCmc ( [ a ] , [ ] , SCOPE ) ;
545+ assert . equal ( out . length , 1 ) ;
546+ assert . equal ( out [ 0 ] . kind , 'service' ) ;
547+ assert . equal ( out [ 0 ] . counterparty . username , 'bridgemiratest' ) ;
548+ } ) ;
549+
550+ it ( '[CTM9] aggregateCmc tolerates missing accept events (acceptedAt: null)' , ( ) => {
551+ const a = counterpartyAccess ( ) ;
552+ const out = Contact . aggregateCmc ( [ a ] , [ ] , SCOPE ) ;
553+ assert . equal ( out . length , 1 ) ;
554+ assert . equal ( out [ 0 ] . cmcRelationships [ 0 ] . acceptedAt , null ) ;
555+ assert . equal ( out [ 0 ] . cmcRelationships [ 0 ] . acceptEventId , null ) ;
556+ } ) ;
557+
558+ it ( '[CTMA] aggregateCmc drops ghost accept events (Q-C2): accepts without matching access' , ( ) => {
559+ // Doctor revoked → patient access deleted, but the accept event still exists.
560+ // aggregator must not create a Contact from the ghost accept event.
561+ const out = Contact . aggregateCmc ( [ ] , [ acceptEvent ( ) ] , SCOPE ) ;
562+ assert . deepEqual ( out , [ ] ) ;
563+ } ) ;
564+
565+ it ( '[CTMB] cmcAllPermissions dedupes across relationships' , ( ) => {
566+ const a1 = counterpartyAccess ( {
567+ id : 'a1' ,
568+ permissions : [ { streamId : 'health' , level : 'read' } , { streamId : 'sleep' , level : 'read' } ]
569+ } ) ;
570+ const a2 = counterpartyAccess ( {
571+ id : 'a2' ,
572+ permissions : [ { streamId : 'sleep' , level : 'read' } , { streamId : 'mood' , level : 'contribute' } ]
573+ } ) ;
574+ const out = Contact . aggregateCmc ( [ a1 , a2 ] , [ ] , SCOPE ) ;
575+ const perms = out [ 0 ] . cmcAllPermissions ;
576+ assert . equal ( perms . length , 3 ) ;
577+ assert . deepEqual ( perms . map ( p => p . streamId ) . sort ( ) , [ 'health' , 'mood' , 'sleep' ] ) ;
578+ } ) ;
579+
580+ it ( '[CTMC] cmcChatStreams only includes chat-enabled relationships' , ( ) => {
581+ const a1 = counterpartyAccess ( {
582+ id : 'a1' ,
583+ cmcOverrides : { role : 'counterparty' , appCode : 'hds-collector' , features : { chat : true , systemMessaging : false } , counterparty : { username : 'drandy' , host : 'demo.datasafe.dev' , apiEndpoint : 'https://t@drandy.x/' , remoteChatStreamId : 'r1' , remoteCollectorStreamId : 'c1' } }
584+ } ) ;
585+ const a2 = counterpartyAccess ( {
586+ id : 'a2' ,
587+ cmcOverrides : { role : 'counterparty' , appCode : 'hds-collector' , features : { chat : false } , counterparty : { username : 'drandy' , host : 'demo.datasafe.dev' , apiEndpoint : 'https://t@drandy.x/' , remoteChatStreamId : null , remoteCollectorStreamId : null } }
588+ } ) ;
589+ const out = Contact . aggregateCmc ( [ a1 , a2 ] , [ ] , SCOPE ) ;
590+ const streams = out [ 0 ] . cmcChatStreams ;
591+ assert . equal ( streams . length , 1 ) ;
592+ assert . equal ( streams [ 0 ] . read , 'r1' ) ;
593+ assert . equal ( streams [ 0 ] . accessId , 'a1' ) ;
594+ } ) ;
595+ } ) ;
431596} ) ;
0 commit comments