fix(provider-selection): name whether a rejection came from availability or target support (#7220)

Distinguish local provider readiness failures from target-support refusals in shared CLI/MCP diagnostics, preserving original codes and evidence. Avoid claiming configured providers are unconfigured.

120 focused tests, static/docs checks and real ELF CLI/MCP verification pass. Hosted CI has the same four ADB failures as exact main d9908fd7a; three canceled shards remain unverified. Merge uses the user's explicit authorization to bypass proven main failures. Fresh Codex and Greptile reviews are clear.

Fixes #7214

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
YingNingandgensokyo-dev authored and GitHub committed 2026-10-26 23:49:11 +08:00
1 parent a5ad164283
commit 4da7e46e9c
6 files changed
+187 -17

No files matched your search

@@ -433,6 +433,131 @@ describe("analysis provider registry: selection failures", () => {
});
});
describe("analysis provider registry: rejection origins", () => {
it("distinguishes recorded readiness failures from an unobserved target refusal", async () => {
const registry = new AnalysisProviderRegistry([
candidate("alpha", { available: false, unconfigured: true }).provider,
candidate("gamma", { available: false, unconfigured: true }).provider,
]);
const selection = await registry.select(DATABASE_TARGET);
expect(selection.ok).toBe(true);
if (!selection.ok) return;
const projected = projectAnalysisError(
registry.unboundOperationError("binary_overview", selection.value),
);
expect(projected.message).toContain(
"Provider selection failed local readiness checks. No target-support refusal was reported.",
);
});
it("reports a configured provider's readiness failure without hiding a ready alternative", async () => {
const invalid = candidate("alpha", { available: false });
const ready = candidate("beta");
const registry = new AnalysisProviderRegistry([
{
...invalid.provider,
inspectAvailability: () => ({
status: "unavailable",
code: "open_options_invalid",
reason: "Configured registration is invalid",
diagnostics: { registration_path: "/configured/alpha.json" },
}),
},
ready.provider,
]);
const selected = await registry.select(DATABASE_TARGET, "alpha");
if (selected.ok)
throw new Error("Expected the selected provider to refuse");
const projected = projectAnalysisError(selected.error);
expect(projected.message).toContain("local readiness");
expect(projected.message).not.toContain(
"No analysis provider is configured",
);
expect(projected.details).toMatchObject({
requested_provider_id: "alpha",
candidate_ids: ["alpha", "beta"],
rejections: [
{
provider_id: "alpha",
origin: "availability",
code: "open_options_invalid",
reason: "Configured registration is invalid",
diagnostics: { registration_path: "/configured/alpha.json" },
},
],
});
expect((await registry.select(DATABASE_TARGET, "beta")).ok).toBe(true);
expect(invalid.created).toEqual([]);
});
it("names whether a rejection came from provider availability or target support", async () => {
const availabilityOnly = new AnalysisProviderRegistry([
candidate("alpha", { available: false, unconfigured: true }).provider,
candidate("gamma", { available: false, unconfigured: true }).provider,
]);
const availabilitySelection =
await availabilityOnly.select(DATABASE_TARGET);
expect(availabilitySelection.ok).toBe(true);
if (availabilitySelection.ok) {
const { details } = projectAnalysisError(
availabilityOnly.unboundOperationError(
"binary_overview",
availabilitySelection.value,
),
);
const rejections = details?.rejections as unknown as readonly {
origin: string;
}[];
expect(rejections.map(({ origin }) => origin)).toEqual([
"availability",
"availability",
]);
}
const unconfiguredButAdmitting: AnalysisProviderCandidate = {
...candidate("alpha", { available: false, unconfigured: true }).provider,
inspectTargetSupport: () => ({
status: "supported",
code: null,
reason: null,
diagnostics: { target_kind: "archive" },
}),
};
const refusingTarget: AnalysisProviderCandidate = {
...candidate("beta").provider,
inspectTargetSupport: () => ({
status: "unsupported",
code: "target_format_unsupported",
reason: "beta refuses archives",
diagnostics: { target_kind: "archive" },
}),
};
const mixed = new AnalysisProviderRegistry([
unconfiguredButAdmitting,
refusingTarget,
]);
const mixedSelection = await mixed.select(ARTIFACT_TARGET);
expect(mixedSelection.ok).toBe(true);
if (mixedSelection.ok) {
const projected = projectAnalysisError(
mixed.unboundOperationError("binary_overview", mixedSelection.value),
);
expect(projected.message).toContain(
"The selected analysis provider is unavailable",
);
const { details } = projected;
const rejections = details?.rejections as unknown as readonly {
origin: string;
code: string;
}[];
expect(rejections).toMatchObject([
{ origin: "availability", code: "not_configured" },
{ origin: "target", code: "target_format_unsupported" },
]);
}
});
});
describe("analysis provider registry: profile and cleanup failures", () => {
it("does not bind a profile with unresolved version or adapter failure", async () => {
const unresolved = candidate("unresolved", { profile: "missing" });
@@ -21,6 +21,7 @@ import {
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
ProviderSelectionError,
type ProviderRejectionOrigin,
type ProviderSelectionErrorOptions,
type ProviderSelectionFailureReason,
type ProviderSelectionRejection,
@@ -880,21 +881,43 @@ const selectionError = (
const candidateRejections = (
candidates: readonly AnalysisProviderCandidateStatus[],
): readonly ProviderSelectionRejection[] =>
candidates.flatMap((candidate) =>
[
...(candidate.availability.status === "unavailable"
? [candidate.availability]
: []),
...(candidate.targetSupport.status === "unsupported"
? [candidate.targetSupport]
: []),
].map((rejection) => ({
providerId: candidate.provider.id,
code: rejection.code,
reason: rejection.reason,
diagnostics: structuredClone(rejection.diagnostics),
})),
);
candidates.flatMap((candidate) => {
const rejections: ProviderSelectionRejection[] = [];
if (candidate.availability.status === "unavailable")
rejections.push(
selectedRejection(candidate, "availability", candidate.availability),
);
if (candidate.targetSupport.status === "unsupported")
rejections.push(
selectedRejection(candidate, "target", candidate.targetSupport),
);
return rejections;
});
/** One availability or target-support observation that rejected a candidate. */
type RejectionObservation =
| Extract<
AnalysisProviderCandidateStatus["availability"],
{ status: "unavailable" }
>
| Extract<
AnalysisProviderCandidateStatus["targetSupport"],
{ status: "unsupported" }
>;
/** Name the observation a rejection came from so availability cannot read as a refusal. */
const selectedRejection = (
candidate: AnalysisProviderCandidateStatus,
origin: ProviderRejectionOrigin,
rejection: RejectionObservation,
): ProviderSelectionRejection => ({
providerId: candidate.provider.id,
origin,
code: rejection.code,
reason: rejection.reason,
// ProviderSelectionError owns the single defensive diagnostic copy.
diagnostics: rejection.diagnostics,
});
const cloneAvailability = (
availability: AnalysisProviderCandidateStatus["availability"],