@@ -18,6 +18,7 @@ import {
1818 renderLaneGraph ,
1919 resolveAdeCodeModulePath ,
2020 resolveRoots ,
21+ runCli ,
2122 shouldAutoRegisterProjectForPlan ,
2223 shouldBlockManualMachineRuntimeSpawn ,
2324 shouldEnforceMachineRuntimeBuildCompatibility ,
@@ -81,6 +82,35 @@ function expectExecutePlan(
8182 return plan ;
8283}
8384
85+ function writeSyncHostSingletonLock ( args : {
86+ lockPath : string ;
87+ pid : number ;
88+ port : number ;
89+ packageChannel : string | null ;
90+ adeHome : string ;
91+ } ) : void {
92+ const now = "2026-06-11T00:00:00.000Z" ;
93+ fs . mkdirSync ( path . dirname ( args . lockPath ) , { recursive : true } ) ;
94+ fs . writeFileSync ( args . lockPath , `${ JSON . stringify ( {
95+ version : 1 ,
96+ owner : {
97+ id : "other-channel-brain" ,
98+ pid : args . pid ,
99+ port : args . port ,
100+ appName : args . packageChannel === "beta" ? "ADE Beta" : "ADE" ,
101+ packageChannel : args . packageChannel ,
102+ adeHome : args . adeHome ,
103+ serviceName : args . packageChannel === "beta" ? "com.ade.runtime.beta" : "com.ade.runtime" ,
104+ socketPath : path . join ( args . adeHome , "sock" , "ade.sock" ) ,
105+ projectRoot : "/Users/admin/Projects/ADE" ,
106+ commandLine : null ,
107+ quitCommand : `ADE_HOME='${ args . adeHome } ' ade brain stop --text` ,
108+ createdAt : now ,
109+ updatedAt : now ,
110+ } ,
111+ } , null , 2 ) } \n`, "utf8" ) ;
112+ }
113+
84114describe ( "ADE CLI" , ( ) => {
85115 it ( "parses global options without stealing command flags" , ( ) => {
86116 const parsed = parseCliArgs ( [
@@ -331,6 +361,48 @@ describe("ADE CLI", () => {
331361 } ) ;
332362 } ) ;
333363
364+ it ( "serve fails instead of exiting successfully when another channel owns mobile sync" , async ( ) => {
365+ const adeHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "ade-cli-serve-conflict-" ) ) ;
366+ const projectRoot = path . join ( adeHome , "project" ) ;
367+ const lockPath = path . join ( adeHome , "sync-host-lock.json" ) ;
368+ const socketPath = path . join ( adeHome , "sock" , "ade.sock" ) ;
369+ fs . mkdirSync ( projectRoot , { recursive : true } ) ;
370+ const originalEnv = {
371+ ADE_HOME : process . env . ADE_HOME ,
372+ ADE_PROJECT_ROOT : process . env . ADE_PROJECT_ROOT ,
373+ ADE_PACKAGE_CHANNEL : process . env . ADE_PACKAGE_CHANNEL ,
374+ ADE_SYNC_HOST_LOCK_PATH : process . env . ADE_SYNC_HOST_LOCK_PATH ,
375+ } ;
376+ process . env . ADE_HOME = adeHome ;
377+ process . env . ADE_PROJECT_ROOT = projectRoot ;
378+ delete process . env . ADE_PACKAGE_CHANNEL ;
379+ process . env . ADE_SYNC_HOST_LOCK_PATH = lockPath ;
380+ writeSyncHostSingletonLock ( {
381+ lockPath,
382+ pid : process . ppid ,
383+ port : 8801 ,
384+ packageChannel : "beta" ,
385+ adeHome : path . join ( os . homedir ( ) , ".ade-beta" ) ,
386+ } ) ;
387+
388+ try {
389+ await expect ( runCli ( [ "serve" , "--socket" , socketPath ] ) ) . rejects . toThrow (
390+ "ADE brain refusing to run without mobile sync." ,
391+ ) ;
392+ expect ( fs . existsSync ( socketPath ) ) . toBe ( false ) ;
393+ } finally {
394+ if ( originalEnv . ADE_HOME === undefined ) delete process . env . ADE_HOME ;
395+ else process . env . ADE_HOME = originalEnv . ADE_HOME ;
396+ if ( originalEnv . ADE_PROJECT_ROOT === undefined ) delete process . env . ADE_PROJECT_ROOT ;
397+ else process . env . ADE_PROJECT_ROOT = originalEnv . ADE_PROJECT_ROOT ;
398+ if ( originalEnv . ADE_PACKAGE_CHANNEL === undefined ) delete process . env . ADE_PACKAGE_CHANNEL ;
399+ else process . env . ADE_PACKAGE_CHANNEL = originalEnv . ADE_PACKAGE_CHANNEL ;
400+ if ( originalEnv . ADE_SYNC_HOST_LOCK_PATH === undefined ) delete process . env . ADE_SYNC_HOST_LOCK_PATH ;
401+ else process . env . ADE_SYNC_HOST_LOCK_PATH = originalEnv . ADE_SYNC_HOST_LOCK_PATH ;
402+ fs . rmSync ( adeHome , { recursive : true , force : true } ) ;
403+ }
404+ } ) ;
405+
334406 it ( "recognizes the hidden PTY host worker entrypoint" , ( ) => {
335407 expect ( buildCliPlan ( [ "__ade-pty-host-worker" ] ) ) . toEqual ( {
336408 kind : "pty-host-worker" ,
@@ -4243,8 +4315,10 @@ describe("ADE CLI", () => {
42434315 it ( "formats preview-match and preview-ensure text as Preview Lab output" , ( ) => {
42444316 const matchPlan = expectExecutePlan ( buildCliPlan ( [ "ios-sim" , "preview-match" , "--source" , "Views/HomeView.swift" ] ) ) ;
42454317 const ensurePlan = expectExecutePlan ( buildCliPlan ( [ "ios-sim" , "preview-ensure" ] ) ) ;
4318+ const currentPlan = expectExecutePlan ( buildCliPlan ( [ "ios-sim" , "preview-current" ] ) ) ;
42464319 expect ( inferFormatter ( matchPlan ) ) . toBe ( "ios-sim-preview" ) ;
42474320 expect ( inferFormatter ( ensurePlan ) ) . toBe ( "ios-sim-preview" ) ;
4321+ expect ( inferFormatter ( currentPlan ) ) . toBe ( "ios-sim-preview" ) ;
42484322
42494323 const output = formatOutput ( {
42504324 status : "missing-preview" ,
@@ -4263,6 +4337,62 @@ describe("ADE CLI", () => {
42634337 expect ( output ) . toContain ( "ADE iOS Preview match" ) ;
42644338 expect ( output ) . toMatch ( / s t a t u s \s + m i s s i n g - p r e v i e w / ) ;
42654339 expect ( output ) . toMatch ( / s u g g e s t e d f i l e \s + a p p s \/ i o s \/ A D E \/ V i e w s \/ H o m e P r e v i e w s \. s w i f t / ) ;
4340+
4341+ const currentOutput = formatOutput ( {
4342+ ok : false ,
4343+ match : {
4344+ status : "no-context" ,
4345+ confidence : "none" ,
4346+ target : null ,
4347+ selectedSourceFile : null ,
4348+ selectedSourceLine : null ,
4349+ reason : "Select a source-backed simulator element first." ,
4350+ } ,
4351+ target : null ,
4352+ render : null ,
4353+ error : "Select a source-backed simulator element first." ,
4354+ } , {
4355+ text : true ,
4356+ pretty : false ,
4357+ } as any , "ios-sim-preview" ) ;
4358+ expect ( currentOutput ) . toContain ( "ADE iOS Preview current" ) ;
4359+ expect ( currentOutput ) . toMatch ( / s t a t u s \s + n o - c o n t e x t / ) ;
4360+ } ) ;
4361+
4362+ it ( "ios-sim preview-current renders the currently selected simulator preview" , ( ) => {
4363+ const plan = expectExecutePlan ( buildCliPlan ( [
4364+ "ios-sim" ,
4365+ "preview-current" ,
4366+ "--source" ,
4367+ "Views/HomeView.swift" ,
4368+ "--line" ,
4369+ "44" ,
4370+ "--label" ,
4371+ "Settings" ,
4372+ "--component-id" ,
4373+ "settings-row" ,
4374+ "--tab" ,
4375+ "tab-1" ,
4376+ "--timeout" ,
4377+ "30" ,
4378+ "--project-root" ,
4379+ "/tmp/app" ,
4380+ ] ) ) ;
4381+ expect ( plan . steps [ 0 ] ?. params ) . toMatchObject ( {
4382+ arguments : {
4383+ domain : "ios_simulator" ,
4384+ action : "renderCurrentPreview" ,
4385+ args : {
4386+ projectRoot : "/tmp/app" ,
4387+ sourceFile : "Views/HomeView.swift" ,
4388+ sourceLine : 44 ,
4389+ elementLabel : "Settings" ,
4390+ componentId : "settings-row" ,
4391+ tabIdentifier : "tab-1" ,
4392+ timeoutSec : 30 ,
4393+ } ,
4394+ } ,
4395+ } ) ;
42664396 } ) ;
42674397
42684398 it ( "ios-sim preview-render requires a source file and forwards render options" , ( ) => {
0 commit comments