@@ -146,6 +146,7 @@ vi.mock('./mcp', () => ({
146146import MudClient from './client' ;
147147import { GMCPClientFileTransfer } from './gmcp' ;
148148import { useOutputStore } from './stores/outputStore' ;
149+ import type { Stream } from './telnet' ;
149150
150151class MockWebSocket {
151152 static CONNECTING = 0 ;
@@ -212,6 +213,7 @@ describe('MudClient lifecycle cleanup', () => {
212213 mockFileTransferManagerInstances . length = 0 ;
213214 mockPreferenceListeners . clear ( ) ;
214215 mockPreferenceSubscribe . mockClear ( ) ;
216+ mockPreferencesState . general . localEcho = false ;
215217 mockPreferencesState . sound . muteInBackground = false ;
216218 mockWebSocketInstances . length = 0 ;
217219 useOutputStore . getState ( ) . reset ( ) ;
@@ -337,6 +339,86 @@ describe('MudClient lifecycle cleanup', () => {
337339 expect ( mcpPackage . reset ) . toHaveBeenCalledOnce ( ) ;
338340 } ) ;
339341
342+ it ( 'clears a closed local transport and rejects later sends' , ( ) => {
343+ const client = new MudClient ( 'example.test' , 443 ) ;
344+ const closeListeners : Array < ( ) => void > = [ ] ;
345+ const stream = {
346+ close : vi . fn ( ( ) => {
347+ closeListeners . forEach ( ( listener ) => {
348+ listener ( ) ;
349+ } ) ;
350+ } ) ,
351+ on : vi . fn ( ( event : string , callback : ( ) => void ) => {
352+ if ( event === 'close' ) closeListeners . push ( callback ) ;
353+ } ) ,
354+ write : vi . fn ( ) ,
355+ } as unknown as Stream & { close ( ) : void } ;
356+ client . connectLocal ( stream ) ;
357+
358+ client . send ( 'look\r\n' ) ;
359+ expect ( stream . write ) . toHaveBeenCalledOnce ( ) ;
360+
361+ client . close ( ) ;
362+
363+ expect ( stream . close ) . toHaveBeenCalledOnce ( ) ;
364+ expect ( client . connected ) . toBe ( false ) ;
365+ expect (
366+ client as unknown as { localMode : boolean ; localStream ?: Stream } ,
367+ ) . toMatchObject ( {
368+ localMode : false ,
369+ localStream : undefined ,
370+ } ) ;
371+ expect ( ( ) => client . send ( 'look\r\n' ) ) . toThrow (
372+ new Error ( 'Cannot send while disconnected' ) ,
373+ ) ;
374+ expect ( stream . write ) . toHaveBeenCalledOnce ( ) ;
375+ } ) ;
376+
377+ it . each ( [
378+ [ 'CONNECTING' , MockWebSocket . CONNECTING ] ,
379+ [ 'CLOSING' , MockWebSocket . CLOSING ] ,
380+ [ 'CLOSED' , MockWebSocket . CLOSED ] ,
381+ ] ) ( 'rejects sends while the WebSocket is %s' , ( _label , readyState ) => {
382+ const client = new MudClient ( 'example.test' , 443 ) ;
383+ client . connect ( ) ;
384+ const socket = mockWebSocketInstances [ 0 ] ;
385+ socket . onopen ?.( new Event ( 'open' ) ) ;
386+ socket . readyState = readyState ;
387+
388+ expect ( ( ) => client . send ( 'look\r\n' ) ) . toThrow (
389+ new Error ( 'Cannot send while disconnected' ) ,
390+ ) ;
391+ expect ( socket . send ) . not . toHaveBeenCalled ( ) ;
392+ client . close ( ) ;
393+ } ) ;
394+
395+ it ( 'sends through a connected open WebSocket' , ( ) => {
396+ const client = new MudClient ( 'example.test' , 443 ) ;
397+ client . connect ( ) ;
398+ const socket = mockWebSocketInstances [ 0 ] ;
399+ socket . onopen ?.( new Event ( 'open' ) ) ;
400+
401+ client . send ( 'look\r\n' ) ;
402+
403+ expect ( socket . send ) . toHaveBeenCalledWith ( 'look\r\n' ) ;
404+ } ) ;
405+
406+ it ( 'reports a disconnected command without adding local echo' , ( ) => {
407+ mockPreferencesState . general . localEcho = true ;
408+ const client = new MudClient ( 'example.test' , 443 ) ;
409+
410+ expect ( ( ) => client . sendCommand ( 'look' ) ) . not . toThrow ( ) ;
411+
412+ expect ( useOutputStore . getState ( ) . entries ) . toEqual ( [
413+ {
414+ id : 1 ,
415+ type : 'error' ,
416+ error : new Error ( 'Cannot send while disconnected' ) ,
417+ } ,
418+ ] ) ;
419+ expect ( mockWebSocketInstances ) . toEqual ( [ ] ) ;
420+ } ) ;
421+
340422 it ( 'buffers text split across frames until the line is complete' , ( ) => {
341423 const client = new MudClient ( 'example.test' , 443 ) ;
342424 client . connect ( ) ;
0 commit comments