@@ -29,6 +29,7 @@ const deleteSessionIfEmpty = vi.hoisted(() => vi.fn());
2929const appendMessage = vi . hoisted ( ( ) => vi . fn ( ) ) ;
3030const updateSessionModel = vi . hoisted ( ( ) => vi . fn ( ) ) ;
3131const saveConfig = vi . hoisted ( ( ) => vi . fn ( ) ) ;
32+ const listModels = vi . hoisted ( ( ) => vi . fn ( ) ) ;
3233
3334vi . mock ( '@/utils' , async ( ) => ( {
3435 ...( await vi . importActual ( '@/utils' ) ) ,
@@ -44,6 +45,9 @@ vi.mock('@/utils', async () => ({
4445 } ) ) ,
4546 saveConfig,
4647 } ,
48+ ollama : {
49+ listModels,
50+ } ,
4751 screen : {
4852 clear : clearScreen ,
4953 } ,
@@ -193,6 +197,34 @@ vi.mock('@/components/SessionManager', () => ({
193197 } ,
194198} ) ) ;
195199
200+ vi . mock ( './ReadinessCheck' , async ( ) => {
201+ const actual =
202+ await vi . importActual < typeof import ( './ReadinessCheck' ) > (
203+ './ReadinessCheck' ,
204+ ) ;
205+
206+ return {
207+ ...actual ,
208+ ReadinessCheck : ( props : {
209+ errorMessage ?: string | null ;
210+ onCommand : ( command : string ) => void ;
211+ setupState : string ;
212+ } ) => {
213+ const { errorMessage, onCommand, setupState } = props ;
214+ capturedCallbacks . onCommand = onCommand ;
215+ const message =
216+ setupState === 'missing-model-config'
217+ ? 'Select or download a model'
218+ : setupState === 'no-installed-models'
219+ ? 'Download a model'
220+ : errorMessage
221+ ? `Unable to load models: ${ errorMessage } `
222+ : setupState ;
223+ return < Text > { `Setup Required ${ message } ` } </ Text > ;
224+ } ,
225+ } ;
226+ } ) ;
227+
196228import { App } from './App' ;
197229
198230describe ( 'App' , ( ) => {
@@ -222,6 +254,8 @@ describe('App', () => {
222254 appendMessage . mockReset ( ) ;
223255 updateSessionModel . mockReset ( ) ;
224256 saveConfig . mockReset ( ) ;
257+ listModels . mockReset ( ) ;
258+ listModels . mockResolvedValue ( [ 'gemma4' ] ) ;
225259
226260 let counter = 0 ;
227261 createSession . mockImplementation ( ( model : string ) => ( {
@@ -409,6 +443,7 @@ describe('App', () => {
409443 it ( 'prints a resume command when the app exits with session messages' , async ( ) => {
410444 deleteSessionIfEmpty . mockReturnValue ( false ) ;
411445 const { unmount } = render ( < App /> ) ;
446+ await time . tick ( ) ;
412447
413448 capturedCallbacks . onMessagesChange ?.( [
414449 { role : 'user' , content : 'saved message' } ,
@@ -429,6 +464,7 @@ describe('App', () => {
429464
430465 it ( 'resets the chat session when /clear is issued' , async ( ) => {
431466 const { lastFrame, rerender } = render ( < App /> ) ;
467+ await time . tick ( ) ;
432468
433469 expect ( lastFrame ( ) ) . toContain ( 'session:session-0' ) ;
434470
@@ -453,6 +489,7 @@ describe('App', () => {
453489
454490 it ( 'opens a selected saved session' , async ( ) => {
455491 const { lastFrame, rerender } = render ( < App /> ) ;
492+ await time . tick ( ) ;
456493 capturedCallbacks . onCommand ?.( '/session' ) ;
457494 rerender ( < App /> ) ;
458495 await time . tick ( ) ;
@@ -469,6 +506,7 @@ describe('App', () => {
469506
470507 it ( 'returns to chat when the current session is selected' , async ( ) => {
471508 const { lastFrame, rerender } = render ( < App /> ) ;
509+ await time . tick ( ) ;
472510 capturedCallbacks . onCommand ?.( '/session' ) ;
473511 rerender ( < App /> ) ;
474512 await time . tick ( ) ;
@@ -489,6 +527,55 @@ describe('App', () => {
489527 expect ( loadSession ) . toHaveBeenCalledWith ( 'resumed-session' ) ;
490528 } ) ;
491529
530+ it ( 'renders setup-needed content when no model is configured' , async ( ) => {
531+ const { config } = await import ( '@/utils' ) ;
532+ vi . mocked ( config . loadConfig ) . mockReturnValueOnce ( {
533+ host : 'http://localhost:11434' ,
534+ model : undefined ,
535+ searxngBaseUrl : undefined ,
536+ theme : 'github-dark' ,
537+ } ) ;
538+
539+ const { lastFrame } = render ( < App /> ) ;
540+ await time . tick ( ) ;
541+
542+ expect ( lastFrame ( ) ) . toContain ( 'Setup Required' ) ;
543+ expect ( lastFrame ( ) ) . toContain ( 'Select or download a model' ) ;
544+ expect ( lastFrame ( ) ) . not . toContain ( 'session:' ) ;
545+ expect ( listModels ) . not . toHaveBeenCalled ( ) ;
546+ } ) ;
547+
548+ it ( 'renders setup-needed content when no models are installed' , async ( ) => {
549+ listModels . mockResolvedValueOnce ( [ ] ) ;
550+
551+ const { lastFrame } = render ( < App /> ) ;
552+ await time . tick ( ) ;
553+ await time . tick ( ) ;
554+
555+ expect ( lastFrame ( ) ) . toContain ( 'Setup Required' ) ;
556+ expect ( lastFrame ( ) ) . toContain ( 'Download a model' ) ;
557+ expect ( lastFrame ( ) ) . not . toContain ( 'session:' ) ;
558+ } ) ;
559+
560+ it ( 'routes to ModelManager from setup-needed state' , async ( ) => {
561+ const { config } = await import ( '@/utils' ) ;
562+ vi . mocked ( config . loadConfig ) . mockReturnValueOnce ( {
563+ host : 'http://localhost:11434' ,
564+ model : undefined ,
565+ searxngBaseUrl : undefined ,
566+ theme : 'github-dark' ,
567+ } ) ;
568+
569+ const { lastFrame, rerender } = render ( < App /> ) ;
570+ await time . tick ( ) ;
571+
572+ capturedCallbacks . onCommand ?.( '/model' ) ;
573+ rerender ( < App /> ) ;
574+ await time . tick ( ) ;
575+
576+ expect ( lastFrame ( ) ) . toContain ( 'ModelManager' ) ;
577+ } ) ;
578+
492579 it ( 'creates a new session from SessionManager' , async ( ) => {
493580 const { lastFrame, rerender } = render ( < App /> ) ;
494581 capturedCallbacks . onCommand ?.( '/session' ) ;
@@ -534,6 +621,7 @@ describe('App', () => {
534621
535622 it ( 'persists newly committed messages and skips turn_aborted markers' , async ( ) => {
536623 render ( < App /> ) ;
624+ await time . tick ( ) ;
537625 appendMessage . mockClear ( ) ;
538626
539627 capturedCallbacks . onMessagesChange ?.( [
@@ -560,13 +648,16 @@ describe('App', () => {
560648 } ) ;
561649
562650 it ( 'does not append when transcript length does not grow' , async ( ) => {
563- render ( < App /> ) ;
651+ const { rerender } = render ( < App /> ) ;
652+ await time . tick ( ) ;
564653
565654 capturedCallbacks . onMessagesChange ?.( [ { role : 'user' , content : 'saved' } ] ) ;
655+ rerender ( < App /> ) ;
566656 await time . tick ( ) ;
567657 appendMessage . mockClear ( ) ;
568658
569659 capturedCallbacks . onMessagesChange ?.( [ { role : 'user' , content : 'saved' } ] ) ;
660+ rerender ( < App /> ) ;
570661 await time . tick ( ) ;
571662
572663 expect ( appendMessage ) . not . toHaveBeenCalled ( ) ;
@@ -612,6 +703,7 @@ describe('App', () => {
612703
613704 it ( 'updates footer mode when Chat changes execution mode' , async ( ) => {
614705 const { lastFrame, rerender } = render ( < App /> ) ;
706+ await time . tick ( ) ;
615707
616708 expect ( lastFrame ( ) ) . toContain ( 'Mode: Safe' ) ;
617709
0 commit comments