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;
|
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 ──────────────────────────────────────
|
// ── Lock serialization test ──────────────────────────────────────
|
||||||
|
|
||||||
describe("write lock serialization", () => {
|
describe("write lock serialization", () => {
|
||||||
|
|||||||
@@ -197,9 +197,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
// Update cache if watcher is active
|
// Update cache if watcher is active
|
||||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
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"
|
const prompt = task.column === "triage"
|
||||||
? `# ${id}: ${heading}\n\n${task.description}\n`
|
? `# ${heading}\n\n${task.description}\n`
|
||||||
: this.generateSpecifiedPrompt(task);
|
: this.generateSpecifiedPrompt(task);
|
||||||
await writeFile(join(dir, "PROMPT.md"), prompt);
|
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")
|
? task.dependencies.map((d) => `- **Task:** ${d}`).join("\n")
|
||||||
: "- **None**";
|
: "- **None**";
|
||||||
|
|
||||||
const heading = task.title || task.description;
|
const heading = task.title ? `${task.id}: ${task.title}` : task.id;
|
||||||
return `# ${task.id}: ${heading}
|
return `# ${heading}
|
||||||
|
|
||||||
**Created:** ${task.createdAt.split("T")[0]}
|
**Created:** ${task.createdAt.split("T")[0]}
|
||||||
**Size:** M
|
**Size:** M
|
||||||
|
|
||||||
## Mission
|
## Mission
|
||||||
|
|
||||||
${task.description || task.title}
|
${task.description}
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export function TaskDetailModal({
|
|||||||
{task.prompt ? (
|
{task.prompt ? (
|
||||||
<div className="markdown-body">
|
<div className="markdown-body">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
{task.prompt}
|
{task.prompt.replace(/^#\s+[^\n]*\n+/, "")}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(markdownDiv!.classList.contains("detail-prompt")).toBe(false);
|
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(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({ prompt: "# Hello\n\nSome **bold** text" })}
|
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");
|
expect(container.querySelector("strong")?.textContent).toBe("bold");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,4 +90,29 @@ describe("TaskDetailModal", () => {
|
|||||||
|
|
||||||
expect(screen.queryByText("PROMPT.md")).toBeNull();
|
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