Skip to content

Commit 33cd1a2

Browse files
committed
fix(onboard): check the provider mapping on every profile path
The unmapped-backend check only guarded an explicit --profile, but the installer and resume paths produce provenance that reaches the same environment application. An unmapped backend arriving that way would set the preset while leaving the provider unresolved — the silent fall-through to the provider menu this change set fixes. Check the profile the run actually settles on, after installer and resume resolution, so all three paths converge on the same authoritative result. Signed-off-by: Yanyun Liao <yanyunl@nvidia.com>
1 parent 67f00e4 commit 33cd1a2

2 files changed

Lines changed: 59 additions & 12 deletions

File tree

src/lib/onboard/command.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,44 @@ describe("onboard command options", () => {
794794
expect(env.NEMOCLAW_PROVIDER).toBeUndefined();
795795
});
796796

797+
it("rejects an unmapped backend on the resume path too (#9313)", () => {
798+
// Explicit --profile, the installer path, and resume all converge on the
799+
// same environment application, so the check lives at the end of the
800+
// lifecycle rather than on the explicit path alone. Resume replays a
801+
// recorded profile: a backend with no provider must be reported instead of
802+
// resuming into the provider menu.
803+
const catalog = loadServingCatalog();
804+
// Retarget preset and recipe together; provenance requires them to agree.
805+
const patchedCatalog = {
806+
...catalog,
807+
presets: catalog.presets.map((preset) => ({
808+
...preset,
809+
spec: { ...preset.spec, plan: { ...preset.spec.plan, backend: "future-backend" } },
810+
})),
811+
recipes: catalog.recipes.map((recipe) => ({
812+
...recipe,
813+
spec: { ...recipe.spec, backend: "future-backend" },
814+
})),
815+
};
816+
const recorded = servingProfileProvenance(
817+
patchedCatalog as never,
818+
catalog.presets[0]!.metadata.id,
819+
);
820+
const errors: string[] = [];
821+
822+
expect(() =>
823+
resolve(
824+
{ resume: true },
825+
{
826+
loadServingCatalog: () => patchedCatalog as never,
827+
loadSession: () => ({ servingProfileProvenance: recorded }) as never,
828+
error: (message = "") => errors.push(message),
829+
},
830+
),
831+
).toThrow("exit:1");
832+
expect(errors.join("\n")).toContain("which onboarding cannot configure");
833+
});
834+
797835
it("maps each serving backend to the provider that can run it (#9313)", () => {
798836
// A backend with no provider returns null, which `resolveServingProfile`
799837
// reports instead of accepting the flag and then asking for a provider.

src/lib/onboard/command.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -275,16 +275,7 @@ function resolveServingProfile(
275275
throw error;
276276
}
277277
validateServingProfileConflicts(selectedProfileId, deps);
278-
const provenance = servingProfileProvenance(catalog, selectedProfileId);
279-
if (!servingProfileProviderKey(provenance)) {
280-
// Reporting this beats the old behaviour of accepting the flag and then
281-
// asking which provider to use, which left the profile silently unapplied.
282-
fail(
283-
deps,
284-
` Serving profile '${selectedProfileId}' uses backend '${provenance.recipe.backend}', which onboarding cannot configure with --profile.`,
285-
);
286-
}
287-
return provenance;
278+
return servingProfileProvenance(catalog, selectedProfileId);
288279
}
289280

290281
function resolveInstallerServingProfile(
@@ -323,8 +314,26 @@ function resolveServingProfileLifecycle(
323314
);
324315
}
325316
const requested = explicit ?? installerProfile;
326-
if (!resume) return requested;
327-
return resolveResumedServingProfile(requested, deps);
317+
const settled = resume ? resolveResumedServingProfile(requested, deps) : requested;
318+
// Check the profile the run will actually apply, not just an explicit
319+
// --profile: the installer and resume paths reach the same environment
320+
// application, and an unmapped backend there would set the preset while
321+
// leaving the provider unresolved — the silent fall-through to the provider
322+
// menu this fixes (#9313).
323+
return assertServingProfileProviderSupported(settled, deps);
324+
}
325+
326+
function assertServingProfileProviderSupported(
327+
provenance: ServingProfileProvenance | null,
328+
deps: ResolveOnboardOptionsDeps,
329+
): ServingProfileProvenance | null {
330+
const unsupported = provenance !== null && servingProfileProviderKey(provenance) === null;
331+
return unsupported
332+
? fail(
333+
deps,
334+
` Serving profile '${provenance.preset.id}' uses backend '${provenance.recipe.backend}', which onboarding cannot configure.`,
335+
)
336+
: provenance;
328337
}
329338

330339
function activeServingProfileId(provenance: ServingProfileProvenance | null): string | null {

0 commit comments

Comments
 (0)