@@ -8,15 +8,31 @@ import {
88 initRemoteIdentity ,
99 initRemoteComms ,
1010 parseOcapURL ,
11+ MAX_URL_RELAY_HINTS ,
12+ MAX_KNOWN_RELAYS ,
1113} from './remote-comms.ts' ;
1214import { createMockRemotesFactory } from '../../../test/remotes-mocks.ts' ;
1315import { makeMapKernelDatabase } from '../../../test/storage.ts' ;
14- import type { KernelStore } from '../../store/index.ts' ;
16+ import type { KernelStore , RelayEntry } from '../../store/index.ts' ;
1517import { makeKernelStore } from '../../store/index.ts' ;
1618import type { KRef , PlatformServices } from '../../types.ts' ;
1719import { mnemonicToSeed } from '../../utils/bip39.ts' ;
1820import type { RemoteMessageHandler } from '../types.ts' ;
1921
22+ /**
23+ * Build learned (non-bootstrap) relay entries from address strings.
24+ *
25+ * @param addrs - Relay multiaddr strings.
26+ * @param lastSeen - Epoch ms for lastSeen (default 100).
27+ * @returns Relay entries with isBootstrap: false.
28+ */
29+ function makeLearnedRelayEntries (
30+ addrs : string [ ] ,
31+ lastSeen = 100 ,
32+ ) : RelayEntry [ ] {
33+ return addrs . map ( ( addr ) => ( { addr, lastSeen, isBootstrap : false } ) ) ;
34+ }
35+
2036describe ( 'remote-comms' , ( ) => {
2137 let mockKernelStore : KernelStore ;
2238 let mockPlatformServices : PlatformServices ;
@@ -80,11 +96,11 @@ describe('remote-comms', () => {
8096 expect ( remoteComms . getPeerId ( ) ) . toBe ( peerId ) ;
8197
8298 const ocapURL = await remoteComms . issueOcapURL ( 'ko1' as KRef ) ;
83- const { oid } = parseOcapURL ( ocapURL ) ;
84- const knownRelays = mockKernelStore . getKnownRelays ( ) ;
85- expect ( Array . isArray ( knownRelays ) ) . toBe ( true ) ;
86- expect ( knownRelays . length ) . toBeGreaterThan ( 0 ) ;
87- expect ( knownRelays ) . toStrictEqual ( testRelays ) ;
99+ const { oid, hints } = parseOcapURL ( ocapURL ) ;
100+ const knownRelayAddresses = mockKernelStore . getKnownRelayAddresses ( ) ;
101+ expect ( knownRelayAddresses ) . toStrictEqual ( testRelays ) ;
102+ // URL should embed the relays (within MAX_URL_RELAY_HINTS cap)
103+ expect ( hints ) . toStrictEqual ( testRelays ) ;
88104 const referenceURL = `ocap:${ oid } @${ peerId } ,${ testRelays . join ( ',' ) } ` ;
89105 expect ( ocapURL ) . toBe ( referenceURL ) ;
90106
@@ -157,12 +173,12 @@ describe('remote-comms', () => {
157173 ) ;
158174 } ) ;
159175
160- it ( 'uses getKnownRelays when options.relays is empty' , async ( ) => {
176+ it ( 'uses stored relays when options.relays is empty' , async ( ) => {
161177 const storedRelays = [
162178 '/dns4/stored-relay1.example/tcp/443/wss/p2p/relay1' ,
163179 '/dns4/stored-relay2.example/tcp/443/wss/p2p/relay2' ,
164180 ] ;
165- mockKernelStore . setKnownRelays ( storedRelays ) ;
181+ mockKernelStore . setRelayEntries ( makeLearnedRelayEntries ( storedRelays ) ) ;
166182 await initRemoteComms (
167183 mockKernelStore ,
168184 mockPlatformServices ,
@@ -339,20 +355,24 @@ describe('remote-comms', () => {
339355 mockRemoteMessageHandler ,
340356 { relays : testRelays } ,
341357 ) ;
342- expect ( mockKernelStore . getKnownRelays ( ) ) . toStrictEqual ( testRelays ) ;
358+ expect ( mockKernelStore . getKnownRelayAddresses ( ) ) . toStrictEqual (
359+ testRelays ,
360+ ) ;
343361 } ) ;
344362
345363 it ( 'does not save relays to KV store when empty' , async ( ) => {
346364 const storedRelays = [ '/dns4/stored-relay.example/tcp/443/wss/p2p/relay' ] ;
347- mockKernelStore . setKnownRelays ( storedRelays ) ;
365+ mockKernelStore . setRelayEntries ( makeLearnedRelayEntries ( storedRelays ) ) ;
348366 await initRemoteComms (
349367 mockKernelStore ,
350368 mockPlatformServices ,
351369 mockRemoteMessageHandler ,
352370 { } , // empty relays
353371 ) ;
354372 // Should not overwrite existing relays
355- expect ( mockKernelStore . getKnownRelays ( ) ) . toStrictEqual ( storedRelays ) ;
373+ expect ( mockKernelStore . getKnownRelayAddresses ( ) ) . toStrictEqual (
374+ storedRelays ,
375+ ) ;
356376 } ) ;
357377 } ) ;
358378
@@ -444,7 +464,7 @@ describe('remote-comms', () => {
444464 '/dns4/relay1.example/tcp/443/wss/p2p-circuit' , // duplicate
445465 ] ) ;
446466
447- const stored = mockKernelStore . getKnownRelays ( ) ;
467+ const stored = mockKernelStore . getKnownRelayAddresses ( ) ;
448468 expect ( stored ) . toStrictEqual ( [
449469 '/dns4/relay1.example/tcp/443/wss/p2p-circuit' ,
450470 '/dns4/relay2.example/tcp/443/wss/p2p-circuit' ,
@@ -475,10 +495,132 @@ describe('remote-comms', () => {
475495
476496 identity . addKnownRelays ( [ ] ) ;
477497
478- const stored = mockKernelStore . getKnownRelays ( ) ;
498+ const stored = mockKernelStore . getKnownRelayAddresses ( ) ;
479499 expect ( stored ) . toStrictEqual ( initialRelays ) ;
480500 } ) ;
481501
502+ it ( 'issueOcapURL caps relay hints to MAX_URL_RELAY_HINTS' , async ( ) => {
503+ const relays = Array . from (
504+ { length : MAX_URL_RELAY_HINTS + 3 } ,
505+ ( _ , i ) => `/dns4/relay${ i } .example/tcp/443/wss/p2p-circuit` ,
506+ ) ;
507+ const { identity } = await initRemoteIdentity ( mockKernelStore , {
508+ relays,
509+ } ) ;
510+
511+ const ocapURL = await identity . issueOcapURL ( 'ko1' as KRef ) ;
512+ const { hints } = parseOcapURL ( ocapURL ) ;
513+ expect ( hints ) . toHaveLength ( MAX_URL_RELAY_HINTS ) ;
514+ } ) ;
515+
516+ it ( 'issueOcapURL prefers bootstrap relays over learned relays' , async ( ) => {
517+ const bootstrapRelays = [
518+ '/dns4/bootstrap1.example/tcp/443/wss/p2p-circuit' ,
519+ '/dns4/bootstrap2.example/tcp/443/wss/p2p-circuit' ,
520+ ] ;
521+ const { identity } = await initRemoteIdentity ( mockKernelStore , {
522+ relays : bootstrapRelays ,
523+ } ) ;
524+
525+ // Add many learned relays (more recent lastSeen)
526+ const learnedRelays = Array . from (
527+ { length : 5 } ,
528+ ( _ , i ) => `/dns4/learned${ i } .example/tcp/443/wss/p2p-circuit` ,
529+ ) ;
530+ identity . addKnownRelays ( learnedRelays ) ;
531+
532+ const ocapURL = await identity . issueOcapURL ( 'ko1' as KRef ) ;
533+ const { hints } = parseOcapURL ( ocapURL ) ;
534+ expect ( hints ) . toHaveLength ( MAX_URL_RELAY_HINTS ) ;
535+ // Bootstrap relays should be included ahead of learned relays
536+ for ( const bootstrap of bootstrapRelays ) {
537+ expect ( hints ) . toContain ( bootstrap ) ;
538+ }
539+ } ) ;
540+
541+ it ( 'addKnownRelays enforces MAX_KNOWN_RELAYS pool cap' , async ( ) => {
542+ const { identity } = await initRemoteIdentity ( mockKernelStore ) ;
543+
544+ // Add more relays than the cap
545+ const relays = Array . from (
546+ { length : MAX_KNOWN_RELAYS + 5 } ,
547+ ( _ , i ) => `/dns4/relay${ i } .example/tcp/443/wss/p2p-circuit` ,
548+ ) ;
549+ identity . addKnownRelays ( relays ) ;
550+
551+ const entries = mockKernelStore . getRelayEntries ( ) ;
552+ expect ( entries ) . toHaveLength ( MAX_KNOWN_RELAYS ) ;
553+ } ) ;
554+
555+ it ( 'addKnownRelays evicts oldest non-bootstrap relays when pool is full' , async ( ) => {
556+ const bootstrapRelays = [
557+ '/dns4/bootstrap.example/tcp/443/wss/p2p-circuit' ,
558+ ] ;
559+ const { identity } = await initRemoteIdentity ( mockKernelStore , {
560+ relays : bootstrapRelays ,
561+ } ) ;
562+
563+ // Fill pool to the cap
564+ const fillerRelays = Array . from (
565+ { length : MAX_KNOWN_RELAYS } ,
566+ ( _ , i ) => `/dns4/relay${ i } .example/tcp/443/wss/p2p-circuit` ,
567+ ) ;
568+ identity . addKnownRelays ( fillerRelays ) ;
569+
570+ const entries = mockKernelStore . getRelayEntries ( ) ;
571+ expect ( entries ) . toHaveLength ( MAX_KNOWN_RELAYS ) ;
572+ // Bootstrap relay must survive eviction
573+ expect ( entries . some ( ( entry ) => entry . addr === bootstrapRelays [ 0 ] ) ) . toBe (
574+ true ,
575+ ) ;
576+ } ) ;
577+
578+ it ( 'addKnownRelays updates lastSeen on re-observed relays' , async ( ) => {
579+ const { identity } = await initRemoteIdentity ( mockKernelStore ) ;
580+
581+ identity . addKnownRelays ( [ '/dns4/relay.example/tcp/443/wss/p2p-circuit' ] ) ;
582+ const firstSeen = mockKernelStore . getRelayEntries ( ) [ 0 ] ?. lastSeen ?? 0 ;
583+
584+ // SES lockdown prevents mocking Date.now; use a real delay instead
585+ await new Promise ( ( resolve ) => {
586+ setTimeout ( resolve , 50 ) ;
587+ } ) ;
588+ identity . addKnownRelays ( [ '/dns4/relay.example/tcp/443/wss/p2p-circuit' ] ) ;
589+ const secondSeen = mockKernelStore . getRelayEntries ( ) [ 0 ] ?. lastSeen ?? 0 ;
590+
591+ expect ( secondSeen ) . toBeGreaterThan ( firstSeen ) ;
592+ } ) ;
593+
594+ it ( 'init marks bootstrap relays and preserves learned relays' , async ( ) => {
595+ // Pre-seed a learned relay
596+ mockKernelStore . setRelayEntries ( [
597+ {
598+ addr : '/dns4/learned.example/tcp/443/wss/p2p-circuit' ,
599+ lastSeen : 100 ,
600+ isBootstrap : false ,
601+ } ,
602+ ] ) ;
603+
604+ const bootstrapRelays = [
605+ '/dns4/bootstrap.example/tcp/443/wss/p2p-circuit' ,
606+ ] ;
607+ await initRemoteIdentity ( mockKernelStore , {
608+ relays : bootstrapRelays ,
609+ } ) ;
610+
611+ const entries = mockKernelStore . getRelayEntries ( ) ;
612+ expect ( entries ) . toHaveLength ( 2 ) ;
613+ expect (
614+ entries . find ( ( entry ) => entry . addr === bootstrapRelays [ 0 ] ) ?. isBootstrap ,
615+ ) . toBe ( true ) ;
616+ expect (
617+ entries . find (
618+ ( entry ) =>
619+ entry . addr === '/dns4/learned.example/tcp/443/wss/p2p-circuit' ,
620+ ) ?. isBootstrap ,
621+ ) . toBe ( false ) ;
622+ } ) ;
623+
482624 it ( 'throws with mnemonic when identity already exists' , async ( ) => {
483625 mockKernelStore . setRemoteIdentityValue ( 'peerId' , 'existing-peer-id' ) ;
484626 mockKernelStore . setRemoteIdentityValue (
@@ -690,7 +832,7 @@ describe('remote-comms', () => {
690832 '/dns4/stored-relay1.example/tcp/443/wss/p2p/relay1' ,
691833 '/dns4/stored-relay2.example/tcp/443/wss/p2p/relay2' ,
692834 ] ;
693- mockKernelStore . setKnownRelays ( storedRelays ) ;
835+ mockKernelStore . setRelayEntries ( makeLearnedRelayEntries ( storedRelays ) ) ;
694836 const remoteComms = await initRemoteComms (
695837 mockKernelStore ,
696838 mockPlatformServices ,
0 commit comments