























@@ -8,6 +8,11 @@ import type { ProviderPlugin } from "../plugins/types.js";
88import type { ProviderAuthMethod } from "../plugins/types.js";
99import type { ApplyAuthChoiceParams } from "./auth-choice.apply.types.js";
101011+type ResolveProviderInstallCatalogEntry =
12+typeof import("../plugins/provider-install-catalog.js").resolveProviderInstallCatalogEntry;
13+type EnsureOnboardingPluginInstalled =
14+typeof import("../commands/onboarding-plugin-install.js").ensureOnboardingPluginInstalled;
15+1116const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => []));
1217const resolveProviderPluginChoice = vi.hoisted(() =>
1318vi.fn<() => { provider: ProviderPlugin; method: ProviderAuthMethod } | null>(),
@@ -60,6 +65,30 @@ vi.mock("../plugins/provider-oauth-flow.js", () => ({
6065 createVpsAwareOAuthHandlers,
6166}));
626768+const resolveProviderInstallCatalogEntry = vi.hoisted(() =>
69+vi.fn<ResolveProviderInstallCatalogEntry>(() => undefined),
70+);
71+vi.mock("../plugins/provider-install-catalog.js", () => ({
72+ resolveProviderInstallCatalogEntry,
73+}));
74+75+const ensureOnboardingPluginInstalled = vi.hoisted(() =>
76+vi.fn<EnsureOnboardingPluginInstalled>(async ({ cfg, entry }) => ({
77+ cfg,
78+installed: false,
79+pluginId: entry?.pluginId ?? "missing-plugin",
80+status: "skipped",
81+})),
82+);
83+vi.mock("../commands/onboarding-plugin-install.js", () => ({
84+ ensureOnboardingPluginInstalled,
85+}));
86+87+const clearPluginDiscoveryCache = vi.hoisted(() => vi.fn());
88+vi.mock("../plugins/discovery.js", () => ({
89+ clearPluginDiscoveryCache,
90+}));
91+6392const LOCAL_PROVIDER_ID = "local-provider";
6493const LOCAL_PROVIDER_LABEL = "Local Provider";
6594const LOCAL_AUTH_METHOD_ID = "local";
@@ -111,6 +140,13 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
111140beforeEach(() => {
112141vi.clearAllMocks();
113142applyAuthProfileConfig.mockImplementation((config) => config);
143+resolveProviderInstallCatalogEntry.mockReturnValue(undefined);
144+ensureOnboardingPluginInstalled.mockImplementation(async ({ cfg, entry }) => ({
145+ cfg,
146+installed: false,
147+pluginId: entry?.pluginId ?? "missing-plugin",
148+status: "skipped",
149+}));
114150});
115151116152it("returns an agent model override when default model application is deferred", async () => {
@@ -252,6 +288,125 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
252288});
253289});
254290291+it("installs a missing provider plugin and retries setup resolution", async () => {
292+const provider = buildProvider();
293+resolveProviderInstallCatalogEntry.mockReturnValue({
294+pluginId: "local-provider-plugin",
295+providerId: LOCAL_PROVIDER_ID,
296+methodId: LOCAL_AUTH_METHOD_ID,
297+choiceId: LOCAL_PROVIDER_ID,
298+choiceLabel: LOCAL_PROVIDER_LABEL,
299+label: LOCAL_PROVIDER_LABEL,
300+origin: "bundled",
301+install: {
302+npmSpec: "@openclaw/local-provider",
303+},
304+});
305+ensureOnboardingPluginInstalled.mockResolvedValue({
306+cfg: {
307+plugins: {
308+entries: {
309+"local-provider-plugin": {
310+enabled: true,
311+},
312+},
313+},
314+},
315+installed: true,
316+pluginId: "local-provider-plugin",
317+status: "installed",
318+});
319+resolvePluginProviders.mockReturnValue([provider]);
320+resolveProviderPluginChoice.mockReturnValueOnce(null).mockReturnValueOnce({
321+ provider,
322+method: provider.auth[0],
323+});
324+325+const result = await applyAuthChoiceLoadedPluginProvider(buildParams());
326+327+expect(ensureOnboardingPluginInstalled).toHaveBeenCalledWith(
328+expect.objectContaining({
329+entry: expect.objectContaining({
330+pluginId: "local-provider-plugin",
331+label: LOCAL_PROVIDER_LABEL,
332+}),
333+workspaceDir: "/tmp/workspace",
334+}),
335+);
336+expect(clearPluginDiscoveryCache).toHaveBeenCalledOnce();
337+expect(resolvePluginProviders).toHaveBeenCalledTimes(2);
338+expect(result?.config.agents?.defaults?.model).toEqual({
339+primary: LOCAL_DEFAULT_MODEL,
340+});
341+});
342+343+it("does not persist plugin enablement when install is skipped", async () => {
344+resolveProviderInstallCatalogEntry.mockReturnValue({
345+pluginId: "local-provider-plugin",
346+providerId: LOCAL_PROVIDER_ID,
347+methodId: LOCAL_AUTH_METHOD_ID,
348+choiceId: LOCAL_PROVIDER_ID,
349+choiceLabel: LOCAL_PROVIDER_LABEL,
350+label: LOCAL_PROVIDER_LABEL,
351+origin: "bundled",
352+install: {
353+npmSpec: "@openclaw/local-provider",
354+},
355+});
356+resolveProviderPluginChoice.mockReturnValue(null);
357+358+const result = await applyAuthChoiceLoadedPluginProvider(buildParams());
359+360+expect(ensureOnboardingPluginInstalled).toHaveBeenCalledOnce();
361+expect(result).toEqual({ config: {}, retrySelection: true });
362+});
363+364+it("preserves install config when the chosen provider still cannot resolve after install", async () => {
365+resolveProviderInstallCatalogEntry.mockReturnValue({
366+pluginId: "local-provider-plugin",
367+providerId: LOCAL_PROVIDER_ID,
368+methodId: LOCAL_AUTH_METHOD_ID,
369+choiceId: LOCAL_PROVIDER_ID,
370+choiceLabel: LOCAL_PROVIDER_LABEL,
371+label: LOCAL_PROVIDER_LABEL,
372+origin: "bundled",
373+install: {
374+npmSpec: "@openclaw/local-provider",
375+},
376+});
377+ensureOnboardingPluginInstalled.mockResolvedValue({
378+cfg: {
379+plugins: {
380+entries: {
381+"local-provider-plugin": {
382+enabled: true,
383+},
384+},
385+},
386+},
387+installed: true,
388+pluginId: "local-provider-plugin",
389+status: "installed",
390+});
391+resolveProviderPluginChoice.mockReturnValue(null);
392+393+const result = await applyAuthChoiceLoadedPluginProvider(buildParams());
394+395+expect(clearPluginDiscoveryCache).toHaveBeenCalledOnce();
396+expect(result).toEqual({
397+config: {
398+plugins: {
399+entries: {
400+"local-provider-plugin": {
401+enabled: true,
402+},
403+},
404+},
405+},
406+retrySelection: true,
407+});
408+});
409+255410it("merges provider config patches and emits provider notes", async () => {
256411applyAuthProfileConfig.mockImplementation(((
257412config: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。