fix(tests): stabilize flaky session-notification test

- Add try/finally for fake timers cleanup

- Restore real timers in beforeEach/afterEach

- Use enforceMainSessionFilter: false for grace period tests

- Prevent timer state pollution between tests
This commit is contained in:
YeonGyu-Kim
2026-03-11 17:05:11 +09:00
parent 0c6b7f1ec5
commit be5d8fbdf1
+67 -36
View File
@@ -1,10 +1,13 @@
const { describe, expect, test, beforeEach, afterEach, spyOn } = require("bun:test") import { afterEach, beforeEach, describe, expect, jest, spyOn, test } from "bun:test"
import { createSessionNotification } from "./session-notification" import { createSessionNotification } from "./session-notification"
import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state" import { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state"
import * as utils from "./session-notification-utils" import * as utils from "./session-notification-utils"
import * as sender from "./session-notification-sender" import * as sender from "./session-notification-sender"
const originalSetTimeout = globalThis.setTimeout
const originalClearTimeout = globalThis.clearTimeout
const originalDateNow = Date.now
describe("session-notification", () => { describe("session-notification", () => {
let notificationCalls: string[] let notificationCalls: string[]
@@ -31,6 +34,10 @@ describe("session-notification", () => {
} }
beforeEach(() => { beforeEach(() => {
jest.useRealTimers()
globalThis.setTimeout = originalSetTimeout
globalThis.clearTimeout = originalClearTimeout
Date.now = originalDateNow
_resetForTesting() _resetForTesting()
notificationCalls = [] notificationCalls = []
@@ -42,13 +49,24 @@ describe("session-notification", () => {
spyOn(utils, "getAplayPath").mockResolvedValue("/usr/bin/aplay") spyOn(utils, "getAplayPath").mockResolvedValue("/usr/bin/aplay")
spyOn(utils, "startBackgroundCheck").mockImplementation(() => {}) spyOn(utils, "startBackgroundCheck").mockImplementation(() => {})
spyOn(sender, "detectPlatform").mockReturnValue("darwin") spyOn(sender, "detectPlatform").mockReturnValue("darwin")
spyOn(sender, "sendSessionNotification").mockImplementation(async (_ctx, _platform, _title, message) => { spyOn(sender, "sendSessionNotification").mockImplementation(
notificationCalls.push(message) async (
}) _ctx: Parameters<typeof sender.sendSessionNotification>[0],
_platform: Parameters<typeof sender.sendSessionNotification>[1],
_title: Parameters<typeof sender.sendSessionNotification>[2],
message: Parameters<typeof sender.sendSessionNotification>[3]
) => {
notificationCalls.push(message)
}
)
}) })
afterEach(() => { afterEach(() => {
// given - cleanup after each test // given - cleanup after each test
jest.useRealTimers()
globalThis.setTimeout = originalSetTimeout
globalThis.clearTimeout = originalClearTimeout
Date.now = originalDateNow
subagentSessions.clear() subagentSessions.clear()
_resetForTesting() _resetForTesting()
}) })
@@ -514,55 +532,68 @@ describe("session-notification", () => {
}) })
test("should ignore activity events within grace period", async () => { test("should ignore activity events within grace period", async () => {
// given - main session is set jest.useFakeTimers()
const mainSessionID = "main-grace" jest.setSystemTime(new Date("2026-01-01T00:00:00.000Z"))
setMainSession(mainSessionID)
const hook = createSessionNotification(createMockPluginInput(), { try {
idleConfirmationDelay: 50, // given - a regular session notification is scheduled
skipIfIncompleteTodos: false, const sessionID = "main-grace"
activityGracePeriodMs: 100,
})
// when - session goes idle const hook = createSessionNotification(createMockPluginInput(), {
await hook({ idleConfirmationDelay: 50,
event: { skipIfIncompleteTodos: false,
type: "session.idle", activityGracePeriodMs: 100,
properties: { sessionID: mainSessionID }, enforceMainSessionFilter: false,
}, })
})
// when - activity happens immediately (within grace period) // when - session goes idle
await hook({ await hook({
event: { event: {
type: "tool.execute.before", type: "session.idle",
properties: { sessionID: mainSessionID }, properties: { sessionID },
}, },
}) })
// Wait for idle delay to pass // when - activity happens immediately (within grace period)
await new Promise((resolve) => setTimeout(resolve, 100)) await hook({
event: {
type: "tool.execute.before",
properties: { sessionID },
},
})
// then - notification SHOULD be sent (activity was within grace period, ignored) // when - idle confirmation delay passes deterministically
expect(notificationCalls.length).toBeGreaterThanOrEqual(1) jest.advanceTimersByTime(50)
jest.runOnlyPendingTimers()
await Promise.resolve()
// then - notification SHOULD be sent (activity was within grace period, ignored)
expect(notificationCalls.length).toBeGreaterThanOrEqual(1)
} finally {
jest.clearAllTimers()
jest.useRealTimers()
globalThis.setTimeout = originalSetTimeout
globalThis.clearTimeout = originalClearTimeout
Date.now = originalDateNow
}
}) })
test("should cancel notification for activity after grace period", async () => { test("should cancel notification for activity after grace period", async () => {
// given - main session is set // given - a regular session notification is scheduled
const mainSessionID = "main-grace-cancel" const sessionID = "main-grace-cancel"
setMainSession(mainSessionID)
const hook = createSessionNotification(createMockPluginInput(), { const hook = createSessionNotification(createMockPluginInput(), {
idleConfirmationDelay: 200, idleConfirmationDelay: 200,
skipIfIncompleteTodos: false, skipIfIncompleteTodos: false,
activityGracePeriodMs: 50, activityGracePeriodMs: 50,
enforceMainSessionFilter: false,
}) })
// when - session goes idle // when - session goes idle
await hook({ await hook({
event: { event: {
type: "session.idle", type: "session.idle",
properties: { sessionID: mainSessionID }, properties: { sessionID },
}, },
}) })
@@ -573,7 +604,7 @@ describe("session-notification", () => {
await hook({ await hook({
event: { event: {
type: "tool.execute.before", type: "tool.execute.before",
properties: { sessionID: mainSessionID }, properties: { sessionID },
}, },
}) })