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

70 lines
2.7 KiB
TypeScript

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":[]}');
});
});