Files
Neon-Desk/lib/server/api/research-copilot.e2e.test.ts

220 lines
5.9 KiB
TypeScript

import { beforeAll, describe, expect, it, mock } from 'bun:test';
const TEST_USER_ID = 'copilot-api-user';
const mockGetSession = mock(async () => ({
id: 1,
user_id: TEST_USER_ID,
ticker: 'NVDA',
title: 'NVDA copilot',
selected_sources: ['documents', 'filings', 'research'],
pinned_artifact_ids: [],
created_at: '2026-03-14T00:00:00.000Z',
updated_at: '2026-03-14T00:00:00.000Z',
messages: []
}));
const mockRunTurn = mock(async () => ({
session: {
id: 1,
user_id: TEST_USER_ID,
ticker: 'NVDA',
title: 'NVDA copilot',
selected_sources: ['filings'],
pinned_artifact_ids: [4],
created_at: '2026-03-14T00:00:00.000Z',
updated_at: '2026-03-14T00:00:01.000Z',
messages: []
},
user_message: {
id: 1,
session_id: 1,
user_id: TEST_USER_ID,
role: 'user',
content_markdown: 'What changed?',
citations: [],
follow_ups: [],
suggested_actions: [],
selected_sources: ['filings'],
pinned_artifact_ids: [4],
memo_section: 'thesis',
created_at: '2026-03-14T00:00:00.000Z'
},
assistant_message: {
id: 2,
session_id: 1,
user_id: TEST_USER_ID,
role: 'assistant',
content_markdown: 'Demand stayed strong [1].',
citations: [{
index: 1,
label: 'NVDA · 0001 [1]',
chunkId: 1,
href: '/analysis/reports/NVDA/0001',
source: 'filings',
sourceKind: 'filing_brief',
sourceRef: '0001',
title: '10-K brief',
ticker: 'NVDA',
accessionNumber: '0001',
filingDate: '2026-02-18',
excerpt: 'Demand stayed strong.',
artifactId: 5
}],
follow_ups: ['What changed in risks?'],
suggested_actions: [],
selected_sources: ['filings'],
pinned_artifact_ids: [4],
memo_section: 'thesis',
created_at: '2026-03-14T00:00:01.000Z'
},
results: []
}));
const mockGenerateBrief = mock(async () => ({
provider: 'test',
model: 'test-model',
bodyMarkdown: '# NVDA brief\n\nDemand held up.',
evidence: []
}));
const mockFindInFlightTask = mock(async () => null);
const mockEnqueueTask = mock(async () => ({
id: 'task-1',
user_id: TEST_USER_ID,
task_type: 'research_brief',
status: 'queued',
stage: 'queued',
stage_detail: 'Queued',
stage_context: null,
resource_key: 'research_brief:NVDA:update the thesis',
notification_read_at: null,
notification_silenced_at: null,
priority: 55,
payload: {
ticker: 'NVDA',
query: 'Update the thesis',
sources: ['filings']
},
result: null,
error: null,
attempts: 0,
max_attempts: 3,
workflow_run_id: 'run-1',
created_at: '2026-03-14T00:00:00.000Z',
updated_at: '2026-03-14T00:00:00.000Z',
finished_at: null
}));
function registerMocks() {
mock.module('@/lib/server/auth-session', () => ({
requireAuthenticatedSession: async () => ({
session: {
user: {
id: TEST_USER_ID,
email: 'copilot@example.com',
name: 'Copilot API User',
image: null
}
},
response: null
})
}));
mock.module('@/lib/server/repos/research-copilot', () => ({
getResearchCopilotSessionByTicker: mockGetSession
}));
mock.module('@/lib/server/research-copilot', () => ({
runResearchCopilotTurn: mockRunTurn,
generateResearchBrief: mockGenerateBrief
}));
mock.module('@/lib/server/tasks', () => ({
enqueueTask: mockEnqueueTask,
findInFlightTask: mockFindInFlightTask,
getTaskById: mock(async () => null),
getTaskQueueSnapshot: mock(async () => ({ items: [], stats: { queued: 0, running: 0, failed: 0 } })),
getTaskTimeline: mock(async () => []),
listRecentTasks: mock(async () => []),
updateTaskNotification: mock(async () => null)
}));
}
describe('research copilot api', () => {
let app: { handle: (request: Request) => Promise<Response> };
beforeAll(async () => {
mock.restore();
registerMocks();
({ app } = await import('./app'));
});
it('returns the ticker-scoped session payload', async () => {
const response = await app.handle(new Request('http://localhost/api/research/copilot/session?ticker=nvda'));
expect(response.status).toBe(200);
const payload = await response.json() as { session: { ticker: string } };
expect(payload.session.ticker).toBe('NVDA');
expect(mockGetSession).toHaveBeenCalled();
});
it('returns turn responses with assistant citations', async () => {
const response = await app.handle(new Request('http://localhost/api/research/copilot/turn', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
ticker: 'nvda',
query: 'What changed?',
sources: ['filings'],
pinnedArtifactIds: [4],
memoSection: 'thesis'
})
}));
expect(response.status).toBe(200);
const payload = await response.json() as {
assistant_message: {
citations: Array<{ artifactId: number | null }>;
};
};
expect(payload.assistant_message.citations[0]?.artifactId).toBe(5);
expect(mockRunTurn).toHaveBeenCalled();
});
it('queues research brief jobs with normalized ticker payloads', async () => {
const response = await app.handle(new Request('http://localhost/api/research/copilot/job', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
ticker: 'nvda',
query: 'Update the thesis',
sources: ['filings']
})
}));
expect(response.status).toBe(200);
const payload = await response.json() as {
task: {
task_type: string;
payload: {
ticker: string;
};
};
};
expect(payload.task.task_type).toBe('research_brief');
expect(payload.task.payload.ticker).toBe('NVDA');
expect(mockFindInFlightTask).toHaveBeenCalledWith(
TEST_USER_ID,
'research_brief',
'research_brief:NVDA:update the thesis'
);
expect(mockEnqueueTask).toHaveBeenCalled();
});
});