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:
Choi Kijin / 최 기진 / チョイ キジン
2026-04-28 19:09:02 +09:00
parent 078e49629e
commit 27fb2d5e79
+27 -120
View File
@@ -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")