feat(FN-5585): add planning branch controls to planning mode modal
Adds planning branch controls to the PlanningModeModal, extending the planning API and routes with supporting tests and documentation. The changeset bumps `@runfusion/fusion` as a minor release. Fusion-Task-Id: FN-5585 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> Fusion-Task-Id: FN-5585
This commit is contained in:
5
.changeset/fn-5585-planning-branch-ui.md
Normal file
5
.changeset/fn-5585-planning-branch-ui.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Planning Mode now lets you pick a branch strategy (project default, auto-named, existing, or custom new) and an optional base/merge-target branch when creating a task from a completed planning session.
|
||||||
@@ -97,6 +97,20 @@ Behavior:
|
|||||||
- Nodes support manual drag repositioning with a 4px movement threshold to separate click from drag, using pointer capture and zoom-aware delta scaling for reliable tracking
|
- Nodes support manual drag repositioning with a 4px movement threshold to separate click from drag, using pointer capture and zoom-aware delta scaling for reliable tracking
|
||||||
- Custom node positions persist per project in browser localStorage (`kb:${projectId}:fusion-plugin-dependency-graph:positions`) across refresh/project switches, and **Fit to graph** clears saved positions and restores auto-layout
|
- Custom node positions persist per project in browser localStorage (`kb:${projectId}:fusion-plugin-dependency-graph:positions`) across refresh/project switches, and **Fit to graph** clears saved positions and restores auto-layout
|
||||||
|
|
||||||
|
## Planning Mode
|
||||||
|
|
||||||
|
Planning Mode now includes branch controls on the summary screen before you create a task.
|
||||||
|
|
||||||
|
- **Branch strategy** options mirror Subtask Breakdown semantics:
|
||||||
|
- `Use project/default branch`
|
||||||
|
- `Create auto-named branch per task`
|
||||||
|
- `Use existing branch`
|
||||||
|
- `Create custom new branch`
|
||||||
|
- **Branch name** is required when using `existing` or `custom new` strategies.
|
||||||
|
- **Merge target / base branch (optional)** lets you set the PR base branch (for example `main` or `develop`).
|
||||||
|
|
||||||
|
These values are sent with the Planning Mode create-task request as `branchSelection`, so created tasks persist branch/base-branch settings consistently with other branch-aware task creation flows.
|
||||||
|
|
||||||
## Chat View
|
## Chat View
|
||||||
|
|
||||||
Chat view provides project-scoped conversations with agents.
|
Chat view provides project-scoped conversations with agents.
|
||||||
|
|||||||
@@ -1058,6 +1058,43 @@ describe("Planning Mode API", () => {
|
|||||||
|
|
||||||
await expect(createTaskFromPlanning("plan-123")).rejects.toThrow("not found");
|
await expect(createTaskFromPlanning("plan-123")).rejects.toThrow("not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes branchSelection options in create-task request body", async () => {
|
||||||
|
const createdTask: Task = {
|
||||||
|
id: "FN-043",
|
||||||
|
title: "Branch aware plan",
|
||||||
|
description: "Create task with branch config",
|
||||||
|
column: "triage",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, createdTask, 201));
|
||||||
|
|
||||||
|
await createTaskFromPlanning("plan-123", undefined, undefined, {
|
||||||
|
branchSelection: {
|
||||||
|
mode: "custom-new",
|
||||||
|
branchName: "feature/planning-ui",
|
||||||
|
baseBranch: "develop",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/planning/create-task", {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId: "plan-123",
|
||||||
|
branchSelection: {
|
||||||
|
mode: "custom-new",
|
||||||
|
branchName: "feature/planning-ui",
|
||||||
|
baseBranch: "develop",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3361,10 +3361,24 @@ export function createTaskFromPlanning(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
summary?: PlanningSummary,
|
summary?: PlanningSummary,
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
|
options?: {
|
||||||
|
branch?: string;
|
||||||
|
baseBranch?: string;
|
||||||
|
branchSelection?: {
|
||||||
|
mode: "project-default" | "auto-new" | "existing" | "custom-new";
|
||||||
|
branchName?: string;
|
||||||
|
baseBranch?: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
return api<Task>(withProjectId("/planning/create-task", projectId), {
|
return api<Task>(withProjectId("/planning/create-task", projectId), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(summary ? { sessionId, summary } : { sessionId }),
|
body: JSON.stringify({
|
||||||
|
...(summary ? { sessionId, summary } : { sessionId }),
|
||||||
|
...(options?.branch !== undefined ? { branch: options.branch } : {}),
|
||||||
|
...(options?.baseBranch !== undefined ? { baseBranch: options.baseBranch } : {}),
|
||||||
|
...(options?.branchSelection ? { branchSelection: options.branchSelection } : {}),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,9 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
const [responseHistory, setResponseHistory] = useState<QuestionResponse[]>([]);
|
const [responseHistory, setResponseHistory] = useState<QuestionResponse[]>([]);
|
||||||
const [conversationHistory, setConversationHistory] = useState<ConversationHistoryEntry[]>([]);
|
const [conversationHistory, setConversationHistory] = useState<ConversationHistoryEntry[]>([]);
|
||||||
const [editedSummary, setEditedSummary] = useState<PlanningSummary | null>(null);
|
const [editedSummary, setEditedSummary] = useState<PlanningSummary | null>(null);
|
||||||
|
const [branchMode, setBranchMode] = useState<"project-default" | "auto-new" | "existing" | "custom-new">("project-default");
|
||||||
|
const [branchName, setBranchName] = useState("");
|
||||||
|
const [baseBranch, setBaseBranch] = useState("");
|
||||||
// Use ref instead of state for hasAutoStarted to handle React StrictMode double-render.
|
// Use ref instead of state for hasAutoStarted to handle React StrictMode double-render.
|
||||||
// In StrictMode, components render twice but state persists across renders,
|
// In StrictMode, components render twice but state persists across renders,
|
||||||
// which would skip auto-start on the second (committed) render. Refs are
|
// which would skip auto-start on the second (committed) render. Refs are
|
||||||
@@ -377,6 +380,9 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
setResponseHistory([]);
|
setResponseHistory([]);
|
||||||
setConversationHistory([]);
|
setConversationHistory([]);
|
||||||
setEditedSummary(null);
|
setEditedSummary(null);
|
||||||
|
setBranchMode("project-default");
|
||||||
|
setBranchName("");
|
||||||
|
setBaseBranch("");
|
||||||
setStreamingOutput("");
|
setStreamingOutput("");
|
||||||
setIsReconnecting(false);
|
setIsReconnecting(false);
|
||||||
setIsRetrying(false);
|
setIsRetrying(false);
|
||||||
@@ -1557,13 +1563,20 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
|
|
||||||
const handleCreateTask = useCallback(async () => {
|
const handleCreateTask = useCallback(async () => {
|
||||||
if (view.type !== "summary") return;
|
if (view.type !== "summary") return;
|
||||||
|
if ((branchMode === "existing" || branchMode === "custom-new") && !branchName.trim()) return;
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsCreatingTask(true);
|
setIsCreatingTask(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const completedSessionId = view.session.sessionId;
|
const completedSessionId = view.session.sessionId;
|
||||||
const task = await createTaskFromPlanning(completedSessionId, editedSummary ?? undefined, projectId);
|
const task = await createTaskFromPlanning(completedSessionId, editedSummary ?? undefined, projectId, {
|
||||||
|
branchSelection: {
|
||||||
|
mode: branchMode,
|
||||||
|
...(branchMode === "existing" || branchMode === "custom-new" ? { branchName: branchName.trim() } : {}),
|
||||||
|
...(baseBranch.trim() ? { baseBranch: baseBranch.trim() } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
onTaskCreated(task);
|
onTaskCreated(task);
|
||||||
// The server cleans up the planning session after task creation. Drop
|
// The server cleans up the planning session after task creation. Drop
|
||||||
// the local selection so a future reopen doesn't try to fetch a deleted
|
// the local selection so a future reopen doesn't try to fetch a deleted
|
||||||
@@ -1582,7 +1595,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
} finally {
|
} finally {
|
||||||
setIsCreatingTask(false);
|
setIsCreatingTask(false);
|
||||||
}
|
}
|
||||||
}, [broadcastCompleted, editedSummary, view, projectId, onTaskCreated, handleClose]);
|
}, [baseBranch, branchMode, branchName, broadcastCompleted, editedSummary, view, projectId, onTaskCreated, handleClose]);
|
||||||
|
|
||||||
const handleStartBreakdown = useCallback(async () => {
|
const handleStartBreakdown = useCallback(async () => {
|
||||||
if (view.type !== "summary") return;
|
if (view.type !== "summary") return;
|
||||||
@@ -2070,6 +2083,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
|||||||
historyEntries={conversationHistory}
|
historyEntries={conversationHistory}
|
||||||
onSummaryChange={setEditedSummary}
|
onSummaryChange={setEditedSummary}
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
|
branchMode={branchMode}
|
||||||
|
branchName={branchName}
|
||||||
|
baseBranch={baseBranch}
|
||||||
|
onBranchModeChange={setBranchMode}
|
||||||
|
onBranchNameChange={setBranchName}
|
||||||
|
onBaseBranchChange={setBaseBranch}
|
||||||
onCreateTask={handleCreateTask}
|
onCreateTask={handleCreateTask}
|
||||||
onBreakIntoTasks={handleStartBreakdown}
|
onBreakIntoTasks={handleStartBreakdown}
|
||||||
onRefine={() => {
|
onRefine={() => {
|
||||||
@@ -2360,6 +2379,12 @@ interface SummaryViewProps {
|
|||||||
historyEntries: ConversationHistoryEntry[];
|
historyEntries: ConversationHistoryEntry[];
|
||||||
onSummaryChange: (summary: PlanningSummary) => void;
|
onSummaryChange: (summary: PlanningSummary) => void;
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
|
branchMode: "project-default" | "auto-new" | "existing" | "custom-new";
|
||||||
|
branchName: string;
|
||||||
|
baseBranch: string;
|
||||||
|
onBranchModeChange: (mode: "project-default" | "auto-new" | "existing" | "custom-new") => void;
|
||||||
|
onBranchNameChange: (name: string) => void;
|
||||||
|
onBaseBranchChange: (branch: string) => void;
|
||||||
onCreateTask: () => void;
|
onCreateTask: () => void;
|
||||||
onBreakIntoTasks: () => void;
|
onBreakIntoTasks: () => void;
|
||||||
onRefine: () => void;
|
onRefine: () => void;
|
||||||
@@ -2371,6 +2396,12 @@ function SummaryView({
|
|||||||
historyEntries,
|
historyEntries,
|
||||||
onSummaryChange,
|
onSummaryChange,
|
||||||
tasks,
|
tasks,
|
||||||
|
branchMode,
|
||||||
|
branchName,
|
||||||
|
baseBranch,
|
||||||
|
onBranchModeChange,
|
||||||
|
onBranchNameChange,
|
||||||
|
onBaseBranchChange,
|
||||||
onCreateTask,
|
onCreateTask,
|
||||||
onBreakIntoTasks,
|
onBreakIntoTasks,
|
||||||
onRefine,
|
onRefine,
|
||||||
@@ -2387,6 +2418,8 @@ function SummaryView({
|
|||||||
deps: [isExpanded],
|
deps: [isExpanded],
|
||||||
});
|
});
|
||||||
const selectedPriority = normalizeTaskPriority(summary.priority);
|
const selectedPriority = normalizeTaskPriority(summary.priority);
|
||||||
|
const isBranchNameRequired = branchMode === "existing" || branchMode === "custom-new";
|
||||||
|
const hasInvalidBranchSelection = isBranchNameRequired && !branchName.trim();
|
||||||
|
|
||||||
const handleDependencyToggle = (taskId: string) => {
|
const handleDependencyToggle = (taskId: string) => {
|
||||||
const newDeps = selectedDependencies.includes(taskId)
|
const newDeps = selectedDependencies.includes(taskId)
|
||||||
@@ -2432,6 +2465,44 @@ function SummaryView({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="task-detail-section">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Branch strategy</label>
|
||||||
|
<select
|
||||||
|
value={branchMode}
|
||||||
|
onChange={(event) => onBranchModeChange(event.target.value as "project-default" | "auto-new" | "existing" | "custom-new")}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<option value="project-default">Use project/default branch</option>
|
||||||
|
<option value="auto-new">Create auto-named branch per task</option>
|
||||||
|
<option value="existing">Use existing branch</option>
|
||||||
|
<option value="custom-new">Create custom new branch</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{isBranchNameRequired && (
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Branch name</label>
|
||||||
|
<input
|
||||||
|
value={branchName}
|
||||||
|
onChange={(event) => onBranchNameChange(event.target.value)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Merge target / base branch (optional)</label>
|
||||||
|
<input
|
||||||
|
value={baseBranch}
|
||||||
|
onChange={(event) => onBaseBranchChange(event.target.value)}
|
||||||
|
disabled={isLoading}
|
||||||
|
placeholder="main"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{hasInvalidBranchSelection && (
|
||||||
|
<div className="form-error planning-error">Branch name is required for this branch strategy.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="planning-summary-meta-row">
|
<div className="planning-summary-meta-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="planning-summary-size">Suggested Size</label>
|
<label htmlFor="planning-summary-size">Suggested Size</label>
|
||||||
@@ -2517,7 +2588,7 @@ function SummaryView({
|
|||||||
Refine Further
|
Refine Further
|
||||||
</button>
|
</button>
|
||||||
<div className="planning-summary-actions-right">
|
<div className="planning-summary-actions-right">
|
||||||
<button className="btn" onClick={onCreateTask} disabled={isLoading}>
|
<button className="btn" onClick={onCreateTask} disabled={isLoading || hasInvalidBranchSelection}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 size={16} className="spin icon-mr-8" />
|
<Loader2 size={16} className="spin icon-mr-8" />
|
||||||
|
|||||||
@@ -1031,6 +1031,7 @@ describe("PlanningModeModal", () => {
|
|||||||
"session-complete-2",
|
"session-complete-2",
|
||||||
expect.objectContaining({ ...resumedSummary, priority: "normal" }),
|
expect.objectContaining({ ...resumedSummary, priority: "normal" }),
|
||||||
undefined,
|
undefined,
|
||||||
|
expect.objectContaining({ branchSelection: { mode: "project-default" } }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1085,10 +1086,104 @@ describe("PlanningModeModal", () => {
|
|||||||
"session-complete-priority",
|
"session-complete-priority",
|
||||||
expect.objectContaining({ priority: "high" }),
|
expect.objectContaining({ priority: "high" }),
|
||||||
undefined,
|
undefined,
|
||||||
|
expect.objectContaining({
|
||||||
|
branchSelection: { mode: "project-default" },
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("surfaces planning branch controls and sends branchSelection in create request", async () => {
|
||||||
|
const resumedSummary: PlanningSummary = {
|
||||||
|
title: "Resume-branch-controls",
|
||||||
|
description: "Recovered summary for branch controls",
|
||||||
|
suggestedSize: "M",
|
||||||
|
suggestedDependencies: [],
|
||||||
|
keyDeliverables: ["Implement", "Verify"],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetchAiSession.mockResolvedValueOnce({
|
||||||
|
id: "session-branch-controls",
|
||||||
|
type: "planning",
|
||||||
|
status: "complete",
|
||||||
|
title: "Resume-branch-controls",
|
||||||
|
inputPayload: JSON.stringify({ initialPlan: "Recover and create with branch controls" }),
|
||||||
|
conversationHistory: "[]",
|
||||||
|
currentQuestion: null,
|
||||||
|
result: JSON.stringify(resumedSummary),
|
||||||
|
thinkingOutput: "",
|
||||||
|
error: null,
|
||||||
|
projectId: null,
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<PlanningModeModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onTaskCreated={mockOnTaskCreated}
|
||||||
|
onTasksCreated={vi.fn()}
|
||||||
|
tasks={mockTasks}
|
||||||
|
resumeSessionId="session-branch-controls"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("button", { name: "Create Single Task" })).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
const branchStrategy = screen.getByRole("combobox", { name: "Branch strategy" }) as HTMLSelectElement;
|
||||||
|
expect(branchStrategy.value).toBe("project-default");
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Create Single Task" }));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockCreateTaskFromPlanning).toHaveBeenCalledWith(
|
||||||
|
"session-branch-controls",
|
||||||
|
expect.any(Object),
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({
|
||||||
|
branchSelection: { mode: "project-default" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(branchStrategy, { target: { value: "existing" } });
|
||||||
|
expect(screen.getByRole("textbox", { name: "Branch name" })).toBeDefined();
|
||||||
|
|
||||||
|
const createSingleTaskButton = screen.getByRole("button", { name: "Create Single Task" });
|
||||||
|
expect(createSingleTaskButton).toBeDisabled();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Branch name" }), {
|
||||||
|
target: { value: "feat/planning-branch" },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Merge target / base branch (optional)" }), {
|
||||||
|
target: { value: "develop" },
|
||||||
|
});
|
||||||
|
fireEvent.click(createSingleTaskButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockCreateTaskFromPlanning).toHaveBeenLastCalledWith(
|
||||||
|
"session-branch-controls",
|
||||||
|
expect.any(Object),
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({
|
||||||
|
branchSelection: {
|
||||||
|
mode: "existing",
|
||||||
|
branchName: "feat/planning-branch",
|
||||||
|
baseBranch: "develop",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(branchStrategy, { target: { value: "auto-new" } });
|
||||||
|
expect(screen.queryByRole("textbox", { name: "Branch name" })).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.change(branchStrategy, { target: { value: "custom-new" } });
|
||||||
|
expect(screen.getByRole("textbox", { name: "Branch name" })).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves per-subtask priority selections when creating tasks from breakdown", async () => {
|
it("preserves per-subtask priority selections when creating tasks from breakdown", async () => {
|
||||||
const resumedSummary: PlanningSummary = {
|
const resumedSummary: PlanningSummary = {
|
||||||
title: "Resume-to-breakdown-priority",
|
title: "Resume-to-breakdown-priority",
|
||||||
|
|||||||
@@ -1070,7 +1070,12 @@ describe("PlanningModeModal", () => {
|
|||||||
fireEvent.click(screen.getByText("Create Single Task"));
|
fireEvent.click(screen.getByText("Create Single Task"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockCreateTaskFromPlanning).toHaveBeenCalledWith("session-123", mockSummary, undefined);
|
expect(mockCreateTaskFromPlanning).toHaveBeenCalledWith(
|
||||||
|
"session-123",
|
||||||
|
mockSummary,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ branchSelection: { mode: "project-default" } }),
|
||||||
|
);
|
||||||
expect(mockOnTaskCreated).toHaveBeenCalledWith(createdTask);
|
expect(mockOnTaskCreated).toHaveBeenCalledWith(createdTask);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1931,6 +1931,51 @@ describe("Planning Mode Routes", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps per-task branch unset for project-default branchSelection", async () => {
|
||||||
|
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
id: "FN-200b",
|
||||||
|
description: "A task created from planning",
|
||||||
|
column: "triage",
|
||||||
|
dependencies: [],
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
const startRes = await REQUEST(
|
||||||
|
buildApp(),
|
||||||
|
"POST",
|
||||||
|
"/api/planning/start",
|
||||||
|
JSON.stringify({ initialPlan: "Build a user auth system" }),
|
||||||
|
{ "Content-Type": "application/json" }
|
||||||
|
);
|
||||||
|
const sessionId = startRes.body.sessionId;
|
||||||
|
|
||||||
|
await REQUEST(buildApp(), "POST", "/api/planning/respond", JSON.stringify({ sessionId, responses: { scope: "medium" } }), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", "/api/planning/respond", JSON.stringify({ sessionId, responses: { requirements: "Must have login" } }), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", "/api/planning/respond", JSON.stringify({ sessionId, responses: { confirm: true } }), { "Content-Type": "application/json" });
|
||||||
|
|
||||||
|
const res = await REQUEST(
|
||||||
|
buildApp(),
|
||||||
|
"POST",
|
||||||
|
"/api/planning/create-task",
|
||||||
|
JSON.stringify({
|
||||||
|
sessionId,
|
||||||
|
branchSelection: {
|
||||||
|
mode: "project-default",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ "Content-Type": "application/json" }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect(store.createTask).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
branch: undefined,
|
||||||
|
baseBranch: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("applies shared branchSelection to all planning subtasks", async () => {
|
it("applies shared branchSelection to all planning subtasks", async () => {
|
||||||
(store.createTask as ReturnType<typeof vi.fn>)
|
(store.createTask as ReturnType<typeof vi.fn>)
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
|
|||||||
Reference in New Issue
Block a user