From 242a91bd0d78ca8269250293d119092ade7fccbc Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:29:44 -0500 Subject: [PATCH] fix: use target agent for session exports --- .../reply/commands-export-session.test.ts | 143 ++++++++++++++++++ .../reply/commands-export-session.ts | 6 +- 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/auto-reply/reply/commands-export-session.test.ts diff --git a/src/auto-reply/reply/commands-export-session.test.ts b/src/auto-reply/reply/commands-export-session.test.ts new file mode 100644 index 00000000000..bf78859eb15 --- /dev/null +++ b/src/auto-reply/reply/commands-export-session.test.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { HandleCommandsParams } from "./commands-types.js"; + +const hoisted = vi.hoisted(() => ({ + resolveDefaultSessionStorePathMock: vi.fn(() => "/tmp/target-store/sessions.json"), + resolveSessionFilePathMock: vi.fn(() => "/tmp/target-store/session.jsonl"), + resolveSessionFilePathOptionsMock: vi.fn( + (params: { agentId: string; storePath: string }) => params, + ), + loadSessionStoreMock: vi.fn(() => ({ + "agent:target:session": { + sessionId: "session-1", + updatedAt: 1, + }, + })), + resolveCommandsSystemPromptBundleMock: vi.fn(async () => ({ + systemPrompt: "system prompt", + tools: [], + skillsPrompt: "", + bootstrapFiles: [], + injectedFiles: [], + sandboxRuntime: { sandboxed: false, mode: "off" }, + })), + getEntriesMock: vi.fn(() => []), + getHeaderMock: vi.fn(() => null), + getLeafIdMock: vi.fn(() => null), + writeFileSyncMock: vi.fn(), + mkdirSyncMock: vi.fn(), + existsSyncMock: vi.fn(() => true), +})); + +vi.mock("@mariozechner/pi-coding-agent", () => ({ + SessionManager: { + open: vi.fn(() => ({ + getEntries: hoisted.getEntriesMock, + getHeader: hoisted.getHeaderMock, + getLeafId: hoisted.getLeafIdMock, + })), + }, +})); + +vi.mock("../../config/sessions/paths.js", () => ({ + resolveDefaultSessionStorePath: hoisted.resolveDefaultSessionStorePathMock, + resolveSessionFilePath: hoisted.resolveSessionFilePathMock, + resolveSessionFilePathOptions: hoisted.resolveSessionFilePathOptionsMock, +})); + +vi.mock("../../config/sessions/store.js", () => ({ + loadSessionStore: hoisted.loadSessionStoreMock, +})); + +vi.mock("./commands-system-prompt.js", () => ({ + resolveCommandsSystemPromptBundle: hoisted.resolveCommandsSystemPromptBundleMock, +})); + +vi.mock("node:fs", async () => { + const actual = await vi.importActual("node:fs"); + return { + ...actual, + existsSync: hoisted.existsSyncMock, + mkdirSync: hoisted.mkdirSyncMock, + writeFileSync: hoisted.writeFileSyncMock, + readFileSync: vi.fn((filePath: string) => { + if (String(filePath).endsWith("template.html")) { + return "{{CSS}}{{JS}}{{SESSION_DATA}}{{MARKED_JS}}{{HIGHLIGHT_JS}}"; + } + return ""; + }), + }; +}); + +function makeParams(): HandleCommandsParams { + return { + cfg: {}, + ctx: { + SessionKey: "agent:main:slash-session", + }, + command: { + commandBodyNormalized: "/export-session", + isAuthorizedSender: true, + senderIsOwner: true, + senderId: "sender-1", + channel: "telegram", + surface: "telegram", + ownerList: [], + rawBodyNormalized: "/export-session", + }, + sessionEntry: { + sessionId: "session-1", + updatedAt: 1, + }, + sessionKey: "agent:target:session", + workspaceDir: "/tmp/workspace", + directives: {}, + elevated: { enabled: true, allowed: true, failures: [] }, + defaultGroupActivation: () => "mention", + resolvedVerboseLevel: "off", + resolvedReasoningLevel: "off", + resolveDefaultThinkingLevel: async () => undefined, + provider: "openai", + model: "gpt-5.4", + contextTokens: 0, + isGroup: false, + } as unknown as HandleCommandsParams; +} + +describe("buildExportSessionReply", () => { + beforeEach(() => { + vi.clearAllMocks(); + hoisted.resolveDefaultSessionStorePathMock.mockReturnValue("/tmp/target-store/sessions.json"); + hoisted.resolveSessionFilePathMock.mockReturnValue("/tmp/target-store/session.jsonl"); + hoisted.resolveSessionFilePathOptionsMock.mockImplementation( + (params: { agentId: string; storePath: string }) => params, + ); + hoisted.loadSessionStoreMock.mockReturnValue({ + "agent:target:session": { + sessionId: "session-1", + updatedAt: 1, + }, + }); + hoisted.resolveCommandsSystemPromptBundleMock.mockResolvedValue({ + systemPrompt: "system prompt", + tools: [], + skillsPrompt: "", + bootstrapFiles: [], + injectedFiles: [], + sandboxRuntime: { sandboxed: false, mode: "off" }, + }); + hoisted.existsSyncMock.mockReturnValue(true); + }); + + it("resolves store and transcript paths from the target session agent", async () => { + const { buildExportSessionReply } = await import("./commands-export-session.js"); + + await buildExportSessionReply(makeParams()); + + expect(hoisted.resolveDefaultSessionStorePathMock).toHaveBeenCalledWith("target"); + expect(hoisted.resolveSessionFilePathOptionsMock).toHaveBeenCalledWith({ + agentId: "target", + storePath: "/tmp/target-store/sessions.json", + }); + }); +}); diff --git a/src/auto-reply/reply/commands-export-session.ts b/src/auto-reply/reply/commands-export-session.ts index ab15002f68a..e5216eb77b8 100644 --- a/src/auto-reply/reply/commands-export-session.ts +++ b/src/auto-reply/reply/commands-export-session.ts @@ -11,6 +11,7 @@ import { import { loadSessionStore } from "../../config/sessions/store.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import { formatErrorMessage } from "../../infra/errors.js"; +import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; import type { ReplyPayload } from "../types.js"; import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js"; import type { HandleCommandsParams } from "./commands-types.js"; @@ -119,7 +120,8 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro return { text: "❌ No active session found." }; } - const storePath = resolveDefaultSessionStorePath(params.agentId); + const targetAgentId = resolveAgentIdFromSessionKey(params.sessionKey) || params.agentId; + const storePath = resolveDefaultSessionStorePath(targetAgentId); const store = loadSessionStore(storePath, { skipCache: true }); const entry = store[params.sessionKey] as SessionEntry | undefined; if (!entry?.sessionId) { @@ -131,7 +133,7 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro sessionFile = resolveSessionFilePath( entry.sessionId, entry, - resolveSessionFilePathOptions({ agentId: params.agentId, storePath }), + resolveSessionFilePathOptions({ agentId: targetAgentId, storePath }), ); } catch (err) { return {