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
17 changes: 17 additions & 0 deletions __tests__/unit/dotenv-quiet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';

const runtimeEntrypoints = [
['MCP stdio server', '../../src/index.ts'],
['standalone auth CLI', '../../src/auth-standalone.ts'],
['Claude install helper', '../../scripts/install-to-claude.js'],
] as const;

describe('dotenv runtime logging', () => {
it.each(runtimeEntrypoints)('%s loads dotenv quietly', (_name, relativePath) => {
const source = readFileSync(resolve(__dirname, relativePath), 'utf-8');

expect(source).toContain('config({ quiet: true })');
expect(source).not.toMatch(/\bconfig\(\s*\)/);
});
});
104 changes: 104 additions & 0 deletions __tests__/unit/notebook-tool-descriptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, expect, it, jest } from '@jest/globals';
import {
enrichToolsWithNotebookDescriptions,
resolveNotebookCacheForToolDescriptions,
} from '../../src/notebook-tool-descriptions';

const baseTools = [
{
name: 'evernote_create_note',
description: 'Create a note',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Title' },
notebookName: { type: 'string', description: 'Name of notebook' },
},
},
},
{
name: 'evernote_update_note',
description: 'Update a note',
inputSchema: {
type: 'object',
properties: {
guid: { type: 'string', description: 'GUID' },
notebookName: { type: 'string', description: 'Move note to this notebook' },
},
},
},
{
name: 'evernote_search_notes',
description: 'Search notes',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Query' },
},
},
},
];

const notebooks = [
{ guid: 'personal-guid', name: 'Personal', defaultNotebook: true },
{ guid: 'work-guid', name: 'Work' },
];

describe('notebook-aware tool descriptions', () => {
it('loads notebooks through ensureAPI when tool discovery happens before any tool call', async () => {
const api = {};
const ensureAPI = jest.fn<() => Promise<object>>().mockResolvedValue(api);
const refreshNotebookCache = jest
.fn<(evernoteApi: object) => Promise<typeof notebooks>>()
.mockResolvedValue(notebooks);

const result = await resolveNotebookCacheForToolDescriptions({
currentCache: null,
currentApi: null,
ensureAPI,
refreshNotebookCache,
logError: jest.fn(),
});

expect(ensureAPI).toHaveBeenCalledTimes(1);
expect(refreshNotebookCache).toHaveBeenCalledWith(api);
expect(result).toEqual(notebooks);
});

it('keeps tool discovery available when API initialization fails', async () => {
const ensureAPI = jest.fn<() => Promise<object>>().mockRejectedValue(new Error('auth failed'));
const refreshNotebookCache = jest.fn<(evernoteApi: object) => Promise<typeof notebooks>>();

const result = await resolveNotebookCacheForToolDescriptions({
currentCache: null,
currentApi: null,
ensureAPI,
refreshNotebookCache,
logError: jest.fn(),
});

expect(result).toBeNull();
expect(refreshNotebookCache).not.toHaveBeenCalled();
});

it('injects live notebook names into create and update tool schemas', () => {
const enriched = enrichToolsWithNotebookDescriptions(baseTools as any, notebooks);
const createNoteTool = enriched.find((tool: any) => tool.name === 'evernote_create_note')!;
const updateNoteTool = enriched.find((tool: any) => tool.name === 'evernote_update_note')!;
const searchTool = enriched.find((tool: any) => tool.name === 'evernote_search_notes')!;
const createNotebookDescription = (createNoteTool.inputSchema as any).properties.notebookName.description;
const updateNotebookDescription = (updateNoteTool.inputSchema as any).properties.notebookName.description;

expect(createNotebookDescription).toContain(
'Available notebooks: "Personal", "Work".',
);
expect(createNotebookDescription).toContain(
'Default: "Personal".',
);
expect(createNotebookDescription).toContain(
'if creation fails, the note will use the default notebook.',
);
expect(updateNotebookDescription).toBe(createNotebookDescription);
expect((searchTool.inputSchema as any).properties).not.toHaveProperty('notebookName');
});
});
9 changes: 9 additions & 0 deletions __tests__/unit/tool-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import {
CreateNoteSchema,
SearchNotesSchema,
UpdateNoteSchema,
DeleteNoteSchema,
PatchNoteSchema,
GetNotebookSchema,
Expand Down Expand Up @@ -61,6 +62,14 @@ describe('tool schemas (M1)', () => {
});
});

describe('UpdateNoteSchema', () => {
it('rejects empty notebookName', () => {
expect(() =>
UpdateNoteSchema.parse({ guid: 'abc-123', notebookName: '' }),
).toThrow(/Notebook name cannot be empty/);
});
});

describe('DeleteNoteSchema', () => {
it('rejects missing guid', () => {
expect(() => DeleteNoteSchema.parse({})).toThrow();
Expand Down
4 changes: 2 additions & 2 deletions scripts/install-to-claude.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { detectEnvironment, getRecommendedSetup } from './detect-environment.js'
import { config } from 'dotenv';

// Load environment variables
config();
config({ quiet: true });

const rl = readline.createInterface({
input: process.stdin,
Expand Down Expand Up @@ -203,4 +203,4 @@ process.on('SIGINT', () => {
main().catch(error => {
console.error('Error:', error);
process.exit(1);
});
});
2 changes: 1 addition & 1 deletion src/auth-standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as readline from 'readline/promises';
import { stdin, stdout } from 'process';

// Load environment variables
config();
config({ quiet: true });

const tokenFile = path.join(process.cwd(), '.evernote-token.json');

Expand Down
Loading
Loading