import { describe, expect, it, vi } from 'vitest' import { flushAnnotateStack } from './flush' import { compactIdentity } from './identity' import { annotateFlushPrompt, packageAnnotatePin, packageAnnotateStack } from './pack' import { addAnnotatePin, type AnnotatePin, emptyAnnotateStack } from './stack' import { ANNOTATE_HTML_BUDGET } from './tokens' const png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' function pin(partial: Partial = {}): AnnotatePin { return { id: 'annotate-1', imageDataUrl: png, kind: 'element', note: 'This button overflows on mobile.', number: 1, pageTitle: 'Pricing', pageUrl: 'http://127.0.0.1:4173/', rect: { height: 40, width: 120, x: 8, y: 8 }, identity: { css: { color: 'rgb(24, 24, 24)', 'font-size': '14px' }, html: '', selector: 'button.plan', tag: 'button', text: 'Select plan' }, ...partial } } describe('packageAnnotatePin', () => { it('carries the selector, markup, and computed styles the agent needs to find the source', () => { const packed = packageAnnotatePin(pin()) expect(packed.prompt).toContain('Selector: button.plan') expect(packed.prompt).toContain('HTML: ') expect(packed.prompt).toContain('color: rgb(24, 24, 24)') expect(packed.prompt).toContain('font-size: 14px') }) it('keeps the target line prose while the DOM detail rides its own labelled lines', () => { const text = 'גם בקיבוץ חולית הקטן יש ילד שעושה את הצעד הראשון במערכת החינוך' const packed = packageAnnotatePin( pin({ identity: { css: { color: 'rgb(0, 0, 0)', 'font-family': 'Moses, NarkisBlock', 'font-size': '18px' }, html: '
…
', selector: 'div.DraftEditor-editorContainer>div.public-DraftEditor-content>div>div.text_editor_paragraph.rtl:nth-of-type(9)', tag: 'div', text }, note: 'תסכם את זה' }) ) const target = packed.prompt.split('\n').find(line => line.startsWith('Target:')) expect(target).toBe(`Target: "${text}"`) expect(target).not.toContain('div') expect(target).not.toContain('DraftEditor') expect(packed.prompt).toContain('Note: תסכם את זה') expect(packed.prompt).toContain('Selector: div.DraftEditor-editorContainer') }) it('packs a numbered crop, compact identity, and the note', () => { const packed = packageAnnotatePin(pin()) expect(packed.number).toBe(1) expect(packed.imageDataUrl).toBe(png) expect(packed.note).toContain('overflows') expect(packed.prompt).toContain('Comment 1') expect(packed.prompt).toContain('Target: button "Select plan"') expect(packed.prompt).toContain('Image 1 marks the target in blue.') expect(packed.prompt).toContain('Note: This button overflows on mobile.') expect(packed.prompt).not.toContain(' { const packed = packageAnnotatePin( pin({ identity: { css: { display: 'block' }, html: '
', selector: '#sales-chart', tag: 'div', text: '' }, note: 'Use the same scale as the chart above.' }) ) expect(packed.prompt).toContain('Target: #sales-chart') }) it('invents no element detail for an area pin', () => { const packed = packageAnnotatePin(pin({ identity: undefined, kind: 'area', note: 'too tight' })) expect(packed.prompt).toContain('area') expect(packed.prompt).toContain('120×40px') expect(packed.prompt).toContain('too tight') expect(packed.prompt).not.toContain('Selector:') expect(packed.prompt).not.toContain('HTML:') expect(packed.prompt).not.toContain('Styles:') }) }) describe('compactIdentity', () => { it('never keeps the whole document and clips long text', () => { const compact = compactIdentity({ css: { color: 'red', display: 'none', margin: '0px' }, selector: 'html>body>div>div>button.submit', tag: 'BUTTON', text: 'x'.repeat(200) }) expect(compact.tag).toBe('button') expect(compact.text.endsWith('…')).toBe(true) expect(compact.text.length).toBeLessThanOrEqual(80) expect(compact.css.display).toBeUndefined() expect(compact.css.margin).toBeUndefined() expect(compact.css.color).toBe('red') }) it('clips markup to the budget rather than pasting a whole section', () => { const compact = compactIdentity({ css: {}, html: `
${'

filler

'.repeat(400)}
`, selector: 'section', tag: 'section', text: '' }) expect(compact.html.length).toBeLessThanOrEqual(ANNOTATE_HTML_BUDGET) expect(compact.html.startsWith('
')).toBe(true) expect(compact.html.endsWith('…')).toBe(true) }) it('tolerates a snapshot with no markup', () => { expect(compactIdentity({ css: {}, selector: 'div', tag: 'div', text: '' }).html).toBe('') }) }) describe('flushAnnotateStack', () => { it('attaches one composer item per pin and does not invoke send', async () => { const first = pin() const second = pin({ id: 'annotate-2', kind: 'area', identity: undefined, note: 'align the chart', number: 2 }) let stack = emptyAnnotateStack() stack = addAnnotatePin(stack, { imageDataUrl: first.imageDataUrl, identity: first.identity, kind: first.kind, note: first.note, pageTitle: first.pageTitle, pageUrl: first.pageUrl, rect: first.rect }) stack = addAnnotatePin(stack, { imageDataUrl: second.imageDataUrl, kind: 'area', note: second.note, pageTitle: second.pageTitle, pageUrl: second.pageUrl, rect: second.rect }) const attachImage = vi.fn() const insertText = vi.fn() const send = vi.fn() const result = await flushAnnotateStack(stack.pins, { attachImage, insertText, send }, 'http://127.0.0.1:4173/') expect(result.sent).toBe(false) expect(result.count).toBe(2) expect(send).not.toHaveBeenCalled() expect(attachImage).toHaveBeenCalledTimes(2) expect(attachImage.mock.calls[0]?.[0]).toBeInstanceOf(File) expect((attachImage.mock.calls[0]?.[0] as File).name).toBe('Comment_1.png') expect((attachImage.mock.calls[1]?.[0] as File).name).toBe('Comment_2.png') expect(insertText).toHaveBeenCalledOnce() expect(insertText.mock.calls[0]?.[0]).toContain('I left 2 comments') expect(insertText.mock.calls[0]?.[0]).toContain('Comment 1') expect(insertText.mock.calls[0]?.[0]).toContain('Comment 2') }) it('a pin save is stacking, not flushing', () => { const stacked = packageAnnotateStack([pin(), pin({ id: 'annotate-2', number: 2, note: 'second' })]) expect(stacked).toHaveLength(2) expect(annotateFlushPrompt(stacked)).toContain('2 comments') }) }) describe('annotateFlushPrompt batching', () => { function at(number: number, selector: string): AnnotatePin { return pin({ id: `annotate-${number}`, number, identity: { css: {}, html: '', selector, tag: 'div', text: '' } }) } const batch = packageAnnotateStack([ at(1, 'body>main>section.hero>h1'), at(2, 'body>main>section.hero>p'), at(3, 'body>main>section.pricing>button'), at(4, 'body>main>section.pricing>span'), at(5, 'body>main>section.faq>li') ]) it('heads each region so a long batch is fewer pieces of work than comments', () => { const prompt = annotateFlushPrompt(batch, 'http://localhost:5173/') expect(prompt).toContain('Group 1 — `section.hero` (2 comments)') expect(prompt).toContain('Group 2 — `section.pricing` (2 comments)') expect(prompt).toContain('Group 3 — `section.faq` (1 comment)') expect(prompt).toContain('Work them as 3 pieces of work, not 5.') }) it('warns against the theme split that would put workers in the same files', () => { const prompt = annotateFlushPrompt(batch) expect(prompt).toContain('delegate whole groups') expect(prompt).toContain('never form new groups by theme') expect(prompt).toContain('Regroup if the code disagrees') }) it('still lists every comment exactly once', () => { const prompt = annotateFlushPrompt(batch) for (const item of batch) { expect(prompt.split(`Comment ${item.number}\n`)).toHaveLength(2) } }) it('leaves a short batch flat — grouping two comments is noise', () => { const prompt = annotateFlushPrompt(batch.slice(0, 2)) expect(prompt).not.toContain('Group 1') expect(prompt).not.toContain('pieces of work') }) it('leaves a batch flat when every comment is in one region', () => { const prompt = annotateFlushPrompt( packageAnnotateStack([ at(1, 'body>div.card>h1'), at(2, 'body>div.card>p'), at(3, 'body>div.card>a'), at(4, 'body>div.card>span') ]) ) expect(prompt).not.toContain('Group 1') }) it('gives dragged areas their own section instead of a guessed region', () => { const prompt = annotateFlushPrompt( packageAnnotateStack([ at(1, 'body>main>section.hero>h1'), at(2, 'body>main>section.pricing>button'), pin({ id: 'annotate-3', identity: undefined, kind: 'area', number: 3 }), at(4, 'body>main>section.faq>li') ]) ) expect(prompt).toContain('Unanchored (dragged areas) (1 comment)') }) })