Add hybrid research copilot workspace
This commit is contained in:
219
lib/server/api/research-copilot.e2e.test.ts
Normal file
219
lib/server/api/research-copilot.e2e.test.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user