feat(KB-630): inherit parent task models in subtasks
- Subtasks now inherit parent task's executor and validator model settings - When triage creates subtasks, modelProvider/modelId and validatorModelProvider/validatorModelId are copied from parent - Clean up legacy mission-store code (mission-store.ts, mission-types.ts, and related tests) - Add comprehensive tests for subtask model inheritance in triage, routes, and db - Remove obsolete changeset for missions-database-schema
This commit is contained in:
@@ -465,6 +465,108 @@ describe("POST /subtasks/*", () => {
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("not found");
|
||||
});
|
||||
|
||||
it("inherits parent task model settings when creating subtasks", async () => {
|
||||
const parentTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "KB-100",
|
||||
title: "Parent Task",
|
||||
column: "triage",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
validatorModelProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
};
|
||||
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(parentTask);
|
||||
(store.createTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage" })
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-102", title: "Second", column: "triage" });
|
||||
(store.updateTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage", size: "S" })
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-102", title: "Second", column: "triage", size: "M" });
|
||||
|
||||
const start = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/start-streaming",
|
||||
JSON.stringify({ description: "Break this feature into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
const createRes = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/create-tasks",
|
||||
JSON.stringify({
|
||||
sessionId: start.body.sessionId,
|
||||
parentTaskId: "KB-100",
|
||||
subtasks: [
|
||||
{ tempId: "subtask-1", title: "First", description: "Do first", size: "S", dependsOn: [] },
|
||||
{ tempId: "subtask-2", title: "Second", description: "Do second", size: "M", dependsOn: ["subtask-1"] },
|
||||
],
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(store.getTask).toHaveBeenCalledWith("KB-100");
|
||||
expect(store.createTask).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||
title: "First",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
validatorModelProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
}));
|
||||
expect(store.createTask).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
title: "Second",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
validatorModelProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
}));
|
||||
});
|
||||
|
||||
it("handles missing parent task gracefully when creating subtasks", async () => {
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Task not found"));
|
||||
(store.createTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage" });
|
||||
(store.updateTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage", size: "S" });
|
||||
|
||||
const start = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/start-streaming",
|
||||
JSON.stringify({ description: "Break this feature into subtasks" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
const createRes = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/subtasks/create-tasks",
|
||||
JSON.stringify({
|
||||
sessionId: start.body.sessionId,
|
||||
parentTaskId: "KB-NONEXISTENT",
|
||||
subtasks: [
|
||||
{ tempId: "subtask-1", title: "First", description: "Do first", size: "S", dependsOn: [] },
|
||||
],
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
expect(store.getTask).toHaveBeenCalledWith("KB-NONEXISTENT");
|
||||
// Subtask created without model inheritance (undefined values)
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: "First",
|
||||
modelProvider: undefined,
|
||||
modelId: undefined,
|
||||
validatorModelProvider: undefined,
|
||||
validatorModelId: undefined,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /tasks/:id/retry", () => {
|
||||
|
||||
@@ -4494,6 +4494,17 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch parent task to inherit model settings if parentTaskId is provided
|
||||
let parentTask: Awaited<ReturnType<typeof store.getTask>> | undefined;
|
||||
if (typeof parentTaskId === "string" && parentTaskId.trim()) {
|
||||
try {
|
||||
parentTask = await store.getTask(parentTaskId);
|
||||
} catch {
|
||||
// Parent task not found or error - proceed without inheritance
|
||||
parentTask = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const createdTasks = [] as Awaited<ReturnType<typeof store.createTask>>[];
|
||||
const tempIdToTaskId = new Map<string, string>();
|
||||
|
||||
@@ -4508,6 +4519,11 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
description: typeof item.description === "string" ? item.description.trim() : item.title.trim(),
|
||||
column: "triage",
|
||||
dependencies: undefined,
|
||||
// Inherit parent's model settings if available
|
||||
modelProvider: parentTask?.modelProvider,
|
||||
modelId: parentTask?.modelId,
|
||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||
validatorModelId: parentTask?.validatorModelId,
|
||||
});
|
||||
|
||||
tempIdToTaskId.set(item.tempId, task.id);
|
||||
|
||||
@@ -535,3 +535,111 @@ describe("requirePlanApproval setting", () => {
|
||||
expect(settings.requirePlanApproval).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("taskCreate tool model inheritance", () => {
|
||||
it("inherits parent task model settings when creating subtasks", async () => {
|
||||
const parentTask: Task = {
|
||||
id: "KB-001",
|
||||
description: "Parent task",
|
||||
column: "triage",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
validatorModelProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
};
|
||||
|
||||
const createdSubtask: Task = {
|
||||
id: "KB-002",
|
||||
description: "Child task description",
|
||||
column: "triage",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
const store = createMockStore({
|
||||
getTask: vi.fn().mockResolvedValue(parentTask),
|
||||
createTask: vi.fn().mockResolvedValue(createdSubtask),
|
||||
});
|
||||
|
||||
// Simulate the taskCreate tool behavior
|
||||
const parentTaskId = "KB-001";
|
||||
const parentTaskResult = await store.getTask(parentTaskId);
|
||||
|
||||
await store.createTask({
|
||||
title: "Child Task",
|
||||
description: "Child task description",
|
||||
dependencies: [],
|
||||
column: "triage",
|
||||
modelProvider: parentTaskResult?.modelProvider,
|
||||
modelId: parentTaskResult?.modelId,
|
||||
validatorModelProvider: parentTaskResult?.validatorModelProvider,
|
||||
validatorModelId: parentTaskResult?.validatorModelId,
|
||||
});
|
||||
|
||||
expect(store.getTask).toHaveBeenCalledWith("KB-001");
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: "Child Task",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
validatorModelProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
}));
|
||||
});
|
||||
|
||||
it("handles missing parent task gracefully when creating subtasks", async () => {
|
||||
const createdSubtask: Task = {
|
||||
id: "KB-002",
|
||||
description: "Child task description",
|
||||
column: "triage",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
const store = createMockStore({
|
||||
getTask: vi.fn().mockRejectedValue(new Error("Task not found")),
|
||||
createTask: vi.fn().mockResolvedValue(createdSubtask),
|
||||
});
|
||||
|
||||
// Simulate the taskCreate tool behavior with missing parent
|
||||
const parentTaskId = "KB-NONEXISTENT";
|
||||
let parentTask;
|
||||
try {
|
||||
parentTask = await store.getTask(parentTaskId);
|
||||
} catch {
|
||||
parentTask = undefined;
|
||||
}
|
||||
|
||||
await store.createTask({
|
||||
title: "Child Task",
|
||||
description: "Child task description",
|
||||
dependencies: [],
|
||||
column: "triage",
|
||||
modelProvider: parentTask?.modelProvider,
|
||||
modelId: parentTask?.modelId,
|
||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||
validatorModelId: parentTask?.validatorModelId,
|
||||
});
|
||||
|
||||
expect(store.getTask).toHaveBeenCalledWith("KB-NONEXISTENT");
|
||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||
modelProvider: undefined,
|
||||
modelId: undefined,
|
||||
validatorModelProvider: undefined,
|
||||
validatorModelId: undefined,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -788,11 +788,25 @@ export class TriageProcessor {
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch parent task to inherit model settings
|
||||
let parentTask: Awaited<ReturnType<typeof store.getTask>> | undefined;
|
||||
try {
|
||||
parentTask = await store.getTask(options.parentTaskId);
|
||||
} catch {
|
||||
// Parent task not found or error - proceed without inheritance
|
||||
parentTask = undefined;
|
||||
}
|
||||
|
||||
const newTask = await store.createTask({
|
||||
title: params.title,
|
||||
description: params.description,
|
||||
dependencies: params.dependencies || [],
|
||||
column: "triage",
|
||||
// Inherit parent's model settings if available
|
||||
modelProvider: parentTask?.modelProvider,
|
||||
modelId: parentTask?.modelId,
|
||||
validatorModelProvider: parentTask?.validatorModelProvider,
|
||||
validatorModelId: parentTask?.validatorModelId,
|
||||
});
|
||||
|
||||
// Track the created subtask
|
||||
|
||||
Reference in New Issue
Block a user