Skip to content

Commit 73b5b89

Browse files
committed
Harden runtime containment and mobile sync
1 parent 9c9b0f3 commit 73b5b89

45 files changed

Lines changed: 2476 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: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
84114
describe("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(/status\s+missing-preview/);
42654339
expect(output).toMatch(/suggested file\s+apps\/ios\/ADE\/Views\/HomePreviews\.swift/);
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(/status\s+no-context/);
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

Comments
 (0)