Files
nexu-io--open-design/apps/web/tests/api-attachment-context.test.ts
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

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',
};
}