Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions apps/code/src/renderer/utils/notifications.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useNavigationStore } from "@stores/navigationStore";
import { beforeEach, describe, expect, it, vi } from "vitest";

const { sendMutate, showDockBadgeMutate, bounceDockMutate, playSound } =
vi.hoisted(() => ({
sendMutate: vi.fn().mockResolvedValue(undefined),
showDockBadgeMutate: vi.fn().mockResolvedValue(undefined),
bounceDockMutate: vi.fn().mockResolvedValue(undefined),
playSound: vi.fn(),
}));

vi.mock("@renderer/trpc/client", () => ({
trpcClient: {
notification: {
send: { mutate: sendMutate },
showDockBadge: { mutate: showDockBadgeMutate },
bounceDock: { mutate: bounceDockMutate },
},
secureStore: {
getItem: { query: vi.fn().mockResolvedValue(null) },
setItem: { query: vi.fn().mockResolvedValue(undefined) },
removeItem: { query: vi.fn().mockResolvedValue(undefined) },
},
},
}));

vi.mock("@utils/logger", () => ({
logger: { scope: () => ({ info: vi.fn(), error: vi.fn(), debug: vi.fn() }) },
}));

vi.mock("@utils/analytics", () => ({ track: vi.fn() }));

vi.mock("@utils/sounds", () => ({
playCompletionSound: playSound,
}));

import { notifyPermissionRequest, notifyPromptComplete } from "./notifications";

const TASK_ID = "task-123";
const OTHER_TASK_ID = "task-999";

function setView(view: {
type: string;
data?: { id: string };
taskId?: string;
}) {
useNavigationStore.setState({
// biome-ignore lint/suspicious/noExplicitAny: test-only narrow cast
view: view as any,
});
}

function setFocus(focused: boolean) {
vi.spyOn(document, "hasFocus").mockReturnValue(focused);
}

describe("notifications", () => {
beforeEach(() => {
sendMutate.mockClear();
showDockBadgeMutate.mockClear();
bounceDockMutate.mockClear();
playSound.mockClear();
useSettingsStore.setState({
desktopNotifications: true,
dockBadgeNotifications: true,
dockBounceNotifications: true,
completionSound: "meep",
completionVolume: 80,
});
setView({ type: "task-input" });
});

describe("shouldNotifyForTask gating (via notifyPermissionRequest)", () => {
it("notifies when the window is unfocused", () => {
setFocus(false);
setView({ type: "task-detail", data: { id: TASK_ID }, taskId: TASK_ID });

notifyPermissionRequest("My task", TASK_ID);

expect(sendMutate).toHaveBeenCalledTimes(1);
expect(playSound).toHaveBeenCalledTimes(1);
});

it("does NOT notify when focused on the same task", () => {
setFocus(true);
setView({ type: "task-detail", data: { id: TASK_ID }, taskId: TASK_ID });

notifyPermissionRequest("My task", TASK_ID);

expect(sendMutate).not.toHaveBeenCalled();
expect(playSound).not.toHaveBeenCalled();
});

it("notifies when focused but viewing a different task", () => {
setFocus(true);
setView({
type: "task-detail",
data: { id: OTHER_TASK_ID },
taskId: OTHER_TASK_ID,
});

notifyPermissionRequest("My task", TASK_ID);

expect(sendMutate).toHaveBeenCalledTimes(1);
expect(playSound).toHaveBeenCalledTimes(1);
});

it("notifies when focused but the view is not a task-detail", () => {
setFocus(true);
setView({ type: "inbox" });

notifyPermissionRequest("My task", TASK_ID);

expect(sendMutate).toHaveBeenCalledTimes(1);
});

it("does NOT notify when focused and no taskId is supplied", () => {
setFocus(true);
setView({ type: "inbox" });

notifyPermissionRequest("My task");

expect(sendMutate).not.toHaveBeenCalled();
});

it("falls back to view.taskId when view.data is missing", () => {
setFocus(true);
setView({ type: "task-detail", taskId: TASK_ID });

notifyPermissionRequest("My task", TASK_ID);

expect(sendMutate).not.toHaveBeenCalled();
});
});

describe("notifyPromptComplete", () => {
it("only fires on end_turn", () => {
setFocus(false);
notifyPromptComplete("My task", "tool_use", TASK_ID);
expect(sendMutate).not.toHaveBeenCalled();

notifyPromptComplete("My task", "end_turn", TASK_ID);
expect(sendMutate).toHaveBeenCalledTimes(1);
});

it("applies the same task-aware gating", () => {
setFocus(true);
setView({ type: "task-detail", data: { id: TASK_ID }, taskId: TASK_ID });
notifyPromptComplete("My task", "end_turn", TASK_ID);
expect(sendMutate).not.toHaveBeenCalled();

setView({
type: "task-detail",
data: { id: OTHER_TASK_ID },
taskId: OTHER_TASK_ID,
});
notifyPromptComplete("My task", "end_turn", TASK_ID);
expect(sendMutate).toHaveBeenCalledTimes(1);
});
});
});
52 changes: 30 additions & 22 deletions apps/code/src/renderer/utils/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { trpcClient } from "@renderer/trpc/client";
import { useNavigationStore } from "@stores/navigationStore";
import { logger } from "@utils/logger";
import { playCompletionSound } from "@utils/sounds";

Expand All @@ -12,6 +13,15 @@ function truncateTitle(title: string): string {
return `${title.slice(0, MAX_TITLE_LENGTH)}...`;
}

function shouldNotifyForTask(taskId?: string): boolean {
if (!document.hasFocus()) return true;
if (!taskId) return false;
const view = useNavigationStore.getState().view;
const viewedTaskId =
view.type === "task-detail" ? (view.data?.id ?? view.taskId) : undefined;
return viewedTaskId !== taskId;
}
Comment on lines +16 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing automated tests for shouldNotifyForTask

The new helper has three distinct, clearly-testable branches (unfocused → always notify; focused + no taskId → never notify; focused + taskId → compare with viewed task), yet no test file exists for notifications.ts. Given the vitest infrastructure already present (see navigationStore.test.ts for the mocking pattern), and simplicity rule #1 ("Passes all the tests"), these branches should have parameterised unit tests covering at minimum: window unfocused, window focused with matching taskId, window focused with different taskId, and window focused with no taskId.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/utils/notifications.ts
Line: 16-23

Comment:
**Missing automated tests for `shouldNotifyForTask`**

The new helper has three distinct, clearly-testable branches (unfocused → always notify; focused + no taskId → never notify; focused + taskId → compare with viewed task), yet no test file exists for `notifications.ts`. Given the vitest infrastructure already present (see `navigationStore.test.ts` for the mocking pattern), and simplicity rule #1 ("Passes all the tests"), these branches should have parameterised unit tests covering at minimum: window unfocused, window focused with matching taskId, window focused with different taskId, and window focused with no taskId.

How can I resolve this? If you propose a fix, please make it concise.


function sendDesktopNotification(
title: string,
body: string,
Expand Down Expand Up @@ -52,8 +62,7 @@ export function notifyPromptComplete(
dockBounceNotifications,
} = useSettingsStore.getState();

const isWindowFocused = document.hasFocus();
if (isWindowFocused) return;
if (!shouldNotifyForTask(taskId)) return;

const willPlayCustomSound = completionSound !== "none";
playCompletionSound(completionSound, completionVolume);
Expand Down Expand Up @@ -85,25 +94,24 @@ export function notifyPermissionRequest(
dockBadgeNotifications,
dockBounceNotifications,
} = useSettingsStore.getState();
const isWindowFocused = document.hasFocus();

if (!isWindowFocused) {
const willPlayCustomSound = completionSound !== "none";
playCompletionSound(completionSound, completionVolume);

if (desktopNotifications) {
sendDesktopNotification(
"PostHog Code",
`"${truncateTitle(taskTitle)}" needs your input`,
willPlayCustomSound,
taskId,
);
}
if (dockBadgeNotifications) {
showDockBadge();
}
if (dockBounceNotifications) {
bounceDock();
}

if (!shouldNotifyForTask(taskId)) return;

const willPlayCustomSound = completionSound !== "none";
playCompletionSound(completionSound, completionVolume);

if (desktopNotifications) {
sendDesktopNotification(
"PostHog Code",
`"${truncateTitle(taskTitle)}" needs your input`,
willPlayCustomSound,
taskId,
);
}
if (dockBadgeNotifications) {
showDockBadge();
}
if (dockBounceNotifications) {
bounceDock();
}
}
Loading