fix(HAI-039): eliminate duplicate description in prompts and modal heading
- Fix prompt heading to use only task ID when title is absent, preventing description duplication - Strip leading markdown heading from TaskDetailModal since the modal has its own header - Add store tests covering prompt generation for tasks with and without titles - Update TaskDetailModal tests to verify heading stripping and single description rendering
This commit is contained in:
@@ -55,6 +55,58 @@ describe("TaskStore", () => {
|
||||
return task;
|
||||
}
|
||||
|
||||
// ── Prompt generation (no duplicate description) ───────────────
|
||||
|
||||
describe("prompt generation", () => {
|
||||
it("triage task without title does not duplicate description in PROMPT.md", async () => {
|
||||
const task = await store.createTask({ description: "Fix the login bug" });
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
// Heading should be just the ID, not the description
|
||||
expect(detail.prompt).toMatch(/^# HAI-001\n/);
|
||||
// Description appears exactly once
|
||||
const count = detail.prompt.split("Fix the login bug").length - 1;
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it("triage task with title uses title in heading and description in body", async () => {
|
||||
const task = await store.createTask({
|
||||
title: "Login bug",
|
||||
description: "Fix the login bug on the settings page",
|
||||
});
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
expect(detail.prompt).toMatch(/^# HAI-001: Login bug\n/);
|
||||
expect(detail.prompt).toContain("Fix the login bug on the settings page");
|
||||
});
|
||||
|
||||
it("generateSpecifiedPrompt does not duplicate when title is absent", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "Implement caching layer",
|
||||
column: "todo",
|
||||
});
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
// Heading should be just the ID
|
||||
expect(detail.prompt).toMatch(/^# HAI-001\n/);
|
||||
// Description appears exactly once (in Mission section)
|
||||
const count = detail.prompt.split("Implement caching layer").length - 1;
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it("generateSpecifiedPrompt uses title in heading when present", async () => {
|
||||
const task = await store.createTask({
|
||||
title: "Add caching",
|
||||
description: "Implement caching layer for API responses",
|
||||
column: "todo",
|
||||
});
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
expect(detail.prompt).toMatch(/^# HAI-001: Add caching\n/);
|
||||
expect(detail.prompt).toContain("Implement caching layer for API responses");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Lock serialization test ──────────────────────────────────────
|
||||
|
||||
describe("write lock serialization", () => {
|
||||
|
||||
@@ -197,9 +197,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
// Update cache if watcher is active
|
||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||
|
||||
const heading = task.title || task.description;
|
||||
const heading = task.title ? `${id}: ${task.title}` : id;
|
||||
const prompt = task.column === "triage"
|
||||
? `# ${id}: ${heading}\n\n${task.description}\n`
|
||||
? `# ${heading}\n\n${task.description}\n`
|
||||
: this.generateSpecifiedPrompt(task);
|
||||
await writeFile(join(dir, "PROMPT.md"), prompt);
|
||||
|
||||
@@ -835,15 +835,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
? task.dependencies.map((d) => `- **Task:** ${d}`).join("\n")
|
||||
: "- **None**";
|
||||
|
||||
const heading = task.title || task.description;
|
||||
return `# ${task.id}: ${heading}
|
||||
const heading = task.title ? `${task.id}: ${task.title}` : task.id;
|
||||
return `# ${heading}
|
||||
|
||||
**Created:** ${task.createdAt.split("T")[0]}
|
||||
**Size:** M
|
||||
|
||||
## Mission
|
||||
|
||||
${task.description || task.title}
|
||||
${task.description}
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ export function TaskDetailModal({
|
||||
{task.prompt ? (
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{task.prompt}
|
||||
{task.prompt.replace(/^#\s+[^\n]*\n+/, "")}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -41,7 +41,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(markdownDiv!.classList.contains("detail-prompt")).toBe(false);
|
||||
});
|
||||
|
||||
it("renders ReactMarkdown output (heading and bold text)", () => {
|
||||
it("strips the leading heading from prompt and renders remaining markdown", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nSome **bold** text" })}
|
||||
@@ -53,7 +53,8 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector("h1")?.textContent).toBe("Hello");
|
||||
// The leading # heading should be stripped (modal has its own header)
|
||||
expect(container.querySelector(".markdown-body h1")).toBeNull();
|
||||
expect(container.querySelector("strong")?.textContent).toBe("bold");
|
||||
});
|
||||
|
||||
@@ -89,4 +90,29 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
expect(screen.queryByText("PROMPT.md")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows description exactly once for a task without title", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
title: undefined,
|
||||
description: "Fix the login bug",
|
||||
prompt: "# HAI-099\n\nFix the login bug\n",
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The heading "HAI-099" should be stripped from the markdown
|
||||
const markdownBody = container.querySelector(".markdown-body");
|
||||
expect(markdownBody?.innerHTML).not.toContain("HAI-099");
|
||||
// Description appears in the markdown body
|
||||
expect(markdownBody?.textContent).toContain("Fix the login bug");
|
||||
// The detail header shows the ID (not duplicated as markdown heading)
|
||||
expect(container.querySelector(".detail-id")?.textContent).toBe("HAI-099");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user