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 413e8b73b7
commit 84cbd256e1
+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 { setMainSession, subagentSessions, _resetForTesting } from "../features/claude-code-session-state"
import * as utils from "./session-notification-utils"
import * as sender from "./session-notification-sender"
const originalSetTimeout = globalThis.setTimeout
const originalClearTimeout = globalThis.clearTimeout
const originalDateNow = Date.now
describe("session-notification", () => {
let notificationCalls: string[]
@@ -31,6 +34,10 @@ describe("session-notification", () => {
}
beforeEach(() => {
jest.useRealTimers()
globalThis.setTimeout = originalSetTimeout
globalThis.clearTimeout = originalClearTimeout
Date.now = originalDateNow
_resetForTesting()
notificationCalls = []
@@ -42,13 +49,24 @@ describe("session-notification", () => {
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)
})
spyOn(sender, "sendSessionNotification").mockImplementation(
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(() => {
// given - cleanup after each test
jest.useRealTimers()
globalThis.setTimeout = originalSetTimeout
globalThis.clearTimeout = originalClearTimeout
Date.now = originalDateNow
subagentSessions.clear()
_resetForTesting()
})
@@ -514,55 +532,68 @@ describe("session-notification", () => {
})
test("should ignore activity events within grace period", async () => {
// given - main session is set
const mainSessionID = "main-grace"
setMainSession(mainSessionID)
jest.useFakeTimers()
jest.setSystemTime(new Date("2026-01-01T00:00:00.000Z"))
const hook = createSessionNotification(createMockPluginInput(), {
idleConfirmationDelay: 50,
skipIfIncompleteTodos: false,
activityGracePeriodMs: 100,
})
try {
// given - a regular session notification is scheduled
const sessionID = "main-grace"
// when - session goes idle
await hook({
event: {
type: "session.idle",
properties: { sessionID: mainSessionID },
},
})
const hook = createSessionNotification(createMockPluginInput(), {
idleConfirmationDelay: 50,
skipIfIncompleteTodos: false,
activityGracePeriodMs: 100,
enforceMainSessionFilter: false,
})
// when - activity happens immediately (within grace period)
await hook({
event: {
type: "tool.execute.before",
properties: { sessionID: mainSessionID },
},
})
// when - session goes idle
await hook({
event: {
type: "session.idle",
properties: { sessionID },
},
})
// Wait for idle delay to pass
await new Promise((resolve) => setTimeout(resolve, 100))
// when - activity happens immediately (within grace period)
await hook({
event: {
type: "tool.execute.before",
properties: { sessionID },
},
})
// then - notification SHOULD be sent (activity was within grace period, ignored)
expect(notificationCalls.length).toBeGreaterThanOrEqual(1)
// when - idle confirmation delay passes deterministically
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 () => {
// given - main session is set
const mainSessionID = "main-grace-cancel"
setMainSession(mainSessionID)
// given - a regular session notification is scheduled
const sessionID = "main-grace-cancel"
const hook = createSessionNotification(createMockPluginInput(), {
idleConfirmationDelay: 200,
skipIfIncompleteTodos: false,
activityGracePeriodMs: 50,
enforceMainSessionFilter: false,
})
// when - session goes idle
await hook({
event: {
type: "session.idle",
properties: { sessionID: mainSessionID },
properties: { sessionID },
},
})
@@ -573,7 +604,7 @@ describe("session-notification", () => {
await hook({
event: {
type: "tool.execute.before",
properties: { sessionID: mainSessionID },
properties: { sessionID },
},
})