test(todo-continuation-enforcer): stabilize fake timers
This commit is contained in:
parent
74d2ae1023
commit
3a690965fd
@ -1,3 +1,4 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
import type { BackgroundManager } from "../../features/background-agent"
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
@ -9,10 +10,13 @@ type TimerCallback = (...args: any[]) => void
|
|||||||
|
|
||||||
interface FakeTimers {
|
interface FakeTimers {
|
||||||
advanceBy: (ms: number, advanceClock?: boolean) => Promise<void>
|
advanceBy: (ms: number, advanceClock?: boolean) => Promise<void>
|
||||||
|
advanceClockBy: (ms: number) => Promise<void>
|
||||||
restore: () => void
|
restore: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFakeTimers(): FakeTimers {
|
function createFakeTimers(): FakeTimers {
|
||||||
|
const FAKE_MIN_DELAY_MS = 500
|
||||||
|
const REAL_MAX_DELAY_MS = 5000
|
||||||
const originalNow = Date.now()
|
const originalNow = Date.now()
|
||||||
let clockNow = originalNow
|
let clockNow = originalNow
|
||||||
let timerNow = 0
|
let timerNow = 0
|
||||||
@ -52,20 +56,41 @@ function createFakeTimers(): FakeTimers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalThis.setTimeout = ((callback: TimerCallback, delay?: number, ...args: any[]) => {
|
globalThis.setTimeout = ((callback: TimerCallback, delay?: number, ...args: any[]) => {
|
||||||
return schedule(callback, delay, null, args) as unknown as ReturnType<typeof setTimeout>
|
const normalized = normalizeDelay(delay)
|
||||||
|
if (normalized < FAKE_MIN_DELAY_MS) {
|
||||||
|
return original.setTimeout(callback, delay, ...args)
|
||||||
|
}
|
||||||
|
if (normalized >= REAL_MAX_DELAY_MS) {
|
||||||
|
return original.setTimeout(callback, delay, ...args)
|
||||||
|
}
|
||||||
|
return schedule(callback, normalized, null, args) as unknown as ReturnType<typeof setTimeout>
|
||||||
}) as typeof setTimeout
|
}) as typeof setTimeout
|
||||||
|
|
||||||
globalThis.setInterval = ((callback: TimerCallback, delay?: number, ...args: any[]) => {
|
globalThis.setInterval = ((callback: TimerCallback, delay?: number, ...args: any[]) => {
|
||||||
const interval = normalizeDelay(delay)
|
const interval = normalizeDelay(delay)
|
||||||
return schedule(callback, delay, interval, args) as unknown as ReturnType<typeof setInterval>
|
if (interval < FAKE_MIN_DELAY_MS) {
|
||||||
|
return original.setInterval(callback, delay, ...args)
|
||||||
|
}
|
||||||
|
if (interval >= REAL_MAX_DELAY_MS) {
|
||||||
|
return original.setInterval(callback, delay, ...args)
|
||||||
|
}
|
||||||
|
return schedule(callback, interval, interval, args) as unknown as ReturnType<typeof setInterval>
|
||||||
}) as typeof setInterval
|
}) as typeof setInterval
|
||||||
|
|
||||||
globalThis.clearTimeout = ((id?: number) => {
|
globalThis.clearTimeout = ((id?: Parameters<typeof clearTimeout>[0]) => {
|
||||||
|
if (typeof id === "number" && timers.has(id)) {
|
||||||
clear(id)
|
clear(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
original.clearTimeout(id)
|
||||||
}) as typeof clearTimeout
|
}) as typeof clearTimeout
|
||||||
|
|
||||||
globalThis.clearInterval = ((id?: number) => {
|
globalThis.clearInterval = ((id?: Parameters<typeof clearInterval>[0]) => {
|
||||||
|
if (typeof id === "number" && timers.has(id)) {
|
||||||
clear(id)
|
clear(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
original.clearInterval(id)
|
||||||
}) as typeof clearInterval
|
}) as typeof clearInterval
|
||||||
|
|
||||||
Date.now = () => clockNow
|
Date.now = () => clockNow
|
||||||
@ -107,6 +132,12 @@ function createFakeTimers(): FakeTimers {
|
|||||||
await Promise.resolve()
|
await Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const advanceClockBy = async (ms: number) => {
|
||||||
|
const clamped = Math.max(0, ms)
|
||||||
|
clockNow += clamped
|
||||||
|
await Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
const restore = () => {
|
const restore = () => {
|
||||||
globalThis.setTimeout = original.setTimeout
|
globalThis.setTimeout = original.setTimeout
|
||||||
globalThis.clearTimeout = original.clearTimeout
|
globalThis.clearTimeout = original.clearTimeout
|
||||||
@ -115,7 +146,7 @@ function createFakeTimers(): FakeTimers {
|
|||||||
Date.now = original.dateNow
|
Date.now = original.dateNow
|
||||||
}
|
}
|
||||||
|
|
||||||
return { advanceBy, restore }
|
return { advanceBy, advanceClockBy, restore }
|
||||||
}
|
}
|
||||||
|
|
||||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||||
@ -510,7 +541,7 @@ describe("todo-continuation-enforcer", () => {
|
|||||||
event: { type: "session.idle", properties: { sessionID } },
|
event: { type: "session.idle", properties: { sessionID } },
|
||||||
})
|
})
|
||||||
await fakeTimers.advanceBy(2500, true)
|
await fakeTimers.advanceBy(2500, true)
|
||||||
await fakeTimers.advanceBy(CONTINUATION_COOLDOWN_MS, true)
|
await fakeTimers.advanceClockBy(CONTINUATION_COOLDOWN_MS)
|
||||||
await hook.handler({
|
await hook.handler({
|
||||||
event: { type: "session.idle", properties: { sessionID } },
|
event: { type: "session.idle", properties: { sessionID } },
|
||||||
})
|
})
|
||||||
@ -518,7 +549,7 @@ describe("todo-continuation-enforcer", () => {
|
|||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(promptCalls).toHaveLength(2)
|
expect(promptCalls).toHaveLength(2)
|
||||||
})
|
}, { timeout: 15000 })
|
||||||
|
|
||||||
test("should keep injecting even when todos remain unchanged across cycles", async () => {
|
test("should keep injecting even when todos remain unchanged across cycles", async () => {
|
||||||
//#given
|
//#given
|
||||||
@ -534,26 +565,26 @@ describe("todo-continuation-enforcer", () => {
|
|||||||
//#when — 5 consecutive idle cycles with unchanged todos
|
//#when — 5 consecutive idle cycles with unchanged todos
|
||||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
await fakeTimers.advanceBy(2500, true)
|
await fakeTimers.advanceBy(2500, true)
|
||||||
await fakeTimers.advanceBy(CONTINUATION_COOLDOWN_MS, true)
|
await fakeTimers.advanceClockBy(CONTINUATION_COOLDOWN_MS)
|
||||||
|
|
||||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
await fakeTimers.advanceBy(2500, true)
|
await fakeTimers.advanceBy(2500, true)
|
||||||
await fakeTimers.advanceBy(CONTINUATION_COOLDOWN_MS, true)
|
await fakeTimers.advanceClockBy(CONTINUATION_COOLDOWN_MS)
|
||||||
|
|
||||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
await fakeTimers.advanceBy(2500, true)
|
await fakeTimers.advanceBy(2500, true)
|
||||||
await fakeTimers.advanceBy(CONTINUATION_COOLDOWN_MS, true)
|
await fakeTimers.advanceClockBy(CONTINUATION_COOLDOWN_MS)
|
||||||
|
|
||||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
await fakeTimers.advanceBy(2500, true)
|
await fakeTimers.advanceBy(2500, true)
|
||||||
await fakeTimers.advanceBy(CONTINUATION_COOLDOWN_MS, true)
|
await fakeTimers.advanceClockBy(CONTINUATION_COOLDOWN_MS)
|
||||||
|
|
||||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
await fakeTimers.advanceBy(2500, true)
|
await fakeTimers.advanceBy(2500, true)
|
||||||
|
|
||||||
//#then — all 5 injections should fire (no stagnation cap)
|
//#then — all 5 injections should fire (no stagnation cap)
|
||||||
expect(promptCalls).toHaveLength(5)
|
expect(promptCalls).toHaveLength(5)
|
||||||
})
|
}, { timeout: 60000 })
|
||||||
|
|
||||||
test("should skip idle handling while injection is in flight", async () => {
|
test("should skip idle handling while injection is in flight", async () => {
|
||||||
//#given
|
//#given
|
||||||
@ -613,7 +644,7 @@ describe("todo-continuation-enforcer", () => {
|
|||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(promptCalls).toHaveLength(2)
|
expect(promptCalls).toHaveLength(2)
|
||||||
})
|
}, { timeout: 15000 })
|
||||||
|
|
||||||
test("should accept skipAgents option without error", async () => {
|
test("should accept skipAgents option without error", async () => {
|
||||||
// given - session with skipAgents configured for Prometheus
|
// given - session with skipAgents configured for Prometheus
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user