import { describe, it, expect, beforeEach, vi } from 'vitest'; vi.mock('../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' })); vi.mock('../lib/telemetry.js', () => ({ trackEvent: vi.fn() })); vi.mock('../lib/feature-flags.js', () => ({ isFeatureEnabled: vi.fn(() => true) })); import { resetMemoryDatastore, TEST_USER_ID, TEST_PRODUCT_ID } from '../test-helpers.js'; import { ensureWing, ensureRoom, storeMemory, addTriple } from '../modules/palace/repository.js'; import { PalaceMcpToolDefinitions, PALACE_MCP_TOOL_NAMES } from './palace-tools.js'; import type { NotesMcpRequest } from './note-tools.js'; const USER_A = TEST_USER_ID; const PRODUCT = TEST_PRODUCT_ID; const mockReq: NotesMcpRequest = { id: 'req-1', headers: { authorization: 'Bearer test-token' }, jwtPayload: { sub: USER_A, role: 'admin', productId: PRODUCT }, log: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {}, }, }; function getTool(name: string) { const tool = PalaceMcpToolDefinitions.find(t => t.name === name); if (!tool) throw new Error(`Tool ${name} not found`); return tool; } describe('Palace MCP Tools (N6)', () => { beforeEach(() => { resetMemoryDatastore(); }); it('all 6 palace MCP tools are registered', () => { expect(PalaceMcpToolDefinitions.length).toBe(6); const names = PalaceMcpToolDefinitions.map(t => t.name); expect(names).toContain(PALACE_MCP_TOOL_NAMES.search); expect(names).toContain(PALACE_MCP_TOOL_NAMES.store); expect(names).toContain(PALACE_MCP_TOOL_NAMES.wakeUp); expect(names).toContain(PALACE_MCP_TOOL_NAMES.queryEntity); expect(names).toContain(PALACE_MCP_TOOL_NAMES.timeline); expect(names).toContain(PALACE_MCP_TOOL_NAMES.listWings); }); it('mempalace_search returns ranked results', async () => { const wing = await ensureWing(USER_A, PRODUCT, 'ws-1', 'Work'); const room = await ensureRoom(USER_A, PRODUCT, wing.id, 'auth'); await storeMemory(USER_A, PRODUCT, wing.id, room.id, 'decisions', 'Use JWT for auth'); await storeMemory(USER_A, PRODUCT, wing.id, room.id, 'events', 'Deployed v2'); const tool = getTool(PALACE_MCP_TOOL_NAMES.search); const result = await tool.execute({ query: 'JWT', limit: 10 }, mockReq) as unknown[]; expect(result.length).toBe(1); }); it('mempalace_store persists and is searchable', async () => { const wing = await ensureWing(USER_A, PRODUCT, 'ws-1', 'Work'); const room = await ensureRoom(USER_A, PRODUCT, wing.id, 'auth'); const storeTool = getTool(PALACE_MCP_TOOL_NAMES.store); const storeResult = await storeTool.execute({ wingId: wing.id, roomId: room.id, hall: 'decisions', content: 'Use Fastify 5 for all new backends', }, mockReq) as { stored: boolean }; expect(storeResult.stored).toBe(true); const searchTool = getTool(PALACE_MCP_TOOL_NAMES.search); const searchResult = await searchTool.execute({ query: 'Fastify', limit: 10 }, mockReq) as unknown[]; expect(searchResult.length).toBe(1); }); it('mempalace_wake_up returns structured context', async () => { const wing = await ensureWing(USER_A, PRODUCT, 'ws-1', 'Work'); const room = await ensureRoom(USER_A, PRODUCT, wing.id, 'auth'); await storeMemory(USER_A, PRODUCT, wing.id, room.id, 'decisions', 'Use JWT'); const tool = getTool(PALACE_MCP_TOOL_NAMES.wakeUp); const result = await tool.execute({ wingId: wing.id }, mockReq) as { wingName: string; text: string }; expect(result.wingName).toBe('Work'); expect(result.text.length).toBeGreaterThan(0); }); it('mempalace_query_entity returns user-scoped triples', async () => { const wing = await ensureWing(USER_A, PRODUCT, 'ws-1', 'Work'); await addTriple(USER_A, PRODUCT, wing.id, 'React', 'replaced_by', 'Svelte', 0.8); const tool = getTool(PALACE_MCP_TOOL_NAMES.queryEntity); const result = await tool.execute({ entity: 'React' }, mockReq) as unknown[]; expect(result.length).toBe(1); }); it('MCP tools enforce userId from JWT', async () => { const reqNoUser: NotesMcpRequest = { ...mockReq, jwtPayload: { role: 'admin', productId: PRODUCT }, }; const tool = getTool(PALACE_MCP_TOOL_NAMES.listWings); await expect(tool.execute({}, reqNoUser)).rejects.toThrow('Authenticated user is required'); }); it('graceful response when palace is empty', async () => { const tool = getTool(PALACE_MCP_TOOL_NAMES.listWings); const result = await tool.execute({}, mockReq) as unknown[]; expect(result).toEqual([]); }); });