| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- import { describe, expect, it } from 'vitest'
- import {
- buildAIMessageUpdatePayload,
- buildPersistedAIMessageContent,
- extractRelatedQuestions,
- hydratePersistedReports,
- normalizeReportsForPersistence,
- shouldClearSummaryForOnlineAnswer,
- splitHtmlIntoTypewriterChunks
- } from './chatHistoryPersistence'
- describe('chatHistoryPersistence', () => {
- it('fills report fields from _fullContent before persistence', () => {
- const reports = [
- {
- file_index: 1,
- status: 'completed',
- report: {
- display_name: '',
- summary: '',
- analysis: '',
- clauses: ''
- },
- _fullContent: {
- display_name: 'bridge-spec.pdf',
- summary: 'full summary',
- analysis: 'full analysis',
- clauses: 'full clauses'
- }
- }
- ]
- expect(normalizeReportsForPersistence(reports)).toEqual([
- expect.objectContaining({
- report: {
- display_name: 'bridge-spec.pdf',
- summary: 'full summary',
- analysis: 'full analysis',
- clauses: 'full clauses'
- }
- })
- ])
- })
- it('repairs persisted reports when history reloads from older incomplete content', () => {
- const reports = [
- {
- file_index: 2,
- status: 'completed',
- report: {
- display_name: '',
- summary: '',
- analysis: '',
- clauses: ''
- },
- _fullContent: {
- display_name: 'concrete-maintenance.pdf',
- summary: 'saved summary',
- analysis: 'saved analysis',
- clauses: ''
- }
- },
- {
- type: 'category_title',
- category: 'National Standards',
- number: 'I',
- count: 1
- }
- ]
- expect(hydratePersistedReports(reports)).toEqual([
- expect.objectContaining({
- report: {
- display_name: 'concrete-maintenance.pdf',
- summary: 'saved summary',
- analysis: 'saved analysis',
- clauses: ''
- }
- }),
- expect.objectContaining({
- type: 'category_title',
- category: 'National Standards'
- })
- ])
- })
- it('builds structured content for professional replies before the stream finishes', () => {
- const content = buildPersistedAIMessageContent({
- reports: [],
- summary: '',
- _fullSummary: 'The assistant identified a professional question and is analyzing the relevant standards.',
- webSearchRaw: null,
- webSearchSummary: null,
- hasWebSearchResults: false,
- content: ''
- })
- expect(JSON.parse(content)).toEqual({
- reports: [],
- webSearchRaw: null,
- webSearchSummary: null,
- hasWebSearchResults: false,
- summary: 'The assistant identified a professional question and is analyzing the relevant standards.'
- })
- })
- it('persists the full summary instead of the partial typewriter text', () => {
- const content = buildPersistedAIMessageContent({
- reports: [],
- summary: 'Short partial summary',
- _fullSummary: 'Long complete summary that should be stored instead of the partial typewriter text.',
- webSearchRaw: null,
- webSearchSummary: null,
- hasWebSearchResults: false,
- content: ''
- })
- expect(JSON.parse(content).summary).toBe(
- 'Long complete summary that should be stored instead of the partial typewriter text.'
- )
- })
- it('keeps thinking content in structured professional payloads', () => {
- const content = buildPersistedAIMessageContent({
- reports: [
- {
- status: 'completed',
- report: { display_name: 'file.pdf', summary: 'overview', analysis: '', clauses: '' }
- }
- ],
- summary: '',
- _fullSummary: 'full overview',
- thinkingContent: 'found the relevant standards and completed the analysis',
- content: ''
- })
- expect(JSON.parse(content).thinkingContent).toBe(
- 'found the relevant standards and completed the analysis'
- )
- })
- it('builds an update payload for completed non-typing messages', () => {
- const payload = buildAIMessageUpdatePayload({
- type: 'ai',
- isTyping: false,
- ai_message_id: 26911,
- reports: [],
- summary: '',
- _fullSummary: 'The construction flow should follow inspection, assembly, trial hoisting, erection, and acceptance.',
- content: ''
- })
- expect(payload).toEqual({
- aiMessageId: 26911,
- content: JSON.stringify({
- reports: [],
- webSearchRaw: null,
- webSearchSummary: null,
- hasWebSearchResults: false,
- summary: 'The construction flow should follow inspection, assembly, trial hoisting, erection, and acceptance.'
- })
- })
- })
- it('does not build an update payload for blank AI content', () => {
- expect(buildAIMessageUpdatePayload({
- type: 'ai',
- isTyping: false,
- ai_message_id: 26911,
- reports: [],
- summary: '',
- content: ''
- })).toBeNull()
- })
- it('returns plain text for direct AI answers without structured report data', () => {
- expect(buildPersistedAIMessageContent({
- reports: [],
- content: 'Hello, I am here.',
- summary: '',
- _fullSummary: ''
- })).toBe('Hello, I am here.')
- })
- it('keeps the intent summary when a professional reply receives an online answer', () => {
- expect(shouldClearSummaryForOnlineAnswer({
- isProfessionalQuestion: true,
- summary: 'The assistant understood the professional scaffold plan question.'
- })).toBe(false)
- })
- it('clears the intent summary for non-professional online answers to avoid duplicate text', () => {
- expect(shouldClearSummaryForOnlineAnswer({
- isProfessionalQuestion: false,
- summary: 'Hello, I can help.'
- })).toBe(true)
- })
- it('extracts related questions from stored JSON payloads', () => {
- expect(
- extractRelatedQuestions('{"questions":["How is it applied on site?","What risks should be checked first?","Which standards are usually cited?"]}')
- ).toEqual([
- 'How is it applied on site?',
- 'What risks should be checked first?',
- 'Which standards are usually cited?'
- ])
- })
- it('extracts related questions from legacy newline text', () => {
- expect(
- extractRelatedQuestions('1. How is it applied on site?\n2. What risks should be checked first?\n3. Which standards are usually cited?')
- ).toEqual([
- 'How is it applied on site?',
- 'What risks should be checked first?',
- 'Which standards are usually cited?'
- ])
- })
- it('supports API payloads that already expose a questions array', () => {
- expect(
- extractRelatedQuestions({
- questions: [
- 'How is it applied on site?',
- 'What risks should be checked first?',
- 'Which standards are usually cited?',
- 'This extra question should be trimmed'
- ]
- })
- ).toEqual([
- 'How is it applied on site?',
- 'What risks should be checked first?',
- 'Which standards are usually cited?'
- ])
- })
- it('filters prompt leakage and placeholder related questions', () => {
- expect(
- extractRelatedQuestions({
- questions: [
- 'Thinking Process:',
- '**Analyze the Request:**',
- '**Role:** Professional question recommendation assistant focused on infrastructure construction technology.',
- 'q1',
- '问题1'
- ]
- })
- ).toEqual([])
- })
- it('splits rendered HTML into safe typewriter chunks', () => {
- expect(
- splitHtmlIntoTypewriterChunks('<p>Hello <strong>world</strong></p>')
- ).toEqual([
- '<p>',
- 'H',
- 'e',
- 'l',
- 'l',
- 'o',
- ' ',
- '<strong>',
- 'w',
- 'o',
- 'r',
- 'l',
- 'd',
- '</strong>',
- '</p>'
- ])
- })
- })
|