feat(FN-3056): merge fusion/fn-3056
This merge ships several feature and infrastructure improvements across the codebase. Task title validation is strengthened in triage with stricter rejection of malformed titles and preference for prompt-declared titles (FN-3056), while task creation now preserves priority settings (FN-3210). The Mi Fusion-Task-Id: FN-3056
This commit is contained in:
@@ -270,15 +270,13 @@ describe("ai-summarize", () => {
|
||||
expect(sanitizeTitle("\n\n hello world \nignored")).toBe("hello world");
|
||||
});
|
||||
|
||||
it("strips chatty markdown reply (FN-3057 incident shape)", () => {
|
||||
const raw =
|
||||
"Created **FN-3058** with the full spec. Let me know if you want changes.";
|
||||
// First line is the whole thing — sanitizer should strip the markdown bold
|
||||
// and trailing period; truncation happens at MAX_TITLE_LENGTH (60).
|
||||
const out = sanitizeTitle(raw)!;
|
||||
expect(out).not.toContain("**");
|
||||
expect(out.length).toBeLessThanOrEqual(60);
|
||||
expect(out.startsWith("Created FN-3058")).toBe(true);
|
||||
it("rejects task-creation confirmation prose (FN-3056 regression)", () => {
|
||||
expect(
|
||||
sanitizeTitle("Created task **FN-3058** in the triage column. Here's a summary."),
|
||||
).toBeNull();
|
||||
expect(
|
||||
sanitizeTitle("Created **FN-3058** with the full spec"),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("strips quotes, backticks, leading bullets", () => {
|
||||
|
||||
@@ -9075,6 +9075,24 @@ Task with acceptance criteria
|
||||
expect(updatedTask.title).toBe("AI Title");
|
||||
});
|
||||
|
||||
it("should ignore malformed confirmation-prose generated titles", async () => {
|
||||
const mockOnSummarize = vi
|
||||
.fn()
|
||||
.mockResolvedValue("Created task **FN-9999** in the triage column. Here's a summary.");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
expect(task.title).toBeUndefined();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const updatedTask = await store.getTask(task.id);
|
||||
expect(updatedTask.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle onSummarize returning null", async () => {
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue(null);
|
||||
|
||||
|
||||
@@ -859,6 +859,12 @@ export function sanitizeTitle(raw: string | undefined | null): string | null {
|
||||
.replace(/(?<![*\w])\*([^*]+)\*(?![*\w])/g, "$1")
|
||||
.replace(/(?<![_\w])_([^_]+)_(?![_\w])/g, "$1");
|
||||
|
||||
// Reject tool/assistant confirmation prose so we never persist
|
||||
// "Created task FN-1234 ..." as a user-visible task title.
|
||||
if (/^created\s+(?:task\s+)?(?:fn-\d+\b|\*\*\s*fn-\d+\s*\*\*)/i.test(title)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Drop trailing punctuation that summary-like sentences leave behind.
|
||||
title = title.replace(/[.!?,;:]+$/, "").trim();
|
||||
if (!title) return null;
|
||||
|
||||
@@ -23,6 +23,7 @@ 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";
|
||||
|
||||
/** Database row shape for the tasks table (all columns). */
|
||||
interface TaskRow {
|
||||
@@ -2083,7 +2084,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
Promise.resolve().then(async () => {
|
||||
try {
|
||||
const generatedTitle = await options.onSummarize!(input.description);
|
||||
const normalizedTitle = generatedTitle?.trim();
|
||||
const normalizedTitle = sanitizeTitle(generatedTitle);
|
||||
if (normalizedTitle) {
|
||||
// Guard against races: read directly from SQLite to avoid extra
|
||||
// prompt/step file I/O in this background path.
|
||||
|
||||
Reference in New Issue
Block a user