From dcdab56acbeeaee827199e0042052bc50549fce1 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:33:15 +0800 Subject: [PATCH] fix(test): use eligible bundle targets for UI availability --- .../boundary/mcp/nativeUiAvailability.test.ts | 88 +++++++++++++++---- 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/tests/boundary/mcp/nativeUiAvailability.test.ts b/tests/boundary/mcp/nativeUiAvailability.test.ts index 8dcc1f6895..6e208f4434 100644 --- a/tests/boundary/mcp/nativeUiAvailability.test.ts +++ b/tests/boundary/mcp/nativeUiAvailability.test.ts @@ -1,4 +1,5 @@ -import { rm } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; import type { Client } from "@modelcontextprotocol/client"; import type { CallToolResult } from "@modelcontextprotocol/server"; @@ -23,6 +24,7 @@ import { } from "../../../src/domain/apple/interfaceBuilderGraph.js"; import { inspectNativeDispatchMetadata } from "../../../src/domain/native/objcSwiftMetadata.js"; import { nativeInvestigationTraceSchema } from "../../../src/domain/native/nativeInvestigationGraph.js"; +import { thinMach } from "../../../src/domain/binaryTarget.fixture.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; import { err, ok, type Result } from "../../../src/domain/result.js"; import type { AnalysisOperation } from "../../../src/application/AnalysisProvider.js"; @@ -33,13 +35,24 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { createSessionMcpHarness } from "./sessionMcpHarness.js"; const resources: Array<{ close(): Promise }> = []; -let directory: string | undefined; + +const writeEligibleAppBundle = async (root: string): Promise => { + const app = join(root, "Eligible.app"); + const contents = join(app, "Contents"); + await mkdir(join(contents, "MacOS"), { recursive: true }); + await writeFile( + join(contents, "MacOS", "Fixture"), + thinMach(0xfeedfacf, 0x0100000c), + ); + await writeFile( + join(contents, "Info.plist"), + "CFBundleExecutableFixture", + ); + return app; +}; afterEach(async () => { await Promise.all(resources.splice(0).map((resource) => resource.close())); - if (directory !== undefined) - await rm(directory, { recursive: true, force: true }); - directory = undefined; }); const identity = { @@ -205,16 +218,19 @@ describe("native UI action availability through available routes", () => { it.each(routeScenarios)( "reports the $name route available when its observed path succeeds", async (scenario) => { - directory = await createTestTempDirectory("rea-native-ui-availability-"); + const directory = await createTestTempDirectory( + "rea-native-ui-availability-", + ); const calls: ProviderOperation[] = []; - const { mcp, first } = await createSessionMcpHarness( + const { mcp } = await createSessionMcpHarness( directory, () => provider(scenario, calls), resources, ); + const app = await writeEligibleAppBundle(directory); const opened = await mcp.callTool({ name: "open_binary", - arguments: { path: first }, + arguments: { path: app }, }); expect(opened.isError).not.toBe(true); @@ -264,16 +280,19 @@ describe("native UI action availability for unmet routes", () => { ])( "reports unmet route sources for $name without hiding partial results", async (scenario) => { - directory = await createTestTempDirectory("rea-native-ui-no-metadata-"); + const directory = await createTestTempDirectory( + "rea-native-ui-no-metadata-", + ); const calls: ProviderOperation[] = []; - const { mcp, first } = await createSessionMcpHarness( + const { mcp } = await createSessionMcpHarness( directory, () => provider(scenario, calls), resources, ); + const app = await writeEligibleAppBundle(directory); const opened = await mcp.callTool({ name: "open_binary", - arguments: { path: first }, + arguments: { path: app }, }); expect(opened.isError).not.toBe(true); @@ -378,20 +397,21 @@ describe("native UI action availability for failures and target gates", () => { it.each([metadataRuntimeFailureScenario, decodeCancelledScenario])( "preserves runtime failures for $name without trying another declared source", async (scenario) => { - directory = await createTestTempDirectory( + const directory = await createTestTempDirectory( "rea-native-ui-source-failure-", ); const calls: ProviderOperation[] = []; - const { mcp, first } = await createSessionMcpHarness( + const { mcp } = await createSessionMcpHarness( directory, () => provider(scenario, calls), resources, ); + const app = await writeEligibleAppBundle(directory); expect( ( await mcp.callTool({ name: "open_binary", - arguments: { path: first }, + arguments: { path: app }, }) ).isError, ).not.toBe(true); @@ -426,7 +446,7 @@ describe("native UI action availability for failures and target gates", () => { ); it("reports target_required before querying provider operations", async () => { - directory = await createTestTempDirectory("rea-native-ui-no-target-"); + const directory = await createTestTempDirectory("rea-native-ui-no-target-"); const calls: ProviderOperation[] = []; const { mcp } = await createSessionMcpHarness( directory, @@ -441,6 +461,44 @@ describe("native UI action availability for failures and target gates", () => { }); expect(calls).toEqual([]); }); + + it("keeps authored UI dispatch unavailable for a non-bundle target", async () => { + const directory = await createTestTempDirectory( + "rea-native-ui-non-bundle-", + ); + const calls: ProviderOperation[] = []; + const { mcp } = await createSessionMcpHarness( + directory, + () => provider(authoredUiDirectScenario, calls), + resources, + ); + const executable = join(directory, "Standalone"); + await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c)); + expect( + ( + await mcp.callTool({ + name: "open_binary", + arguments: { path: executable }, + }) + ).isError, + ).not.toBe(true); + + try { + const entry = dispatchAvailability(await readInventory(mcp)); + const authored = entry?.modes?.find( + (mode) => mode.name === "authored_ui_dispatch", + ); + expect(authored).toMatchObject({ + available: false, + missing_operations: ["decode_interface_builder"], + }); + expect(calls).toEqual([]); + } finally { + expect( + (await mcp.callTool({ name: "close_binary", arguments: {} })).isError, + ).not.toBe(true); + } + }); }); const readInventory = async (mcp: Client) => {