@@ -19,6 +19,10 @@ import { resetCapabilitiesCache, setCapabilities } from '@moonshot-ai/pi-tui';
1919
2020import { registerWebCommand } from '#/cli/sub/web' ;
2121import type { LegacyKillDeps } from '#/cli/sub/web/legacy-kill' ;
22+ import type {
23+ RemoteControlCloseReason ,
24+ RemoteControlStatus ,
25+ } from '#/cli/sub/web/remote-control' ;
2226import type { WebCommandDeps } from '#/cli/sub/web/run' ;
2327import type { ParsedServerOptions } from '#/cli/sub/web/shared' ;
2428import { darkColors } from '#/tui/theme/colors' ;
@@ -464,6 +468,100 @@ describe('`kimi web` opens the browser', () => {
464468 ) . rejects . toThrow ( '--remote-control requires a loopback host.' ) ;
465469 } ) ;
466470
471+ it ( 'requests foreground shutdown when the relay takes the device offline' , async ( ) => {
472+ const { handleWebCommand } = await import ( '#/cli/sub/web/run' ) ;
473+ const home = mkdtempSync ( join ( tmpdir ( ) , 'kimi-web-rc-' ) ) ;
474+ vi . stubEnv ( 'KIMI_CODE_HOME' , home ) ;
475+ const { stdout, stderr, readStdout } = makeIo ( ) ;
476+ const requestShutdown = vi . fn ( ) ;
477+ const runner : ForegroundRunner = async ( _options , hooks ) => {
478+ await hooks ?. onReady ?.( 'http://127.0.0.1:58627' , { requestShutdown } ) ;
479+ return undefined as never ;
480+ } ;
481+ let emitStatus : ( ( status : RemoteControlStatus ) => void ) | undefined ;
482+ let resolveClosed : ( ( reason : RemoteControlCloseReason ) => void ) | undefined ;
483+ const startRemoteControl : WebCommandDeps [ 'startRemoteControl' ] = async ( options ) => {
484+ emitStatus = options . onStatus ;
485+ return {
486+ deviceId : 'device-1' ,
487+ deviceName : 'example-device' ,
488+ url : 'https://example.test/devices/device-1/?rc=1&from=kimi_code_cli' ,
489+ relayOrigin : 'https://example.test' ,
490+ closed : new Promise < RemoteControlCloseReason > ( ( resolve ) => {
491+ resolveClosed = resolve ;
492+ } ) ,
493+ close : async ( ) => { } ,
494+ } ;
495+ } ;
496+
497+ try {
498+ await handleWebCommand (
499+ { remoteControl : true , open : false } ,
500+ {
501+ startServerForeground : runner ,
502+ startRemoteControl,
503+ resolveToken : ( ) => 'tok' ,
504+ openUrl : vi . fn ( ) ,
505+ stdout,
506+ stderr,
507+ } ,
508+ ) ;
509+
510+ emitStatus ?.( 'relay_terminated' ) ;
511+ resolveClosed ?.( 'user_requested' ) ;
512+ await vi . waitFor ( ( ) => {
513+ expect ( requestShutdown ) . toHaveBeenCalledWith ( 'remote_control_user_requested' ) ;
514+ } ) ;
515+ expect ( readStdout ( ) ) . toContain ( 'offline' ) ;
516+ expect ( readStdout ( ) ) . not . toContain ( 'reconnecting' ) ;
517+ } finally {
518+ rmSync ( home , { recursive : true , force : true } ) ;
519+ }
520+ } ) ;
521+
522+ it ( 'does not request shutdown when the tunnel closes locally' , async ( ) => {
523+ const { handleWebCommand } = await import ( '#/cli/sub/web/run' ) ;
524+ const home = mkdtempSync ( join ( tmpdir ( ) , 'kimi-web-rc-' ) ) ;
525+ vi . stubEnv ( 'KIMI_CODE_HOME' , home ) ;
526+ const { stdout, stderr } = makeIo ( ) ;
527+ const requestShutdown = vi . fn ( ) ;
528+ const runner : ForegroundRunner = async ( _options , hooks ) => {
529+ await hooks ?. onReady ?.( 'http://127.0.0.1:58627' , { requestShutdown } ) ;
530+ return undefined as never ;
531+ } ;
532+ let resolveClosed : ( ( reason : RemoteControlCloseReason ) => void ) | undefined ;
533+ const startRemoteControl : WebCommandDeps [ 'startRemoteControl' ] = async ( ) => ( {
534+ deviceId : 'device-1' ,
535+ deviceName : 'example-device' ,
536+ url : 'https://example.test/devices/device-1/?rc=1&from=kimi_code_cli' ,
537+ relayOrigin : 'https://example.test' ,
538+ closed : new Promise < RemoteControlCloseReason > ( ( resolve ) => {
539+ resolveClosed = resolve ;
540+ } ) ,
541+ close : async ( ) => { } ,
542+ } ) ;
543+
544+ try {
545+ await handleWebCommand (
546+ { remoteControl : true , open : false } ,
547+ {
548+ startServerForeground : runner ,
549+ startRemoteControl,
550+ resolveToken : ( ) => 'tok' ,
551+ openUrl : vi . fn ( ) ,
552+ stdout,
553+ stderr,
554+ } ,
555+ ) ;
556+
557+ resolveClosed ?.( 'local_close' ) ;
558+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
559+ expect ( requestShutdown ) . not . toHaveBeenCalled ( ) ;
560+ } finally {
561+ rmSync ( home , { recursive : true , force : true } ) ;
562+ }
563+ } ) ;
564+
467565 it ( 'shows --remote-control in help' , ( ) => {
468566 const remoteControlOption = makeProgram ( )
469567 . commands . find ( ( command ) => command . name ( ) === 'web' ) !
0 commit comments