feat(FN-794): raise auto-summarization threshold from 140 to 200 characters
- Change MIN_DESCRIPTION_LENGTH threshold from 140 to 200 in ai-summarize.ts - Update shouldAutoSummarize logic and tests for new 200-char threshold - Update UI copy in SettingsModal and API/routes references - Update store tests to reflect the new threshold value - Remove obsolete dashboard activity-log mobile layout test files and unused CSS - Add changeset for patch bump
This commit is contained in:
@@ -29,7 +29,7 @@ describe("ai-summarize", () => {
|
||||
});
|
||||
|
||||
it("should have correct length limits", () => {
|
||||
expect(MIN_DESCRIPTION_LENGTH).toBe(141);
|
||||
expect(MIN_DESCRIPTION_LENGTH).toBe(201);
|
||||
expect(MAX_DESCRIPTION_LENGTH).toBe(2000);
|
||||
expect(MAX_TITLE_LENGTH).toBe(60);
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe("ai-summarize", () => {
|
||||
|
||||
describe("validateDescription", () => {
|
||||
it("should accept valid description length", () => {
|
||||
const desc = "a".repeat(200);
|
||||
const desc = "a".repeat(201);
|
||||
expect(validateDescription(desc)).toBe(desc);
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("ai-summarize", () => {
|
||||
it("should throw for description too short", () => {
|
||||
const desc = "a".repeat(100);
|
||||
expect(() => validateDescription(desc)).toThrow(ValidationError);
|
||||
expect(() => validateDescription(desc)).toThrow("at least 141 characters");
|
||||
expect(() => validateDescription(desc)).toThrow("at least 201 characters");
|
||||
});
|
||||
|
||||
it("should throw for description too long", () => {
|
||||
@@ -74,7 +74,7 @@ describe("ai-summarize", () => {
|
||||
});
|
||||
|
||||
it("should accept description at minimum boundary", () => {
|
||||
const desc = "a".repeat(141);
|
||||
const desc = "a".repeat(201);
|
||||
expect(validateDescription(desc)).toBe(desc);
|
||||
});
|
||||
|
||||
@@ -139,21 +139,21 @@ describe("ai-summarize", () => {
|
||||
// ── summarizeTitle ─────────────────────────────────────────────────────────
|
||||
|
||||
describe("summarizeTitle", () => {
|
||||
it("should return null for descriptions <= 140 characters", async () => {
|
||||
it("should return null for descriptions <= 200 characters", async () => {
|
||||
const result = await summarizeTitle("Short description", "/tmp");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should throw AiServiceError when engine not available", async () => {
|
||||
// In test environment, the dynamic import fails, so createKbAgent is undefined
|
||||
const longDesc = "a".repeat(200);
|
||||
const longDesc = "a".repeat(201);
|
||||
await expect(summarizeTitle(longDesc, "/tmp")).rejects.toThrow(AiServiceError);
|
||||
await expect(summarizeTitle(longDesc, "/tmp")).rejects.toThrow("AI engine not available");
|
||||
});
|
||||
|
||||
it("should accept optional provider and modelId", async () => {
|
||||
// Since engine isn't available in tests, this will throw
|
||||
const longDesc = "a".repeat(200);
|
||||
const longDesc = "a".repeat(201);
|
||||
await expect(
|
||||
summarizeTitle(longDesc, "/tmp", "anthropic", "claude-sonnet-4-5")
|
||||
).rejects.toThrow(AiServiceError);
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
*
|
||||
* Provides AI-powered title generation from task descriptions.
|
||||
* Automatically generates concise titles (≤60 characters) from descriptions
|
||||
* longer than 140 characters.
|
||||
* longer than 200 characters.
|
||||
*
|
||||
* Features:
|
||||
* - Rate limiting per IP (10 requests per hour)
|
||||
* - Dynamic import of @fusion/engine for AI agent creation
|
||||
* - Text length validation (141-2000 characters)
|
||||
* - Text length validation (201-2000 characters)
|
||||
*/
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
@@ -53,7 +53,7 @@ Your job is to create a concise title (max 60 characters) that summarizes the gi
|
||||
export const MAX_DESCRIPTION_LENGTH = 2000;
|
||||
|
||||
/** Minimum description length for summarization in characters */
|
||||
export const MIN_DESCRIPTION_LENGTH = 141;
|
||||
export const MIN_DESCRIPTION_LENGTH = 201;
|
||||
|
||||
/** Maximum title length in characters */
|
||||
export const MAX_TITLE_LENGTH = 60;
|
||||
@@ -215,7 +215,7 @@ export function validateDescription(description: unknown): string {
|
||||
|
||||
/**
|
||||
* Summarize a task description into a concise title using AI.
|
||||
* @param description - The task description to summarize (must be 141-2000 chars)
|
||||
* @param description - The task description to summarize (must be 201-2000 chars)
|
||||
* @param rootDir - Project root directory for AI agent context
|
||||
* @param provider - Optional AI model provider (e.g., "anthropic")
|
||||
* @param modelId - Optional AI model ID (e.g., "claude-sonnet-4-5")
|
||||
@@ -228,7 +228,7 @@ export async function summarizeTitle(
|
||||
modelId?: string
|
||||
): Promise<string | null> {
|
||||
// Validate description length first
|
||||
if (description.length <= 140) {
|
||||
if (description.length <= 200) {
|
||||
return null; // Too short for summarization
|
||||
}
|
||||
|
||||
|
||||
@@ -4394,7 +4394,7 @@ Task with acceptance criteria
|
||||
|
||||
describe("createTask with title summarization", () => {
|
||||
it("should use generated title when onSummarize returns a title", async () => {
|
||||
const longDescription = "a".repeat(200);
|
||||
const longDescription = "a".repeat(201);
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Generated Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
@@ -4410,7 +4410,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ title: "User Title", description: "a".repeat(200) },
|
||||
{ title: "User Title", description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
@@ -4435,7 +4435,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(200) },
|
||||
{ description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: false } }
|
||||
);
|
||||
|
||||
@@ -4447,7 +4447,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(200) },
|
||||
{ description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize }
|
||||
);
|
||||
|
||||
@@ -4459,7 +4459,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(200), summarize: true },
|
||||
{ description: "a".repeat(201), summarize: true },
|
||||
{ onSummarize: mockOnSummarize }
|
||||
);
|
||||
|
||||
@@ -4471,7 +4471,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue(null);
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(200) },
|
||||
{ description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
@@ -4483,7 +4483,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockRejectedValue(new Error("AI service failed"));
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(200) },
|
||||
{ description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
@@ -4497,8 +4497,8 @@ Task with acceptance criteria
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should trigger summarization at exactly 141 characters", async () => {
|
||||
const boundaryDescription = "a".repeat(141);
|
||||
it("should trigger summarization at exactly 201 characters", async () => {
|
||||
const boundaryDescription = "a".repeat(201);
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
@@ -4509,8 +4509,8 @@ Task with acceptance criteria
|
||||
expect(mockOnSummarize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not trigger summarization at exactly 140 characters", async () => {
|
||||
const boundaryDescription = "a".repeat(140);
|
||||
it("should not trigger summarization at exactly 200 characters", async () => {
|
||||
const boundaryDescription = "a".repeat(200);
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
@@ -4525,7 +4525,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ title: "User Title", description: "a".repeat(200), summarize: true },
|
||||
{ title: "User Title", description: "a".repeat(201), summarize: true },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
@@ -4537,7 +4537,7 @@ Task with acceptance criteria
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("Generated Task Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: "a".repeat(200) },
|
||||
{ description: "a".repeat(201) },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
@@ -4546,6 +4546,22 @@ Task with acceptance criteria
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.prompt).toMatch(/^# FN-\d+: Generated Task Title\n/);
|
||||
});
|
||||
|
||||
it("should preserve original description when generating a title", async () => {
|
||||
const originalDescription = "a".repeat(201);
|
||||
const mockOnSummarize = vi.fn().mockResolvedValue("AI Summary Title");
|
||||
|
||||
const task = await store.createTask(
|
||||
{ description: originalDescription },
|
||||
{ onSummarize: mockOnSummarize, settings: { autoSummarizeTitles: true } }
|
||||
);
|
||||
|
||||
expect(task.title).toBe("AI Summary Title");
|
||||
expect(task.description).toBe(originalDescription);
|
||||
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.description).toBe(originalDescription);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event emissions", () => {
|
||||
|
||||
@@ -672,7 +672,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
let title = input.title?.trim() || undefined;
|
||||
const shouldSummarize =
|
||||
!title && // Only if no title provided
|
||||
input.description.length > 140 && // Only if description is long enough
|
||||
input.description.length > 200 && // Only if description is long enough
|
||||
(input.summarize === true || // Explicit request
|
||||
options?.settings?.autoSummarizeTitles === true); // Auto-enabled
|
||||
|
||||
|
||||
@@ -776,7 +776,7 @@ export interface ProjectSettings {
|
||||
autoBackupRetention?: number;
|
||||
/** Directory for backup files, relative to project root. Default: ".fusion/backups". */
|
||||
autoBackupDir?: string;
|
||||
/** When true, tasks created without titles but with descriptions longer than 140
|
||||
/** When true, tasks created without titles but with descriptions longer than 200
|
||||
* characters will automatically receive an AI-generated title (max 60 chars).
|
||||
* Default: false. */
|
||||
autoSummarizeTitles?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user