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:
@@ -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 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user