feat(KB-604): include task context in notifications with description fallback

- Add formatTaskIdentifier helper to format task references with title or description fallback
- Update notification messages to include task identifier context
- Show truncated description (200 chars) prefixed with task ID when no title
- Add comprehensive tests for formatTaskIdentifier function
- Add changeset for notification improvements and update AGENTS.md
This commit is contained in:
gsxdsm
2026-03-31 17:08:05 -07:00
parent ab3dff2100
commit f4dad9197f
4 changed files with 66 additions and 6 deletions

View File

@@ -220,18 +220,56 @@ describe("NtfyNotifier", () => {
expect(fetchMock).not.toHaveBeenCalled();
});
it("uses task ID when title is not available", async () => {
it("uses task ID and description when title is not available", async () => {
notifier = new NtfyNotifier(store);
await notifier.start();
store.triggerTaskMoved(createTask("KB-001"), "in-progress", "in-review");
const taskWithoutTitle = { ...createTask("KB-001"), description: "Implement user authentication flow" };
store.triggerTaskMoved(taskWithoutTitle, "in-progress", "in-review");
await new Promise(resolve => setTimeout(resolve, 10));
expect(fetchMock).toHaveBeenCalledWith(
"https://ntfy.sh/test-topic",
expect.objectContaining({
body: 'Task "KB-001" is ready for review',
body: 'Task "KB-001: Implement user authentication flow" is ready for review',
})
);
});
it("truncates description to 200 characters when no title is set", async () => {
notifier = new NtfyNotifier(store);
await notifier.start();
const longDescription = "A".repeat(250);
const taskWithoutTitle = { ...createTask("KB-001"), description: longDescription };
store.triggerTaskMoved(taskWithoutTitle, "in-progress", "in-review");
await new Promise(resolve => setTimeout(resolve, 10));
const expectedSnippet = "A".repeat(200) + "...";
expect(fetchMock).toHaveBeenCalledWith(
"https://ntfy.sh/test-topic",
expect.objectContaining({
body: `Task "KB-001: ${expectedSnippet}" is ready for review`,
})
);
});
it("does not truncate description at exactly 200 characters", async () => {
notifier = new NtfyNotifier(store);
await notifier.start();
const exactDescription = "B".repeat(200);
const taskWithoutTitle = { ...createTask("KB-001"), description: exactDescription };
store.triggerTaskMoved(taskWithoutTitle, "in-progress", "in-review");
await new Promise(resolve => setTimeout(resolve, 10));
expect(fetchMock).toHaveBeenCalledWith(
"https://ntfy.sh/test-topic",
expect.objectContaining({
body: `Task "KB-001: ${exactDescription}" is ready for review`,
})
);
});

View File

@@ -7,6 +7,22 @@ export interface NtfyNotifierOptions {
ntfyBaseUrl?: string;
}
/**
* Format a task identifier for notifications.
* - If title exists: returns "{title}"
* - If no title: returns "{id}: {first 200 chars of description}" (truncated with "..." if > 200)
*/
function formatTaskIdentifier(task: Task): string {
if (task.title) {
return task.title;
}
const maxLen = 200;
const snippet = task.description.length > maxLen
? task.description.slice(0, maxLen) + "..."
: task.description;
return `${task.id}: ${snippet}`;
}
/** Minimal store interface needed by NtfyNotifier */
interface NtfyNotifierStore {
getSettings(): Promise<Settings> | Settings;
@@ -104,7 +120,7 @@ export class NtfyNotifier {
this.sendNotification(
this.config.topic!,
`Task ${task.id} completed`,
`Task "${task.title ?? task.id}" is ready for review`,
`Task "${formatTaskIdentifier(task)}" is ready for review`,
"default",
),
);
@@ -123,7 +139,7 @@ export class NtfyNotifier {
this.sendNotification(
this.config.topic!,
`Task ${task.id} failed`,
`Task "${task.title ?? task.id}" has failed and needs attention`,
`Task "${formatTaskIdentifier(task)}" has failed and needs attention`,
"high",
),
);
@@ -139,7 +155,7 @@ export class NtfyNotifier {
this.sendNotification(
this.config.topic!,
`Task ${result.task.id} merged`,
`Task "${result.task.title ?? result.task.id}" has been merged to main`,
`Task "${formatTaskIdentifier(result.task)}" has been merged to main`,
"default",
),
);