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:
gsxdsm
2026-04-03 17:58:07 -07:00
parent 80712060b7
commit c0f71e0db6
10 changed files with 60 additions and 39 deletions

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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", () => {

View File

@@ -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

View File

@@ -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;