@@ -43,6 +43,8 @@ type AnyMock = jest.Mock<(...args: any[]) => any>;
4343type KiloClawClientMock = {
4444 KiloClawInternalClient : AnyMock ;
4545 __getStatusMock : AnyMock ;
46+ __getLatestVersionMock : AnyMock ;
47+ __getLatestVersionForInstanceMock : AnyMock ;
4648 __destroyMock : AnyMock ;
4749 __startMock : AnyMock ;
4850} ;
@@ -109,11 +111,15 @@ jest.mock('@/lib/config.server', () => {
109111
110112jest . mock ( '@/lib/kiloclaw/kiloclaw-internal-client' , ( ) => {
111113 const getStatusMock = jest . fn ( ) ;
114+ const getLatestVersionMock = jest . fn ( ) ;
115+ const getLatestVersionForInstanceMock = jest . fn ( ) ;
112116 const destroyMock = jest . fn ( ) ;
113117 const startMock = jest . fn ( ) ;
114118 return {
115119 KiloClawInternalClient : jest . fn ( ) . mockImplementation ( ( ) => ( {
116120 getStatus : getStatusMock ,
121+ getLatestVersion : getLatestVersionMock ,
122+ getLatestVersionForInstance : getLatestVersionForInstanceMock ,
117123 start : startMock ,
118124 destroy : destroyMock ,
119125 } ) ) ,
@@ -127,13 +133,16 @@ jest.mock('@/lib/kiloclaw/kiloclaw-internal-client', () => {
127133 }
128134 } ,
129135 __getStatusMock : getStatusMock ,
136+ __getLatestVersionMock : getLatestVersionMock ,
137+ __getLatestVersionForInstanceMock : getLatestVersionForInstanceMock ,
130138 __destroyMock : destroyMock ,
131139 __startMock : startMock ,
132140 } ;
133141} ) ;
134142
135143let createCaller : ( ctx : { user : Awaited < ReturnType < typeof insertTestUser > > } ) => {
136144 getStatus : ( ) => Promise < unknown > ;
145+ latestVersion : ( input ?: { currentImageTag ?: string } ) => Promise < unknown > ;
137146 getNavState : ( ) => Promise < { hasActiveInstance : boolean } > ;
138147 validateWeatherLocation : ( input : { location : string } ) => Promise < {
139148 location : string ;
@@ -490,6 +499,55 @@ describe('kiloclawRouter getStatus', () => {
490499 } ) ;
491500} ) ;
492501
502+ describe ( 'kiloclawRouter latestVersion' , ( ) => {
503+ beforeEach ( async ( ) => {
504+ await cleanupDbForTest ( ) ;
505+ kiloclawClientMock . KiloClawInternalClient . mockClear ( ) ;
506+ kiloclawClientMock . __getLatestVersionMock . mockReset ( ) ;
507+ kiloclawClientMock . __getLatestVersionForInstanceMock . mockReset ( ) ;
508+ } ) ;
509+
510+ it ( 'passes the active instance row for server-derived rollout lookup' , async ( ) => {
511+ kiloclawClientMock . __getLatestVersionForInstanceMock . mockResolvedValue ( {
512+ imageTag : 'candidate-tag' ,
513+ } ) ;
514+ const user = await insertTestUser ( {
515+ google_user_email : `kiloclaw-latest-version-${ crypto . randomUUID ( ) } @example.com` ,
516+ } ) ;
517+ const instanceId = crypto . randomUUID ( ) ;
518+ await db . insert ( kiloclaw_instances ) . values ( {
519+ id : instanceId ,
520+ user_id : user . id ,
521+ sandbox_id : `ki_${ instanceId . replace ( / - / g, '' ) } ` ,
522+ } ) ;
523+
524+ const caller = createCaller ( { user } ) ;
525+ await caller . latestVersion ( { currentImageTag : 'current-tag' } ) ;
526+
527+ expect ( kiloclawClientMock . __getLatestVersionForInstanceMock ) . toHaveBeenCalledWith ( {
528+ instanceId,
529+ currentImageTag : 'current-tag' ,
530+ } ) ;
531+ expect ( kiloclawClientMock . __getLatestVersionMock ) . not . toHaveBeenCalled ( ) ;
532+ } ) ;
533+
534+ it ( 'uses anonymous latest version lookup when the user has no active instance' , async ( ) => {
535+ kiloclawClientMock . __getLatestVersionMock . mockResolvedValue ( {
536+ imageTag : 'anonymous-tag' ,
537+ } ) ;
538+ const user = await insertTestUser ( {
539+ google_user_email : `kiloclaw-latest-version-${ crypto . randomUUID ( ) } @example.com` ,
540+ } ) ;
541+
542+ const caller = createCaller ( { user } ) ;
543+ const result = await caller . latestVersion ( { currentImageTag : 'current-tag' } ) ;
544+
545+ expect ( result ) . toEqual ( { imageTag : 'anonymous-tag' } ) ;
546+ expect ( kiloclawClientMock . __getLatestVersionMock ) . toHaveBeenCalledWith ( ) ;
547+ expect ( kiloclawClientMock . __getLatestVersionForInstanceMock ) . not . toHaveBeenCalled ( ) ;
548+ } ) ;
549+ } ) ;
550+
493551describe ( 'kiloclawRouter getNavState' , ( ) => {
494552 beforeEach ( async ( ) => {
495553 await cleanupDbForTest ( ) ;
0 commit comments