070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
224 lines
7.7 KiB
TypeScript
224 lines
7.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { historyWithApiAttachmentContext } from '../src/api-attachment-context';
|
|
import {
|
|
fetchProjectFilePreview,
|
|
fetchProjectFileText,
|
|
} from '../src/providers/registry';
|
|
import type { ChatMessage, ProjectFile } from '../src/types';
|
|
|
|
vi.mock('../src/providers/registry', async () => {
|
|
const actual = await vi.importActual<typeof import('../src/providers/registry')>(
|
|
'../src/providers/registry',
|
|
);
|
|
return {
|
|
...actual,
|
|
fetchProjectFilePreview: vi.fn().mockResolvedValue(null),
|
|
fetchProjectFileText: vi.fn().mockResolvedValue(null),
|
|
};
|
|
});
|
|
|
|
const mockedFetchProjectFilePreview = vi.mocked(fetchProjectFilePreview);
|
|
const mockedFetchProjectFileText = vi.mocked(fetchProjectFileText);
|
|
|
|
describe('historyWithApiAttachmentContext', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('adds extracted document previews to the target user message', async () => {
|
|
mockedFetchProjectFilePreview.mockResolvedValue({
|
|
kind: 'document',
|
|
title: 'brief.docx',
|
|
sections: [{ title: 'Document', lines: ['Hello world', 'Second line'] }],
|
|
});
|
|
|
|
const history = await historyWithApiAttachmentContext(
|
|
[userMessage('msg-1', 'Summarize this', [{ path: 'brief.docx', name: 'brief.docx', kind: 'file' }])],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile('brief.docx', 'document')],
|
|
);
|
|
|
|
expect(mockedFetchProjectFilePreview).toHaveBeenCalledWith('project-1', 'brief.docx');
|
|
expect(history[0]?.content).toContain('<attached-project-files>');
|
|
expect(history[0]?.content).toContain('user-visible order');
|
|
expect(history[0]?.content).toContain('### Attachment 1: brief.docx');
|
|
expect(history[0]?.content).toContain('Hello world');
|
|
expect(history[0]?.content).toContain('Second line');
|
|
});
|
|
|
|
it('preserves uploaded attachment order with numbered headings', async () => {
|
|
const history = await historyWithApiAttachmentContext(
|
|
[
|
|
userMessage('msg-1', 'Compare the first and second image', [
|
|
{ path: 'uploads/first.png', name: 'image.png', kind: 'image' },
|
|
{ path: 'uploads/second.png', name: 'image.png', kind: 'image' },
|
|
]),
|
|
],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile('uploads/first.png', 'image'), projectFile('uploads/second.png', 'image')],
|
|
);
|
|
|
|
const content = history[0]?.content ?? '';
|
|
expect(content.indexOf('### Attachment 1:')).toBeLessThan(content.indexOf('### Attachment 2:'));
|
|
expect(content).toContain('path: uploads/first.png');
|
|
expect(content).toContain('path: uploads/second.png');
|
|
});
|
|
|
|
it('uses the explicit user-visible order before numbering attachments', async () => {
|
|
const history = await historyWithApiAttachmentContext(
|
|
[
|
|
userMessage('msg-1', 'Compare the first and second image', [
|
|
{ path: 'uploads/second.png', name: 'second.png', kind: 'image', order: 1 },
|
|
{ path: 'uploads/first.png', name: 'first.png', kind: 'image', order: 0 },
|
|
]),
|
|
],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile('uploads/first.png', 'image'), projectFile('uploads/second.png', 'image')],
|
|
);
|
|
|
|
const content = history[0]?.content ?? '';
|
|
expect(content.indexOf('### Attachment 1: first.png')).toBeLessThan(
|
|
content.indexOf('### Attachment 2: second.png'),
|
|
);
|
|
});
|
|
|
|
it('reads raw text attachments with a cache buster from file metadata', async () => {
|
|
mockedFetchProjectFileText.mockResolvedValue('const answer = 42;');
|
|
|
|
const history = await historyWithApiAttachmentContext(
|
|
[userMessage('msg-1', 'Use this code', [{ path: 'src/demo.ts', name: 'demo.ts', kind: 'file' }])],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile('src/demo.ts', 'code')],
|
|
);
|
|
|
|
expect(mockedFetchProjectFileText).toHaveBeenCalledWith(
|
|
'project-1',
|
|
'src/demo.ts',
|
|
{ cache: 'no-store', cacheBustKey: 123 },
|
|
);
|
|
expect(history[0]?.content).toContain('```ts');
|
|
expect(history[0]?.content).toContain('const answer = 42;');
|
|
});
|
|
|
|
it('does not fetch raw text for sketch image attachments', async () => {
|
|
const history = await historyWithApiAttachmentContext(
|
|
[userMessage('msg-1', 'Use this sketch', [{ path: 'sketch-board.png', name: 'sketch-board.png', kind: 'image' }])],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile('sketch-board.png', 'sketch')],
|
|
);
|
|
|
|
expect(mockedFetchProjectFileText).not.toHaveBeenCalled();
|
|
expect(mockedFetchProjectFilePreview).not.toHaveBeenCalled();
|
|
expect(history[0]?.content).toContain('kind: sketch');
|
|
expect(history[0]?.content).toContain('Content preview unavailable');
|
|
});
|
|
|
|
it('omits image attachment metadata when the provider sends native image blocks', async () => {
|
|
for (const path of ['hero.png', 'hero.jpg', 'hero.jpeg', 'hero.gif', 'hero.webp']) {
|
|
const history = await historyWithApiAttachmentContext(
|
|
[
|
|
userMessage('msg-1', 'Describe this image', [
|
|
{ path, name: path, kind: 'image' },
|
|
]),
|
|
],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile(path, 'image')],
|
|
{ omitNativeImageAttachments: true },
|
|
);
|
|
|
|
expect(history[0]?.content).toBe('Describe this image');
|
|
}
|
|
expect(mockedFetchProjectFileText).not.toHaveBeenCalled();
|
|
expect(mockedFetchProjectFilePreview).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('omits sketch-prefixed raster image metadata when native image blocks carry them', async () => {
|
|
const history = await historyWithApiAttachmentContext(
|
|
[
|
|
userMessage('msg-1', 'Describe this image', [
|
|
{ path: 'sketch-hero.png', name: 'sketch-hero.png', kind: 'image' },
|
|
]),
|
|
],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile('sketch-hero.png', 'sketch')],
|
|
{ omitNativeImageAttachments: true },
|
|
);
|
|
|
|
expect(history[0]?.content).toBe('Describe this image');
|
|
expect(mockedFetchProjectFileText).not.toHaveBeenCalled();
|
|
expect(mockedFetchProjectFilePreview).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps unsupported image metadata when native image blocks cannot carry them', async () => {
|
|
for (const path of ['hero.avif', 'hero.bmp']) {
|
|
const history = await historyWithApiAttachmentContext(
|
|
[
|
|
userMessage('msg-1', 'Describe this image', [
|
|
{ path, name: path, kind: 'image' },
|
|
]),
|
|
],
|
|
'msg-1',
|
|
'project-1',
|
|
[projectFile(path, 'image')],
|
|
{ omitNativeImageAttachments: true },
|
|
);
|
|
|
|
expect(history[0]?.content).toContain('<attached-project-files>');
|
|
expect(history[0]?.content).toContain(`path: ${path}`);
|
|
expect(history[0]?.content).toContain('Content preview unavailable');
|
|
}
|
|
});
|
|
|
|
it('uses filename inference when the project file list has not refreshed yet', async () => {
|
|
mockedFetchProjectFilePreview.mockResolvedValue({
|
|
kind: 'pdf',
|
|
title: 'report.pdf',
|
|
sections: [{ title: 'PDF', lines: ['Quarterly results'] }],
|
|
});
|
|
|
|
const history = await historyWithApiAttachmentContext(
|
|
[userMessage('msg-1', 'Read this', [{ path: 'report.pdf', name: 'report.pdf', kind: 'file' }])],
|
|
'msg-1',
|
|
'project-1',
|
|
[],
|
|
);
|
|
|
|
expect(mockedFetchProjectFilePreview).toHaveBeenCalledWith('project-1', 'report.pdf');
|
|
expect(history[0]?.content).toContain('Quarterly results');
|
|
});
|
|
});
|
|
|
|
function userMessage(
|
|
id: string,
|
|
content: string,
|
|
attachments: NonNullable<ChatMessage['attachments']>,
|
|
): ChatMessage {
|
|
return {
|
|
id,
|
|
role: 'user',
|
|
content,
|
|
createdAt: 1,
|
|
attachments,
|
|
};
|
|
}
|
|
|
|
function projectFile(path: string, kind: ProjectFile['kind']): ProjectFile {
|
|
return {
|
|
name: path.split('/').pop() ?? path,
|
|
path,
|
|
type: 'file',
|
|
size: 100,
|
|
mtime: 123,
|
|
kind,
|
|
mime: 'application/octet-stream',
|
|
};
|
|
}
|