@@ -9,7 +9,12 @@ import { setTimeout as nativeSleep } from "node:timers/promises";
99import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
1010import { resolveConfigPath , resolveStateDir } from "../config/paths.js" ;
1111import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js" ;
12- import { acquireGatewayLock , GatewayLockError , type GatewayLockOptions } from "./gateway-lock.js" ;
12+ import {
13+ acquireGatewayLock ,
14+ GatewayLockError ,
15+ readActiveGatewayLockPort ,
16+ type GatewayLockOptions ,
17+ } from "./gateway-lock.js" ;
1318
1419type GatewayLock = NonNullable < Awaited < ReturnType < typeof acquireGatewayLock > > > ;
1520
@@ -94,11 +99,17 @@ function makeProcStat(pid: number, startTime: number) {
9499 return `${ pid } (node) ${ fields . join ( " " ) } ` ;
95100}
96101
97- function createLockPayload ( params : { configPath : string ; startTime : number ; createdAt ?: string } ) {
102+ function createLockPayload ( params : {
103+ configPath : string ;
104+ startTime : number ;
105+ createdAt ?: string ;
106+ port ?: number ;
107+ } ) {
98108 return {
99109 pid : process . pid ,
100110 createdAt : params . createdAt ?? new Date ( ) . toISOString ( ) ,
101111 configPath : params . configPath ,
112+ ...( params . port ? { port : params . port } : { } ) ,
102113 startTime : params . startTime ,
103114 } ;
104115}
@@ -199,6 +210,66 @@ describe("gateway lock", () => {
199210 await expectGatewayLock ( lock2 ) . release ( ) ;
200211 } ) ;
201212
213+ it ( "records and reads the active runtime port from a verified gateway lock" , async ( ) => {
214+ const env = await makeEnv ( ) ;
215+ const lock = expectGatewayLock (
216+ await acquireForTest ( env , {
217+ platform : "darwin" ,
218+ port : 48789 ,
219+ readProcessCmdline : ( ) => [ "openclaw-gateway" ] ,
220+ } ) ,
221+ ) ;
222+
223+ try {
224+ await expect (
225+ readActiveGatewayLockPort ( {
226+ env,
227+ lockDir : resolveTestLockDir ( ) ,
228+ platform : "darwin" ,
229+ readProcessCmdline : ( ) => [ "openclaw-gateway" ] ,
230+ } ) ,
231+ ) . resolves . toBe ( 48789 ) ;
232+ } finally {
233+ await lock . release ( ) ;
234+ }
235+ } ) ;
236+
237+ it ( "keeps a retitled gateway lock owned during concurrent acquisition" , async ( ) => {
238+ const env = await makeEnv ( ) ;
239+ const lock = expectGatewayLock ( await acquireForTest ( env , { platform : "darwin" , port : 48789 } ) ) ;
240+ const connectSpy = createPortProbeConnectionSpy ( "connect" ) ;
241+
242+ try {
243+ await expect (
244+ acquireForTest ( env , {
245+ platform : "darwin" ,
246+ port : 48789 ,
247+ timeoutMs : 15 ,
248+ readProcessCmdline : ( ) => [ "openclaw-gateway" ] ,
249+ } ) ,
250+ ) . rejects . toBeInstanceOf ( GatewayLockError ) ;
251+ expect ( connectSpy ) . toHaveBeenCalled ( ) ;
252+ } finally {
253+ await lock . release ( ) ;
254+ }
255+ } ) ;
256+
257+ it ( "ignores active-port metadata when the lock owner cannot be verified" , async ( ) => {
258+ const env = await makeEnv ( ) ;
259+ const { lockPath, configPath } = resolveLockPath ( env ) ;
260+ const payload = createLockPayload ( { configPath, startTime : 111 , port : 48789 } ) ;
261+ await fs . writeFile ( lockPath , JSON . stringify ( payload ) , "utf8" ) ;
262+
263+ await expect (
264+ readActiveGatewayLockPort ( {
265+ env,
266+ lockDir : resolveTestLockDir ( ) ,
267+ platform : "darwin" ,
268+ readProcessCmdline : ( ) => null ,
269+ } ) ,
270+ ) . resolves . toBeUndefined ( ) ;
271+ } ) ;
272+
202273 it ( "treats recycled linux pid as stale when start time mismatches" , async ( ) => {
203274 const env = await makeEnv ( ) ;
204275 const { lockPath, configPath } = resolveLockPath ( env ) ;
0 commit comments