Skip to content
Merged
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
206 changes: 205 additions & 1 deletion __tests__/integration/mcp-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('MCP Tools Integration', () => {

expect(result).toHaveProperty('tools');
expect(Array.isArray(result.tools)).toBe(true);
expect(result.tools).toHaveLength(11);
expect(result.tools).toHaveLength(28);

const toolNames = result.tools.map((tool: any) => tool.name);
expect(toolNames).toContain('evernote_create_note');
Expand All @@ -96,6 +96,7 @@ describe('MCP Tools Integration', () => {
expect(toolNames).toContain('evernote_create_tag');
expect(toolNames).toContain('evernote_get_user_info');
expect(toolNames).toContain('evernote_health_check');
expect(toolNames).toContain('evernote_patch_note');
});

it('should include proper tool schemas', async () => {
Expand Down Expand Up @@ -929,6 +930,209 @@ describe('MCP Tools Integration', () => {
});
});

describe('Patch Note Operations', () => {
const sampleNoteWithContent = {
...sampleNote,
content: '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>Status: Pending<br/>Task: TODO: Review code<br/>Priority: High</en-note>'
};

beforeEach(() => {
mockNoteStore.getNote.mockResolvedValue(sampleNoteWithContent);
mockNoteStore.updateNote.mockImplementation((note: any) => Promise.resolve(note));
});

it('should patch note with single replacement', async () => {
const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: [
{ find: 'Status: Pending', replace: 'Status: Complete' }
]
}
}
};

const result = await callToolHandler(request);

expect(mockNoteStore.getNote).toHaveBeenCalledWith('note-123', true, true, false, false);
expect(mockNoteStore.updateNote).toHaveBeenCalled();
expect(result.content[0].text).toContain('Note patched successfully');
expect(result.content[0].text).toContain('found 1x');
expect(result.content[0].text).toContain('replaced 1x');
});

it('should patch note with multiple replacements', async () => {
const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: [
{ find: 'Status: Pending', replace: 'Status: Complete' },
{ find: 'TODO:', replace: 'DONE:' }
]
}
}
};

const result = await callToolHandler(request);

expect(result.content[0].text).toContain('Note patched successfully');
});

it('should return warning when no matches found', async () => {
const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: [
{ find: 'Nonexistent text', replace: 'Replacement' }
]
}
}
};

const result = await callToolHandler(request);

expect(result.content[0].text).toContain('Note patch failed');
expect(result.content[0].text).toContain('No matches found');
expect(mockNoteStore.updateNote).not.toHaveBeenCalled();
});

it('should replace only first occurrence when replaceAll is false', async () => {
const noteWithDuplicates = {
...sampleNote,
content: '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>TODO: First task<br/>TODO: Second task</en-note>'
};
mockNoteStore.getNote.mockResolvedValue(noteWithDuplicates);

const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: [
{ find: 'TODO:', replace: 'DONE:', replaceAll: false }
]
}
}
};

const result = await callToolHandler(request);

expect(result.content[0].text).toContain('found 2x');
expect(result.content[0].text).toContain('replaced 1x');
});

it('should reject patch that would result in empty content', async () => {
const noteWithMinimalContent = {
...sampleNote,
content: '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>Only content</en-note>'
};
mockNoteStore.getNote.mockResolvedValue(noteWithMinimalContent);

const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: [
{ find: 'Only content', replace: '' }
]
}
}
};

const result = await callToolHandler(request);

expect(result.content[0].text).toContain('Note patch failed');
expect(result.content[0].text).toContain('empty note content');
expect(mockNoteStore.updateNote).not.toHaveBeenCalled();
});

it('should throw error when no replacements provided', async () => {
const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: []
}
}
};

await expect(callToolHandler(request)).rejects.toThrow('At least one replacement must be provided');
});

it('should throw error when find string is empty', async () => {
const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: [
{ find: '', replace: 'replacement' }
]
}
}
};

await expect(callToolHandler(request)).rejects.toThrow('Each replacement must have a non-empty "find" string');
});

it('should preserve existing resources after patch', async () => {
const noteWithResources = {
...sampleNote,
content: '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>Status: Pending</en-note>',
resources: [sampleResource]
};
mockNoteStore.getNote.mockResolvedValue(noteWithResources);
mockNoteStore.updateNote.mockImplementation((note: any) => {
// Verify resources are preserved
expect(note.resources).toBeDefined();
expect(note.resources.length).toBeGreaterThan(0);
return Promise.resolve(note);
});

const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'note-123',
replacements: [
{ find: 'Status: Pending', replace: 'Status: Complete' }
]
}
}
};

const result = await callToolHandler(request);

expect(result.content[0].text).toContain('Note patched successfully');
});

it('should handle note not found error', async () => {
mockNoteStore.getNote.mockRejectedValue(new Error('Note not found'));

const request = {
params: {
name: 'evernote_patch_note',
arguments: {
guid: 'nonexistent-guid',
replacements: [
{ find: 'test', replace: 'replacement' }
]
}
}
};

await expect(callToolHandler(request)).rejects.toThrow('Note not found');
});
});

describe('Error Handling', () => {
it('should handle unknown tool error', async () => {
const request = {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/unit/search-preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ function enmlToPlainText(enmlContent: string): string {
// Remove all remaining HTML/XML tags
text = text.replace(/<[^>]+>/g, '');

// Decode common HTML entities
// Decode common HTML entities (decode &amp; LAST to avoid double-unescaping)
text = text.replace(/&nbsp;/gi, ' ');
text = text.replace(/&amp;/gi, '&');
text = text.replace(/&lt;/gi, '<');
text = text.replace(/&gt;/gi, '>');
text = text.replace(/&quot;/gi, '"');
text = text.replace(/&#39;/gi, "'");
text = text.replace(/&amp;/gi, '&');

// Normalize whitespace
text = text.replace(/\n\s*\n/g, '\n');
Expand Down
Loading
Loading