feat(KB-040): add break-into-subtasks toggle for task creation

- Add breakIntoSubtasks flag to core Task model and persist to store
- Plumb subtask flag through dashboard API routes with validation
- Add subtask toggle UI to inline task creation card
- Update triage agent to handle automatic subtask breakdown when enabled
- Add comprehensive tests for store, API routes, and UI components
- Include changeset for patch release documenting the new feature
This commit is contained in:
gsxdsm
2026-03-29 22:23:47 -07:00
parent 1b5511c968
commit f99ae366f7
11 changed files with 327 additions and 13 deletions

View File

@@ -150,6 +150,78 @@ describe("GET /tasks/:id", () => {
});
});
describe("POST /tasks", () => {
let store: TaskStore;
beforeEach(() => {
store = createMockStore();
});
function buildApp() {
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
return app;
}
it("creates a task and forwards breakIntoSubtasks", async () => {
const createdTask = {
...FAKE_TASK_DETAIL,
column: "triage",
breakIntoSubtasks: true,
};
(store.createTask as ReturnType<typeof vi.fn>).mockResolvedValue(createdTask);
const res = await REQUEST(
buildApp(),
"POST",
"/api/tasks",
JSON.stringify({
description: "Big initiative",
breakIntoSubtasks: true,
}),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(201);
expect(store.createTask).toHaveBeenCalledWith({
title: undefined,
description: "Big initiative",
column: undefined,
dependencies: undefined,
breakIntoSubtasks: true,
});
});
it("returns 400 when description is missing", async () => {
const res = await REQUEST(
buildApp(),
"POST",
"/api/tasks",
JSON.stringify({ breakIntoSubtasks: true }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("description is required");
expect(store.createTask).not.toHaveBeenCalled();
});
it("returns 400 when breakIntoSubtasks is not a boolean", async () => {
const res = await REQUEST(
buildApp(),
"POST",
"/api/tasks",
JSON.stringify({ description: "Big initiative", breakIntoSubtasks: "yes" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("breakIntoSubtasks must be a boolean");
expect(store.createTask).not.toHaveBeenCalled();
});
});
describe("POST /tasks/:id/retry", () => {
let store: TaskStore;

View File

@@ -610,16 +610,21 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
// Create task
router.post("/tasks", async (req, res) => {
try {
const { title, description, column, dependencies } = req.body;
const { title, description, column, dependencies, breakIntoSubtasks } = req.body;
if (!description || typeof description !== "string") {
res.status(400).json({ error: "description is required" });
return;
}
if (breakIntoSubtasks !== undefined && typeof breakIntoSubtasks !== "boolean") {
res.status(400).json({ error: "breakIntoSubtasks must be a boolean" });
return;
}
const task = await store.createTask({
title,
description,
column,
dependencies,
breakIntoSubtasks,
});
res.status(201).json(task);
} catch (err: any) {