// @vitest-environment node import { mkdtemp, readdir, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { AgentBrowserModule } from '../../electron/agent-browser'; import { createCodingComposition } from '../../electron/api/coding-composition'; import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store'; import { createMemoryCodingProjectStorage } from '../../electron/coding-projects/project-store'; import type { PiConversationRuntimeOptions } from '../../electron/coding-runtime/pi/runtime'; const captured = vi.hoisted(() => ({ options: undefined as PiConversationRuntimeOptions | undefined })); vi.mock('../../electron/coding-runtime/pi/runtime', async (importOriginal) => { const original = await importOriginal(); return { ...original, PiConversationRuntime: class extends original.PiConversationRuntime { constructor(options: PiConversationRuntimeOptions) { super(options); captured.options = options; } } }; }); const roots: string[] = []; afterEach(async () => { captured.options = undefined; await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); describe('coding composition images', () => { it('connects live and persisted Pi images to readable attachments and reuses their bytes after restart', async () => { const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-composition-images-')); roots.push(userDataDir); const composition = createCodingComposition({ storage: createMemoryCodingProjectStorage(), browser: { close: vi.fn(async () => undefined) } as unknown as AgentBrowserModule, paths: { executablePath: process.execPath, cliPath: path.join(userDataDir, 'unused-cli.js'), serverPath: path.join(userDataDir, 'unused-server.mjs'), userDataDir, bundledSkillsDir: path.resolve('resources/coding-skills'), }, }); try { const bytes = Buffer.from('image bytes'); const uploaded = await composition.attachments.put(bytes, 'image/png'); const images = await captured.options!.resolveImages!([uploaded]); expect(images).toEqual([{ type: 'image', data: bytes.toString('base64'), mimeType: 'image/png' }]); expect(captured.options!.projectImage).toBeTypeOf('function'); for (const source of ['live', 'session'] as const) { const projected = await captured.options!.projectImage!({ conversationId: 'conversation-image', source, data: images[0].data, mime: images[0].mimeType, }); expect(projected.attachmentId).toBe(uploaded.attachmentId); expect((await composition.attachments.read(projected.attachmentId)).data).toEqual(bytes); } const root = path.join(userDataDir, 'coding-runtime', 'attachments'); const reopened = new CodingAttachmentStore(root); expect(await reopened.put(bytes, 'image/png')).toEqual(uploaded); expect((await readdir(root)).sort()).toEqual([ `${uploaded.attachmentId}.bin`, `${uploaded.attachmentId}.json`, ]); expect((await reopened.put(bytes, 'image/jpeg')).attachmentId).not.toBe(uploaded.attachmentId); } finally { await composition.shutdown(); } }); });