feat(FN-1530): merge fusion/fn-1530
This commit is contained in:
@@ -5998,8 +5998,16 @@ Task with acceptance criteria
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
expect(task.title).toBe("AI Generated Title");
|
||||
// Title is not set synchronously - summarization happens async
|
||||
expect(task.title).toBeUndefined();
|
||||
expect(mockOnSummarize).toHaveBeenCalledWith(longDescription);
|
||||
|
||||
// Wait for async summarization to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
// Verify title was set asynchronously
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBe("AI Generated Title");
|
||||
});
|
||||
|
||||
it("should not call onSummarize when title is already provided", async () => {
|
||||
@@ -6059,8 +6067,16 @@ Task with acceptance criteria
|
||||
{ onSummarize: mockOnSummarize }
|
||||
);
|
||||
|
||||
expect(task.title).toBe("AI Title");
|
||||
// Title is not set synchronously
|
||||
expect(task.title).toBeUndefined();
|
||||
expect(mockOnSummarize).toHaveBeenCalled();
|
||||
|
||||
// Wait for async summarization to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
// Verify title was set asynchronously
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBe("AI Title");
|
||||
});
|
||||
|
||||
it("should handle onSummarize returning null", async () => {
|
||||
@@ -6071,7 +6087,15 @@ Task with acceptance criteria
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
// Task created without title
|
||||
expect(task.title).toBeUndefined();
|
||||
|
||||
// Wait for async summarization to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
// Title should remain undefined
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle onSummarize throwing error gracefully", async () => {
|
||||
@@ -6085,6 +6109,10 @@ Task with acceptance criteria
|
||||
|
||||
expect(task.title).toBeUndefined();
|
||||
expect(task.id).toMatch(/^FN-\d+$/); // Task still created
|
||||
|
||||
// Wait for async error to be logged
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(consoleSpy.mock.calls[0][0]).toMatch(/Title summarization failed for task/);
|
||||
expect(consoleSpy.mock.calls[0][0]).toMatch(/AI service failed/);
|
||||
expect(consoleSpy.mock.calls[0][0]).toMatch(/desc length: 201/);
|
||||
@@ -6103,6 +6131,12 @@ Task with acceptance criteria
|
||||
);
|
||||
|
||||
expect(mockOnSummarize).toHaveBeenCalled();
|
||||
|
||||
// Wait for async summarization
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBe("AI Title");
|
||||
});
|
||||
|
||||
it("should not trigger summarization at exactly 200 characters", async () => {
|
||||
@@ -6115,6 +6149,7 @@ Task with acceptance criteria
|
||||
);
|
||||
|
||||
expect(mockOnSummarize).not.toHaveBeenCalled();
|
||||
expect(task.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should prioritize explicit title over summarize flag", async () => {
|
||||
@@ -6137,10 +6172,16 @@ Task with acceptance criteria
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
expect(task.title).toBe("Generated Task Title");
|
||||
// Title not set synchronously
|
||||
expect(task.title).toBeUndefined();
|
||||
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.prompt).toMatch(/^# FN-\d+: Generated Task Title\n/);
|
||||
// Wait for async summarization
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
// Verify title and PROMPT.md were updated
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBe("Generated Task Title");
|
||||
expect(updatedTask.prompt).toMatch(/^# FN-\d+: Generated Task Title\n/);
|
||||
});
|
||||
|
||||
it("should preserve original description when generating a title", async () => {
|
||||
@@ -6152,11 +6193,39 @@ Task with acceptance criteria
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
expect(task.title).toBe("AI Summary Title");
|
||||
// Title not set synchronously
|
||||
expect(task.title).toBeUndefined();
|
||||
expect(task.description).toBe(originalDescription);
|
||||
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.description).toBe(originalDescription);
|
||||
// Wait for async summarization
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBe("AI Summary Title");
|
||||
expect(updatedTask.description).toBe(originalDescription);
|
||||
});
|
||||
|
||||
it("should not overwrite user-set title during async summarization", async () => {
|
||||
const mockOnSummarize = vi.fn().mockImplementation(async () => {
|
||||
// Simulate slow AI response
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
return "AI Title";
|
||||
});
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
// Immediately update with user title
|
||||
await store.updateTask(task.id, { title: "User Title" });
|
||||
|
||||
// Wait for delayed onSummarize to resolve
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Title should still be "User Title" (race guard should have prevented overwrite)
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBe("User Title");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1116,30 +1116,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
|
||||
// Determine if we should try to summarize the title
|
||||
let title = input.title?.trim() || undefined;
|
||||
const title = input.title?.trim() || undefined;
|
||||
const shouldSummarize =
|
||||
!title && // Only if no title provided
|
||||
input.description.length > 200 && // Only if description is long enough
|
||||
(input.summarize === true || // Explicit request
|
||||
options?.settings?.autoSummarizeTitles === true); // Auto-enabled
|
||||
|
||||
if (shouldSummarize && options?.onSummarize) {
|
||||
try {
|
||||
const generatedTitle = await options.onSummarize(input.description);
|
||||
if (generatedTitle) {
|
||||
title = generatedTitle;
|
||||
}
|
||||
} catch (err) {
|
||||
// Log warning but don't block task creation
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
const autoEnabled = options?.settings?.autoSummarizeTitles === true;
|
||||
console.warn(
|
||||
`[TaskStore] Title summarization failed for task ${id}: ${errorMsg}` +
|
||||
` (desc length: ${input.description.length}, auto-summarize: ${autoEnabled})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine enabledWorkflowSteps: explicit input takes precedence, otherwise auto-apply default-on steps
|
||||
let resolvedWorkflowSteps: string[] | undefined = input.enabledWorkflowSteps?.length
|
||||
? await this.resolveEnabledWorkflowSteps(input.enabledWorkflowSteps)
|
||||
@@ -1163,6 +1146,46 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
resolvedWorkflowSteps = undefined;
|
||||
}
|
||||
|
||||
// Create the task immediately with current title (may be undefined)
|
||||
const task = await this._createTaskInternal(input, title, resolvedWorkflowSteps, id);
|
||||
|
||||
// Fire async background handler for title summarization (non-blocking)
|
||||
if (shouldSummarize && options?.onSummarize) {
|
||||
Promise.resolve().then(async () => {
|
||||
try {
|
||||
const generatedTitle = await options.onSummarize!(input.description);
|
||||
if (generatedTitle) {
|
||||
// Guard against races: fetch current task and only update if no title set
|
||||
const currentTask = await this.getTask(id);
|
||||
if (currentTask && !currentTask.title) {
|
||||
await this.updateTask(id, { title: generatedTitle });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Log warning but don't crash
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
const autoEnabled = options?.settings?.autoSummarizeTitles === true;
|
||||
console.warn(
|
||||
`[TaskStore] Title summarization failed for task ${id}: ${errorMsg}` +
|
||||
` (desc length: ${input.description.length}, auto-summarize: ${autoEnabled})`
|
||||
);
|
||||
}
|
||||
}).catch(() => {}); // Prevent unhandled rejection
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper for task creation. Used by createTask() and potentially other
|
||||
* internal methods that need to create tasks without triggering summarization.
|
||||
*/
|
||||
private async _createTaskInternal(
|
||||
input: TaskCreateInput,
|
||||
title: string | undefined,
|
||||
resolvedWorkflowSteps: string[] | undefined,
|
||||
id: string
|
||||
): Promise<Task> {
|
||||
const now = new Date().toISOString();
|
||||
const task: Task = {
|
||||
id,
|
||||
|
||||
Reference in New Issue
Block a user