fix(hooks): stabilize session notification checks in parallel tests
Use sender-module indirection and an optional main-session filter guard to keep notification assertions deterministic across concurrent test execution. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ const { describe, expect, test, beforeEach, afterEach, spyOn } = require("bun:te
|
||||
const { createSessionNotification } = require("./session-notification")
|
||||
const { setMainSession, subagentSessions, _resetForTesting } = require("../features/claude-code-session-state")
|
||||
const utils = require("./session-notification-utils")
|
||||
const sender = require("./session-notification-sender")
|
||||
|
||||
describe("session-notification input-needed events", () => {
|
||||
let notificationCalls: string[]
|
||||
@@ -37,6 +38,10 @@ describe("session-notification input-needed events", () => {
|
||||
spyOn(utils, "getNotifySendPath").mockResolvedValue("/usr/bin/notify-send")
|
||||
spyOn(utils, "getPowershellPath").mockResolvedValue("powershell")
|
||||
spyOn(utils, "startBackgroundCheck").mockImplementation(() => {})
|
||||
spyOn(sender, "detectPlatform").mockReturnValue("darwin")
|
||||
spyOn(sender, "sendSessionNotification").mockImplementation(async (_ctx: unknown, _platform: unknown, _title: unknown, message: string) => {
|
||||
notificationCalls.push(message)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -47,7 +52,7 @@ describe("session-notification input-needed events", () => {
|
||||
test("sends question notification when question tool asks for input", async () => {
|
||||
const sessionID = "main-question"
|
||||
setMainSession(sessionID)
|
||||
const hook = createSessionNotification(createMockPluginInput())
|
||||
const hook = createSessionNotification(createMockPluginInput(), { enforceMainSessionFilter: false })
|
||||
|
||||
await hook({
|
||||
event: {
|
||||
@@ -74,7 +79,7 @@ describe("session-notification input-needed events", () => {
|
||||
test("sends permission notification for permission events", async () => {
|
||||
const sessionID = "main-permission"
|
||||
setMainSession(sessionID)
|
||||
const hook = createSessionNotification(createMockPluginInput())
|
||||
const hook = createSessionNotification(createMockPluginInput(), { enforceMainSessionFilter: false })
|
||||
|
||||
await hook({
|
||||
event: {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
const { describe, expect, test, beforeEach, afterEach, spyOn } = require("bun:test")
|
||||
|
||||
import { createSessionNotification } from "./session-notification"
|
||||
import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state"
|
||||
import * as utils from "./session-notification-utils"
|
||||
import * as sender from "./session-notification-sender"
|
||||
|
||||
describe("session-notification", () => {
|
||||
let notificationCalls: string[]
|
||||
@@ -40,6 +41,10 @@ describe("session-notification", () => {
|
||||
spyOn(utils, "getPaplayPath").mockResolvedValue("/usr/bin/paplay")
|
||||
spyOn(utils, "getAplayPath").mockResolvedValue("/usr/bin/aplay")
|
||||
spyOn(utils, "startBackgroundCheck").mockImplementation(() => {})
|
||||
spyOn(sender, "detectPlatform").mockReturnValue("darwin")
|
||||
spyOn(sender, "sendSessionNotification").mockImplementation(async (_ctx, _platform, _title, message) => {
|
||||
notificationCalls.push(message)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -105,6 +110,7 @@ describe("session-notification", () => {
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 10,
|
||||
skipIfIncompleteTodos: false,
|
||||
enforceMainSessionFilter: false,
|
||||
})
|
||||
|
||||
// when - main session goes idle
|
||||
@@ -332,6 +338,7 @@ describe("session-notification", () => {
|
||||
const hook = createSessionNotification(createMockPluginInput(), {
|
||||
idleConfirmationDelay: 10,
|
||||
skipIfIncompleteTodos: false,
|
||||
enforceMainSessionFilter: false,
|
||||
})
|
||||
|
||||
// when - session goes idle twice
|
||||
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
startBackgroundCheck,
|
||||
} from "./session-notification-utils"
|
||||
import {
|
||||
detectPlatform,
|
||||
getDefaultSoundPath,
|
||||
playSessionNotificationSound,
|
||||
sendSessionNotification,
|
||||
type Platform,
|
||||
} from "./session-notification-sender"
|
||||
import * as sessionNotificationSender from "./session-notification-sender"
|
||||
import { hasIncompleteTodos } from "./session-todo-status"
|
||||
import { createIdleNotificationScheduler } from "./session-notification-scheduler"
|
||||
|
||||
@@ -25,13 +23,14 @@ interface SessionNotificationConfig {
|
||||
skipIfIncompleteTodos?: boolean
|
||||
/** Maximum number of sessions to track before cleanup (default: 100) */
|
||||
maxTrackedSessions?: number
|
||||
enforceMainSessionFilter?: boolean
|
||||
}
|
||||
export function createSessionNotification(
|
||||
ctx: PluginInput,
|
||||
config: SessionNotificationConfig = {}
|
||||
) {
|
||||
const currentPlatform = detectPlatform()
|
||||
const defaultSoundPath = getDefaultSoundPath(currentPlatform)
|
||||
const currentPlatform: Platform = sessionNotificationSender.detectPlatform()
|
||||
const defaultSoundPath = sessionNotificationSender.getDefaultSoundPath(currentPlatform)
|
||||
|
||||
startBackgroundCheck(currentPlatform)
|
||||
|
||||
@@ -45,6 +44,7 @@ export function createSessionNotification(
|
||||
idleConfirmationDelay: 1500,
|
||||
skipIfIncompleteTodos: true,
|
||||
maxTrackedSessions: 100,
|
||||
enforceMainSessionFilter: true,
|
||||
...config,
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ export function createSessionNotification(
|
||||
platform: currentPlatform,
|
||||
config: mergedConfig,
|
||||
hasIncompleteTodos,
|
||||
send: sendSessionNotification,
|
||||
playSound: playSessionNotificationSound,
|
||||
send: sessionNotificationSender.sendSessionNotification,
|
||||
playSound: sessionNotificationSender.playSessionNotificationSound,
|
||||
})
|
||||
|
||||
const QUESTION_TOOLS = new Set(["question", "ask_user_question", "askuserquestion"])
|
||||
@@ -81,8 +81,10 @@ export function createSessionNotification(
|
||||
const shouldNotifyForSession = (sessionID: string): boolean => {
|
||||
if (subagentSessions.has(sessionID)) return false
|
||||
|
||||
const mainSessionID = getMainSessionID()
|
||||
if (mainSessionID && sessionID !== mainSessionID) return false
|
||||
if (mergedConfig.enforceMainSessionFilter) {
|
||||
const mainSessionID = getMainSessionID()
|
||||
if (mainSessionID && sessionID !== mainSessionID) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -146,9 +148,14 @@ export function createSessionNotification(
|
||||
if (!shouldNotifyForSession(sessionID)) return
|
||||
|
||||
scheduler.markSessionActivity(sessionID)
|
||||
await sendSessionNotification(ctx, currentPlatform, mergedConfig.title, mergedConfig.permissionMessage)
|
||||
await sessionNotificationSender.sendSessionNotification(
|
||||
ctx,
|
||||
currentPlatform,
|
||||
mergedConfig.title,
|
||||
mergedConfig.permissionMessage,
|
||||
)
|
||||
if (mergedConfig.playSound && mergedConfig.soundPath) {
|
||||
await playSessionNotificationSound(ctx, currentPlatform, mergedConfig.soundPath)
|
||||
await sessionNotificationSender.playSessionNotificationSound(ctx, currentPlatform, mergedConfig.soundPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -168,9 +175,9 @@ export function createSessionNotification(
|
||||
? mergedConfig.permissionMessage
|
||||
: mergedConfig.questionMessage
|
||||
|
||||
await sendSessionNotification(ctx, currentPlatform, mergedConfig.title, message)
|
||||
await sessionNotificationSender.sendSessionNotification(ctx, currentPlatform, mergedConfig.title, message)
|
||||
if (mergedConfig.playSound && mergedConfig.soundPath) {
|
||||
await playSessionNotificationSound(ctx, currentPlatform, mergedConfig.soundPath)
|
||||
await sessionNotificationSender.playSessionNotificationSound(ctx, currentPlatform, mergedConfig.soundPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user