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:
@@ -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" });
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ? [
|
||||
|
||||
Reference in New Issue
Block a user