import { describe, it, expect, vi, afterAll, beforeEach } from "vitest"; import { existsSync, rmSync } from "node:fs"; vi.mock("iii-sdk", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, getContext: () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() }, }), }; }); vi.mock("../src/functions/search.js", () => ({ getSearchIndex: () => ({ add: vi.fn(), }), vectorIndexAddGuarded: vi.fn().mockResolvedValue(false), })); const mockTrigger = vi.fn().mockResolvedValue(undefined); const mockSdk = { trigger: mockTrigger } as any; function mockKV() { const store = new Map>(); return { get: async (scope: string, key: string): Promise => { return (store.get(scope)?.get(key) as T) ?? null; }, set: async (scope: string, key: string, data: T): Promise => { if (!store.has(scope)) store.set(scope, new Map()); store.get(scope)!.set(key, data); return data; }, delete: async (scope: string, key: string): Promise => { store.get(scope)?.delete(key); }, list: async (scope: string): Promise => { if (!store.has(scope)) return []; return Array.from(store.get(scope)!.values()) as T[]; }, getStore: () => store, }; } const kv = mockKV() as any; import { registerObserveFunction } from "../src/functions/observe.js"; import { registerCompressFunction } from "../src/functions/compress.js"; import type { RawObservation, CompressedObservation, MemoryProvider } from "../src/types.js"; const VALID_COMPRESS_XML = `image Screenshot of Red Dot Test image observation Image shows a single red pixel on white background A vision model described a screenshot showing a red dot on a white background testingscreenshot 5`; describe("End-to-End Multimodal Flow", () => { let savedImagePath: string | undefined; afterAll(() => { if (savedImagePath && existsSync(savedImagePath)) { rmSync(savedImagePath); console.log(`Cleanup: Removed test image at ${savedImagePath}`); } }); beforeEach(() => { mockTrigger.mockClear(); }); it("Step 1: Agent image should be successfully saved to hard drive", async () => { let observeCallback: any = null; const sdkMocker = { ...mockSdk, registerFunction: vi.fn((id, cb) => { if (id === "mem::observe") observeCallback = cb; }) }; registerObserveFunction(sdkMocker, kv); const fakeIncomingData = { hookType: "post_tool_use", sessionId: "test-session", timestamp: new Date().toISOString(), data: { tool_name: "screenshot", tool_output: { image_data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==" } } }; const res = await observeCallback(fakeIncomingData); expect(res.observationId).toBeDefined(); const obsList = await kv.list("mem:obs:test-session"); expect(obsList.length).toBe(1); const raw = obsList[0] as RawObservation; expect(raw.modality).toBe("mixed"); expect(raw.imageData).toBeDefined(); expect(typeof raw.imageData).toBe("string"); expect(existsSync(raw.imageData!)).toBe(true); savedImagePath = raw.imageData; const deltaCalls = mockTrigger.mock.calls.filter( (c: any[]) => c[0]?.function_id === "mem::disk-size-delta" ); expect(deltaCalls.length).toBe(1); expect(deltaCalls[0][0].payload.deltaBytes).toBeGreaterThan(0); }); it("Step 2 & 3: mem::compress should call the vision model and store compressed observation in KV", async () => { const mockProvider: MemoryProvider = { name: "mock-vision", compress: async (_systemPrompt, userPrompt) => { expect(userPrompt).toContain("TEST_VISION_RESULT: I see a red dot"); return VALID_COMPRESS_XML; }, summarize: async () => "", describeImage: async (_base64, _mimeType, _prompt) => { return "TEST_VISION_RESULT: I see a red dot"; }, }; let compressCallback: any = null; const sdkMocker = { ...mockSdk, registerFunction: vi.fn((id, cb) => { if (id === "mem::compress") compressCallback = cb; }), }; registerCompressFunction(sdkMocker, kv, mockProvider); expect(compressCallback).not.toBeNull(); const rawObsList = await kv.list("mem:obs:test-session"); const raw = rawObsList[0] as RawObservation; expect(raw.modality).toBeDefined(); expect(raw.imageData).toBe(savedImagePath); const result = await compressCallback({ observationId: raw.id, sessionId: raw.sessionId, raw, }); expect(result.success).toBe(true); expect(result.compressed).toBeDefined(); const compressed = result.compressed as CompressedObservation; expect(compressed.imageDescription).toBe("TEST_VISION_RESULT: I see a red dot"); expect(compressed.imageRef).toBe(savedImagePath); expect(compressed.modality).toBe("mixed"); expect(compressed.title).toBe("Screenshot of Red Dot"); expect(compressed.narrative).toContain("red dot"); const stored = await kv.get("mem:obs:test-session", raw.id!) as CompressedObservation | null; expect(stored).not.toBeNull(); expect(stored!.imageDescription).toBe("TEST_VISION_RESULT: I see a red dot"); expect(stored!.imageRef).toBe(savedImagePath); }); }); describe("Disk Size Manager", () => { it("should increment disk size and trigger cleanup when over quota", async () => { const localKv = mockKV() as any; const localTrigger = vi.fn().mockResolvedValue(undefined); const localSdk = { trigger: localTrigger } as any; let managerCallback: any = null; const sdkMocker = { ...localSdk, registerFunction: vi.fn((id: string, cb: any) => { if (id === "mem::disk-size-delta") managerCallback = cb; }), }; const { registerDiskSizeManager } = await import("../src/functions/disk-size-manager.js"); registerDiskSizeManager(sdkMocker, localKv); expect(managerCallback).not.toBeNull(); const res1 = await managerCallback({ deltaBytes: 1000 }); expect(res1.success).toBe(true); expect(res1.currentTotal).toBe(1000); const res2 = await managerCallback({ deltaBytes: 2000 }); expect(res2.success).toBe(true); expect(res2.currentTotal).toBe(3000); expect(localTrigger).not.toHaveBeenCalled(); }); it("should trigger cleanup when total exceeds max bytes", async () => { const originalEnv = process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES; try { process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES = "5000"; const localKv = mockKV() as any; const localTrigger = vi.fn().mockResolvedValue(undefined); const localSdk = { trigger: localTrigger } as any; let managerCallback: any = null; const sdkMocker = { ...localSdk, registerFunction: vi.fn((id: string, cb: any) => { if (id === "mem::disk-size-delta") managerCallback = cb; }), }; const { registerDiskSizeManager } = await import("../src/functions/disk-size-manager.js"); registerDiskSizeManager(sdkMocker, localKv); await managerCallback({ deltaBytes: 3000 }); expect(localTrigger).not.toHaveBeenCalled(); await managerCallback({ deltaBytes: 3000 }); expect(localTrigger).toHaveBeenCalledWith( expect.objectContaining({ function_id: "mem::image-quota-cleanup", payload: {}, }), ); } finally { if (originalEnv === undefined) { delete process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES; } else { process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES = originalEnv; } } }); it("should clamp negative totals to zero", async () => { const localKv = mockKV() as any; const localSdk = { trigger: vi.fn().mockResolvedValue(undefined) } as any; let managerCallback: any = null; const sdkMocker = { ...localSdk, registerFunction: vi.fn((id: string, cb: any) => { if (id === "mem::disk-size-delta") managerCallback = cb; }), }; const { registerDiskSizeManager } = await import("../src/functions/disk-size-manager.js"); registerDiskSizeManager(sdkMocker, localKv); const res = await managerCallback({ deltaBytes: -99999 }); expect(res.currentTotal).toBe(0); }); it("should reject invalid deltaBytes", async () => { const localKv = mockKV() as any; const localSdk = { trigger: vi.fn().mockResolvedValue(undefined) } as any; let managerCallback: any = null; const sdkMocker = { ...localSdk, registerFunction: vi.fn((id: string, cb: any) => { if (id === "mem::disk-size-delta") managerCallback = cb; }), }; const { registerDiskSizeManager } = await import("../src/functions/disk-size-manager.js"); registerDiskSizeManager(sdkMocker, localKv); const res = await managerCallback({ deltaBytes: "not-a-number" }); expect(res.success).toBe(false); }); }); describe("Image Refs", () => { it("should increment and decrement ref counts correctly with deletion parity", async () => { const localKv = mockKV() as any; const localTrigger = vi.fn().mockResolvedValue(undefined); const localSdk = { trigger: localTrigger } as any; const { saveImageToDisk } = await import("../src/utils/image-store.js"); const { incrementImageRef, decrementImageRef, getImageRefCount } = await import("../src/functions/image-refs.js"); const result = await saveImageToDisk( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==" ); const testFile = result.filePath; expect(existsSync(testFile)).toBe(true); // Initial state expect(await getImageRefCount(localKv, testFile)).toBe(0); // Increment to 1 await incrementImageRef(localKv, testFile); expect(await getImageRefCount(localKv, testFile)).toBe(1); // Increment to 2 (shared image) await incrementImageRef(localKv, testFile); expect(await getImageRefCount(localKv, testFile)).toBe(2); // Decrement from 2 to 1 await decrementImageRef(localKv, localSdk, testFile); expect(await getImageRefCount(localKv, testFile)).toBe(1); // (c) shared image with refcount >= 2 is NOT deleted when one decrements expect(existsSync(testFile)).toBe(true); expect(localTrigger).not.toHaveBeenCalled(); // (a) decrementing to zero triggers image file deletion and negative delta await decrementImageRef(localKv, localSdk, testFile); expect(await getImageRefCount(localKv, testFile)).toBe(0); expect(existsSync(testFile)).toBe(false); const deltaCalls = localTrigger.mock.calls.filter( (c: any[]) => c[0]?.function_id === "mem::disk-size-delta" ); expect(deltaCalls.length).toBe(1); expect(deltaCalls[0][0].payload.deltaBytes).toBeLessThan(0); // (b) decrementing an already-zero/unknown ref is a no-op localTrigger.mockClear(); await decrementImageRef(localKv, localSdk, "/fake/unknown/path.png"); expect(await getImageRefCount(localKv, "/fake/unknown/path.png")).toBe(0); const noOpDeltaCalls = localTrigger.mock.calls.filter( (c: any[]) => c[0]?.function_id === "mem::disk-size-delta" ); expect(noOpDeltaCalls.length).toBe(0); }); }); describe("Image Store", () => { let testFilePath: string | undefined; afterAll(() => { if (testFilePath && existsSync(testFilePath)) { rmSync(testFilePath); } }); it("saveImageToDisk should return filePath and bytesWritten", async () => { const { saveImageToDisk } = await import("../src/utils/image-store.js"); const result = await saveImageToDisk( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==" ); expect(result.filePath).toBeDefined(); expect(typeof result.filePath).toBe("string"); expect(result.filePath.length).toBeGreaterThan(0); expect(result.bytesWritten).toBeGreaterThan(0); expect(existsSync(result.filePath)).toBe(true); testFilePath = result.filePath; }); it("deleteImage should return deletedBytes after successful deletion", async () => { const { deleteImage } = await import("../src/utils/image-store.js"); expect(testFilePath).toBeDefined(); expect(existsSync(testFilePath!)).toBe(true); const result = await deleteImage(testFilePath!); expect(result.deletedBytes).toBeGreaterThan(0); expect(existsSync(testFilePath!)).toBe(false); testFilePath = undefined; }); it("deleteImage should return 0 for non-existent files", async () => { const { deleteImage } = await import("../src/utils/image-store.js"); const result = await deleteImage("/non/existent/file.png"); expect(result.deletedBytes).toBe(0); }); it("saveImageToDisk should return empty for empty input", async () => { const { saveImageToDisk } = await import("../src/utils/image-store.js"); const result = await saveImageToDisk(""); expect(result.filePath).toBe(""); expect(result.bytesWritten).toBe(0); }); });