Skip to content

Commit 5e933be

Browse files
committed
Harden runtime containment and mobile sync
1 parent 9c9b0f3 commit 5e933be

45 files changed

Lines changed: 2491 additions & 511 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: 145 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,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+
84115
describe("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(/status\s+missing-preview/);
42654354
expect(output).toMatch(/suggested file\s+apps\/ios\/ADE\/Views\/HomePreviews\.swift/);
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(/status\s+no-context/);
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

Comments
 (0)