import { beforeEach, describe, expect, it, vi } from 'vitest'; import { NOTES_MCP_TOOL_NAMES } from './note-tool-contracts.js'; const { listNotesMock, getNoteMock, createNoteMock, createNoteAgentActionMock } = vi.hoisted(() => ({ listNotesMock: vi.fn(), getNoteMock: vi.fn(), createNoteMock: vi.fn(), createNoteAgentActionMock: vi.fn(), })); vi.mock('../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' })); vi.mock('../modules/notes/repository.js', () => ({ listNotes: listNotesMock, getNote: getNoteMock, createNote: createNoteMock, })); vi.mock('../modules/note-agent-actions/repository.js', () => ({ createNoteAgentAction: createNoteAgentActionMock, })); import { NotesExecutableMcpTools, getNotesExecutableMcpTool } from './note-tools.js'; const req = { id: 'req_1', headers: {}, jwtPayload: { sub: 'user_1', role: 'admin', productId: 'notelett' }, log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, }; describe('note executable MCP tools', () => { beforeEach(() => { vi.clearAllMocks(); }); it('exposes executable tools for each core note contract', () => { expect(NotesExecutableMcpTools.map(tool => tool.name)).toEqual([ NOTES_MCP_TOOL_NAMES.list, NOTES_MCP_TOOL_NAMES.get, NOTES_MCP_TOOL_NAMES.search, NOTES_MCP_TOOL_NAMES.createDraft, NOTES_MCP_TOOL_NAMES.updateNote, NOTES_MCP_TOOL_NAMES.linkNotes, NOTES_MCP_TOOL_NAMES.extractTasks, NOTES_MCP_TOOL_NAMES.attachArtifact, NOTES_MCP_TOOL_NAMES.suggestTags, NOTES_MCP_TOOL_NAMES.checkDuplicates, NOTES_MCP_TOOL_NAMES.suggestLinks, NOTES_MCP_TOOL_NAMES.runPrompt, ]); }); it('lists notes through the repository', async () => { listNotesMock.mockResolvedValue({ items: [ { id: 'note_1', productId: 'notelett', workspaceId: 'ws_1', userId: 'user_1', title: 'Draft', body: 'Body', status: 'draft', tags: ['alpha'], links: [], createdAt: '2026-03-10T00:00:00.000Z', updatedAt: '2026-03-10T00:00:00.000Z', createdBy: 'user_1', updatedBy: 'user_1', }, ], total: 1, }); const tool = getNotesExecutableMcpTool(NOTES_MCP_TOOL_NAMES.list); const result = await tool?.execute( tool.inputSchema.parse({ workspaceId: 'ws_1', limit: 10, offset: 0 }), req ); expect(listNotesMock).toHaveBeenCalledWith('user_1', 'notelett', { workspaceId: 'ws_1', limit: 10, offset: 0, }); expect(result).toMatchObject({ total: 1, limit: 10, offset: 0 }); }); it('searches notes and computes match fields', async () => { listNotesMock.mockResolvedValue({ items: [ { id: 'note_1', productId: 'notelett', workspaceId: 'ws_1', userId: 'user_1', title: 'Retention policy', body: 'Body text', status: 'active', tags: ['compliance'], links: [], createdAt: '2026-03-10T00:00:00.000Z', updatedAt: '2026-03-10T00:00:00.000Z', createdBy: 'user_1', updatedBy: 'user_1', }, ], total: 1, }); const tool = getNotesExecutableMcpTool(NOTES_MCP_TOOL_NAMES.search); const result = await tool?.execute( tool.inputSchema.parse({ workspaceId: 'ws_1', query: 'retention' }), req ); expect(result).toMatchObject({ query: 'retention', items: [{ matchFields: ['title'] }], }); }); it('gets a scoped note', async () => { getNoteMock.mockResolvedValue({ id: 'note_1', productId: 'notelett', workspaceId: 'ws_1', userId: 'user_1', title: 'Note', body: 'Body', status: 'draft', tags: [], links: [], createdAt: '2026-03-10T00:00:00.000Z', updatedAt: '2026-03-10T00:00:00.000Z', createdBy: 'user_1', updatedBy: 'user_1', }); const tool = getNotesExecutableMcpTool(NOTES_MCP_TOOL_NAMES.get); const result = await tool?.execute( tool.inputSchema.parse({ noteId: 'note_1', workspaceId: 'ws_1' }), req ); expect(result).toMatchObject({ id: 'note_1', workspaceId: 'ws_1' }); }); it('returns a dry-run draft without persistence', async () => { const tool = getNotesExecutableMcpTool(NOTES_MCP_TOOL_NAMES.createDraft); const result = await tool?.execute( tool.inputSchema.parse({ workspaceId: 'ws_1', title: 'Draft title', body: 'Draft body', dryRun: true, }), req ); expect(createNoteMock).not.toHaveBeenCalled(); expect(createNoteAgentActionMock).not.toHaveBeenCalled(); expect(result).toMatchObject({ dryRun: true, state: 'proposed' }); }); it('creates a persisted draft and audit action', async () => { createNoteMock.mockImplementation(async (doc: unknown) => doc); createNoteAgentActionMock.mockResolvedValue(undefined); const tool = getNotesExecutableMcpTool(NOTES_MCP_TOOL_NAMES.createDraft); const result = await tool?.execute( tool.inputSchema.parse({ workspaceId: 'ws_1', title: 'Draft title', body: 'Draft body', agentId: 'agent_1', idempotencyKey: 'idem_1', correlationId: 'corr_1', }), req ); expect(createNoteMock).toHaveBeenCalledTimes(1); expect(createNoteAgentActionMock).toHaveBeenCalledTimes(1); expect(createNoteAgentActionMock).toHaveBeenCalledWith( expect.objectContaining({ productId: 'notelett', workspaceId: 'ws_1', userId: 'user_1', actorId: 'agent_1', actorType: 'agent', toolName: NOTES_MCP_TOOL_NAMES.createDraft, actionType: 'create', state: 'proposed', idempotencyKey: 'idem_1', correlationId: 'corr_1', workflowId: 'req_1', }) ); expect(result).toMatchObject({ dryRun: false, state: 'draft', idempotencyKey: 'idem_1', correlationId: 'corr_1', }); }); });