77 createInFlightGuard ,
88 DuplicateInFlightError ,
99 normalizeSiteHost ,
10+ subscriptionHosts ,
1011 findCrossTypeDuplicates ,
11- crossTypePairsForHost ,
12+ crossTypeDuplicatesForAdded ,
1213} from './subscriptionDedup' ;
1314import type { Subscription } from '$lib/types' ;
1415
@@ -447,20 +448,54 @@ describe('findCrossTypeDuplicates', () => {
447448} ) ;
448449
449450/**
450- * crossTypePairsForHost is the add-time counterpart: the just-added sub may not
451- * have a resolved siteUrl yet (a fresh RSS row), so the host is passed in
452- * explicitly rather than read off the row.
451+ * subscriptionHosts is the single source of truth both the add-time check and
452+ * the /sources scan derive their hosts from.
453453 */
454- describe ( 'crossTypePairsForHost' , ( ) => {
455- it ( 'pairs a freshly-added RSS sub (no siteUrl) with an existing standard.site on the host' , ( ) => {
454+ describe ( 'subscriptionHosts' , ( ) => {
455+ it ( 'collects both the siteUrl host and the feedUrl host' , ( ) => {
456+ const s = sub ( {
457+ feedUrl : 'https://feeds.example.com/rss' ,
458+ siteUrl : 'https://www.example.com' ,
459+ } ) ;
460+ expect ( [ ...subscriptionHosts ( s ) ] . sort ( ) ) . toEqual ( [ 'example.com' , 'feeds.example.com' ] ) ;
461+ } ) ;
462+
463+ it ( 'falls back to the feedUrl host when siteUrl is unresolved' , ( ) => {
464+ const s = sub ( { feedUrl : 'https://blog.example.com/feed.xml' , siteUrl : undefined } ) ;
465+ expect ( [ ...subscriptionHosts ( s ) ] ) . toEqual ( [ 'blog.example.com' ] ) ;
466+ } ) ;
467+
468+ it ( 'ignores an at:// feedUrl (standard.site streams match on siteUrl only)' , ( ) => {
469+ const s = sub ( {
470+ sourceType : 'atproto.documents' ,
471+ feedUrl : 'at://did:plc:abc/site.standard.publication/blog' ,
472+ siteUrl : 'https://example.com' ,
473+ } ) ;
474+ expect ( [ ...subscriptionHosts ( s ) ] ) . toEqual ( [ 'example.com' ] ) ;
475+ } ) ;
476+ } ) ;
477+
478+ /**
479+ * crossTypeDuplicatesForAdded is the add-time counterpart of
480+ * findCrossTypeDuplicates. It shares subscriptionHosts and the same kind/host
481+ * matching, so the two can never disagree — the consistency tests below pin
482+ * that down on the exact case the old split logic got wrong.
483+ */
484+ describe ( 'crossTypeDuplicatesForAdded' , ( ) => {
485+ it ( 'pairs a freshly-added RSS sub (no siteUrl) with an existing standard.site via feedUrl host' , ( ) => {
456486 const std = sub ( {
457487 id : 1 ,
458488 sourceType : 'atproto.documents' ,
459489 subjectDid : 'did:plc:abc' ,
460490 siteUrl : 'https://blog.example.com' ,
461491 } ) ;
462- const addedRss = sub ( { id : 2 , sourceType : 'rss' , siteUrl : undefined } ) ;
463- const pairs = crossTypePairsForHost ( [ std , addedRss ] , addedRss , 'blog.example.com' ) ;
492+ const addedRss = sub ( {
493+ id : 2 ,
494+ sourceType : 'rss' ,
495+ feedUrl : 'https://blog.example.com/feed.xml' ,
496+ siteUrl : undefined ,
497+ } ) ;
498+ const pairs = crossTypeDuplicatesForAdded ( [ std , addedRss ] , addedRss ) ;
464499 expect ( pairs ) . toHaveLength ( 1 ) ;
465500 expect ( pairs [ 0 ] . rss . id ) . toBe ( 2 ) ;
466501 expect ( pairs [ 0 ] . standard . id ) . toBe ( 1 ) ;
@@ -475,7 +510,7 @@ describe('crossTypePairsForHost', () => {
475510 subjectDid : 'd' ,
476511 siteUrl : 'https://example.com' ,
477512 } ) ;
478- const pairs = crossTypePairsForHost ( [ rss , addedStd ] , addedStd , 'example.com' ) ;
513+ const pairs = crossTypeDuplicatesForAdded ( [ rss , addedStd ] , addedStd ) ;
479514 expect ( pairs ) . toHaveLength ( 1 ) ;
480515 expect ( pairs [ 0 ] . rss . id ) . toBe ( 1 ) ;
481516 expect ( pairs [ 0 ] . standard . id ) . toBe ( 2 ) ;
@@ -485,17 +520,52 @@ describe('crossTypePairsForHost', () => {
485520 const addedRss = sub ( { id : 1 , sourceType : 'rss' , siteUrl : 'https://example.com' } ) ;
486521 const otherRss = sub ( { id : 2 , sourceType : 'rss' , siteUrl : 'https://example.com' } ) ;
487522 // Only same-type rows on the host: no opposite type to pair with.
488- expect ( crossTypePairsForHost ( [ addedRss , otherRss ] , addedRss , 'example.com' ) ) . toEqual ( [ ] ) ;
523+ expect ( crossTypeDuplicatesForAdded ( [ addedRss , otherRss ] , addedRss ) ) . toEqual ( [ ] ) ;
489524 } ) ;
490525
491- it ( 'returns nothing when no existing sub shares the host' , ( ) => {
526+ it ( 'returns nothing when no existing sub shares a host' , ( ) => {
492527 const std = sub ( {
493528 id : 1 ,
494529 sourceType : 'atproto.documents' ,
495530 subjectDid : 'd' ,
496531 siteUrl : 'https://other.com' ,
497532 } ) ;
498- const addedRss = sub ( { id : 2 , sourceType : 'rss' , siteUrl : undefined } ) ;
499- expect ( crossTypePairsForHost ( [ std , addedRss ] , addedRss , 'example.com' ) ) . toEqual ( [ ] ) ;
533+ const addedRss = sub ( {
534+ id : 2 ,
535+ sourceType : 'rss' ,
536+ feedUrl : 'https://example.com/feed.xml' ,
537+ siteUrl : undefined ,
538+ } ) ;
539+ expect ( crossTypeDuplicatesForAdded ( [ std , addedRss ] , addedRss ) ) . toEqual ( [ ] ) ;
540+ } ) ;
541+
542+ // The regression that motivated unifying the two paths: an existing RSS feed
543+ // with no resolved siteUrl (only a feedUrl host) that a newly-added
544+ // standard.site duplicates. The old add-time check matched existing subs on
545+ // siteUrl alone, so it stayed silent while /sources later flagged the pair.
546+ it ( 'warns at add time on the same pair /sources would show (RSS has only a feedUrl host)' , ( ) => {
547+ const existingRss = sub ( {
548+ id : 1 ,
549+ sourceType : 'rss' ,
550+ feedUrl : 'https://blog.example.com/feed.xml' ,
551+ siteUrl : undefined ,
552+ } ) ;
553+ const addedStd = sub ( {
554+ id : 2 ,
555+ sourceType : 'atproto.documents' ,
556+ subjectDid : 'd' ,
557+ siteUrl : 'https://blog.example.com' ,
558+ } ) ;
559+ const all = [ existingRss , addedStd ] ;
560+
561+ const addTime = crossTypeDuplicatesForAdded ( all , addedStd ) ;
562+ const onSources = findCrossTypeDuplicates ( all ) ;
563+
564+ expect ( addTime ) . toHaveLength ( 1 ) ;
565+ expect ( onSources ) . toHaveLength ( 1 ) ;
566+ // Both paths agree: same RSS, same standard, same host.
567+ expect ( addTime [ 0 ] . rss . id ) . toBe ( onSources [ 0 ] . rss . id ) ;
568+ expect ( addTime [ 0 ] . standard . id ) . toBe ( onSources [ 0 ] . standard . id ) ;
569+ expect ( addTime [ 0 ] . host ) . toBe ( onSources [ 0 ] . host ) ;
500570 } ) ;
501571} ) ;
0 commit comments