2222 */
2323
2424import $ , { type CommandChild } from "@david/dax" ;
25+ import { openTunnel , type Tunnel } from "@hongminhee/localtunnel" ;
2526import { configure , getConsoleSink , getLogger } from "@logtape/logtape" ;
2627import { fromFileUrl , join } from "@std/path" ;
2728
@@ -283,6 +284,11 @@ const SKIPPED_EXAMPLES: SkippedExample[] = [
283284 reason :
284285 "No actor dispatcher configured; federation lookup cannot be verified" ,
285286 } ,
287+ {
288+ name : "rfc-9421-test" ,
289+ reason :
290+ "Requires live interaction with external fediverse servers (Bonfire, Mastodon)" ,
291+ } ,
286292] ;
287293
288294// ─── ANSI Colors ──────────────────────────────────────────────────────────────
@@ -395,70 +401,22 @@ function forceKillChild(child: CommandChild): void {
395401// ─── Tunnel ───────────────────────────────────────────────────────────────────
396402
397403/**
398- * Starts `fedify tunnel -s pinggy.io <port>` and waits up to `timeoutMs`
399- * for the tunnel URL to appear in its output. The tunnel process is kept
400- * alive and returned to the caller; it must be killed when no longer needed.
401- *
402- * Returns `null` if the URL was not found before the timeout.
404+ * Opens a tunnel via `@hongminhee/localtunnel` (pinggy.io) to expose
405+ * a local port. Returns the {@link Tunnel} object or `null` on failure.
403406 */
404- async function startTunnel (
405- port : number ,
406- timeoutMs : number ,
407- ) : Promise < { child : CommandChild ; url : string } | null > {
407+ async function startTunnel ( port : number ) : Promise < Tunnel | null > {
408408 const tunnelLogger = getLogger ( [ "fedify" , "examples" , "tunnel" ] ) ;
409- tunnelLogger . info ( "Opening localhost.run tunnel on port {port}" , { port } ) ;
410-
411- const child = $ `deno task cli tunnel -s pinggy.io ${ String ( port ) } `
412- . cwd ( REPO_ROOT )
413- . stdout ( "piped" )
414- . stderr ( "piped" )
415- . noThrow ( )
416- . spawn ( ) ;
417-
418- // Accumulate text from both streams while logging each chunk at DEBUG.
419- const textChunks : string [ ] = [ ] ;
420- const decoder = new TextDecoder ( ) ;
421-
422- const readStream = ( stream : ReadableStream < Uint8Array > ) => {
423- ( async ( ) => {
424- const reader = stream . getReader ( ) ;
425- try {
426- while ( true ) {
427- const { done, value } = await reader . read ( ) ;
428- if ( done ) break ;
429- const text = decoder . decode ( value , { stream : true } ) ;
430- textChunks . push ( text ) ;
431- const trimmed = text . trim ( ) ;
432- if ( trimmed ) tunnelLogger . debug ( "{output}" , { output : trimmed } ) ;
433- }
434- } catch {
435- // Stream may error when the process is killed.
436- }
437- } ) ( ) ;
438- } ;
439-
440- readStream ( child . stdout ( ) ) ;
441- readStream ( child . stderr ( ) ) ;
442-
443- // Poll until we find an https URL in the accumulated output.
444- // The `message` template tag from @optique/run may wrap the URL in double
445- // quotes in non-TTY output, so we stop matching at whitespace or quotes.
446- const deadline = Date . now ( ) + timeoutMs ;
447- while ( Date . now ( ) < deadline ) {
448- const match = textChunks . join ( "" ) . match ( / h t t p s : \/ \/ [ ^ \s " ' ] + / ) ;
449- if ( match ) {
450- tunnelLogger . info ( "Tunnel established at {url}" , { url : match [ 0 ] } ) ;
451- return { child, url : match [ 0 ] } ;
452- }
453- await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
409+ tunnelLogger . info ( "Opening tunnel on port {port}" , { port } ) ;
410+ try {
411+ const tunnel = await openTunnel ( { port, service : "pinggy.io" } ) ;
412+ tunnelLogger . info ( "Tunnel established at {url}" , {
413+ url : tunnel . url . href ,
414+ } ) ;
415+ return tunnel ;
416+ } catch ( error ) {
417+ tunnelLogger . error ( "Failed to open tunnel: {error}" , { error } ) ;
418+ return null ;
454419 }
455-
456- tunnelLogger . error (
457- "Tunnel did not produce a URL within {timeout} ms" ,
458- { timeout : timeoutMs } ,
459- ) ;
460- forceKillChild ( child ) ;
461- return null ;
462420}
463421
464422// ─── Test Runners ─────────────────────────────────────────────────────────────
@@ -549,7 +507,7 @@ async function testServerExample(
549507 const collectServerOutput = ( ) =>
550508 stdoutChunks . join ( "" ) + stderrChunks . join ( "" ) ;
551509
552- let tunnelChild : CommandChild | null = null ;
510+ let activeTunnel : Tunnel | null = null ;
553511
554512 try {
555513 console . log (
@@ -572,18 +530,18 @@ async function testServerExample(
572530 } ) ;
573531 console . log ( c . dim ( ` server ready — opening tunnel on port ${ port } …` ) ) ;
574532
575- const tunnel = await startTunnel ( port , 30_000 ) ;
533+ const tunnel = await startTunnel ( port ) ;
576534 if ( tunnel == null ) {
577- const error = "fedify tunnel did not produce a URL within 30s " ;
535+ const error = "Failed to open tunnel " ;
578536 serverLogger . error ( "{error}" , { error } ) ;
579537 return { name, status : "fail" , error, output : collectServerOutput ( ) } ;
580538 }
581539
582- tunnelChild = tunnel . child ;
583- const tunnelHostname = new URL ( tunnel . url ) . hostname ;
540+ activeTunnel = tunnel ;
541+ const tunnelHostname = tunnel . url . hostname ;
584542 const handle = `@${ actor } @${ tunnelHostname } ` ;
585543
586- console . log ( c . dim ( ` tunnel URL : ${ tunnel . url } ` ) ) ;
544+ console . log ( c . dim ( ` tunnel URL : ${ tunnel . url . href } ` ) ) ;
587545 console . log ( c . dim ( ` running : fedify lookup ${ handle } -d` ) ) ;
588546 serverLogger . info ( "Running fedify lookup {handle}" , { handle } ) ;
589547
@@ -606,10 +564,10 @@ async function testServerExample(
606564 serverLogger . error ( "{error}" , { error } ) ;
607565 return { name, status : "fail" , error, output : lookupOutput } ;
608566 } finally {
609- // Force-kill tunnel first (it holds a connection to the server).
610- if ( tunnelChild != null ) {
611- serverLogger . debug ( "Force-killing tunnel process " ) ;
612- forceKillChild ( tunnelChild ) ;
567+ // Close tunnel first (it holds a connection to the server).
568+ if ( activeTunnel != null ) {
569+ serverLogger . debug ( "Closing tunnel" ) ;
570+ await activeTunnel . close ( ) ;
613571 }
614572 serverLogger . debug ( "Force-killing server process" ) ;
615573 forceKillChild ( serverChild ) ;
0 commit comments