feat(FN-3901): add builtin research web-search provider as default

The merge adds a builtin web-search research provider as the default, replacing the need for external research provider configuration and removing the setup gate for builtin defaults. Dashboard settings UX for research defaults was aligned (ResearchView, SettingsModal), CLI research commands were up

Fusion-Task-Id: FN-3901
This commit is contained in:
Fusion
2026-05-09 19:37:31 -07:00
committed by gsxdsm
parent 986aef3f8e
commit e69631ec24
21 changed files with 493 additions and 187 deletions

View File

@@ -1445,6 +1445,31 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
});
describe("research tools", () => {
it("fn_research_run treats builtin as configured when no provider is explicitly set", async () => {
const store = new TaskStore(tmpDir);
await store.init();
await store.updateGlobalSettings({
researchGlobalEnabled: true,
experimentalFeatures: { researchView: true } as Record<string, boolean>,
});
await store.updateSettings({
researchEnabled: true,
researchSettings: { enabled: true },
});
const tool = api.tools.get("fn_research_run")!;
const result = await tool.execute(
"research-run-builtin",
{ query: "builtin default" },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.details.setup).toBeNull();
expect(result.details.status).toBe("queued");
});
it("fn_research_list status parameter matches RESEARCH_RUN_STATUSES", () => {
const tool = api.tools.get("fn_research_list") as any;
const statusSchema = tool.parameters.properties.status;

View File

@@ -88,6 +88,24 @@ describe("research extension tools", () => {
expect(retryResult.details.setup.code).toBe("feature-disabled");
});
it("treats builtin as configured when no provider is explicitly set", async () => {
const store = new TaskStore(tmpDir);
await store.init();
await store.updateGlobalSettings({
experimentalFeatures: { researchView: true } as Record<string, boolean>,
researchGlobalEnabled: true,
});
await store.updateSettings({
researchSettings: { enabled: true },
});
const runTool = api.tools.get("fn_research_run")!;
const result = await runTool.execute("call-builtin", { query: "fusion" }, undefined, undefined, makeCtx(tmpDir));
expect(result.details.setup).toBeNull();
expect(result.details.status).toBe("queued");
});
it("returns actionable missing-credentials response", async () => {
const store = new TaskStore(tmpDir);
await store.init();

View File

@@ -82,6 +82,21 @@ describe("research commands", () => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Created research run"));
});
it("creates a run when provider is unset by defaulting to builtin", async () => {
storeMock.getSettings.mockResolvedValueOnce({ researchSettings: { enabled: true } });
resolveResearchSettingsMock.mockReturnValueOnce({
enabled: true,
searchProvider: "builtin",
limits: { maxConcurrentRuns: 2, maxSourcesPerRun: 5, requestTimeoutMs: 1000, maxDurationMs: 5000 },
});
providerRegistryMock.mockReturnValueOnce({ getAvailableProviders: () => ["web-search"], getProvider: () => ({ type: "web-search" }) });
await runResearchCreate({ query: "hello builtin" });
expect(orchestratorMock.createRun).toHaveBeenCalled();
expect(errorSpy).not.toHaveBeenCalled();
});
it("lists runs as json", async () => {
await runResearchList({ json: true, status: "completed", limit: 3 });
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"runs"'));

View File

@@ -42,7 +42,8 @@ async function getStore(projectName?: string): Promise<TaskStore> {
}
function hasProviderCredentials(settings: Awaited<ReturnType<TaskStore["getSettings"]>>, providerId: string | undefined): boolean {
if (!providerId) return false;
if (!providerId || providerId === "builtin") return true;
if (providerId === "none") return false;
if (providerId === "searxng") return Boolean(settings.researchGlobalSearxngUrl);
if (providerId === "brave") return Boolean(settings.researchGlobalBraveApiKey);
if (providerId === "google") return Boolean(settings.researchGlobalGoogleSearchApiKey && settings.researchGlobalGoogleSearchCx);
@@ -57,11 +58,8 @@ async function getResearchRuntime(store: TaskStore) {
throw new Error("feature-disabled: Research is disabled in settings.");
}
const configuredProvider = (resolved.searchProvider as string | undefined) ?? settings.researchGlobalWebSearchProvider;
if (!configuredProvider) {
throw new Error("provider-unavailable: Research providers are not configured. Add provider credentials in settings.");
}
if (!hasProviderCredentials(settings, configuredProvider)) {
const configuredProvider = (resolved.searchProvider as string | undefined) ?? settings.researchGlobalWebSearchProvider ?? "builtin";
if (configuredProvider !== "builtin" && configuredProvider !== "none" && !hasProviderCredentials(settings, configuredProvider)) {
throw new Error(`missing-credentials: ${configuredProvider} credentials are missing. Configure Authentication and Research defaults in settings.`);
}

View File

@@ -218,20 +218,20 @@ async function getResearchAvailability(store: TaskStore): Promise<{ ok: boolean;
return { ok: false, code: "feature-disabled", message: "Research is disabled in settings." };
}
const backend = (resolved.searchProvider as string | undefined) ?? settings.researchGlobalWebSearchProvider;
const configured = backend === "searxng"
? Boolean(settings.researchGlobalSearxngUrl)
: backend === "brave"
? Boolean(settings.researchGlobalBraveApiKey)
: backend === "google"
? Boolean(settings.researchGlobalGoogleSearchApiKey && settings.researchGlobalGoogleSearchCx)
: backend === "tavily"
? Boolean(settings.researchGlobalTavilyApiKey)
: false;
if (!backend) {
return { ok: false, code: "provider-unavailable", message: "Research provider is not configured. Set research provider credentials in Settings." };
}
const backend = (resolved.searchProvider as string | undefined) ?? settings.researchGlobalWebSearchProvider ?? "builtin";
const configured = backend === "builtin"
? true
: backend === "none"
? false
: backend === "searxng"
? Boolean(settings.researchGlobalSearxngUrl)
: backend === "brave"
? Boolean(settings.researchGlobalBraveApiKey)
: backend === "google"
? Boolean(settings.researchGlobalGoogleSearchApiKey && settings.researchGlobalGoogleSearchCx)
: backend === "tavily"
? Boolean(settings.researchGlobalTavilyApiKey)
: false;
if (!configured) {
return { ok: false, code: "missing-credentials", message: `Missing credentials for ${backend}. Add provider keys in Authentication and verify Research defaults.` };