feat(FN-3705): gate research tools behind experimental flag

This merge introduces two major themes. First, research tools in both the engine and CLI are now gated behind an experimental flag, using a shared helper from core — the research tools are documented as experimental and the dashboard settings reference is updated. Second, the testing suite receives

Fusion-Task-Id: FN-3705
This commit is contained in:
Fusion
2026-05-07 10:29:03 -07:00
committed by gsxdsm
parent 273ca03052
commit 66fa56b45e
13 changed files with 243 additions and 32 deletions

View File

@@ -4684,8 +4684,11 @@ const mockedReviewStep = vi.mocked(mockedReviewStepFn);
* Helper: executes a task and captures the custom tools passed to createFnAgent.
* Returns a map of tool name → tool execute function for direct testing.
*/
async function captureTools(): Promise<Record<string, (id: string, params: any) => Promise<any>>> {
async function captureTools(settingsOverride?: Record<string, unknown>): Promise<Record<string, (id: string, params: any) => Promise<any>>> {
const store = createMockStore();
if (settingsOverride) {
store.getSettings.mockResolvedValue({ ...(await store.getSettings()), ...settingsOverride });
}
// Simulate the real TaskStore: forward transitions persist, but in-progress
// regressions on done/skipped steps are rejected so executor.ts can surface
// the "already <status>" diagnostic.
@@ -4908,14 +4911,22 @@ describe("Code review verdict enforcement - fn_task_update blocking", () => {
expect(allowed.content[0].text).toContain("→ done");
});
it("registers research runtime tools in customTools", async () => {
const tools = await captureTools();
it("registers research runtime tools in customTools when researchView experimental flag is enabled", async () => {
const tools = await captureTools({ experimentalFeatures: { researchView: true } });
expect(tools.fn_research_run).toBeTypeOf("function");
expect(tools.fn_research_list).toBeTypeOf("function");
expect(tools.fn_research_get).toBeTypeOf("function");
expect(tools.fn_research_cancel).toBeTypeOf("function");
});
it("does not register research runtime tools when researchView experimental flag is disabled", async () => {
const tools = await captureTools({ experimentalFeatures: { researchView: false } });
expect(tools.fn_research_run).toBeUndefined();
expect(tools.fn_research_list).toBeUndefined();
expect(tools.fn_research_get).toBeUndefined();
expect(tools.fn_research_cancel).toBeUndefined();
});
it("REVISE tool response text includes re-review instructions", async () => {
mockedReviewStep.mockResolvedValue({ verdict: "REVISE", review: "Bug found", summary: "Issues" });

View File

@@ -838,6 +838,14 @@ describe("fast-mode triage", () => {
await mkdir(join(rootDir, ".fusion", "tasks", task.id), { recursive: true });
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 10000,
groupOverlappingFiles: false,
autoMerge: true,
experimentalFeatures: { researchView: true },
} as Settings),
getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }),
parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]),
parseStepsFromPrompt: vi.fn().mockResolvedValue([]),
@@ -880,6 +888,43 @@ describe("fast-mode triage", () => {
await cleanupTriageFixtureRoot(rootDir);
}
});
it("omits research tools when researchView experimental flag is disabled", async () => {
const task = createTriageTask({ id: "FN-FAST-005", executionMode: "fast" });
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 10000,
groupOverlappingFiles: false,
autoMerge: true,
experimentalFeatures: { researchView: false },
} as Settings),
getTask: vi.fn().mockResolvedValue({ ...mockTaskDetail, id: task.id, attachments: [], comments: [] }),
});
let capturedTools: any[] = [];
mockCreateFnAgent.mockImplementationOnce(async (opts: any) => {
capturedTools = opts.customTools;
return {
session: {
state: {},
sessionManager: { getLeafId: vi.fn().mockReturnValue(null) },
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
navigateTree: vi.fn(),
},
};
});
const processor = new TriageProcessor(store, "/tmp/root");
await processor.specifyTask(task);
expect(capturedTools.some((tool: any) => tool.name === "fn_research_run")).toBe(false);
expect(capturedTools.some((tool: any) => tool.name === "fn_research_list")).toBe(false);
expect(capturedTools.some((tool: any) => tool.name === "fn_research_get")).toBe(false);
expect(capturedTools.some((tool: any) => tool.name === "fn_research_cancel")).toBe(false);
});
});
describe("readAttachmentContents", () => {

View File

@@ -10,6 +10,7 @@ import {
buildExecutionMemoryInstructions,
getTaskMergeBlocker,
isEphemeralAgent,
isResearchExperimentalEnabled,
resolveAgentPrompt,
resolveProjectDefaultModel,
type RunCommandResult,
@@ -2826,11 +2827,13 @@ export class TaskExecutor {
this.createSpawnAgentTool(task.id, worktreePath, settings),
this.createTaskDocumentWriteTool(task.id),
this.createTaskDocumentReadTool(task.id),
...createResearchTools({
store: this.store,
rootDir: this.rootDir,
getSettings: async () => this.store.getSettings(),
}),
...(isResearchExperimentalEnabled(settings)
? createResearchTools({
store: this.store,
rootDir: this.rootDir,
getSettings: async () => this.store.getSettings(),
})
: []),
...createMemoryTools(this.rootDir, settings, assignedAgent ? {
agentMemory: {
agentId: assignedAgent.id,

View File

@@ -8,6 +8,7 @@ import type {
} from "@fusion/core";
import {
buildTriageMemoryInstructions,
isResearchExperimentalEnabled,
resolveAgentPrompt,
sortTasksByPriorityThenAgeAndId,
} from "@fusion/core";
@@ -941,11 +942,13 @@ export class TriageProcessor {
}),
createTaskDocumentWriteTool(this.store, task.id),
createTaskDocumentReadTool(this.store, task.id),
...createResearchTools({
store: this.store,
rootDir: this.rootDir,
getSettings: async () => this.store.getSettings(),
}),
...(isResearchExperimentalEnabled(settings)
? createResearchTools({
store: this.store,
rootDir: this.rootDir,
getSettings: async () => this.store.getSettings(),
})
: []),
...createMemoryTools(this.rootDir, settings),
// Agent delegation tools — discover and delegate work to other agents.
...(this.options.agentStore ? [