learning_ai_notes/backend/src/mcp/palace-tools.test.ts

114 lines
4.5 KiB
TypeScript

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([]);
});
});