feat(FN-4942): complete Step 1 — auto-attach summarizer in createTask

Fusion-Task-Id: FN-4942
Fusion-Task-Lineage: 20865705-e5ec-4261-932c-4d47c2d8dc12
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 16:38:24 -07:00
committed by gsxdsm
parent f5cd0dd472
commit fc98f46196
2 changed files with 83 additions and 6 deletions

View File

@@ -1,5 +1,17 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { summarizeTitleMock } = vi.hoisted(() => ({
summarizeTitleMock: vi.fn(),
}));
vi.mock("../ai-summarize.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../ai-summarize.js")>();
return {
...actual,
summarizeTitle: summarizeTitleMock,
};
});
import { setTaskCreatedHook } from "../task-creation-hooks.js";
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
@@ -8,6 +20,7 @@ describe("task creation hook", () => {
beforeEach(async () => {
setTaskCreatedHook(undefined);
summarizeTitleMock.mockReset();
await harness.beforeEach();
});
@@ -202,5 +215,40 @@ describe("task creation hook", () => {
expect(hook).toHaveBeenCalledTimes(1);
expect(onSummarize).not.toHaveBeenCalled();
});
it("auto-attaches summarizer from settings when options are omitted", async () => {
const store = harness.store();
const hook = vi.fn();
summarizeTitleMock.mockResolvedValue("Auto Generated Title");
setTaskCreatedHook(hook);
await store.updateSettings({
autoSummarizeTitles: true,
titleSummarizerProvider: "openai",
titleSummarizerModelId: "gpt-5-mini",
});
await store.createTask({ description: "a".repeat(201) });
expect(hook).not.toHaveBeenCalled();
await vi.waitFor(() => {
expect(hook).toHaveBeenCalledTimes(1);
});
expect(summarizeTitleMock).toHaveBeenCalledTimes(1);
expect(hook).toHaveBeenCalledWith(expect.objectContaining({ title: "Auto Generated Title" }), store);
});
it("fires hook synchronously when auto-summarize is enabled but no model resolves", async () => {
const store = harness.store();
const hook = vi.fn();
setTaskCreatedHook(hook);
await store.updateSettings({ autoSummarizeTitles: true });
await store.createTask({ description: "b".repeat(201) });
expect(hook).toHaveBeenCalledTimes(1);
expect(summarizeTitleMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -33,8 +33,9 @@ import { ensureMemoryFileWithBackend } from "./project-memory.js";
import { runCommandAsync } from "./run-command.js";
import { createLogger } from "./logger.js";
import { validateNodeOverrideChange } from "./node-override-guard.js";
import { sanitizeTitle } from "./ai-summarize.js";
import { sanitizeTitle, summarizeTitle } from "./ai-summarize.js";
import { extractTaskIdTokens, normalizeTitleForTaskId } from "./task-title-id-drift.js";
import { resolveTitleSummarizerSettingsModel } from "./model-resolution.js";
import { getErrorMessage } from "./error-message.js";
import { getTaskCreatedHook } from "./task-creation-hooks.js";
import { assertProjectRootDir } from "./project-root-guard.js";
@@ -2940,13 +2941,41 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
);
}
let resolvedSettings = options?.settings;
if (!resolvedSettings) {
try {
resolvedSettings = await this.getSettings();
} catch {
resolvedSettings = {};
}
}
let onSummarize = options?.onSummarize;
if (!onSummarize && resolvedSettings?.autoSummarizeTitles === true) {
const summarizerModel = resolveTitleSummarizerSettingsModel(resolvedSettings);
if (summarizerModel.provider && summarizerModel.modelId) {
onSummarize = async (description: string) => {
try {
return await summarizeTitle(
description,
this.getRootDir(),
summarizerModel.provider,
summarizerModel.modelId,
);
} catch {
return null;
}
};
}
}
// Determine if we should try to summarize the title
const title = input.title?.trim() || undefined;
const shouldSummarize =
!title &&
input.description.length > 200 &&
(input.summarize === true || options?.settings?.autoSummarizeTitles === true);
const hasPendingSummarization = shouldSummarize && typeof options?.onSummarize === "function";
(input.summarize === true || resolvedSettings?.autoSummarizeTitles === true);
const hasPendingSummarization = shouldSummarize && typeof onSummarize === "function";
const shouldInvokeTaskCreatedHook = options?.invokeTaskCreatedHook !== false;
// Determine enabledWorkflowSteps: explicit input takes precedence, otherwise auto-apply default-on steps
@@ -2994,7 +3023,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const id = task.id;
Promise.resolve().then(async () => {
try {
const generatedTitle = await options.onSummarize!(input.description);
const generatedTitle = await onSummarize!(input.description);
const sanitizedTitle = sanitizeTitle(generatedTitle);
if (sanitizedTitle) {
const currentTask = this.readTaskFromDb(id);
@@ -3006,7 +3035,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
}
} catch (err) {
const autoEnabled = options?.settings?.autoSummarizeTitles === true;
const autoEnabled = resolvedSettings?.autoSummarizeTitles === true;
const errorMessage = err instanceof Error ? err.message : String(err);
storeLog.warn(
`Title summarization failed for task ${id}: ${errorMessage} (desc length: ${input.description.length}, auto-summarize: ${autoEnabled})`,
@@ -3036,7 +3065,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
});
}
}).catch((err) => {
const autoEnabled = options?.settings?.autoSummarizeTitles === true;
const autoEnabled = resolvedSettings?.autoSummarizeTitles === true;
storeLog.error("Unexpected title summarization promise-chain failure", {
taskId: id,
descriptionLength: input.description.length,