341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { parseChatGPTExport, parseClaudeExport, extractKnowledge, processImport } from '../src/memory-import';
|
|
|
|
describe('ChatGPT Export Parser', () => {
|
|
it('parses conversations with mapping structure', () => {
|
|
const data = [
|
|
{
|
|
title: 'Test Chat',
|
|
create_time: 1709000000,
|
|
mapping: {
|
|
'node1': {
|
|
message: {
|
|
author: { role: 'user' },
|
|
content: { parts: ['Hello, I need help with React'] },
|
|
create_time: 1709000001,
|
|
},
|
|
},
|
|
'node2': {
|
|
message: {
|
|
author: { role: 'assistant' },
|
|
content: { parts: ['Sure, I can help with React!'] },
|
|
create_time: 1709000002,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const result = parseChatGPTExport(data);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].title).toBe('Test Chat');
|
|
expect(result[0].messages).toHaveLength(2);
|
|
expect(result[0].messages[0].role).toBe('user');
|
|
expect(result[0].messages[0].text).toBe('Hello, I need help with React');
|
|
expect(result[0].source).toBe('chatgpt');
|
|
});
|
|
|
|
it('skips system messages', () => {
|
|
const data = [
|
|
{
|
|
title: 'Test',
|
|
mapping: {
|
|
'sys': { message: { author: { role: 'system' }, content: { parts: ['System prompt'] } } },
|
|
'user': { message: { author: { role: 'user' }, content: { parts: ['Hi'] }, create_time: 1 } },
|
|
},
|
|
},
|
|
];
|
|
const result = parseChatGPTExport(data);
|
|
expect(result[0].messages).toHaveLength(1);
|
|
expect(result[0].messages[0].role).toBe('user');
|
|
});
|
|
|
|
it('handles empty/missing conversations gracefully', () => {
|
|
expect(parseChatGPTExport(null)).toEqual([]);
|
|
expect(parseChatGPTExport({})).toEqual([]);
|
|
expect(parseChatGPTExport([])).toEqual([]);
|
|
});
|
|
|
|
it('sorts messages chronologically by create_time', () => {
|
|
const data = [
|
|
{
|
|
title: 'Order Test',
|
|
mapping: {
|
|
'late': {
|
|
message: {
|
|
author: { role: 'user' },
|
|
content: { parts: ['Second message'] },
|
|
create_time: 200,
|
|
},
|
|
},
|
|
'early': {
|
|
message: {
|
|
author: { role: 'user' },
|
|
content: { parts: ['First message'] },
|
|
create_time: 100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
const result = parseChatGPTExport(data);
|
|
expect(result[0].messages[0].text).toBe('First message');
|
|
expect(result[0].messages[1].text).toBe('Second message');
|
|
});
|
|
|
|
it('filters out conversations with no messages', () => {
|
|
const data = [
|
|
{ title: 'Empty', mapping: {} },
|
|
{
|
|
title: 'Has Messages',
|
|
mapping: {
|
|
'n1': { message: { author: { role: 'user' }, content: { parts: ['Hello'] }, create_time: 1 } },
|
|
},
|
|
},
|
|
];
|
|
const result = parseChatGPTExport(data);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].title).toBe('Has Messages');
|
|
});
|
|
|
|
it('handles wrapped format with conversations key', () => {
|
|
const data = {
|
|
conversations: [
|
|
{
|
|
title: 'Wrapped',
|
|
mapping: {
|
|
'n1': { message: { author: { role: 'user' }, content: { parts: ['Test'] }, create_time: 1 } },
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const result = parseChatGPTExport(data);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].title).toBe('Wrapped');
|
|
});
|
|
});
|
|
|
|
describe('Claude Export Parser', () => {
|
|
it('parses conversations with chat_messages', () => {
|
|
const data = [
|
|
{
|
|
name: 'Claude Chat',
|
|
created_at: '2024-03-01T10:00:00Z',
|
|
chat_messages: [
|
|
{ sender: 'human', text: 'What is TypeScript?', created_at: '2024-03-01T10:00:01Z' },
|
|
{ sender: 'assistant', text: 'TypeScript is a superset of JavaScript.', created_at: '2024-03-01T10:00:05Z' },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = parseClaudeExport(data);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].title).toBe('Claude Chat');
|
|
expect(result[0].messages).toHaveLength(2);
|
|
expect(result[0].messages[0].role).toBe('user');
|
|
expect(result[0].source).toBe('claude');
|
|
});
|
|
|
|
it('handles empty exports gracefully', () => {
|
|
expect(parseClaudeExport(null)).toEqual([]);
|
|
expect(parseClaudeExport([])).toEqual([]);
|
|
});
|
|
|
|
it('handles alternative field names (title, messages, role, content)', () => {
|
|
const data = [
|
|
{
|
|
title: 'Alt Format',
|
|
messages: [
|
|
{ role: 'user', content: 'Hello from alt format', timestamp: '2024-03-01T10:00:00Z' },
|
|
],
|
|
},
|
|
];
|
|
const result = parseClaudeExport(data);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].title).toBe('Alt Format');
|
|
expect(result[0].messages[0].text).toBe('Hello from alt format');
|
|
});
|
|
});
|
|
|
|
describe('Knowledge Extraction', () => {
|
|
it('extracts decisions from user messages', () => {
|
|
const convs = [{
|
|
title: 'Tech Discussion',
|
|
messages: [
|
|
{ role: 'user' as const, text: 'I decided to use React for the frontend and Node.js for the backend' },
|
|
{ role: 'assistant' as const, text: 'Great choice!' },
|
|
],
|
|
source: 'chatgpt' as const,
|
|
}];
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
const decisions = knowledge.filter(k => k.type === 'decision');
|
|
expect(decisions.length).toBeGreaterThan(0);
|
|
expect(decisions[0].content).toContain('decided');
|
|
expect(decisions[0].importance).toBe('important');
|
|
});
|
|
|
|
it('extracts preferences from user messages', () => {
|
|
const convs = [{
|
|
title: 'Preferences',
|
|
messages: [
|
|
{ role: 'user' as const, text: 'I prefer bullet-point summaries over long paragraphs' },
|
|
],
|
|
source: 'claude' as const,
|
|
}];
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
const prefs = knowledge.filter(k => k.type === 'preference');
|
|
expect(prefs.length).toBeGreaterThan(0);
|
|
expect(prefs[0].content).toContain('prefer');
|
|
expect(prefs[0].importance).toBe('important');
|
|
});
|
|
|
|
it('extracts facts about the user', () => {
|
|
const convs = [{
|
|
title: 'About me',
|
|
messages: [
|
|
{ role: 'user' as const, text: 'I work at Egzakta Advisory as a partner and consultant' },
|
|
],
|
|
source: 'chatgpt' as const,
|
|
}];
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
const facts = knowledge.filter(k => k.type === 'fact');
|
|
expect(facts.length).toBeGreaterThan(0);
|
|
expect(facts[0].content).toContain('Egzakta');
|
|
});
|
|
|
|
it('extracts conversation topics', () => {
|
|
const convs = [{
|
|
title: 'Building a SaaS Platform',
|
|
messages: [
|
|
{ role: 'user' as const, text: 'Help me plan the architecture' },
|
|
],
|
|
source: 'chatgpt' as const,
|
|
}];
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
const topics = knowledge.filter(k => k.type === 'topic');
|
|
expect(topics.length).toBeGreaterThan(0);
|
|
expect(topics[0].content).toContain('Building a SaaS Platform');
|
|
});
|
|
|
|
it('caps extraction at 100 items', () => {
|
|
const convs = Array.from({ length: 200 }, (_, i) => ({
|
|
title: `Conversation ${i}`,
|
|
messages: [
|
|
{ role: 'user' as const, text: `I decided to use approach ${i} for the implementation` },
|
|
],
|
|
source: 'chatgpt' as const,
|
|
}));
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
expect(knowledge.length).toBeLessThanOrEqual(100);
|
|
});
|
|
|
|
it('deduplicates similar content', () => {
|
|
const convs = [
|
|
{
|
|
title: 'Chat 1',
|
|
messages: [{ role: 'user' as const, text: 'I prefer TypeScript over JavaScript for type safety' }],
|
|
source: 'chatgpt' as const,
|
|
},
|
|
{
|
|
title: 'Chat 2',
|
|
messages: [{ role: 'user' as const, text: 'I prefer TypeScript over JavaScript for type safety' }],
|
|
source: 'chatgpt' as const,
|
|
},
|
|
];
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
const prefs = knowledge.filter(k => k.type === 'preference');
|
|
expect(prefs.length).toBe(1); // Deduped
|
|
});
|
|
|
|
it('skips very short and very long messages', () => {
|
|
const convs = [{
|
|
title: 'Test',
|
|
messages: [
|
|
{ role: 'user' as const, text: 'Hi' }, // Too short
|
|
{ role: 'user' as const, text: 'x'.repeat(600) }, // Too long
|
|
{ role: 'user' as const, text: 'I decided to use Python for data analysis tasks' }, // Just right
|
|
],
|
|
source: 'chatgpt' as const,
|
|
}];
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
const decisions = knowledge.filter(k => k.type === 'decision');
|
|
expect(decisions.length).toBe(1);
|
|
});
|
|
|
|
it('skips assistant messages', () => {
|
|
const convs = [{
|
|
title: 'Test',
|
|
messages: [
|
|
{ role: 'assistant' as const, text: 'I decided to use a different approach for this solution' },
|
|
],
|
|
source: 'chatgpt' as const,
|
|
}];
|
|
|
|
const knowledge = extractKnowledge(convs);
|
|
const decisions = knowledge.filter(k => k.type === 'decision');
|
|
expect(decisions.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('processImport (end-to-end)', () => {
|
|
it('processes a ChatGPT export end-to-end', () => {
|
|
const data = [
|
|
{
|
|
title: 'Project Planning',
|
|
create_time: 1709000000,
|
|
mapping: {
|
|
'n1': { message: { author: { role: 'user' }, content: { parts: ['I decided to use React with TypeScript for the frontend'] }, create_time: 1 } },
|
|
'n2': { message: { author: { role: 'assistant' }, content: { parts: ['Great choice!'] }, create_time: 2 } },
|
|
'n3': { message: { author: { role: 'user' }, content: { parts: ['I prefer concise responses without filler words'] }, create_time: 3 } },
|
|
},
|
|
},
|
|
];
|
|
|
|
const result = processImport(data, 'chatgpt');
|
|
expect(result.source).toBe('chatgpt');
|
|
expect(result.conversationsFound).toBe(1);
|
|
expect(result.conversationsParsed).toBe(1);
|
|
expect(result.knowledgeExtracted.length).toBeGreaterThan(0);
|
|
expect(result.errors).toHaveLength(0);
|
|
|
|
const decisions = result.knowledgeExtracted.filter(k => k.type === 'decision');
|
|
expect(decisions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('processes a Claude export end-to-end', () => {
|
|
const data = [
|
|
{
|
|
name: 'Architecture Discussion',
|
|
created_at: '2024-03-01T10:00:00Z',
|
|
chat_messages: [
|
|
{ sender: 'human', text: 'I work at Acme Corp as a senior engineer', created_at: '2024-03-01T10:00:01Z' },
|
|
{ sender: 'assistant', text: 'Nice to meet you!', created_at: '2024-03-01T10:00:05Z' },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = processImport(data, 'claude');
|
|
expect(result.source).toBe('claude');
|
|
expect(result.conversationsFound).toBe(1);
|
|
expect(result.knowledgeExtracted.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('returns errors for invalid data', () => {
|
|
const result = processImport('not json', 'chatgpt');
|
|
expect(result.conversationsFound).toBe(0);
|
|
expect(result.knowledgeExtracted).toHaveLength(0);
|
|
});
|
|
|
|
it('reports when no conversations found', () => {
|
|
const result = processImport([], 'chatgpt');
|
|
expect(result.errors).toContain('No conversations found in export');
|
|
});
|
|
});
|