@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import type WebSocket from 'ws' ;
import type WebSocket from 'ws' ;
import type { ClientEvent , ServerEvent } from '../../core/protocol.js' ;
import type { ClientEvent , ServerEvent } from '../../core/protocol.js' ;
import type { ToolDefinition } from '../../core/tools.js' ;
import type { ToolDefinition } from '../../core/tools.js' ;
import type { AnalyticsRecorder } from './analytics.js' ;
import { SqliteAnalyticsRecorder , type AnalyticsRecorder } from './analytics.js' ;
import type { ProviderSession , RealtimeProvider } from './provider.js' ;
import type { ProviderSession , RealtimeProvider } from './provider.js' ;
import { createRealtimeSession , resolveSessionConfig } from './session.js' ;
import { createRealtimeSession , resolveSessionConfig } from './session.js' ;
import { createToolRegistry } from './tools.js' ;
import { createToolRegistry } from './tools.js' ;
@ -36,7 +36,7 @@ class FakeBrowserSocket extends EventEmitter {
return this . sentRaw . map ( ( raw ) = > JSON . parse ( raw ) ) ;
return this . sentRaw . map ( ( raw ) = > JSON . parse ( raw ) ) ;
}
}
receive ( event : ClientEvent ) {
receive ( event : unknown ) {
this . emit ( 'message' , Buffer . from ( JSON . stringify ( event ) ) , false ) ;
this . emit ( 'message' , Buffer . from ( JSON . stringify ( event ) ) , false ) ;
}
}
}
}
@ -210,6 +210,83 @@ describe('RealtimeSession', () => {
) ;
) ;
} ) ;
} ) ;
it ( 'rejects input_text.send without text before it can violate SQLite constraints' , async ( ) = > {
const provider = new FakeProvider ( ) ;
const recorder = new SqliteAnalyticsRecorder ( ':memory:' , {
messages : true ,
toolCalls : true ,
events : true
} ) ;
const { socket , asWs } = makeSocket ( ) ;
createRealtimeSession ( asWs , provider , DEFAULTS , recorder , TEST_TOOL_TIMEOUT_MS ) ;
socket . receive ( { type : 'session.start' } ) ;
await vi . waitFor ( ( ) = > expect ( provider . sessions ) . toHaveLength ( 1 ) ) ;
socket . receive ( { type : 'input_text.send' } ) ;
await vi . waitFor ( ( ) = >
expect ( socket . sent ) . toContainEqual ( {
type : 'error' ,
code : 'invalid_client_event' ,
message : 'Invalid client event.'
} )
) ;
expect ( provider . sessions [ 0 ] . sent ) . not . toContainEqual (
expect . objectContaining ( { type : 'input_text.send' } )
) ;
expect ( recorder . rawDb . prepare ( 'SELECT * FROM messages' ) . all ( ) ) . toEqual ( [ ] ) ;
// The malformed frame is isolated: this same session remains usable.
socket . receive ( { type : 'input_text.send' , text : 'still alive' } ) ;
await vi . waitFor ( ( ) = >
expect ( provider . sessions [ 0 ] . sent ) . toContainEqual ( {
type : 'input_text.send' ,
text : 'still alive'
} )
) ;
expect ( recorder . rawDb . prepare ( 'SELECT text FROM messages' ) . all ( ) ) . toEqual ( [
{ text : 'still alive' }
] ) ;
socket . emit ( 'close' ) ;
recorder . close ( ) ;
} ) ;
it ( 'contains unexpected async message failures and leaves other sessions usable' , async ( ) = > {
const provider = new FakeProvider ( ) ;
const recorder = new FakeAnalyticsRecorder ( ) ;
recorder . messageLogged = ( ) = > {
throw new Error ( 'analytics unavailable' ) ;
} ;
const affected = makeSocket ( ) ;
const other = makeSocket ( ) ;
createRealtimeSession ( affected . asWs , provider , DEFAULTS , recorder , TEST_TOOL_TIMEOUT_MS ) ;
createRealtimeSession ( other . asWs , provider , DEFAULTS , recorder , TEST_TOOL_TIMEOUT_MS ) ;
affected . socket . receive ( { type : 'session.start' } ) ;
await vi . waitFor ( ( ) = > expect ( provider . sessions ) . toHaveLength ( 1 ) ) ;
other . socket . receive ( { type : 'session.start' } ) ;
await vi . waitFor ( ( ) = > expect ( provider . sessions ) . toHaveLength ( 2 ) ) ;
affected . socket . receive ( { type : 'input_text.send' , text : 'hello' } ) ;
await vi . waitFor ( ( ) = >
expect ( affected . socket . sent ) . toContainEqual ( {
type : 'error' ,
code : 'internal_error' ,
message : 'Failed to handle client event.'
} )
) ;
expect ( provider . sessions [ 0 ] . closed ) . toBe ( true ) ;
expect ( provider . sessions [ 0 ] . closeReason ) . toBe ( 'internal_error' ) ;
expect ( affected . socket . closeCalls ) . toBe ( 1 ) ;
expect ( provider . sessions [ 1 ] . closed ) . toBe ( false ) ;
other . socket . receive ( { type : 'response.cancel' } ) ;
await vi . waitFor ( ( ) = >
expect ( provider . sessions [ 1 ] . sent ) . toContainEqual ( { type : 'response.cancel' } )
) ;
} ) ;
it ( 'relays provider server events back to the browser socket' , async ( ) = > {
it ( 'relays provider server events back to the browser socket' , async ( ) = > {
const provider = new FakeProvider ( ) ;
const provider = new FakeProvider ( ) ;
const recorder = new FakeAnalyticsRecorder ( ) ;
const recorder = new FakeAnalyticsRecorder ( ) ;