@@ -24,6 +24,12 @@ import {
2424} from '../utils/process-identity.ts' ;
2525import { ensureMetroCompanion , stopMetroCompanion } from '../client-metro-companion.ts' ;
2626
27+ const TEST_BRIDGE_SCOPE = {
28+ tenantId : 'tenant-1' ,
29+ runId : 'run-1' ,
30+ leaseId : 'lease-1' ,
31+ } ;
32+
2733afterEach ( ( ) => {
2834 vi . clearAllMocks ( ) ;
2935 vi . restoreAllMocks ( ) ;
@@ -59,6 +65,7 @@ test('companion ownership is profile-scoped and consumer-counted', async () => {
5965 serverBaseUrl : 'https://bridge.example.test' ,
6066 bearerToken : 'token' ,
6167 localBaseUrl : 'http://127.0.0.1:8081' ,
68+ bridgeScope : TEST_BRIDGE_SCOPE ,
6269 launchUrl : 'myapp://staging' ,
6370 profileKey : '/tmp/staging.json' ,
6471 consumerKey : 'session-a' ,
@@ -68,6 +75,7 @@ test('companion ownership is profile-scoped and consumer-counted', async () => {
6875 serverBaseUrl : 'https://bridge.example.test' ,
6976 bearerToken : 'token' ,
7077 localBaseUrl : 'http://127.0.0.1:8081' ,
78+ bridgeScope : TEST_BRIDGE_SCOPE ,
7179 launchUrl : 'myapp://staging' ,
7280 profileKey : '/tmp/staging.json' ,
7381 consumerKey : 'session-b' ,
@@ -77,6 +85,7 @@ test('companion ownership is profile-scoped and consumer-counted', async () => {
7785 serverBaseUrl : 'https://bridge.example.test' ,
7886 bearerToken : 'token' ,
7987 localBaseUrl : 'http://127.0.0.1:8081' ,
88+ bridgeScope : TEST_BRIDGE_SCOPE ,
8089 launchUrl : 'myapp://prod' ,
8190 profileKey : '/tmp/prod.json' ,
8291 consumerKey : 'session-prod' ,
@@ -155,6 +164,7 @@ test('launchUrl changes force a companion respawn for the same profile', async (
155164 serverBaseUrl : 'https://bridge.example.test' ,
156165 bearerToken : 'token' ,
157166 localBaseUrl : 'http://127.0.0.1:8081' ,
167+ bridgeScope : TEST_BRIDGE_SCOPE ,
158168 launchUrl : 'myapp://first' ,
159169 profileKey : '/tmp/profile.json' ,
160170 consumerKey : 'session-a' ,
@@ -164,6 +174,7 @@ test('launchUrl changes force a companion respawn for the same profile', async (
164174 serverBaseUrl : 'https://bridge.example.test' ,
165175 bearerToken : 'token' ,
166176 localBaseUrl : 'http://127.0.0.1:8081' ,
177+ bridgeScope : TEST_BRIDGE_SCOPE ,
167178 launchUrl : 'myapp://second' ,
168179 profileKey : '/tmp/profile.json' ,
169180 consumerKey : 'session-a' ,
@@ -184,3 +195,54 @@ test('launchUrl changes force a companion respawn for the same profile', async (
184195 fs . rmSync ( projectRoot , { recursive : true , force : true } ) ;
185196 }
186197} ) ;
198+
199+ test ( 'legacy state without bridge scope is stopped before respawn' , async ( ) => {
200+ const projectRoot = fs . mkdtempSync (
201+ path . join ( os . tmpdir ( ) , 'agent-device-metro-companion-legacy-' ) ,
202+ ) ;
203+ const statePath = path . join ( projectRoot , '.agent-device' , 'metro-companion.json' ) ;
204+ try {
205+ fs . mkdirSync ( path . dirname ( statePath ) , { recursive : true } ) ;
206+ fs . writeFileSync (
207+ statePath ,
208+ `${ JSON . stringify ( {
209+ pid : 555 ,
210+ startTime : 'start-555' ,
211+ command : `${ process . execPath } src/metro-companion.ts --agent-device-run-metro-companion` ,
212+ serverBaseUrl : 'https://bridge.example.test' ,
213+ localBaseUrl : 'http://127.0.0.1:8081' ,
214+ tokenHash : 'legacy-token-hash' ,
215+ consumers : [ 'session-a' ] ,
216+ } ) } \n`,
217+ ) ;
218+
219+ vi . mocked ( runCmdDetached ) . mockReturnValueOnce ( 666 ) ;
220+ vi . mocked ( isProcessAlive ) . mockReturnValue ( true ) ;
221+ vi . mocked ( readProcessStartTime ) . mockReturnValue ( 'start-555' ) ;
222+ vi . mocked ( readProcessCommand ) . mockReturnValue (
223+ `${ process . execPath } src/metro-companion.ts --agent-device-run-metro-companion` ,
224+ ) ;
225+ vi . mocked ( waitForProcessExit ) . mockResolvedValue ( true ) ;
226+ const killSpy = vi . spyOn ( process , 'kill' ) . mockImplementation ( ( ) => true ) ;
227+
228+ const spawned = await ensureMetroCompanion ( {
229+ projectRoot,
230+ serverBaseUrl : 'https://bridge.example.test' ,
231+ bearerToken : 'token' ,
232+ localBaseUrl : 'http://127.0.0.1:8081' ,
233+ bridgeScope : TEST_BRIDGE_SCOPE ,
234+ consumerKey : 'session-a' ,
235+ } ) ;
236+
237+ assert . equal ( spawned . spawned , true ) ;
238+ assert . equal ( spawned . pid , 666 ) ;
239+ assert . equal ( vi . mocked ( runCmdDetached ) . mock . calls . length , 1 ) ;
240+ assert . deepEqual ( killSpy . mock . calls [ 0 ] , [ 555 , 'SIGTERM' ] ) ;
241+ const state = JSON . parse ( fs . readFileSync ( spawned . statePath , 'utf8' ) ) as {
242+ bridgeScope ?: unknown ;
243+ } ;
244+ assert . deepEqual ( state . bridgeScope , TEST_BRIDGE_SCOPE ) ;
245+ } finally {
246+ fs . rmSync ( projectRoot , { recursive : true , force : true } ) ;
247+ }
248+ } ) ;
0 commit comments