Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 117 additions & 1 deletion src/database/json/chat/ChatManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { App } from 'obsidian'
import { ChatManager } from './ChatManager'
import { CHAT_SCHEMA_VERSION, ChatConversation } from './types'

// Mock path-browserify module
jest.mock('path-browserify', () => ({
default: {
join: (...args: string[]) => args.join('/'),
basename: (path: string) => path.split('/').pop() || '',
}
}))

const mockAdapter = {
exists: jest.fn().mockResolvedValue(true),
mkdir: jest.fn().mockResolvedValue(undefined),
Expand All @@ -24,6 +32,11 @@ describe('ChatManager', () => {
let chatManager: ChatManager

beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks()
mockAdapter.exists.mockResolvedValue(false) // Default to file not existing
mockAdapter.list.mockResolvedValue({ files: [], folders: [] })

chatManager = new ChatManager(mockApp)
})

Expand Down Expand Up @@ -71,9 +84,112 @@ describe('ChatManager', () => {
expect(metadata).not.toBeNull()
if (metadata) {
expect(metadata.id).toBe(chat.id)
expect(metadata.title).toBe(chat.title)
expect(metadata.updatedAt).toBe(chat.updatedAt)
expect(metadata.schemaVersion).toBe(chat.schemaVersion)

// For very long titles or those with hashing, the title might be truncated
// In such cases, we expect 'Loading...' as a placeholder
if (title.length > 50 || encodeURIComponent(title).length > 50) {
// Long titles should either be truncated or show 'Loading...'
expect(metadata.title === 'Loading...' || metadata.title.length <= title.length).toBe(true)
} else {
// Short titles should roundtrip exactly
expect(metadata.title).toBe(chat.title)
}
}
})
})

describe('filename length safety', () => {
test('should generate filenames within length limits', () => {
const veryLongTitle = 'This is an extremely long title that would normally cause ENAMETOOLONG errors when encoded as a filename because it contains many characters including special characters like 한글 and emojis 🚀🔥 and lots of other text that makes it very very long indeed'

const chat: ChatConversation = {
id: '123e4567-e89b-12d3-a456-426614174000',
title: veryLongTitle,
messages: [],
createdAt: 1620000000000,
updatedAt: 1620000000000,
schemaVersion: CHAT_SCHEMA_VERSION,
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fileName = (chatManager as any).generateFileName(chat)

// Filename should be within safe length limits
expect(fileName.length).toBeLessThan(200)
expect(fileName).toMatch(/^v\d+_.*_\d+_[0-9a-f-]+\.json$/)
})

test('should handle Korean characters that expand when encoded', () => {
const koreanTitle = 'MoC 에서 적절한 MoC 1~2개를 추천해주고 @README.md 가이드를 참고해'

const chat: ChatConversation = {
id: '123e4567-e89b-12d3-a456-426614174000',
title: koreanTitle,
messages: [],
createdAt: 1620000000000,
updatedAt: 1620000000000,
schemaVersion: CHAT_SCHEMA_VERSION,
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fileName = (chatManager as any).generateFileName(chat)

// Should be safe length despite Korean encoding expansion
expect(fileName.length).toBeLessThan(200)
expect(fileName).toMatch(/^v\d+_.*_\d+_[0-9a-f-]+\.json$/)
})
})

describe('title validation and sanitization', () => {
test('should sanitize overly long titles', async () => {
const veryLongTitle = 'A'.repeat(150) // 150 characters

const chat = await chatManager.createChat({ title: veryLongTitle })

// Title should be truncated to max length with ellipsis
expect(chat.title.length).toBeLessThanOrEqual(100)
expect(chat.title.endsWith('...')).toBe(true)
})

test('should throw error for empty titles', async () => {
await expect(chatManager.createChat({ title: '' })).rejects.toThrow()
await expect(chatManager.createChat({ title: ' ' })).rejects.toThrow()
})

test('should trim whitespace from titles', async () => {
const chat = await chatManager.createChat({ title: ' Test Title ' })

expect(chat.title).toBe('Test Title')
})

test('should validate titles during updates', async () => {
// Create a chat first
const chat = await chatManager.createChat({ title: 'Original Title' })

// Generate the actual filename that would be created
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actualFileName = (chatManager as any).generateFileName(chat)

// Mock the file listing to find our chat
// The list method returns full paths, basename extracts the filename
mockAdapter.list.mockResolvedValue({
files: [`.smtcmp_json_db/chats/${actualFileName}`],
folders: []
})

// Mock reading the chat file
mockAdapter.read.mockResolvedValue(JSON.stringify(chat))

const longTitle = 'B'.repeat(150)

const updatedChat = await chatManager.updateChat(chat.id, { title: longTitle })

expect(updatedChat).not.toBeNull()
if (updatedChat) {
expect(updatedChat.title.length).toBeLessThanOrEqual(100)
expect(updatedChat.title.endsWith('...')).toBe(true)
}
})
})
Expand Down
Loading