@@ -3828,6 +3828,29 @@ test("shutdown candidates prefer the most recently released compatible alias", (
38283828 assert . equal ( selected . alias , second . alias ) ;
38293829} ) ;
38303830
3831+ test ( "candidate selection falls back to host order when either release timestamp is missing" , ( ) => {
3832+ const paths = makePaths ( ) ;
3833+ writeBaseHostConfig ( paths . hostConfigPath ) ;
3834+ writeBaseProject ( paths . projectFilePath ) ;
3835+ const resolvedPaths = brokerPaths ( paths ) ;
3836+ initBroker ( resolvedPaths , runtimeOptions ( paths , { processExists : ( ) => true } ) ) ;
3837+ const registry = readJson ( resolvedPaths . registryPath ) ;
3838+ registry . aliases [ "ui-1" ] . lastLeaseReleasedAt = null ;
3839+ registry . aliases [ "ui-2" ] . lastLeaseReleasedAt = "2026-01-01T00:00:00.000Z" ;
3840+ writeJson ( resolvedPaths . registryPath , registry ) ;
3841+
3842+ const selected = acquireLeaseBroker ( resolvedPaths , {
3843+ actorId : "agent-host-order" ,
3844+ actorType : "agent" ,
3845+ ownerPid : process . pid ,
3846+ processExists : ( pid ) => pid === process . pid ,
3847+ purposeId : "agent-ui-session" ,
3848+ simctlAdapter : paths . simctl . adapter ,
3849+ } ) . lease ;
3850+
3851+ assert . equal ( selected . alias , "ui-1" ) ;
3852+ } ) ;
3853+
38313854test ( "idle policy is absent by default, strictly bounded, and stored outside project state" , ( ) => {
38323855 const paths = makePaths ( ) ;
38333856 writeBaseHostConfig ( paths . hostConfigPath ) ;
@@ -3981,6 +4004,101 @@ test("idle policy read errors stay public-safe while preserving diagnostics", (t
39814004 }
39824005} ) ;
39834006
4007+ test ( "idle state read errors stay public-safe across status, reconcile, and cleanup" , ( t ) => {
4008+ const originalReadFileSync = fs . readFileSync ;
4009+ t . after ( ( ) => {
4010+ fs . readFileSync = originalReadFileSync ;
4011+ } ) ;
4012+
4013+ const scenarios = [
4014+ {
4015+ name : "host config during status" ,
4016+ prepare ( paths , resolvedPaths ) {
4017+ return {
4018+ operation : ( ) => idleStatusBroker ( resolvedPaths , runtimeOptions ( paths , { processExists : ( ) => true } ) ) ,
4019+ targetPath : resolvedPaths . hostConfigPath ,
4020+ } ;
4021+ } ,
4022+ } ,
4023+ {
4024+ name : "registry during status" ,
4025+ prepare ( paths , resolvedPaths ) {
4026+ return {
4027+ operation : ( ) => idleStatusBroker ( resolvedPaths , runtimeOptions ( paths , { processExists : ( ) => true } ) ) ,
4028+ targetPath : resolvedPaths . registryPath ,
4029+ } ;
4030+ } ,
4031+ } ,
4032+ {
4033+ name : "pin during reconciliation" ,
4034+ prepare ( paths , resolvedPaths ) {
4035+ enableIdlePolicyBroker ( resolvedPaths , {
4036+ actorId : "operator" ,
4037+ actorType : "human" ,
4038+ graceSeconds : 60 ,
4039+ } ) ;
4040+ const pin = createPinBroker ( resolvedPaths , runtimeOptions ( paths , {
4041+ actorId : "operator" ,
4042+ actorType : "human" ,
4043+ alias : "ui-1" ,
4044+ processExists : ( ) => true ,
4045+ purposeId : "agent-ui-session" ,
4046+ } ) ) . pin ;
4047+ return {
4048+ operation : ( ) => reconcileIdleBroker ( resolvedPaths , runtimeOptions ( paths , { processExists : ( ) => true } ) ) ,
4049+ targetPath : path . join ( resolvedPaths . pinsDir , `${ pin . pinId } .json` ) ,
4050+ } ;
4051+ } ,
4052+ } ,
4053+ {
4054+ name : "lease during cleanup preview" ,
4055+ prepare ( paths , resolvedPaths ) {
4056+ const lease = acquireLeaseBroker ( resolvedPaths , runtimeOptions ( paths , {
4057+ actorId : "agent-cleanup-read" ,
4058+ actorType : "agent" ,
4059+ ownerPid : process . pid ,
4060+ processExists : ( pid ) => pid === process . pid ,
4061+ purposeId : "agent-ui-session" ,
4062+ } ) ) . lease ;
4063+ return {
4064+ operation : ( ) => cleanupIdleBroker ( resolvedPaths , runtimeOptions ( paths , {
4065+ processExists : ( pid ) => pid === process . pid ,
4066+ } ) ) ,
4067+ targetPath : path . join ( resolvedPaths . leasesDir , `${ lease . leaseId } .json` ) ,
4068+ } ;
4069+ } ,
4070+ } ,
4071+ ] ;
4072+
4073+ for ( const scenario of scenarios ) {
4074+ fs . readFileSync = originalReadFileSync ;
4075+ const paths = makePaths ( ) ;
4076+ writeBaseHostConfig ( paths . hostConfigPath ) ;
4077+ writeBaseProject ( paths . projectFilePath ) ;
4078+ const resolvedPaths = brokerPaths ( paths ) ;
4079+ initBroker ( resolvedPaths , runtimeOptions ( paths , { processExists : ( ) => true } ) ) ;
4080+ const { operation, targetPath } = scenario . prepare ( paths , resolvedPaths ) ;
4081+ fs . readFileSync = ( filePath , ...args ) => {
4082+ if ( filePath === targetPath ) {
4083+ const error = new Error ( `EACCES: permission denied, open '${ targetPath } '` ) ;
4084+ error . code = "EACCES" ;
4085+ throw error ;
4086+ }
4087+ return originalReadFileSync ( filePath , ...args ) ;
4088+ } ;
4089+
4090+ assert . throws ( operation , ( error ) => {
4091+ const serialized = JSON . stringify ( error . payload ) ;
4092+ return error instanceof BrokerError
4093+ && error . payload ?. reasonCode === "internal-error"
4094+ && error . payload ?. error === "Idle broker state could not be read."
4095+ && serialized . includes ( paths . root ) === false
4096+ && "stack" in error . payload === false
4097+ && error . cause ?. message . includes ( targetPath ) ;
4098+ } , scenario . name ) ;
4099+ }
4100+ } ) ;
4101+
39844102test ( "malformed idle policy shapes reject non-number grace durations" , ( ) => {
39854103 const paths = makePaths ( ) ;
39864104 writeBaseHostConfig ( paths . hostConfigPath ) ;
0 commit comments