moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,340 @@
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');
});
});