114 lines
4.5 KiB
TypeScript
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([]);
|
|
});
|
|
});
|