import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { existsSync } from "node:fs"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { appendTranscriptDeltaJsonl, buildAutoReflectionPayload, buildParentMemorySnapshot, buildReflectionSubagentPrompt, finalizeAutoReflectionPayload, getReflectionTranscriptPaths, } from "../../cli/helpers/reflectionTranscript"; import { DIRECTORY_LIMIT_ENV } from "../../utils/directoryLimits"; const DIRECTORY_LIMIT_ENV_KEYS = Object.values(DIRECTORY_LIMIT_ENV); const ORIGINAL_DIRECTORY_ENV = Object.fromEntries( DIRECTORY_LIMIT_ENV_KEYS.map((key) => [key, process.env[key]]), ) as Record; function restoreDirectoryLimitEnv(): void { for (const key of DIRECTORY_LIMIT_ENV_KEYS) { const original = ORIGINAL_DIRECTORY_ENV[key]; if (original === undefined) { delete process.env[key]; } else { process.env[key] = original; } } } describe("reflectionTranscript helper", () => { const agentId = "agent-test"; const conversationId = "conv-test"; let testRoot: string; beforeEach(async () => { testRoot = await mkdtemp(join(tmpdir(), "letta-transcript-test-")); process.env.LETTA_TRANSCRIPT_ROOT = testRoot; }); afterEach(async () => { restoreDirectoryLimitEnv(); delete process.env.LETTA_TRANSCRIPT_ROOT; await rm(testRoot, { recursive: true, force: true }); }); test("auto payload advances cursor on success", async () => { await appendTranscriptDeltaJsonl(agentId, conversationId, [ { kind: "user", id: "u1", text: "hello" }, { kind: "assistant", id: "a1", text: "hi there", phase: "finished", }, ]); const payload = await buildAutoReflectionPayload(agentId, conversationId); expect(payload).not.toBeNull(); if (!payload) return; const payloadText = await readFile(payload.payloadPath, "utf-8"); expect(payloadText).toContain("hello"); expect(payloadText).toContain("hi there"); await finalizeAutoReflectionPayload( agentId, conversationId, payload.payloadPath, payload.endSnapshotLine, true, ); expect(existsSync(payload.payloadPath)).toBe(true); const paths = getReflectionTranscriptPaths(agentId, conversationId); const stateRaw = await readFile(paths.statePath, "utf-8"); const state = JSON.parse(stateRaw) as { auto_cursor_line: number }; expect(state.auto_cursor_line).toBe(payload.endSnapshotLine); const secondPayload = await buildAutoReflectionPayload( agentId, conversationId, ); expect(secondPayload).toBeNull(); }); test("auto payload keeps cursor on failure", async () => { await appendTranscriptDeltaJsonl(agentId, conversationId, [ { kind: "user", id: "u1", text: "remember this" }, ]); const payload = await buildAutoReflectionPayload(agentId, conversationId); expect(payload).not.toBeNull(); if (!payload) return; await finalizeAutoReflectionPayload( agentId, conversationId, payload.payloadPath, payload.endSnapshotLine, false, ); const paths = getReflectionTranscriptPaths(agentId, conversationId); const stateRaw = await readFile(paths.statePath, "utf-8"); const state = JSON.parse(stateRaw) as { auto_cursor_line: number }; expect(state.auto_cursor_line).toBe(0); const retried = await buildAutoReflectionPayload(agentId, conversationId); expect(retried).not.toBeNull(); }); test("auto payload clamps out-of-range cursor and resumes on new transcript lines", async () => { await appendTranscriptDeltaJsonl(agentId, conversationId, [ { kind: "user", id: "u1", text: "first" }, ]); const paths = getReflectionTranscriptPaths(agentId, conversationId); await writeFile( paths.statePath, `${JSON.stringify({ auto_cursor_line: 999 })}\n`, "utf-8", ); const firstAttempt = await buildAutoReflectionPayload( agentId, conversationId, ); expect(firstAttempt).toBeNull(); const clampedRaw = await readFile(paths.statePath, "utf-8"); const clamped = JSON.parse(clampedRaw) as { auto_cursor_line: number }; expect(clamped.auto_cursor_line).toBe(1); await appendTranscriptDeltaJsonl(agentId, conversationId, [ { kind: "assistant", id: "a2", text: "second", phase: "finished" }, ]); const secondAttempt = await buildAutoReflectionPayload( agentId, conversationId, ); expect(secondAttempt).not.toBeNull(); if (!secondAttempt) return; const payloadText = await readFile(secondAttempt.payloadPath, "utf-8"); expect(payloadText).toContain("second"); }); test("buildParentMemorySnapshot renders tree descriptions and system blocks", async () => { const memoryDir = join(testRoot, "memory"); const normalizedMemoryDir = memoryDir.replace(/\\/g, "/"); await mkdir(join(memoryDir, "system"), { recursive: true }); await mkdir(join(memoryDir, "reference"), { recursive: true }); await mkdir(join(memoryDir, "skills", "bird"), { recursive: true }); await writeFile( join(memoryDir, "system", "human.md"), "---\ndescription: User context\n---\nDr. Wooders prefers direct answers.\n", "utf-8", ); await writeFile( join(memoryDir, "reference", "project.md"), "---\ndescription: Project notes\n---\nletta-code CLI details\n", "utf-8", ); await writeFile( join(memoryDir, "skills", "bird", "SKILL.md"), "---\nname: bird\ndescription: X/Twitter CLI for posting\n---\nThis body should not be inlined into parent memory.\n", "utf-8", ); const snapshot = await buildParentMemorySnapshot(memoryDir); expect(snapshot).toContain(""); expect(snapshot).toContain(""); expect(snapshot).toContain("/memory/"); expect(snapshot).toContain("system/"); expect(snapshot).toContain("reference/"); expect(snapshot).toContain("skills/"); expect(snapshot).toContain("project.md (Project notes)"); expect(snapshot).toContain("SKILL.md (X/Twitter CLI for posting)"); expect(snapshot).toContain(""); expect(snapshot).toContain( `${normalizedMemoryDir}/system/human.md`, ); expect(snapshot).toContain("Dr. Wooders prefers direct answers."); expect(snapshot).toContain(""); expect(snapshot).not.toContain( `${normalizedMemoryDir}/reference/project.md`, ); expect(snapshot).not.toContain("letta-code CLI details"); expect(snapshot).not.toContain( "This body should not be inlined into parent memory.", ); expect(snapshot).toContain(""); }); test("buildParentMemorySnapshot collapses large users directory with omission marker", async () => { process.env[DIRECTORY_LIMIT_ENV.memfsTreeMaxChildrenPerDir] = "3"; const memoryDir = join(testRoot, "memory-large-users"); await mkdir(join(memoryDir, "system"), { recursive: true }); await mkdir(join(memoryDir, "users"), { recursive: true }); await writeFile( join(memoryDir, "system", "human.md"), "---\ndescription: User context\n---\nSystem content\n", "utf-8", ); for (let idx = 0; idx < 10; idx += 1) { const suffix = String(idx).padStart(2, "0"); await writeFile( join(memoryDir, "users", `user_${suffix}.md`), `---\ndescription: User block ${suffix}\n---\ncontent ${suffix}\n`, "utf-8", ); } const snapshot = await buildParentMemorySnapshot(memoryDir); expect(snapshot).toContain("users/"); expect(snapshot).toContain("… (7 more entries)"); expect(snapshot).not.toContain("user_09.md"); }); test("buildReflectionSubagentPrompt uses expanded reflection instructions", () => { const prompt = buildReflectionSubagentPrompt({ transcriptPath: "/tmp/transcript.txt", memoryDir: "/tmp/memory", cwd: "/tmp/work", parentMemory: "snapshot", }); expect(prompt).toContain("Review the conversation transcript"); expect(prompt).toContain("Your current working directory is: /tmp/work"); expect(prompt).toContain( "The current conversation transcript has been saved", ); expect(prompt).toContain( "In-context memory (in the parent agent's system prompt) is stored in the `system/` folder and are rendered in tags below.", ); expect(prompt).toContain( "Additional memory files (such as skills and external memory) may also be read and modified.", ); expect(prompt).toContain("snapshot"); }); });