diff --git a/bridge/native/ReaNativeUI.swift b/bridge/native/ReaNativeUI.swift index eed5bfd7f5..b123a57f24 100644 --- a/bridge/native/ReaNativeUI.swift +++ b/bridge/native/ReaNativeUI.swift @@ -49,7 +49,7 @@ func observe(_ request: Request) async throws -> [String: Any] { guard let url = app.executableURL else { try fail("application-executable-unavailable", "The selected application does not expose an executable URL; select a PID with an identifiable application executable") } - guard let canonicalPointer = realpath(url.path, nil) else { try fail("target-mismatch", "Running executable path cannot be resolved") } + guard let canonicalPointer = realpath(url.path, nil) else { try fail("executable-path-unavailable", "Running executable path cannot be resolved") } defer { free(canonicalPointer) } let canonicalExecutable = String(cString: canonicalPointer) guard canonicalExecutable == request.executable else { diff --git a/docs/native-investigation.md b/docs/native-investigation.md index a41180af69..df190bfafe 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -738,8 +738,10 @@ including unsafe integers, negative zero and non-finite numbers, remain null with a path-specific gap and incomplete capture coverage. Null alone does not establish that an accessibility value was successfully read or absent. -An initial PID or window selection refusal is `invalid_request` on `pid` or -`window_id` and leaves provider availability intact. Identity or window loss +An initial PID or window mismatch is `invalid_request` on `pid` or +`window_id` and leaves provider availability intact. An unavailable executable +URL or unresolved executable path leaves identity unknown and reports +`capability_unavailable`. Identity or window loss after the initial capture remains an observation failure with collected facts. Permission and other helper failures retain their specific remediation text. diff --git a/src/native/NativeUiObservation.ts b/src/native/NativeUiObservation.ts index c18ae5034c..7067201413 100644 --- a/src/native/NativeUiObservation.ts +++ b/src/native/NativeUiObservation.ts @@ -52,7 +52,6 @@ const responseSchema = z.discriminatedUnion("ok", [ ]); const initialSelectionFields = new Map([ ["pid-not-application", "pid"], - ["application-executable-unavailable", "pid"], ["target-mismatch", "pid"], ["window-mismatch", "window_id"], ["ambiguous-window", "window_id"], diff --git a/tests/boundary/mcp/nativeUiFactsMcp.test.ts b/tests/boundary/mcp/nativeUiFactsMcp.test.ts index 6073aa8d17..1175ef8a7c 100644 --- a/tests/boundary/mcp/nativeUiFactsMcp.test.ts +++ b/tests/boundary/mcp/nativeUiFactsMcp.test.ts @@ -137,7 +137,6 @@ it("keeps native UI availability after initial selection errors and preserves la const { client, replies, snapshot } = await connectNativeUi(); for (const [code, field] of [ ["pid-not-application", "pid"], - ["application-executable-unavailable", "pid"], ["target-mismatch", "pid"], ["window-mismatch", "window_id"], ] as const) @@ -219,3 +218,24 @@ it("keeps native UI availability after initial selection errors and preserves la }); await client.ping(); }); + +it.each([ + "application-executable-unavailable", + "executable-path-unavailable", +])("preserves unknown executable identity from %s", async (code) => { + const { client, replies } = await connectNativeUi(); + const message = `Fixture ${code} diagnostic`; + replies.push({ ok: false, code, message }); + const response = await client.callTool({ + name: "observe_native_ui", + arguments: scope, + }); + expect(parseMcpToolError(response)).toMatchObject({ + error: { + code: "capability_unavailable", + message, + details: { reason: `${code}: ${message}` }, + }, + }); + await client.ping(); +});