Skip to content

Commit 594e5db

Browse files
authored
Mobile App Cleanup (#559)
* Refine iOS connection empty states * Fix chat scrollback and runtime fallback * Harden runtime containment and mobile sync
1 parent 66e351f commit 594e5db

51 files changed

Lines changed: 2892 additions & 574 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/ade-cli/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ ade --socket ios-sim apps --text
290290
ade --socket ios-sim launch --target target-id --text
291291
ade --socket ios-sim preview-match --source apps/ios/ADE/Views/Home.swift --line 42 --text
292292
ade --socket ios-sim preview-ensure --source apps/ios/ADE/Views/Home.swift --line 42 --text
293+
ade --socket ios-sim preview-current --text
293294
ade --socket ios-sim preview-render --source apps/ios/ADE/Views/Home.swift --index 0 --text
294295
ade --socket app-control launch --command "npm run dev" --text
295296
ade --socket app-control focus --text

apps/ade-cli/src/cli.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spawn } from "node:child_process";
12
import fs from "node:fs";
23
import os from "node:os";
34
import path from "node:path";
@@ -18,7 +19,9 @@ import {
1819
renderLaneGraph,
1920
resolveAdeCodeModulePath,
2021
resolveRoots,
22+
runCli,
2123
shouldAutoRegisterProjectForPlan,
24+
shouldBlockManualMachineRuntimeSpawn,
2225
shouldEnforceMachineRuntimeBuildCompatibility,
2326
shouldAttemptDesktopSocketConnection,
2427
summarizeExecution,
@@ -31,6 +34,8 @@ type ResolveRootsOptions = Parameters<typeof resolveRoots>[0];
3134
process.env.ADE_ENABLE_AUTOMATIONS = "1";
3235
process.env.ADE_ENABLE_MACOS_VM = "1";
3336

37+
const crdtHostIt = process.platform === "darwin" ? it : it.skip;
38+
3439
function withEnv<T>(updates: Record<string, string | undefined>, run: () => T): T {
3540
const previous = new Map<string, string | undefined>();
3641
for (const key of Object.keys(updates)) {
@@ -80,6 +85,35 @@ function expectExecutePlan(
8085
return plan;
8186
}
8287

88+
function writeSyncHostSingletonLock(args: {
89+
lockPath: string;
90+
pid: number;
91+
port: number;
92+
packageChannel: string | null;
93+
adeHome: string;
94+
}): void {
95+
const now = "2026-06-11T00:00:00.000Z";
96+
fs.mkdirSync(path.dirname(args.lockPath), { recursive: true });
97+
fs.writeFileSync(args.lockPath, `${JSON.stringify({
98+
version: 1,
99+
owner: {
100+
id: "other-channel-brain",
101+
pid: args.pid,
102+
port: args.port,
103+
appName: args.packageChannel === "beta" ? "ADE Beta" : "ADE",
104+
packageChannel: args.packageChannel,
105+
adeHome: args.adeHome,
106+
serviceName: args.packageChannel === "beta" ? "com.ade.runtime.beta" : "com.ade.runtime",
107+
socketPath: path.join(args.adeHome, "sock", "ade.sock"),
108+
projectRoot: "/Users/admin/Projects/ADE",
109+
commandLine: null,
110+
quitCommand: `ADE_HOME='${args.adeHome}' ade brain stop --text`,
111+
createdAt: now,
112+
updatedAt: now,
113+
},
114+
}, null, 2)}\n`, "utf8");
115+
}
116+
83117
describe("ADE CLI", () => {
84118
it("parses global options without stealing command flags", () => {
85119
const parsed = parseCliArgs([
@@ -330,6 +364,62 @@ describe("ADE CLI", () => {
330364
});
331365
});
332366

367+
crdtHostIt("serve fails instead of exiting successfully when another channel owns mobile sync", async () => {
368+
const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-serve-conflict-"));
369+
const projectRoot = path.join(adeHome, "project");
370+
const lockPath = path.join(adeHome, "sync-host-lock.json");
371+
const socketPath = path.join(adeHome, "sock", "ade.sock");
372+
fs.mkdirSync(projectRoot, { recursive: true });
373+
const originalEnv = {
374+
ADE_HOME: process.env.ADE_HOME,
375+
ADE_PROJECT_ROOT: process.env.ADE_PROJECT_ROOT,
376+
ADE_PACKAGE_CHANNEL: process.env.ADE_PACKAGE_CHANNEL,
377+
ADE_SYNC_HOST_LOCK_PATH: process.env.ADE_SYNC_HOST_LOCK_PATH,
378+
ADE_SYNC_HOST_SINGLETON_TEST_MODE: process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE,
379+
};
380+
const ownerProcess = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000);"], {
381+
stdio: "ignore",
382+
});
383+
ownerProcess.on("error", () => {});
384+
ownerProcess.unref();
385+
if (!ownerProcess.pid) {
386+
throw new Error("Failed to start fake sync-host owner process.");
387+
}
388+
389+
try {
390+
process.env.ADE_HOME = adeHome;
391+
process.env.ADE_PROJECT_ROOT = projectRoot;
392+
delete process.env.ADE_PACKAGE_CHANNEL;
393+
process.env.ADE_SYNC_HOST_LOCK_PATH = lockPath;
394+
process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE = "1";
395+
writeSyncHostSingletonLock({
396+
lockPath,
397+
pid: ownerProcess.pid,
398+
port: 8801,
399+
packageChannel: "beta",
400+
adeHome: path.join(os.homedir(), ".ade-beta"),
401+
});
402+
403+
await expect(runCli(["serve", "--socket", socketPath])).rejects.toThrow(
404+
"ADE brain refusing to run without mobile sync.",
405+
);
406+
expect(fs.existsSync(socketPath)).toBe(false);
407+
} finally {
408+
if (originalEnv.ADE_HOME === undefined) delete process.env.ADE_HOME;
409+
else process.env.ADE_HOME = originalEnv.ADE_HOME;
410+
if (originalEnv.ADE_PROJECT_ROOT === undefined) delete process.env.ADE_PROJECT_ROOT;
411+
else process.env.ADE_PROJECT_ROOT = originalEnv.ADE_PROJECT_ROOT;
412+
if (originalEnv.ADE_PACKAGE_CHANNEL === undefined) delete process.env.ADE_PACKAGE_CHANNEL;
413+
else process.env.ADE_PACKAGE_CHANNEL = originalEnv.ADE_PACKAGE_CHANNEL;
414+
if (originalEnv.ADE_SYNC_HOST_LOCK_PATH === undefined) delete process.env.ADE_SYNC_HOST_LOCK_PATH;
415+
else process.env.ADE_SYNC_HOST_LOCK_PATH = originalEnv.ADE_SYNC_HOST_LOCK_PATH;
416+
if (originalEnv.ADE_SYNC_HOST_SINGLETON_TEST_MODE === undefined) delete process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE;
417+
else process.env.ADE_SYNC_HOST_SINGLETON_TEST_MODE = originalEnv.ADE_SYNC_HOST_SINGLETON_TEST_MODE;
418+
ownerProcess.kill("SIGKILL");
419+
fs.rmSync(adeHome, { recursive: true, force: true });
420+
}
421+
});
422+
333423
it("recognizes the hidden PTY host worker entrypoint", () => {
334424
expect(buildCliPlan(["__ade-pty-host-worker"])).toEqual({
335425
kind: "pty-host-worker",
@@ -345,6 +435,19 @@ describe("ADE CLI", () => {
345435
expect(isEphemeralRuntimeSocketPath("tcp://127.0.0.1:8765")).toBe(false);
346436
});
347437

438+
it("blocks manual service-socket runtime spawn when service mutation is disabled", () => {
439+
expect(shouldBlockManualMachineRuntimeSpawn("/Users/example/.ade-beta/sock/ade.sock", {
440+
ADE_DISABLE_RUNTIME_SERVICE_INSTALL: "1",
441+
})).toBe(true);
442+
expect(shouldBlockManualMachineRuntimeSpawn("/Users/example/.ade-beta/sock/ade.sock", {})).toBe(false);
443+
expect(shouldBlockManualMachineRuntimeSpawn("tcp://127.0.0.1:9999", {
444+
ADE_DISABLE_RUNTIME_SERVICE_INSTALL: "1",
445+
})).toBe(false);
446+
expect(shouldBlockManualMachineRuntimeSpawn(path.join(os.tmpdir(), "ade-code-test", "ade.sock"), {
447+
ADE_DISABLE_RUNTIME_SERVICE_INSTALL: "1",
448+
})).toBe(false);
449+
});
450+
348451
it("parses runtime idle expiry with a minimum clamp", () => {
349452
expect(readRuntimeIdleExitMs({ ADE_RUNTIME_IDLE_EXIT_MS: "30000" } as NodeJS.ProcessEnv)).toBe(30_000);
350453
expect(readRuntimeIdleExitMs({ ADE_RUNTIME_IDLE_EXIT_MS: "100" } as NodeJS.ProcessEnv)).toBe(5_000);
@@ -4229,8 +4332,10 @@ describe("ADE CLI", () => {
42294332
it("formats preview-match and preview-ensure text as Preview Lab output", () => {
42304333
const matchPlan = expectExecutePlan(buildCliPlan(["ios-sim", "preview-match", "--source", "Views/HomeView.swift"]));
42314334
const ensurePlan = expectExecutePlan(buildCliPlan(["ios-sim", "preview-ensure"]));
4335+
const currentPlan = expectExecutePlan(buildCliPlan(["ios-sim", "preview-current"]));
42324336
expect(inferFormatter(matchPlan)).toBe("ios-sim-preview");
42334337
expect(inferFormatter(ensurePlan)).toBe("ios-sim-preview");
4338+
expect(inferFormatter(currentPlan)).toBe("ios-sim-preview");
42344339

42354340
const output = formatOutput({
42364341
status: "missing-preview",
@@ -4249,6 +4354,62 @@ describe("ADE CLI", () => {
42494354
expect(output).toContain("ADE iOS Preview match");
42504355
expect(output).toMatch(/status\s+missing-preview/);
42514356
expect(output).toMatch(/suggested file\s+apps\/ios\/ADE\/Views\/HomePreviews\.swift/);
4357+
4358+
const currentOutput = formatOutput({
4359+
ok: false,
4360+
match: {
4361+
status: "no-context",
4362+
confidence: "none",
4363+
target: null,
4364+
selectedSourceFile: null,
4365+
selectedSourceLine: null,
4366+
reason: "Select a source-backed simulator element first.",
4367+
},
4368+
target: null,
4369+
render: null,
4370+
error: "Select a source-backed simulator element first.",
4371+
}, {
4372+
text: true,
4373+
pretty: false,
4374+
} as any, "ios-sim-preview");
4375+
expect(currentOutput).toContain("ADE iOS Preview current");
4376+
expect(currentOutput).toMatch(/status\s+no-context/);
4377+
});
4378+
4379+
it("ios-sim preview-current renders the currently selected simulator preview", () => {
4380+
const plan = expectExecutePlan(buildCliPlan([
4381+
"ios-sim",
4382+
"preview-current",
4383+
"--source",
4384+
"Views/HomeView.swift",
4385+
"--line",
4386+
"44",
4387+
"--label",
4388+
"Settings",
4389+
"--component-id",
4390+
"settings-row",
4391+
"--tab",
4392+
"tab-1",
4393+
"--timeout",
4394+
"30",
4395+
"--project-root",
4396+
"/tmp/app",
4397+
]));
4398+
expect(plan.steps[0]?.params).toMatchObject({
4399+
arguments: {
4400+
domain: "ios_simulator",
4401+
action: "renderCurrentPreview",
4402+
args: {
4403+
projectRoot: "/tmp/app",
4404+
sourceFile: "Views/HomeView.swift",
4405+
sourceLine: 44,
4406+
elementLabel: "Settings",
4407+
componentId: "settings-row",
4408+
tabIdentifier: "tab-1",
4409+
timeoutSec: 30,
4410+
},
4411+
},
4412+
});
42524413
});
42534414

42544415
it("ios-sim preview-render requires a source file and forwards render options", () => {

0 commit comments

Comments
 (0)