fix: use target agent for session exports

This commit is contained in:
Tak Hoffman
2026-04-10 18:29:44 -05:00
parent 4ad2006811
commit 242a91bd0d
2 changed files with 147 additions and 2 deletions

View File

@@ -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<typeof import("node:fs")>("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 "<html>{{CSS}}{{JS}}{{SESSION_DATA}}{{MARKED_JS}}{{HIGHLIGHT_JS}}</html>";
}
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",
});
});
});

View File

@@ -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 {