test(plugin): clean up event handler test helpers
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+27
-120
@@ -12,55 +12,56 @@ type EventHandlerArgs = Parameters<typeof createEventHandler>[0]
|
|||||||
type EventHandlerInput = Parameters<ReturnType<typeof createEventHandler>>[0]
|
type EventHandlerInput = Parameters<ReturnType<typeof createEventHandler>>[0]
|
||||||
type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0]
|
type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0]
|
||||||
|
|
||||||
|
function cast<T>(value: unknown): T {
|
||||||
|
return value as T
|
||||||
|
}
|
||||||
|
|
||||||
function asEventHandlerInput(input: EventInput): EventHandlerInput {
|
function asEventHandlerInput(input: EventInput): EventHandlerInput {
|
||||||
return input as unknown as EventHandlerInput
|
return cast<EventHandlerInput>(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] {
|
function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] {
|
||||||
return ctx as unknown as EventHandlerArgs["ctx"]
|
return cast<EventHandlerArgs["ctx"]>(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] {
|
function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] {
|
||||||
return ctx as unknown as ChatMessageHandlerArgs["ctx"]
|
return cast<ChatMessageHandlerArgs["ctx"]>(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] {
|
function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] {
|
||||||
return config as unknown as EventHandlerArgs["pluginConfig"]
|
return cast<EventHandlerArgs["pluginConfig"]>(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] {
|
function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] {
|
||||||
return config as unknown as ChatMessageHandlerArgs["pluginConfig"]
|
return cast<ChatMessageHandlerArgs["pluginConfig"]>(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandlerManagers(
|
function createEventHandlerManagers(
|
||||||
overrides: Record<string, unknown> = {},
|
overrides: Record<string, unknown> = {},
|
||||||
): EventHandlerArgs["managers"] {
|
): EventHandlerArgs["managers"] {
|
||||||
return {
|
return cast<EventHandlerArgs["managers"]>({
|
||||||
...({} as EventHandlerArgs["managers"]),
|
|
||||||
tmuxSessionManager: {
|
tmuxSessionManager: {
|
||||||
onSessionCreated: async () => {},
|
onSessionCreated: async () => {},
|
||||||
onSessionDeleted: async () => {},
|
onSessionDeleted: async () => {},
|
||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as EventHandlerArgs["managers"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandlerHooks(
|
function createEventHandlerHooks(
|
||||||
overrides: Record<string, unknown>,
|
overrides: Record<string, unknown> = {},
|
||||||
): EventHandlerArgs["hooks"] {
|
): EventHandlerArgs["hooks"] {
|
||||||
return {
|
return cast<EventHandlerArgs["hooks"]>(overrides)
|
||||||
...({} as EventHandlerArgs["hooks"]),
|
|
||||||
...overrides,
|
|
||||||
} as unknown as EventHandlerArgs["hooks"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createChatMessageHandlerHooks(
|
function createChatMessageHandlerHooks(
|
||||||
overrides: Record<string, unknown>,
|
overrides: Record<string, unknown> = {},
|
||||||
): ChatMessageHandlerArgs["hooks"] {
|
): ChatMessageHandlerArgs["hooks"] {
|
||||||
return {
|
return cast<ChatMessageHandlerArgs["hooks"]>(overrides)
|
||||||
...({} as ChatMessageHandlerArgs["hooks"]),
|
}
|
||||||
...overrides,
|
|
||||||
} as unknown as ChatMessageHandlerArgs["hooks"]
|
async function wait(ms: number): Promise<void> {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
function createIdleTrackingEventHandler(dispatchCalls: EventInput[]): ReturnType<typeof createEventHandler> {
|
function createIdleTrackingEventHandler(dispatchCalls: EventInput[]): ReturnType<typeof createEventHandler> {
|
||||||
@@ -95,29 +96,21 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe("event error extraction", () => {
|
describe("event error extraction", () => {
|
||||||
it("prefers nested APIError message over generic top-level message", async () => {
|
it("prefers nested APIError message over generic top-level message", async () => {
|
||||||
//#given
|
|
||||||
const error = {
|
const error = {
|
||||||
name: "APIError",
|
name: "APIError",
|
||||||
message: "Error",
|
message: "Error",
|
||||||
data: { message: "Forbidden: Selected provider is forbidden" },
|
data: { message: "Forbidden: Selected provider is forbidden" },
|
||||||
}
|
}
|
||||||
|
|
||||||
//#when
|
|
||||||
const result = extractErrorMessage(error)
|
const result = extractErrorMessage(error)
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(result).toBe("Forbidden: Selected provider is forbidden")
|
expect(result).toBe("Forbidden: Selected provider is forbidden")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("createEventHandler - idle deduplication", () => {
|
describe("createEventHandler - idle deduplication", () => {
|
||||||
it("#given synthetic idle fires first #when real idle arrives within 500ms #then real idle dispatched", async () => {
|
it("dispatches both idle events when the real idle arrives within 500ms", async () => {
|
||||||
//#given
|
|
||||||
const dispatchCalls: EventInput[] = []
|
const dispatchCalls: EventInput[] = []
|
||||||
const eventHandler = createIdleTrackingEventHandler(dispatchCalls)
|
const eventHandler = createIdleTrackingEventHandler(dispatchCalls)
|
||||||
const sessionId = "ses_test123"
|
const sessionId = "ses_test123"
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.status",
|
type: "session.status",
|
||||||
@@ -135,8 +128,6 @@ describe("event error extraction", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(dispatchCalls).toHaveLength(2)
|
expect(dispatchCalls).toHaveLength(2)
|
||||||
expect(dispatchCalls[0]?.event.type).toBe("session.idle")
|
expect(dispatchCalls[0]?.event.type).toBe("session.idle")
|
||||||
expect(dispatchCalls[1]?.event.type).toBe("session.idle")
|
expect(dispatchCalls[1]?.event.type).toBe("session.idle")
|
||||||
@@ -144,13 +135,10 @@ describe("event error extraction", () => {
|
|||||||
expect((dispatchCalls[1]?.event.properties as { sessionID?: string } | undefined)?.sessionID).toBe(sessionId)
|
expect((dispatchCalls[1]?.event.properties as { sessionID?: string } | undefined)?.sessionID).toBe(sessionId)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("#given real idle fires first #when synthetic arrives within 500ms #then synthetic dropped", async () => {
|
it("drops the synthetic idle when a real idle already arrived within 500ms", async () => {
|
||||||
//#given
|
|
||||||
const dispatchCalls: EventInput[] = []
|
const dispatchCalls: EventInput[] = []
|
||||||
const eventHandler = createIdleTrackingEventHandler(dispatchCalls)
|
const eventHandler = createIdleTrackingEventHandler(dispatchCalls)
|
||||||
const sessionId = "ses_test456"
|
const sessionId = "ses_test456"
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.idle",
|
type: "session.idle",
|
||||||
@@ -168,15 +156,12 @@ describe("event error extraction", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(dispatchCalls).toHaveLength(1)
|
expect(dispatchCalls).toHaveLength(1)
|
||||||
expect(dispatchCalls[0]?.event.type).toBe("session.idle")
|
expect(dispatchCalls[0]?.event.type).toBe("session.idle")
|
||||||
expect((dispatchCalls[0]?.event.properties as { sessionID?: string } | undefined)?.sessionID).toBe(sessionId)
|
expect((dispatchCalls[0]?.event.properties as { sessionID?: string } | undefined)?.sessionID).toBe(sessionId)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("both maps pruned on every event", async () => {
|
it("prunes both maps on every event", async () => {
|
||||||
//#given
|
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: {} as any,
|
ctx: {} as any,
|
||||||
pluginConfig: {} as any,
|
pluginConfig: {} as any,
|
||||||
@@ -213,7 +198,6 @@ describe("event error extraction", () => {
|
|||||||
} as any,
|
} as any,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Trigger some synthetic idles
|
|
||||||
await eventHandler({
|
await eventHandler({
|
||||||
event: {
|
event: {
|
||||||
type: "session.status",
|
type: "session.status",
|
||||||
@@ -234,7 +218,6 @@ describe("event error extraction", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Trigger some real idles
|
|
||||||
await eventHandler({
|
await eventHandler({
|
||||||
event: {
|
event: {
|
||||||
type: "session.idle",
|
type: "session.idle",
|
||||||
@@ -252,19 +235,13 @@ describe("event error extraction", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
await wait(600)
|
||||||
|
|
||||||
//#when - wait for dedup window to expire (600ms > 500ms)
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
|
||||||
|
|
||||||
// Trigger any event to trigger pruning
|
|
||||||
await eventHandler({
|
await eventHandler({
|
||||||
event: {
|
event: {
|
||||||
type: "message.updated",
|
type: "message.updated",
|
||||||
},
|
},
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
//#then - both maps should be pruned (no dedup should occur for new events)
|
|
||||||
// We verify by checking that a new idle event for same session is dispatched
|
|
||||||
const dispatchCalls: EventInput[] = []
|
const dispatchCalls: EventInput[] = []
|
||||||
const eventHandlerWithMock = createEventHandler({
|
const eventHandlerWithMock = createEventHandler({
|
||||||
ctx: {} as any,
|
ctx: {} as any,
|
||||||
@@ -319,8 +296,7 @@ describe("event error extraction", () => {
|
|||||||
expect(dispatchCalls[0].event.type).toBe("session.idle")
|
expect(dispatchCalls[0].event.type).toBe("session.idle")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("dedup only applies within window - outside window both dispatch", async () => {
|
it("dispatches both idle events once the dedup window expires", async () => {
|
||||||
//#given
|
|
||||||
const dispatchCalls: EventInput[] = []
|
const dispatchCalls: EventInput[] = []
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: {} as any,
|
ctx: {} as any,
|
||||||
@@ -365,8 +341,6 @@ describe("event error extraction", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const sessionId = "ses_outside_window"
|
const sessionId = "ses_outside_window"
|
||||||
|
|
||||||
//#when - synthetic idle first
|
|
||||||
await eventHandler({
|
await eventHandler({
|
||||||
event: {
|
event: {
|
||||||
type: "session.status",
|
type: "session.status",
|
||||||
@@ -376,14 +350,8 @@ describe("event error extraction", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
//#then - synthetic dispatched
|
|
||||||
expect(dispatchCalls.length).toBe(1)
|
expect(dispatchCalls.length).toBe(1)
|
||||||
|
await wait(600)
|
||||||
//#when - wait for dedup window to expire (600ms > 500ms)
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
|
||||||
|
|
||||||
//#when - real idle arrives outside window
|
|
||||||
await eventHandler({
|
await eventHandler({
|
||||||
event: {
|
event: {
|
||||||
type: "session.idle",
|
type: "session.idle",
|
||||||
@@ -392,8 +360,6 @@ describe("event error extraction", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
//#then - real idle dispatched (outside dedup window)
|
|
||||||
expect(dispatchCalls.length).toBe(2)
|
expect(dispatchCalls.length).toBe(2)
|
||||||
expect(dispatchCalls[0].event.type).toBe("session.idle")
|
expect(dispatchCalls[0].event.type).toBe("session.idle")
|
||||||
expect(dispatchCalls[1].event.type).toBe("session.idle")
|
expect(dispatchCalls[1].event.type).toBe("session.idle")
|
||||||
@@ -402,7 +368,6 @@ describe("event error extraction", () => {
|
|||||||
|
|
||||||
describe("createEventHandler - event forwarding", () => {
|
describe("createEventHandler - event forwarding", () => {
|
||||||
it("forwards message activity events to tmux session manager", async () => {
|
it("forwards message activity events to tmux session manager", async () => {
|
||||||
//#given
|
|
||||||
const forwardedEvents: EventInput[] = []
|
const forwardedEvents: EventInput[] = []
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: asEventHandlerContext({}),
|
ctx: asEventHandlerContext({}),
|
||||||
@@ -434,22 +399,17 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
}),
|
}),
|
||||||
hooks: createEventHandlerHooks({}),
|
hooks: createEventHandlerHooks({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "message.part.delta",
|
type: "message.part.delta",
|
||||||
properties: { sessionID: "ses_tmux_activity", field: "text", delta: "x" },
|
properties: { sessionID: "ses_tmux_activity", field: "text", delta: "x" },
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(forwardedEvents.length).toBe(1)
|
expect(forwardedEvents.length).toBe(1)
|
||||||
expect(forwardedEvents[0]?.event.type).toBe("message.part.delta")
|
expect(forwardedEvents[0]?.event.type).toBe("message.part.delta")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not forward tmux activity events when tmux integration is disabled", async () => {
|
it("does not forward tmux activity events when tmux integration is disabled", async () => {
|
||||||
//#given
|
|
||||||
const forwardedEvents: EventInput[] = []
|
const forwardedEvents: EventInput[] = []
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: asEventHandlerContext({}),
|
ctx: asEventHandlerContext({}),
|
||||||
@@ -481,21 +441,16 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
}),
|
}),
|
||||||
hooks: createEventHandlerHooks({}),
|
hooks: createEventHandlerHooks({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "message.part.delta",
|
type: "message.part.delta",
|
||||||
properties: { sessionID: "ses_tmux_disabled", field: "text", delta: "x" },
|
properties: { sessionID: "ses_tmux_disabled", field: "text", delta: "x" },
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(forwardedEvents).toHaveLength(0)
|
expect(forwardedEvents).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not forward session.created to tmux session manager when tmux integration is disabled", async () => {
|
it("does not forward session.created to tmux session manager when tmux integration is disabled", async () => {
|
||||||
//#given
|
|
||||||
const createdSessions: string[] = []
|
const createdSessions: string[] = []
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: asEventHandlerContext({}),
|
ctx: asEventHandlerContext({}),
|
||||||
@@ -529,21 +484,16 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
}),
|
}),
|
||||||
hooks: createEventHandlerHooks({}),
|
hooks: createEventHandlerHooks({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.created",
|
type: "session.created",
|
||||||
properties: { info: { id: "ses_tmux_disabled", parentID: "ses_parent" } },
|
properties: { info: { id: "ses_tmux_disabled", parentID: "ses_parent" } },
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(createdSessions).toHaveLength(0)
|
expect(createdSessions).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("dispatches OpenClaw after session.created for main sessions (no parentID)", async () => {
|
it("dispatches OpenClaw after session.created for main sessions (no parentID)", async () => {
|
||||||
//#given
|
|
||||||
const openClawSpy = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent").mockResolvedValue(null)
|
const openClawSpy = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent").mockResolvedValue(null)
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: asEventHandlerContext({ directory: "/tmp/project-created" }),
|
ctx: asEventHandlerContext({ directory: "/tmp/project-created" }),
|
||||||
@@ -572,16 +522,12 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
}),
|
}),
|
||||||
hooks: createEventHandlerHooks({}),
|
hooks: createEventHandlerHooks({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when - main session created (no parentID)
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.created",
|
type: "session.created",
|
||||||
properties: { info: { id: "ses_openclaw_created" } },
|
properties: { info: { id: "ses_openclaw_created" } },
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then - OpenClaw dispatch called for main session
|
|
||||||
const [call] = openClawSpy.mock.calls[0] ?? []
|
const [call] = openClawSpy.mock.calls[0] ?? []
|
||||||
expect(call).toMatchObject({
|
expect(call).toMatchObject({
|
||||||
rawEvent: "session.created",
|
rawEvent: "session.created",
|
||||||
@@ -593,8 +539,7 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does NOT dispatch OpenClaw for subagent sessions (with parentID)", async () => {
|
it("does not dispatch OpenClaw for subagent sessions with a parentID", async () => {
|
||||||
//#given
|
|
||||||
const openClawSpy = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent").mockResolvedValue(null)
|
const openClawSpy = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent").mockResolvedValue(null)
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: asEventHandlerContext({ directory: "/tmp/project-created" }),
|
ctx: asEventHandlerContext({ directory: "/tmp/project-created" }),
|
||||||
@@ -623,21 +568,16 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
}),
|
}),
|
||||||
hooks: createEventHandlerHooks({}),
|
hooks: createEventHandlerHooks({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when - subagent session created (with parentID)
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.created",
|
type: "session.created",
|
||||||
properties: { info: { id: "ses_subagent", parentID: "ses_parent" } },
|
properties: { info: { id: "ses_subagent", parentID: "ses_parent" } },
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then - OpenClaw dispatch NOT called for subagent session (handled by specialized callbacks)
|
|
||||||
expect(openClawSpy.mock.calls.length).toBe(0)
|
expect(openClawSpy.mock.calls.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("forwards session.deleted to write-existing-file-guard hook", async () => {
|
it("forwards session.deleted to write-existing-file-guard hook", async () => {
|
||||||
//#given
|
|
||||||
const forwardedEvents: EventInput[] = []
|
const forwardedEvents: EventInput[] = []
|
||||||
const disconnectedSessions: string[] = []
|
const disconnectedSessions: string[] = []
|
||||||
const deletedSessions: string[] = []
|
const deletedSessions: string[] = []
|
||||||
@@ -679,16 +619,12 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
} as never,
|
} as never,
|
||||||
})
|
})
|
||||||
const sessionID = "ses_forward_delete_event"
|
const sessionID = "ses_forward_delete_event"
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.deleted",
|
type: "session.deleted",
|
||||||
properties: { info: { id: sessionID } },
|
properties: { info: { id: sessionID } },
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(forwardedEvents.length).toBe(1)
|
expect(forwardedEvents.length).toBe(1)
|
||||||
expect(forwardedEvents[0]?.event.type).toBe("session.deleted")
|
expect(forwardedEvents[0]?.event.type).toBe("session.deleted")
|
||||||
expect(disconnectedSessions).toEqual([sessionID])
|
expect(disconnectedSessions).toEqual([sessionID])
|
||||||
@@ -734,7 +670,6 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("clears stored prompt params on session.deleted", async () => {
|
it("clears stored prompt params on session.deleted", async () => {
|
||||||
//#given
|
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: {} as never,
|
ctx: {} as never,
|
||||||
pluginConfig: {} as never,
|
pluginConfig: {} as never,
|
||||||
@@ -759,23 +694,18 @@ describe("createEventHandler - event forwarding", () => {
|
|||||||
topP: 0.7,
|
topP: 0.7,
|
||||||
options: { reasoningEffort: "high" },
|
options: { reasoningEffort: "high" },
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.deleted",
|
type: "session.deleted",
|
||||||
properties: { info: { id: sessionID } },
|
properties: { info: { id: sessionID } },
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(getSessionPromptParams(sessionID)).toBeUndefined()
|
expect(getSessionPromptParams(sessionID)).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("createEventHandler - retry dedupe lifecycle", () => {
|
describe("createEventHandler - retry dedupe lifecycle", () => {
|
||||||
it("re-handles same retry key after session recovers to idle status", async () => {
|
it("re-handles same retry key after session recovers to idle status", async () => {
|
||||||
//#given
|
|
||||||
const sessionID = "ses_retry_recovery_rearm"
|
const sessionID = "ses_retry_recovery_rearm"
|
||||||
setMainSession(sessionID)
|
setMainSession(sessionID)
|
||||||
const abortCalls: string[] = []
|
const abortCalls: string[] = []
|
||||||
@@ -861,8 +791,6 @@ describe("createEventHandler - retry dedupe lifecycle", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#when - first retry key is handled
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.status",
|
type: "session.status",
|
||||||
@@ -882,8 +810,6 @@ describe("createEventHandler - retry dedupe lifecycle", () => {
|
|||||||
},
|
},
|
||||||
firstOutput,
|
firstOutput,
|
||||||
)
|
)
|
||||||
|
|
||||||
//#when - session recovers to non-retry idle state
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.status",
|
type: "session.status",
|
||||||
@@ -893,8 +819,6 @@ describe("createEventHandler - retry dedupe lifecycle", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#when - same retry key appears again after recovery
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.status",
|
type: "session.status",
|
||||||
@@ -904,8 +828,6 @@ describe("createEventHandler - retry dedupe lifecycle", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(abortCalls).toEqual([sessionID, sessionID])
|
expect(abortCalls).toEqual([sessionID, sessionID])
|
||||||
expect(promptCalls).toEqual([sessionID, sessionID])
|
expect(promptCalls).toEqual([sessionID, sessionID])
|
||||||
})
|
})
|
||||||
@@ -913,7 +835,6 @@ describe("createEventHandler - retry dedupe lifecycle", () => {
|
|||||||
|
|
||||||
describe("createEventHandler - session recovery compaction", () => {
|
describe("createEventHandler - session recovery compaction", () => {
|
||||||
it("triggers compaction before sending continue after session error recovery", async () => {
|
it("triggers compaction before sending continue after session error recovery", async () => {
|
||||||
//#given
|
|
||||||
const sessionID = "ses_recovery_compaction"
|
const sessionID = "ses_recovery_compaction"
|
||||||
setMainSession(sessionID)
|
setMainSession(sessionID)
|
||||||
const callOrder: string[] = []
|
const callOrder: string[] = []
|
||||||
@@ -949,8 +870,6 @@ describe("createEventHandler - session recovery compaction", () => {
|
|||||||
stopContinuationGuard: { isStopped: () => false },
|
stopContinuationGuard: { isStopped: () => false },
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.error",
|
type: "session.error",
|
||||||
@@ -961,13 +880,10 @@ describe("createEventHandler - session recovery compaction", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then - summarize (compaction) must be called before prompt (continue)
|
|
||||||
expect(callOrder).toEqual(["summarize", "prompt"])
|
expect(callOrder).toEqual(["summarize", "prompt"])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("sends continue even if compaction fails", async () => {
|
it("sends continue even if compaction fails", async () => {
|
||||||
//#given
|
|
||||||
const sessionID = "ses_recovery_compaction_fail"
|
const sessionID = "ses_recovery_compaction_fail"
|
||||||
setMainSession(sessionID)
|
setMainSession(sessionID)
|
||||||
const callOrder: string[] = []
|
const callOrder: string[] = []
|
||||||
@@ -1003,8 +919,6 @@ describe("createEventHandler - session recovery compaction", () => {
|
|||||||
stopContinuationGuard: { isStopped: () => false },
|
stopContinuationGuard: { isStopped: () => false },
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when
|
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
event: {
|
event: {
|
||||||
type: "session.error",
|
type: "session.error",
|
||||||
@@ -1015,13 +929,10 @@ describe("createEventHandler - session recovery compaction", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//#then - continue is still sent even when compaction fails
|
|
||||||
expect(callOrder).toEqual(["summarize", "prompt"])
|
expect(callOrder).toEqual(["summarize", "prompt"])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("continues dispatching later event hooks when an earlier hook throws", async () => {
|
it("continues dispatching later event hooks when an earlier hook throws", async () => {
|
||||||
//#given
|
|
||||||
const runtimeFallbackCalls: EventInput[] = []
|
const runtimeFallbackCalls: EventInput[] = []
|
||||||
|
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
@@ -1054,8 +965,6 @@ describe("createEventHandler - session recovery compaction", () => {
|
|||||||
stopContinuationGuard: { isStopped: () => false },
|
stopContinuationGuard: { isStopped: () => false },
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
//#when
|
|
||||||
let thrownError: unknown
|
let thrownError: unknown
|
||||||
try {
|
try {
|
||||||
await eventHandler(asEventHandlerInput({
|
await eventHandler(asEventHandlerInput({
|
||||||
@@ -1070,8 +979,6 @@ describe("createEventHandler - session recovery compaction", () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
thrownError = error
|
thrownError = error
|
||||||
}
|
}
|
||||||
|
|
||||||
//#then
|
|
||||||
expect(thrownError).toBeUndefined()
|
expect(thrownError).toBeUndefined()
|
||||||
expect(runtimeFallbackCalls).toHaveLength(1)
|
expect(runtimeFallbackCalls).toHaveLength(1)
|
||||||
expect(runtimeFallbackCalls[0]?.event.type).toBe("session.error")
|
expect(runtimeFallbackCalls[0]?.event.type).toBe("session.error")
|
||||||
|
|||||||
Reference in New Issue
Block a user