1+ import { spawn } from "node:child_process" ;
12import fs from "node:fs" ;
23import os from "node:os" ;
34import path from "node:path" ;
@@ -18,6 +19,7 @@ import {
1819 renderLaneGraph ,
1920 resolveAdeCodeModulePath ,
2021 resolveRoots ,
22+ runCli ,
2123 shouldAutoRegisterProjectForPlan ,
2224 shouldBlockManualMachineRuntimeSpawn ,
2325 shouldEnforceMachineRuntimeBuildCompatibility ,
@@ -81,6 +83,35 @@ function expectExecutePlan(
8183 return plan ;
8284}
8385
86+ function writeSyncHostSingletonLock ( args : {
87+ lockPath : string ;
88+ pid : number ;
89+ port : number ;
90+ packageChannel : string | null ;
91+ adeHome : string ;
92+ } ) : void {
93+ const now = "2026-06-11T00:00:00.000Z" ;
94+ fs . mkdirSync ( path . dirname ( args . lockPath ) , { recursive : true } ) ;
95+ fs . writeFileSync ( args . lockPath , `${ JSON . stringify ( {
96+ version : 1 ,
97+ owner : {
98+ id : "other-channel-brain" ,
99+ pid : args . pid ,
100+ port : args . port ,
101+ appName : args . packageChannel === "beta" ? "ADE Beta" : "ADE" ,
102+ packageChannel : args . packageChannel ,
103+ adeHome : args . adeHome ,
104+ serviceName : args . packageChannel === "beta" ? "com.ade.runtime.beta" : "com.ade.runtime" ,
105+ socketPath : path . join ( args . adeHome , "sock" , "ade.sock" ) ,
106+ projectRoot : "/Users/admin/Projects/ADE" ,
107+ commandLine : null ,
108+ quitCommand : `ADE_HOME='${ args . adeHome } ' ade brain stop --text` ,
109+ createdAt : now ,
110+ updatedAt : now ,
111+ } ,
112+ } , null , 2 ) } \n`, "utf8" ) ;
113+ }
114+
84115describe ( "ADE CLI" , ( ) => {
85116 it ( "parses global options without stealing command flags" , ( ) => {
86117 const parsed = parseCliArgs ( [
@@ -331,6 +362,62 @@ describe("ADE CLI", () => {
331362 } ) ;
332363 } ) ;
333364
365+ it ( "serve fails instead of exiting successfully when another channel owns mobile sync" , async ( ) => {
366+ const adeHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "ade-cli-serve-conflict-" ) ) ;
367+ const projectRoot = path . join ( adeHome , "project" ) ;
368+ const lockPath = path . join ( adeHome , "sync-host-lock.json" ) ;
369+ const socketPath = path . join ( adeHome , "sock" , "ade.sock" ) ;
370+ fs . mkdirSync ( projectRoot , { recursive : true } ) ;
371+ const originalEnv = {
372+ ADE_HOME : process . env . ADE_HOME ,
373+ ADE_PROJECT_ROOT : process . env . ADE_PROJECT_ROOT ,
374+ ADE_PACKAGE_CHANNEL : process . env . ADE_PACKAGE_CHANNEL ,
375+ ADE_SYNC_HOST_LOCK_PATH : process . env . ADE_SYNC_HOST_LOCK_PATH ,
376+ ADE_SYNC_HOST_SINGLETON_TEST_MODE : process . env . ADE_SYNC_HOST_SINGLETON_TEST_MODE ,
377+ } ;
378+ const ownerProcess = spawn ( process . execPath , [ "-e" , "setInterval(() => {}, 1000);" ] , {
379+ stdio : "ignore" ,
380+ } ) ;
381+ ownerProcess . on ( "error" , ( ) => { } ) ;
382+ ownerProcess . unref ( ) ;
383+ if ( ! ownerProcess . pid ) {
384+ throw new Error ( "Failed to start fake sync-host owner process." ) ;
385+ }
386+
387+ try {
388+ process . env . ADE_HOME = adeHome ;
389+ process . env . ADE_PROJECT_ROOT = projectRoot ;
390+ delete process . env . ADE_PACKAGE_CHANNEL ;
391+ process . env . ADE_SYNC_HOST_LOCK_PATH = lockPath ;
392+ process . env . ADE_SYNC_HOST_SINGLETON_TEST_MODE = "1" ;
393+ writeSyncHostSingletonLock ( {
394+ lockPath,
395+ pid : ownerProcess . pid ,
396+ port : 8801 ,
397+ packageChannel : "beta" ,
398+ adeHome : path . join ( os . homedir ( ) , ".ade-beta" ) ,
399+ } ) ;
400+
401+ await expect ( runCli ( [ "serve" , "--socket" , socketPath ] ) ) . rejects . toThrow (
402+ "ADE brain refusing to run without mobile sync." ,
403+ ) ;
404+ expect ( fs . existsSync ( socketPath ) ) . toBe ( false ) ;
405+ } finally {
406+ if ( originalEnv . ADE_HOME === undefined ) delete process . env . ADE_HOME ;
407+ else process . env . ADE_HOME = originalEnv . ADE_HOME ;
408+ if ( originalEnv . ADE_PROJECT_ROOT === undefined ) delete process . env . ADE_PROJECT_ROOT ;
409+ else process . env . ADE_PROJECT_ROOT = originalEnv . ADE_PROJECT_ROOT ;
410+ if ( originalEnv . ADE_PACKAGE_CHANNEL === undefined ) delete process . env . ADE_PACKAGE_CHANNEL ;
411+ else process . env . ADE_PACKAGE_CHANNEL = originalEnv . ADE_PACKAGE_CHANNEL ;
412+ if ( originalEnv . ADE_SYNC_HOST_LOCK_PATH === undefined ) delete process . env . ADE_SYNC_HOST_LOCK_PATH ;
413+ else process . env . ADE_SYNC_HOST_LOCK_PATH = originalEnv . ADE_SYNC_HOST_LOCK_PATH ;
414+ if ( originalEnv . ADE_SYNC_HOST_SINGLETON_TEST_MODE === undefined ) delete process . env . ADE_SYNC_HOST_SINGLETON_TEST_MODE ;
415+ else process . env . ADE_SYNC_HOST_SINGLETON_TEST_MODE = originalEnv . ADE_SYNC_HOST_SINGLETON_TEST_MODE ;
416+ ownerProcess . kill ( "SIGKILL" ) ;
417+ fs . rmSync ( adeHome , { recursive : true , force : true } ) ;
418+ }
419+ } ) ;
420+
334421 it ( "recognizes the hidden PTY host worker entrypoint" , ( ) => {
335422 expect ( buildCliPlan ( [ "__ade-pty-host-worker" ] ) ) . toEqual ( {
336423 kind : "pty-host-worker" ,
@@ -4243,8 +4330,10 @@ describe("ADE CLI", () => {
42434330 it ( "formats preview-match and preview-ensure text as Preview Lab output" , ( ) => {
42444331 const matchPlan = expectExecutePlan ( buildCliPlan ( [ "ios-sim" , "preview-match" , "--source" , "Views/HomeView.swift" ] ) ) ;
42454332 const ensurePlan = expectExecutePlan ( buildCliPlan ( [ "ios-sim" , "preview-ensure" ] ) ) ;
4333+ const currentPlan = expectExecutePlan ( buildCliPlan ( [ "ios-sim" , "preview-current" ] ) ) ;
42464334 expect ( inferFormatter ( matchPlan ) ) . toBe ( "ios-sim-preview" ) ;
42474335 expect ( inferFormatter ( ensurePlan ) ) . toBe ( "ios-sim-preview" ) ;
4336+ expect ( inferFormatter ( currentPlan ) ) . toBe ( "ios-sim-preview" ) ;
42484337
42494338 const output = formatOutput ( {
42504339 status : "missing-preview" ,
@@ -4263,6 +4352,62 @@ describe("ADE CLI", () => {
42634352 expect ( output ) . toContain ( "ADE iOS Preview match" ) ;
42644353 expect ( output ) . toMatch ( / s t a t u s \s + m i s s i n g - p r e v i e w / ) ;
42654354 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 / ) ;
4355+
4356+ const currentOutput = formatOutput ( {
4357+ ok : false ,
4358+ match : {
4359+ status : "no-context" ,
4360+ confidence : "none" ,
4361+ target : null ,
4362+ selectedSourceFile : null ,
4363+ selectedSourceLine : null ,
4364+ reason : "Select a source-backed simulator element first." ,
4365+ } ,
4366+ target : null ,
4367+ render : null ,
4368+ error : "Select a source-backed simulator element first." ,
4369+ } , {
4370+ text : true ,
4371+ pretty : false ,
4372+ } as any , "ios-sim-preview" ) ;
4373+ expect ( currentOutput ) . toContain ( "ADE iOS Preview current" ) ;
4374+ expect ( currentOutput ) . toMatch ( / s t a t u s \s + n o - c o n t e x t / ) ;
4375+ } ) ;
4376+
4377+ it ( "ios-sim preview-current renders the currently selected simulator preview" , ( ) => {
4378+ const plan = expectExecutePlan ( buildCliPlan ( [
4379+ "ios-sim" ,
4380+ "preview-current" ,
4381+ "--source" ,
4382+ "Views/HomeView.swift" ,
4383+ "--line" ,
4384+ "44" ,
4385+ "--label" ,
4386+ "Settings" ,
4387+ "--component-id" ,
4388+ "settings-row" ,
4389+ "--tab" ,
4390+ "tab-1" ,
4391+ "--timeout" ,
4392+ "30" ,
4393+ "--project-root" ,
4394+ "/tmp/app" ,
4395+ ] ) ) ;
4396+ expect ( plan . steps [ 0 ] ?. params ) . toMatchObject ( {
4397+ arguments : {
4398+ domain : "ios_simulator" ,
4399+ action : "renderCurrentPreview" ,
4400+ args : {
4401+ projectRoot : "/tmp/app" ,
4402+ sourceFile : "Views/HomeView.swift" ,
4403+ sourceLine : 44 ,
4404+ elementLabel : "Settings" ,
4405+ componentId : "settings-row" ,
4406+ tabIdentifier : "tab-1" ,
4407+ timeoutSec : 30 ,
4408+ } ,
4409+ } ,
4410+ } ) ;
42664411 } ) ;
42674412
42684413 it ( "ios-sim preview-render requires a source file and forwards render options" , ( ) => {
0 commit comments