diff --git a/src/kuna/KunaProjectReader.ts b/src/kuna/KunaProjectReader.ts index d4608b11f2..6b534ef6e0 100644 --- a/src/kuna/KunaProjectReader.ts +++ b/src/kuna/KunaProjectReader.ts @@ -4,7 +4,6 @@ import { join } from "node:path"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { - AnalysisCapabilityUnavailableError, AnalysisCancelledError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; @@ -19,6 +18,7 @@ import { } from "./KunaProjectFiles.js"; import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** The exact producing range for one facet, including a failed C block. */ export interface KunaProjectFileSpan { @@ -134,8 +134,7 @@ export class KunaProjectReader { const span = index.value.code.get(address); if (span === undefined) return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, + kunaUnavailable( operation, `Kuna's export lists no code at ${address}; import slots and data entries have no listing.`, ), diff --git a/src/kuna/KunaProvider.ts b/src/kuna/KunaProvider.ts index ac5c0bc5f8..c2a103431a 100644 --- a/src/kuna/KunaProvider.ts +++ b/src/kuna/KunaProvider.ts @@ -8,10 +8,7 @@ import type { ProviderAvailability, ProviderTargetSupport, } from "../application/AnalysisProvider.js"; -import { - AnalysisCancelledError, - AnalysisCapabilityUnavailableError, -} from "../domain/analysisErrorCore.js"; +import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { createAnalysisProfile, type AnalysisProfileCommitment, @@ -33,6 +30,7 @@ import { import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js"; import { KunaRunSlots } from "./KunaRunSlots.js"; import { KunaSessionClient } from "./KunaSessionClient.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** Architectures REA verified Kuna on: x86-64 and AArch64 ELF and Mach-O, plus ARM Thumb. */ const VERIFIED_ARCHITECTURES: ReadonlySet = new Set([ @@ -169,14 +167,7 @@ export class KunaProvider implements AnalysisProviderCandidate { ...(options?.signal === undefined ? {} : { signal: options.signal }), }); if (!installation.ok) - return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - "open_binary", - installation.error.reason, - { userMessage: installation.error.reason }, - ), - ); + return err(kunaUnavailable("open_binary", installation.error.reason)); const { value } = installation; return ok({ profile: createAnalysisProfile( @@ -218,13 +209,7 @@ export class KunaProvider implements AnalysisProviderCandidate { if (!configuration.ok) return { execute: async (operation) => - err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - operation, - configuration.error.reason, - ), - ), + err(kunaUnavailable(operation, configuration.error.reason)), close: async () => ok(null), }; return new KunaSessionClient(target, profile, { diff --git a/src/kuna/KunaReferenceQueries.ts b/src/kuna/KunaReferenceQueries.ts index ffc8b45b4d..44256f705e 100644 --- a/src/kuna/KunaReferenceQueries.ts +++ b/src/kuna/KunaReferenceQueries.ts @@ -1,7 +1,6 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisCapabilityUnavailableError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; import type { JsonValue } from "../domain/jsonValue.js"; @@ -21,6 +20,7 @@ import { } from "./KunaReferenceFiles.js"; import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; import type { KunaPendingQuery, KunaQueryWork } from "./KunaQueryWork.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** Charge raw JSON bytes conservatively; retained projections are smaller. */ const XREF_CACHE_BYTES = 16 * 1024 * 1024; @@ -158,8 +158,7 @@ export class KunaReferenceQueries { const node = graph.nodes.get(address); if (node === undefined) return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, + kunaUnavailable( operation, `Kuna's graph has no function node at ${address}; its relationships are unknown.`, ), diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index 07254050a2..e8a44a7627 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -69,6 +69,7 @@ import { KunaProjectReader, type KunaProjectFileSpan, } from "./KunaProjectReader.js"; +import { kunaUnavailable } from "./KunaUnavailable.js"; /** How often a request waiting on the project export reports that it still waits. */ const WAIT_PROGRESS_INTERVAL_MS = 10_000; @@ -278,13 +279,7 @@ export class KunaSessionClient implements AnalysisClient { options: ExecutionOptions = {}, ): Promise> { if (this.#closing) - return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - operation, - "This Kuna session is closed.", - ), - ); + return err(kunaUnavailable(operation, "This Kuna session is closed.")); return this.#own( this.#execute(operation, parameters, { ...options, @@ -312,8 +307,7 @@ export class KunaSessionClient implements AnalysisClient { return err(new AnalysisCancelledError(operation)); if (operation !== "health" && !isKunaOperation(operation)) return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, + kunaUnavailable( operation, "Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.", ), @@ -1229,14 +1223,7 @@ export class KunaSessionClient implements AnalysisClient { this.#installation ??= inspectKunaInstallation(this.#environment).then( (inspected): Result => { if (!inspected.ok) - return err( - new AnalysisCapabilityUnavailableError( - KUNA_PROVIDER_ID, - "health", - inspected.error.reason, - { userMessage: inspected.error.reason }, - ), - ); + return err(kunaUnavailable("health", inspected.error.reason)); const committed = this.#profile?.parameters["kuna_sha256"]; if (committed !== undefined && committed !== inspected.value.sha256) return err( diff --git a/src/kuna/KunaUnavailable.ts b/src/kuna/KunaUnavailable.ts new file mode 100644 index 0000000000..514c7c486c --- /dev/null +++ b/src/kuna/KunaUnavailable.ts @@ -0,0 +1,18 @@ +import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; +import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js"; +import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; + +/** + * A Kuna capability gap whose reason is what the caller needs to read. Without + * a user message the projection shows only "choose another analysis or + * target", which hides facts such as "no code at this address". + */ +export const kunaUnavailable = ( + operation: string, + reason: string, + options: AnalysisErrorOptions = {}, +): AnalysisCapabilityUnavailableError => + new AnalysisCapabilityUnavailableError(KUNA_PROVIDER_ID, operation, reason, { + ...options, + userMessage: reason, + }); diff --git a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts index 30aec2b7a1..426f18eb8a 100644 --- a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts +++ b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts @@ -80,3 +80,41 @@ it("reports a graph beyond REA's buffer once per session instead of rebuilding i expect(await client.close()).toEqual({ ok: true, value: null }); } }); + +it("says why Kuna cannot answer instead of a generic unavailable message", async () => { + const { open } = await session("rea-kuna-unavailable-reasons-"); + const client = open(); + try { + const cases = [ + { + operation: "procedure_assembly", + parameters: { procedure: "0x2000" }, + message: + "Kuna's export lists no code at 0x2000; import slots and data entries have no listing.", + }, + { + operation: "procedure_callers", + parameters: { procedure: "0x1004" }, + message: + "Kuna's graph has no function node at 0x1004; its relationships are unknown.", + }, + { + operation: "list_segments", + parameters: {}, + message: + "Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.", + }, + ] as const; + for (const { operation, parameters, message } of cases) { + const result = await client.execute(operation, parameters); + if (result.ok) throw new Error(`${operation} unexpectedly succeeded`); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "capability_unavailable", + message, + retryable: false, + }); + } + } finally { + expect(await client.close()).toEqual({ ok: true, value: null }); + } +});