Files
fusion/packages/engine/src/__tests__/agent-task-read-tools.test.ts
Fusion Agent eb3eeb887a FN-074: remove task splitting and parent deletion
Remove complex-task splitting and parent-deletion behavior across core, dashboard, and engine.

- Remove subtask splitting, breakdown UI, and related API routes and lifecycle handling.
- Add the migration and prompt/documentation updates that enforce the simplified task model.
- Update affected tests and integrations for the removed behavior.

Files changed:
 .changeset/fn-074-removal.md                       |    7 +
 docs/dashboard-guide.md                            |   17 +-
 docs/gitlab-parity-inventory.md                    |    7 +-
 docs/mcp.md                                        |    6 +-
 docs/settings-reference.md                         |   16 +-
 docs/task-management.md                            |   31 +-
 docs/workflow-steps.md                             |   10 +-
 .../cli/skill/fusion/references/task-structure.md  |    2 +-
 .../cli/skill/fusion/workflows/specifications.md   |    4 +-
 packages/core/src/__tests__/agent-prompts.test.ts  |   19 +-
 .../builtin-workflow-settings-triage.test.ts       |   32 +-
 .../src/__tests__/postgres/schema-applier.test.ts  |    2 +-
 .../core/src/__tests__/prompt-overrides.test.ts    |    3 +-
 ...-deleted-observed-dispatch-side-effects.test.ts |    3 -
 .../task-deleted-outbox-consumer-backoff.test.ts   |    1 -
 packages/core/src/agents/agent-prompts.ts          |   56 +-
 packages/core/src/index.ts                         |    2 +-
 .../0062_remove_task_subtask_splitting.sql         |    2 +
 packages/core/src/postgres/schema-applier.ts       |   14 +-
 packages/core/src/postgres/schema/project.ts       |    1 -
 packages/core/src/store.ts                         |   12 +-
 packages/core/src/task-delete-attribution.ts       |   20 +-
 packages/core/src/task-delete-notice.ts            |    7 +-
 .../__tests__/lifecycle-outbox-writer.test.ts      |    4 +-
 .../core/src/task-store/archive-lifecycle-2.ts     |   13 +-
 packages/core/src/task-store/archive-lifecycle.ts  |    6 +-
 packages/core/src/task-store/lifecycle-outbox.ts   |    2 -
 packages/core/src/task-store/persistence.ts        |    2 -
 packages/core/src/task-store/serialization.ts      |    2 -
 packages/core/src/task-store/task-creation.ts      |    2 -
 .../src/task-store/task-deleted-outbox-consumer.ts |    4 -
 packages/core/src/task-store/task-mutation-ops.ts  |    2 +-
 packages/core/src/task-store/task-row-mappers.ts   |    2 +-
 packages/core/src/tasks/prompt-overrides.ts        |   35 -
 packages/core/src/types.ts                         |   12 -
 packages/core/src/types/mesh/archive-planning.ts   |    1 -
 packages/core/src/types/task/task-core.ts          |    3 -
 .../src/workflows/builtin-workflow-settings.ts     |   86 --
 packages/dashboard/app/App.tsx                     |   15 -
 .../mission-planning-modals-mobile.test.ts         |   15 -
 packages/dashboard/app/api/legacy.ts               |   18 +-
 packages/dashboard/app/api/planning/ai-text.ts     |  171 +---
 packages/dashboard/app/api/planning/planning.ts    |   46 -
 packages/dashboard/app/api/tasks/tasks.ts          |    4 +-
 packages/dashboard/app/components/AppModals.tsx    |   39 +-
 packages/dashboard/app/components/Board.tsx        |   10 +-
 packages/dashboard/app/components/Column.tsx       |    9 +-
 .../dashboard/app/components/InlineCreateCard.tsx  |   63 +-
 packages/dashboard/app/components/Lane.tsx         |    3 +-
 packages/dashboard/app/components/ListView.tsx     |   10 +-
 packages/dashboard/app/components/NewTaskModal.tsx |    8 +-
 .../dashboard/app/components/PlanningModeModal.css |  128 ---
 .../dashboard/app/components/PlanningModeModal.tsx |  566 +----------
 .../dashboard/app/components/QuickEntryBox.tsx     |   43 +-
 .../dashboard/app/components/SettingsModal.tsx     |    2 -
 .../app/components/SubtaskBreakdownModal.tsx       |  932 -----------------
 packages/dashboard/app/components/TaskCard.tsx     |    1 -
 packages/dashboard/app/components/TaskForm.tsx     |   23 +-
 .../__tests__/AgentPromptsManager.test.tsx         |    1 -
 .../app/components/__tests__/AppModals.test.tsx    |   92 +-
 .../app/components/__tests__/Board.test.tsx        |   69 +-
 .../components/__tests__/InlineCreateCard.test.tsx |  170 ----
 .../app/components/__tests__/ListView.test.tsx     |   38 -
 .../app/components/__tests__/NewTaskModal.test.tsx |   48 -
 .../components/__tests__/QuickEntryBox.test.tsx    |  293 +-----
 .../__tests__/SubtaskBreakdownModal.test.tsx       |  933 -----------------
 ...skDetail.mobile-transition.board-panel.test.tsx |    1 -
 .../__tests__/TaskDetail.swipe-back.test.tsx       |    1 -
 .../app/components/__tests__/TaskForm.test.tsx     |   67 --
 .../__tests__/WorkflowSettingsPanel.test.tsx       |   51 -
 .../app/components/__tests__/board-mobile.test.tsx |   20 -
 .../composer-settings-read-isolation.test.tsx      |   30 -
 .../components/__tests__/migratedModalFixtures.tsx |    2 -
 .../app/components/dashboard/MainContent.tsx       |    5 -
 .../dashboard/__tests__/DashboardBanners.test.tsx  |    1 -
 .../__tests__/MainContent.graph-popout.test.tsx    |    1 -
 .../dashboard/app/components/dashboard/types.ts    |    2 -
 .../app/components/workflow-setting-display.ts     |   10 -
 ...ckgroundSessions.resume-instrumentation.test.ts |    1 -
 .../hooks/__tests__/useBackgroundSessions.test.ts  |   25 +-
 .../app/hooks/__tests__/useModalManager.test.ts    |    3 -
 packages/dashboard/app/hooks/modalPersistence.ts   |   14 -
 .../dashboard/app/hooks/useBackgroundSessions.ts   |    9 +-
 packages/dashboard/app/hooks/useModalManager.ts    |   58 +-
 packages/dashboard/app/hooks/useProjectActions.ts  |    2 +-
 packages/dashboard/app/hooks/useTaskHandlers.ts    |    9 -
 packages/dashboard/app/utils/builtinPrompts.ts     |    7 -
 packages/dashboard/app/utils/projectStorage.ts     |    1 -
 .../src/__tests__/ai-session-diagnostics.test.ts   |    5 +-
 .../src/__tests__/github-tracking-state.test.ts    |   97 --
 .../src/__tests__/gitlab-delete-close.test.ts      |    6 +-
 .../gitlab-parity-inventory-documentation.test.ts  |    1 -
 .../src/__tests__/gitlab-split-close.test.ts       |  125 ---
 .../planning-flow-diagnostics-guardrail.test.ts    |    1 -
 .../src/__tests__/process-lifecycle.test.ts        |    1 -
 .../dashboard/src/__tests__/routes-auth.test.ts    |    2 -
 .../src/__tests__/routes-automation.test.ts        |    2 -
 .../dashboard/src/__tests__/routes-git.test.ts     |    2 -
 .../dashboard/src/__tests__/routes-github.test.ts  |   92 --
 .../src/__tests__/routes-planning-tracking.test.ts |  130 ---
 .../src/__tests__/routes-planning.test.ts          | 1062 +-------------------
 .../dashboard/src/__tests__/routes-system.test.ts  |    2 -
 .../dashboard/src/__tests__/routes-tasks.test.ts   |  902 -----------------
 .../shared-branch-group-entry-points.test.ts       |   93 --
 packages/dashboard/src/ai-session-diagnostics.ts   |    3 +-
 packages/dashboard/src/ai-session-store.ts         |    6 +-
 packages/dashboard/src/ai-session-timeout.ts       |    3 +-
 packages/dashboard/src/github-tracking-state.ts    |   43 +-
 packages/dashboard/src/gitlab-delete-close.ts      |    7 +-
 packages/dashboard/src/gitlab-lifecycle.ts         |    4 +-
 packages/dashboard/src/gitlab-split-close.ts       |  103 --
 packages/dashboard/src/gitlab-tracking-state.ts    |    2 +-
 packages/dashboard/src/index.ts                    |    1 -
 packages/dashboard/src/planning.ts                 |  171 ----
 packages/dashboard/src/routes.ts                   |    6 -
 ...er-planning-subtask-routes.parent-close.test.ts |  120 ---
 .../dashboard/src/routes/register-git-github.ts    |    6 -
 .../src/routes/register-planning-subtask-routes.ts |  773 +-------------
 .../src/routes/register-task-workflow-routes.ts    |    6 +-
 packages/dashboard/src/server.ts                   |    8 -
 packages/dashboard/src/subtask-breakdown.ts        |  833 ---------------
 packages/dashboard/src/triage-trait.ts             |   93 +-
 .../src/__tests__/agent-task-read-tools.test.ts    |    1 -
 .../triage-duplicate-search-regression.test.ts     |    3 -
 .../triage-split-into-subtasks-delete.test.ts      |  222 ----
 packages/engine/src/__tests__/triage.test.ts       |  519 +---------
 packages/engine/src/triage.ts                      |  239 +----
 127 files changed, 214 insertions(+), 10001 deletions(-)

Fusion-Task-Id: FN-074

Fusion-Task-Lineage: 23207799-4ec4-4ec7-bcd7-83cba4d98f53

Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-21 04:32:53 +00:00

170 lines
7.9 KiB
TypeScript

import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it, vi } from "vitest";
import { MAX_TASK_LIST_TEXT_CHARS, type AgentPermissionPolicy, type Task, type TaskDetail, type TaskStore } from "@fusion/core";
import { createPlanningBoardTools } from "../../../dashboard/src/planning-board-tools.js";
import { createTaskReadTools } from "../agent-tools.js";
import { HeartbeatMonitor } from "../agent-heartbeat.js";
import { evaluateAgentActionGate } from "../agents/agent-action-gate.js";
import { COORDINATION_EXEMPT_TOOLS, READONLY_FN_TOOLS } from "../execution/gating-classifications.js";
import { classifyPermanentAgentToolCall } from "../agents/permanent-agent-gating.js";
import { TriageProcessor } from "../triage.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const lockedDownPolicy: AgentPermissionPolicy = {
presetId: "locked-down",
rules: {
git_write: "block",
file_write_delete: "block",
command_execution: "block",
network_api: "block",
task_agent_mutation: "block",
},
};
type TaskReadResult = Awaited<ReturnType<ReturnType<typeof createTaskReadTools>[number]["execute"]>>;
function textOf(result: TaskReadResult): string {
expect(result.content).toHaveLength(1);
expect(result.content[0]).toMatchObject({ type: "text" });
const text = result.content[0]?.text ?? "";
expect(text.trim().length).toBeGreaterThan(0);
return text;
}
function task(overrides: Partial<Task> & Pick<Task, "id">): Task {
return {
id: overrides.id,
title: overrides.title,
description: overrides.description ?? `Description for ${overrides.id}`,
column: overrides.column ?? "todo",
dependencies: overrides.dependencies ?? [],
steps: overrides.steps ?? [],
currentStep: overrides.currentStep ?? 0,
...overrides,
} as Task;
}
function taskDetail(overrides: Partial<TaskDetail> & Pick<TaskDetail, "id">): TaskDetail {
return {
...task(overrides),
prompt: overrides.prompt ?? "# Prompt body",
} as TaskDetail;
}
function createStore(overrides: Partial<TaskStore> = {}): TaskStore {
return {
listTasks: vi.fn(async () => []),
searchTasks: vi.fn(async () => []),
getTask: vi.fn(async (id: string) => taskDetail({ id })),
getSettings: vi.fn(async () => ({})),
on: vi.fn(),
off: vi.fn(),
...overrides,
} as unknown as TaskStore;
}
function toolNames(tools: Array<{ name: string }>): string[] {
return tools.map((tool) => tool.name);
}
function extractRegisteredCliTaskReadNames(): string[] {
const source = readFileSync(resolve(__dirname, "../../../cli/src/extension.ts"), "utf8");
return [...source.matchAll(/name:\s*"(fn_task_(?:list|show|search|get))"/g)].map((match) => match[1]!);
}
describe("shared task read tools", () => {
it("returns the canonical heartbeat task-read names in order", () => {
expect(toolNames(createTaskReadTools(createStore()))).toEqual([
"fn_task_list",
"fn_task_show",
"fn_task_search",
]);
});
it("returns non-empty bounded text for empty, populated, no-match, and oversized boards", async () => {
const emptyTools = createTaskReadTools(createStore());
expect(textOf(await emptyTools[0]!.execute("list-empty", {}))).toBe("No active tasks.");
expect(textOf(await emptyTools[2]!.execute("search-empty", { query: "missing" }))).toBe("No tasks matched.");
const populatedStore = createStore({
listTasks: vi.fn(async () => [
task({ id: "FN-001", title: "Active task", column: "todo", dependencies: ["FN-000"] }),
task({ id: "FN-002", title: "Done task", column: "done" }),
]),
searchTasks: vi.fn(async () => [task({ id: "FN-003", title: "Search hit", column: "done" })]),
getTask: vi.fn(async () => taskDetail({ id: "FN-004", title: "Show me", prompt: "# Prompt" })),
});
const populatedTools = createTaskReadTools(populatedStore);
const listText = textOf(await populatedTools[0]!.execute("list-populated", {}));
expect(listText).toContain("FN-001 (todo): Active task [deps: FN-000]");
expect(listText).not.toContain("FN-002");
expect(textOf(await populatedTools[1]!.execute("show-populated", { id: "FN-004" }))).toContain("PROMPT.md:");
expect(textOf(await populatedTools[2]!.execute("search-populated", { query: "search", includeDone: true }))).toContain("FN-003 (done): Search hit");
const oversizedTasks = Array.from({ length: 120 }, (_, index) => task({
id: `FN-${String(index + 100).padStart(3, "0")}`,
title: `Oversized ${index} ${"x".repeat(120)}`,
column: "todo",
}));
const oversizedStore = createStore({
listTasks: vi.fn(async () => oversizedTasks),
searchTasks: vi.fn(async () => oversizedTasks),
});
const oversizedTools = createTaskReadTools(oversizedStore);
const oversizedListText = textOf(await oversizedTools[0]!.execute("list-oversized", {}));
const oversizedSearchText = textOf(await oversizedTools[2]!.execute("search-oversized", { query: "oversized" }));
expect(oversizedListText.length).toBeLessThanOrEqual(MAX_TASK_LIST_TEXT_CHARS);
expect(oversizedSearchText.length).toBeLessThanOrEqual(MAX_TASK_LIST_TEXT_CHARS);
expect(oversizedListText).toContain("truncated to fit");
});
it("exposes task reads through the shared heartbeat helper and task-scoped heartbeat tools", () => {
const monitor = new HeartbeatMonitor({ store: {} as never, taskStore: createStore(), rootDir: "/tmp/fn-test" });
const sharedNames = toolNames((monitor as unknown as { createSharedHeartbeatWorkTools: (store: TaskStore) => Array<{ name: string }> }).createSharedHeartbeatWorkTools(createStore()));
expect(sharedNames.slice(0, 3)).toEqual(["fn_task_list", "fn_task_show", "fn_task_search"]);
const taskScopedNames = toolNames(monitor.createHeartbeatTools("agent-1", createStore(), "FN-001"));
expect(taskScopedNames).toEqual(expect.arrayContaining(["fn_task_list", "fn_task_show", "fn_task_search"]));
});
it("positively classifies all task-read names as read-only", () => {
for (const toolName of ["fn_task_search", "fn_task_get", "fn_task_list", "fn_task_show"] as const) {
expect(READONLY_FN_TOOLS.has(toolName)).toBe(true);
expect((COORDINATION_EXEMPT_TOOLS as readonly string[]).includes(toolName)).toBe(true);
expect(classifyPermanentAgentToolCall(toolName)).toEqual({ category: "none", recognized: true });
const decision = evaluateAgentActionGate({ agentId: "agent-1", toolName, args: {}, permissionPolicy: lockedDownPolicy });
expect(decision).toMatchObject({ disposition: "allow", category: "exempt", operation: toolName });
}
});
it("pins per-surface task-read tool name parity on canonical fn_task_show", () => {
const triageProcessor = new TriageProcessor(createStore() as never, "/tmp/fn-test");
const triageNames = toolNames((triageProcessor as unknown as { createTriageTools: (opts: unknown) => Array<{ name: string }> }).createTriageTools({
parentTaskId: "FN-TRIAGE",
allowTaskCreate: true,
})).filter((name) => name.startsWith("fn_task_") && name !== "fn_task_create");
const surfaces = {
triage: triageNames,
planningBoard: toolNames(createPlanningBoardTools(createStore())).filter((name) => name.startsWith("fn_task_")),
cliExtension: extractRegisteredCliTaskReadNames(),
sharedFactory: toolNames(createTaskReadTools(createStore())),
};
expect(surfaces).toEqual({
triage: ["fn_task_list", "fn_task_search", "fn_task_show"],
planningBoard: ["fn_task_list", "fn_task_show"],
cliExtension: ["fn_task_list", "fn_task_show"],
sharedFactory: ["fn_task_list", "fn_task_show", "fn_task_search"],
});
const deprecatedGetName = ["fn_task", "get"].join("_");
for (const names of Object.values(surfaces)) {
expect(names).toContain("fn_task_show");
expect(names).not.toContain(deprecatedGetName);
}
});
});