openpencil/tests/engine/app/ai/reference-image.test.ts
2026-08-14 12:46:29 +03:00

96 lines
3.2 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import type { LanguageModel } from 'ai'
import { SceneGraph } from '@open-pencil/scene-graph'
import {
analyzeReferenceImage,
designMessageWithReferenceFindings,
type ReferenceImageAnalysisDependencies
} from '@/app/ai/reference-image/analyze'
import { validateReferenceImageFile } from '@/app/ai/reference-image/prepare'
import type { PreparedReferenceImage } from '@/app/ai/reference-image/types'
import type { EditorStore } from '@/app/editor/session/create'
const reference: PreparedReferenceImage = {
data: new Uint8Array([4, 5, 6]),
blob: new Blob(),
mediaType: 'image/png',
originalWidth: 1600,
originalHeight: 900,
width: 1280,
height: 720
}
describe('reference image analysis', () => {
test('rejects unsupported and oversized source files', () => {
expect(
validateReferenceImageFile(new File(['x'], 'reference.gif', { type: 'image/gif' }))
).toBe('Choose a PNG, JPEG, or WebP image.')
expect(
validateReferenceImageFile(
new File([new Uint8Array(20 * 1024 * 1024 + 1)], 'reference.png', {
type: 'image/png'
})
)
).toBe('Reference images must be 20 MB or smaller.')
})
test('sends bounded images only to Vision and returns text findings', async () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const frame = graph.createNode('FRAME', page.id, { width: 2560, height: 1600 })
const requests: unknown[] = []
const store = {
graph,
state: { currentPageId: page.id, selectedIds: new Set([frame.id]) },
renderExportImage: async () => new Uint8Array([1, 2, 3])
} as EditorStore
const dependencies: ReferenceImageAnalysisDependencies = {
createRuntime: async () =>
({
kind: 'direct',
model: {} as LanguageModel,
role: {
requestedRole: 'vision',
profile: { maxOutputTokens: 8000, reasoningEffort: 'low' },
connection: { providerID: 'openrouter' }
}
}) as never,
inspect: async (options) => {
requests.push(options)
return { text: 'Use a tighter grid and stronger heading contrast.' } as never
}
}
const findings = await analyzeReferenceImage(
store,
'Match this layout',
reference,
dependencies
)
expect(findings).toBe('Use a tighter grid and stronger heading contrast.')
const request = requests[0] as {
providerOptions?: unknown
messages: Array<{ content: Array<{ type: string; data?: Uint8Array }> }>
}
expect(request.providerOptions).toEqual({ openrouter: { reasoning: { effort: 'low' } } })
expect(request.messages[0]?.content.map((part) => part.type)).toEqual(['text', 'file', 'file'])
expect(request.messages[0]?.content[1]?.data).toEqual(reference.data)
})
test('passes textual findings rather than image data to Design', () => {
const message = designMessageWithReferenceFindings(
'Match this layout',
'reference.png',
'Use a 12-column grid.'
)
expect(message).toContain('Match this layout')
expect(message).toContain('Use a 12-column grid.')
expect(message).not.toContain('base64')
})
})