Add hybrid research copilot workspace
This commit is contained in:
69
lib/server/research-copilot-format.test.ts
Normal file
69
lib/server/research-copilot-format.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type { SearchResult } from '@/lib/types';
|
||||
import {
|
||||
extractJsonObject,
|
||||
parseCopilotResponse
|
||||
} from '@/lib/server/research-copilot-format';
|
||||
|
||||
function result(overrides: Partial<SearchResult> = {}): SearchResult {
|
||||
return {
|
||||
chunkId: 1,
|
||||
documentId: 1,
|
||||
source: 'filings',
|
||||
sourceKind: 'filing_brief',
|
||||
sourceRef: '0001',
|
||||
title: '10-K brief',
|
||||
ticker: 'NVDA',
|
||||
accessionNumber: '0001',
|
||||
filingDate: '2026-02-18',
|
||||
citationLabel: 'NVDA · 0001 [1]',
|
||||
headingPath: null,
|
||||
chunkText: 'Demand stayed strong and margins expanded.',
|
||||
snippet: 'Demand stayed strong and margins expanded.',
|
||||
score: 0.9,
|
||||
vectorRank: 1,
|
||||
lexicalRank: 1,
|
||||
href: '/analysis/reports/NVDA/0001',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('research copilot format helpers', () => {
|
||||
it('parses strict json responses with suggested actions', () => {
|
||||
const parsed = parseCopilotResponse(JSON.stringify({
|
||||
answerMarkdown: 'Demand stayed strong [1]. The setup still looks constructive [2].',
|
||||
followUps: ['What disconfirms the bull case?', 'Which risks changed most?'],
|
||||
suggestedActions: [{
|
||||
type: 'draft_memo_section',
|
||||
label: 'Use as thesis draft',
|
||||
section: 'thesis',
|
||||
contentMarkdown: 'Maintain a constructive stance while monitoring concentration.',
|
||||
citationIndexes: [1, 2]
|
||||
}]
|
||||
}), [result(), result({ chunkId: 2, citationLabel: 'NVDA · 0002 [2]', sourceRef: '0002' })], 'What changed?', 'thesis');
|
||||
|
||||
expect(parsed.citationIndexes).toEqual([1, 2]);
|
||||
expect(parsed.followUps).toHaveLength(2);
|
||||
expect(parsed.suggestedActions[0]?.type).toBe('draft_memo_section');
|
||||
expect(parsed.suggestedActions[0]?.section).toBe('thesis');
|
||||
});
|
||||
|
||||
it('falls back to plain text and default actions when json parsing fails', () => {
|
||||
const parsed = parseCopilotResponse(
|
||||
'Plain text answer without json wrapper',
|
||||
[result(), result({ chunkId: 2, citationLabel: 'NVDA · 0002 [2]', sourceRef: '0002' })],
|
||||
'Summarize the setup',
|
||||
null
|
||||
);
|
||||
|
||||
expect(parsed.answerMarkdown).toContain('Plain text answer');
|
||||
expect(parsed.citationIndexes).toEqual([1, 2]);
|
||||
expect(parsed.suggestedActions.some((action) => action.type === 'draft_note')).toBe(true);
|
||||
expect(parsed.suggestedActions.some((action) => action.type === 'queue_research_brief')).toBe(true);
|
||||
});
|
||||
|
||||
it('extracts the first json object from fenced responses', () => {
|
||||
const extracted = extractJsonObject('```json\n{"answerMarkdown":"A [1]","followUps":[],"suggestedActions":[]}\n```');
|
||||
expect(extracted).toBe('{"answerMarkdown":"A [1]","followUps":[],"suggestedActions":[]}');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user