From b594fbc3e739ade1fbb9ce38846ed1c3a991273e Mon Sep 17 00:00:00 2001
From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Date: Wed, 28 Oct 2026 13:28:03 +0800
Subject: [PATCH] fix(apple): scope application roots to the inventory format
(#7714)
---
src/domain/apple/appleApplication.test.ts | 40 +++++-
src/domain/apple/appleBundleAnatomy.ts | 13 +-
.../cli/appleContainerRootCli.test.ts | 114 ++++++++++++++++++
3 files changed, 160 insertions(+), 7 deletions(-)
create mode 100644 tests/boundary/cli/appleContainerRootCli.test.ts
diff --git a/src/domain/apple/appleApplication.test.ts b/src/domain/apple/appleApplication.test.ts
index 0fedfde02b..72fcb1509f 100644
--- a/src/domain/apple/appleApplication.test.ts
+++ b/src/domain/apple/appleApplication.test.ts
@@ -685,7 +685,7 @@ describe("Apple application bridge candidates", () => {
),
);
const result = project(
- inventoryEvidence("zip", "Fixture.zip", [...scripts, ...native]),
+ inventoryEvidence("ipa", "Fixture.ipa", [...scripts, ...native]),
);
expect(result.components.javascript).toHaveLength(129);
@@ -970,3 +970,41 @@ it("preserves observed unavailable bundle metadata and signing paths without inv
expect(result.platforms).toEqual(["ios"]);
expect(result.unavailable_components).toHaveLength(3);
});
+
+it.each(["zip", "directory", "dmg"] as const)(
+ "does not apply IPA roots to a %s inventory",
+ (format) => {
+ const result = project(
+ inventoryEvidence(format, `Sample.${format}`, [
+ { path: "Payload/App.app/Info.plist", format: "plist" },
+ { path: "Extras/Other.app/Info.plist", format: "plist" },
+ ]),
+ );
+ expect(result.root_format).toBe(format);
+ expect(result.application_roots).toEqual([]);
+ expect(result.platforms).toEqual([]);
+ expect(result.bundles).toEqual([]);
+ },
+);
+
+it.each(["ipa", "zip", "directory", "dmg"] as const)(
+ "uses only the %s root convention in a mixed inventory",
+ (format) => {
+ const source = inventoryEvidence(format, `Mixed.${format}`, [
+ { path: "Payload/App.app/Info.plist", format: "plist" },
+ { path: "Extras/Other.app/Contents/Info.plist", format: "plist" },
+ macho("Extras/Other.app/Contents/MacOS/Other"),
+ {
+ path: "Extras/Other.app/Contents/Resources/app.js",
+ format: "javascript-bundle",
+ },
+ ]);
+ const result = project(source);
+ expect(result.application_roots).toEqual([
+ format === "ipa" ? "Payload/App.app" : "Extras/Other.app",
+ ]);
+ expect(result.platforms).toEqual([format === "ipa" ? "ios" : "macos"]);
+ expect(result.components.javascript).toHaveLength(1);
+ expect(result.components.executables).toHaveLength(1);
+ },
+);
diff --git a/src/domain/apple/appleBundleAnatomy.ts b/src/domain/apple/appleBundleAnatomy.ts
index 2265938f0d..6be7696292 100644
--- a/src/domain/apple/appleBundleAnatomy.ts
+++ b/src/domain/apple/appleBundleAnatomy.ts
@@ -123,12 +123,13 @@ const applicationRoots = (
for (const path of directoryPaths(tree)) {
const segments = path.split("/");
const app = segments[1];
- if (
- anatomyNameIs(segments[0] ?? "", "Payload") &&
- app !== undefined &&
- /\.app$/iu.test(app)
- ) {
- found.add(`${segments[0]}/${app}`);
+ if (subject.format === "ipa") {
+ if (
+ anatomyNameIs(segments[0] ?? "", "Payload") &&
+ app !== undefined &&
+ /\.app$/iu.test(app)
+ )
+ found.add(`${segments[0]}/${app}`);
continue;
}
const index = segments.findIndex(
diff --git a/tests/boundary/cli/appleContainerRootCli.test.ts b/tests/boundary/cli/appleContainerRootCli.test.ts
new file mode 100644
index 0000000000..1426745b8c
--- /dev/null
+++ b/tests/boundary/cli/appleContainerRootCli.test.ts
@@ -0,0 +1,114 @@
+import { readFile, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import {
+ TextReader,
+ Uint8ArrayReader,
+ Uint8ArrayWriter,
+ ZipWriter,
+} from "@zip.js/zip.js";
+import { expect } from "vitest";
+import { appleApplicationProjectionResultSchema } from "../../../src/domain/apple/appleApplication.js";
+import { parseEvidence } from "../../../src/domain/evidence.js";
+import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
+import { cliTest } from "../../support/cli/cliFixture.js";
+
+cliTest.for([
+ {
+ name: "flat ZIP",
+ extension: "zip",
+ payload: true,
+ deep: false,
+ roots: [],
+ platforms: [],
+ },
+ {
+ name: "flat IPA",
+ extension: "ipa",
+ payload: true,
+ deep: false,
+ roots: ["Payload/Main.app"],
+ platforms: ["ios"],
+ },
+ {
+ name: "deep ZIP",
+ extension: "zip",
+ payload: false,
+ deep: true,
+ roots: ["Extras/Helper.app"],
+ platforms: ["macos"],
+ },
+ {
+ name: "IPA with unrooted deep helper",
+ extension: "ipa",
+ payload: true,
+ deep: true,
+ roots: ["Payload/Main.app"],
+ platforms: ["ios"],
+ },
+])(
+ "keeps format-specific Apple roots through CLI and MCP ($name)",
+ async (fixture, { cli }) => {
+ const root = await createTestTempDirectory("rea-apple-root-format-");
+ const path = join(root, `Sample.${fixture.extension}`);
+ const writer = new ZipWriter(new Uint8ArrayWriter());
+ if (fixture.payload)
+ await writer.add(
+ "Payload/Main.app/Info.plist",
+ new TextReader(""),
+ );
+ await writer.add(
+ "Extras/Other.app/Info.plist",
+ new TextReader(""),
+ );
+ if (fixture.deep) {
+ await writer.add(
+ "Extras/Helper.app/Contents/Info.plist",
+ new TextReader(""),
+ );
+ const macho = Buffer.concat([
+ Buffer.from("cffaedfe070000010300008002000000", "hex"),
+ Buffer.alloc(40),
+ ]);
+ await writer.add(
+ "Extras/Helper.app/Contents/MacOS/Helper",
+ new Uint8ArrayReader(macho),
+ );
+ await writer.add("Shared/other.js", new TextReader("console.log(2)"));
+ }
+ const bytes = await writer.close();
+ await writeFile(path, bytes);
+ const inspected = await cli.run({
+ arguments: ["inspect-artifact", path, "--json"],
+ });
+ expect(inspected.exitCode, inspected.stderr).toBe(0);
+ const source = parseEvidence(inspected.json);
+ const input = { inventory_evidence: [source] };
+ const request = join(root, "request.json");
+ await writeFile(request, JSON.stringify(input));
+ const projected = await cli.run({
+ arguments: ["project-apple-application-graph", request, "--json"],
+ });
+ expect(projected.exitCode, projected.stderr).toBe(0);
+ const cliGraph = appleApplicationProjectionResultSchema.parse(
+ parseEvidence(projected.json).normalized_result,
+ );
+ expect(cliGraph.root_format).toBe(fixture.extension);
+ expect(cliGraph.application_roots).toEqual(fixture.roots);
+ expect(cliGraph.platforms).toEqual(fixture.platforms);
+ if (fixture.deep && fixture.extension === "ipa")
+ expect(cliGraph.bridge_candidates).toContainEqual({
+ source_path: "Shared/other.js",
+ native_path: "Extras/Helper.app/Contents/MacOS/Helper",
+ basis: "javascript-and-native-content",
+ });
+ const call = await cli.sharedMcpCall?.();
+ if (call === undefined) throw new Error("Missing production MCP session");
+ const reply = await call("project_apple_application_graph", input);
+ expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
+ const mcpGraph = appleApplicationProjectionResultSchema.parse(
+ parseEvidence(reply.structuredContent).normalized_result,
+ );
+ expect(mcpGraph).toEqual(cliGraph);
+ expect(Buffer.compare(await readFile(path), Buffer.from(bytes))).toBe(0);
+ },
+);