import { describe, expect, it, vi } from "vitest"; import { BARE_SESSION_RESET_PROMPT } from "../../auto-reply/reply/session-reset-prompt.js"; import { agentHandlers } from "./agent.js"; import type { GatewayRequestContext } from "./types.js"; const mocks = vi.hoisted(() => ({ loadSessionEntry: vi.fn(), updateSessionStore: vi.fn(), agentCommand: vi.fn(), registerAgentRunContext: vi.fn(), sessionsResetHandler: vi.fn(), loadConfigReturn: {} as Record, })); vi.mock("../session-utils.js", async () => { const actual = await vi.importActual("../session-utils.js"); return { ...actual, loadSessionEntry: mocks.loadSessionEntry, }; }); vi.mock("../../config/sessions.js", async () => { const actual = await vi.importActual( "../../config/sessions.js", ); return { ...actual, updateSessionStore: mocks.updateSessionStore, resolveAgentIdFromSessionKey: () => "main", resolveExplicitAgentSessionKey: () => undefined, resolveAgentMainSessionKey: ({ cfg, agentId, }: { cfg?: { session?: { mainKey?: string } }; agentId: string; }) => `agent:${agentId}:${cfg?.session?.mainKey ?? "main"}`, }; }); vi.mock("../../commands/agent.js", () => ({ agentCommand: mocks.agentCommand, })); vi.mock("../../config/config.js", () => ({ loadConfig: () => mocks.loadConfigReturn, })); vi.mock("../../agents/agent-scope.js", () => ({ listAgentIds: () => ["main"], })); vi.mock("../../infra/agent-events.js", () => ({ registerAgentRunContext: mocks.registerAgentRunContext, onAgentEvent: vi.fn(), })); vi.mock("./sessions.js", () => ({ sessionsHandlers: { "sessions.reset": (...args: unknown[]) => (mocks.sessionsResetHandler as (...args: unknown[]) => unknown)(...args), }, })); vi.mock("../../sessions/send-policy.js", () => ({ resolveSendPolicy: () => "allow", })); vi.mock("../../utils/delivery-context.js", async () => { const actual = await vi.importActual( "../../utils/delivery-context.js", ); return { ...actual, normalizeSessionDeliveryFields: () => ({}), }; }); const makeContext = (): GatewayRequestContext => ({ dedupe: new Map(), addChatRun: vi.fn(), logGateway: { info: vi.fn(), error: vi.fn() }, }) as unknown as GatewayRequestContext; type AgentHandlerArgs = Parameters[0]; type AgentParams = AgentHandlerArgs["params"]; type AgentIdentityGetHandlerArgs = Parameters<(typeof agentHandlers)["agent.identity.get"]>[0]; type AgentIdentityGetParams = AgentIdentityGetHandlerArgs["params"]; function mockMainSessionEntry(entry: Record, cfg: Record = {}) { mocks.loadSessionEntry.mockReturnValue({ cfg, storePath: "/tmp/sessions.json", entry: { sessionId: "existing-session-id", updatedAt: Date.now(), ...entry, }, canonicalKey: "agent:main:main", }); } function captureUpdatedMainEntry() { let capturedEntry: Record | undefined; mocks.updateSessionStore.mockImplementation(async (_path, updater) => { const store: Record = {}; await updater(store); capturedEntry = store["agent:main:main"] as Record; }); return () => capturedEntry; } function primeMainAgentRun(params?: { sessionId?: string; cfg?: Record }) { mockMainSessionEntry( { sessionId: params?.sessionId ?? "existing-session-id" }, params?.cfg ?? {}, ); mocks.updateSessionStore.mockResolvedValue(undefined); mocks.agentCommand.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 100 }, }); } async function runMainAgent(message: string, idempotencyKey: string) { const respond = vi.fn(); await invokeAgent( { message, agentId: "main", sessionKey: "agent:main:main", idempotencyKey, }, { respond, reqId: idempotencyKey }, ); return respond; } function readLastAgentCommandCall(): | { message?: string; sessionId?: string; } | undefined { return mocks.agentCommand.mock.calls.at(-1)?.[0] as | { message?: string; sessionId?: string } | undefined; } function mockSessionResetSuccess(params: { reason: "new" | "reset"; key?: string; sessionId?: string; }) { const key = params.key ?? "agent:main:main"; const sessionId = params.sessionId ?? "reset-session-id"; mocks.sessionsResetHandler.mockImplementation( async (opts: { params: { key: string; reason: string }; respond: (ok: boolean, payload?: unknown) => void; }) => { expect(opts.params.key).toBe(key); expect(opts.params.reason).toBe(params.reason); opts.respond(true, { ok: true, key, entry: { sessionId }, }); }, ); } async function invokeAgent( params: AgentParams, options?: { respond?: ReturnType; reqId?: string; context?: GatewayRequestContext; }, ) { const respond = options?.respond ?? vi.fn(); await agentHandlers.agent({ params, respond: respond as never, context: options?.context ?? makeContext(), req: { type: "req", id: options?.reqId ?? "agent-test-req", method: "agent" }, client: null, isWebchatConnect: () => false, }); return respond; } async function invokeAgentIdentityGet( params: AgentIdentityGetParams, options?: { respond?: ReturnType; reqId?: string; context?: GatewayRequestContext; }, ) { const respond = options?.respond ?? vi.fn(); await agentHandlers["agent.identity.get"]({ params, respond: respond as never, context: options?.context ?? makeContext(), req: { type: "req", id: options?.reqId ?? "agent-identity-test-req", method: "agent.identity.get", }, client: null, isWebchatConnect: () => false, }); return respond; } describe("gateway agent handler", () => { it("preserves cliSessionIds from existing session entry", async () => { const existingCliSessionIds = { "claude-cli": "abc-123-def" }; const existingClaudeCliSessionId = "abc-123-def"; mockMainSessionEntry({ cliSessionIds: existingCliSessionIds, claudeCliSessionId: existingClaudeCliSessionId, }); const getCapturedEntry = captureUpdatedMainEntry(); mocks.agentCommand.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 100 }, }); await runMainAgent("test", "test-idem"); expect(mocks.updateSessionStore).toHaveBeenCalled(); const capturedEntry = getCapturedEntry(); expect(capturedEntry).toBeDefined(); expect(capturedEntry?.cliSessionIds).toEqual(existingCliSessionIds); expect(capturedEntry?.claudeCliSessionId).toBe(existingClaudeCliSessionId); }); it("injects a timestamp into the message passed to agentCommand", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-29T01:30:00.000Z")); // Wed Jan 28, 8:30 PM EST mocks.agentCommand.mockClear(); mocks.loadConfigReturn = { agents: { defaults: { userTimezone: "America/New_York", }, }, }; primeMainAgentRun({ cfg: mocks.loadConfigReturn }); await invokeAgent( { message: "Is it the weekend?", agentId: "main", sessionKey: "agent:main:main", idempotencyKey: "test-timestamp-inject", }, { reqId: "ts-1" }, ); // Wait for the async agentCommand call await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); const callArgs = mocks.agentCommand.mock.calls[0][0]; expect(callArgs.message).toBe("[Wed 2026-01-28 20:30 EST] Is it the weekend?"); mocks.loadConfigReturn = {}; vi.useRealTimers(); }); it("respects explicit bestEffortDeliver=false for main session runs", async () => { mocks.agentCommand.mockClear(); primeMainAgentRun(); await invokeAgent( { message: "strict delivery", agentId: "main", sessionKey: "agent:main:main", deliver: true, replyChannel: "telegram", to: "123", bestEffortDeliver: false, idempotencyKey: "test-strict-delivery", }, { reqId: "strict-1" }, ); await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as Record; expect(callArgs.bestEffortDeliver).toBe(false); }); it("handles missing cliSessionIds gracefully", async () => { mockMainSessionEntry({}); const getCapturedEntry = captureUpdatedMainEntry(); mocks.agentCommand.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 100 }, }); await runMainAgent("test", "test-idem-2"); expect(mocks.updateSessionStore).toHaveBeenCalled(); const capturedEntry = getCapturedEntry(); expect(capturedEntry).toBeDefined(); // Should be undefined, not cause an error expect(capturedEntry?.cliSessionIds).toBeUndefined(); expect(capturedEntry?.claudeCliSessionId).toBeUndefined(); }); it("prunes legacy main alias keys when writing a canonical session entry", async () => { mocks.loadSessionEntry.mockReturnValue({ cfg: { session: { mainKey: "work" }, agents: { list: [{ id: "main", default: true }] }, }, storePath: "/tmp/sessions.json", entry: { sessionId: "existing-session-id", updatedAt: Date.now(), }, canonicalKey: "agent:main:work", }); let capturedStore: Record | undefined; mocks.updateSessionStore.mockImplementation(async (_path, updater) => { const store: Record = { "agent:main:work": { sessionId: "existing-session-id", updatedAt: 10 }, "agent:main:MAIN": { sessionId: "legacy-session-id", updatedAt: 5 }, }; await updater(store); capturedStore = store; }); mocks.agentCommand.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 100 }, }); await invokeAgent( { message: "test", agentId: "main", sessionKey: "main", idempotencyKey: "test-idem-alias-prune", }, { reqId: "3" }, ); expect(mocks.updateSessionStore).toHaveBeenCalled(); expect(capturedStore).toBeDefined(); expect(capturedStore?.["agent:main:work"]).toBeDefined(); expect(capturedStore?.["agent:main:MAIN"]).toBeUndefined(); }); it("handles bare /new by resetting the same session and sending reset greeting prompt", async () => { mockSessionResetSuccess({ reason: "new" }); primeMainAgentRun({ sessionId: "reset-session-id" }); await invokeAgent( { message: "/new", sessionKey: "agent:main:main", idempotencyKey: "test-idem-new", }, { reqId: "4" }, ); await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); expect(mocks.sessionsResetHandler).toHaveBeenCalledTimes(1); const call = readLastAgentCommandCall(); expect(call?.message).toBe(BARE_SESSION_RESET_PROMPT); expect(call?.message).toContain("Execute your Session Startup sequence now"); expect(call?.sessionId).toBe("reset-session-id"); }); it("uses /reset suffix as the post-reset message and still injects timestamp", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-29T01:30:00.000Z")); // Wed Jan 28, 8:30 PM EST mocks.agentCommand.mockClear(); mocks.loadConfigReturn = { agents: { defaults: { userTimezone: "America/New_York", }, }, }; mockSessionResetSuccess({ reason: "reset" }); mocks.sessionsResetHandler.mockClear(); primeMainAgentRun({ sessionId: "reset-session-id", cfg: mocks.loadConfigReturn, }); await invokeAgent( { message: "/reset check status", sessionKey: "agent:main:main", idempotencyKey: "test-idem-reset-suffix", }, { reqId: "4b" }, ); await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); expect(mocks.sessionsResetHandler).toHaveBeenCalledTimes(1); const call = readLastAgentCommandCall(); expect(call?.message).toBe("[Wed 2026-01-28 20:30 EST] check status"); expect(call?.sessionId).toBe("reset-session-id"); mocks.loadConfigReturn = {}; vi.useRealTimers(); }); it("rejects malformed agent session keys early in agent handler", async () => { mocks.agentCommand.mockClear(); const respond = await invokeAgent( { message: "test", sessionKey: "agent:main", idempotencyKey: "test-malformed-session-key", }, { reqId: "4" }, ); expect(mocks.agentCommand).not.toHaveBeenCalled(); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ message: expect.stringContaining("malformed session key"), }), ); }); it("rejects malformed session keys in agent.identity.get", async () => { const respond = await invokeAgentIdentityGet( { sessionKey: "agent:main", }, { reqId: "5" }, ); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ message: expect.stringContaining("malformed session key"), }), ); }); });