import { describe, expect, test } from "bun:test"; import { getCollectionSummary } from "../../src/server/mcp/resources/collection.ts"; import { registerCategoryTools } from "../../src/server/mcp/tools/categories.ts"; import { registerItemTools } from "../../src/server/mcp/tools/items.ts"; import { registerSetupTools } from "../../src/server/mcp/tools/setups.ts"; import { registerThreadTools } from "../../src/server/mcp/tools/threads.ts"; import { createSecondTestUser, createTestDb } from "../helpers/db.ts"; function parseResult(result: { content: Array<{ type: string; text: string }>; }) { return JSON.parse(result.content[0].text); } describe("MCP Item Tools", () => { test("list_items returns array", async () => { const { db, userId } = await createTestDb(); const tools = registerItemTools(db, userId); const result = await tools.list_items({}); const data = parseResult(result); expect(Array.isArray(data)).toBe(true); }); test("create_item creates and returns item", async () => { const { db, userId } = await createTestDb(); const tools = registerItemTools(db, userId); const result = await tools.create_item({ name: "Test Tent", categoryId: 1, weightGrams: 1200, priceCents: 35000, }); const data = parseResult(result); expect(data.name).toBe("Test Tent"); expect(data.weightGrams).toBe(1200); expect(data.priceCents).toBe(35000); expect(data.id).toBeDefined(); }); test("get_item retrieves by ID", async () => { const { db, userId } = await createTestDb(); const tools = registerItemTools(db, userId); const created = parseResult( await tools.create_item({ name: "Sleeping Bag", categoryId: 1 }), ); const result = await tools.get_item({ id: created.id }); const data = parseResult(result); expect(data.name).toBe("Sleeping Bag"); expect(data.id).toBe(created.id); }); test("get_item returns error for missing item", async () => { const { db, userId } = await createTestDb(); const tools = registerItemTools(db, userId); const result = await tools.get_item({ id: 999 }); const data = parseResult(result); expect(data.error).toContain("not found"); }); test("delete_item removes item", async () => { const { db, userId } = await createTestDb(); const tools = registerItemTools(db, userId); const created = parseResult( await tools.create_item({ name: "To Delete", categoryId: 1 }), ); const deleteResult = await tools.delete_item({ id: created.id }); const data = parseResult(deleteResult); expect(data.deleted).toBe(true); // Verify it's gone const getResult = await tools.get_item({ id: created.id }); const getData = parseResult(getResult); expect(getData.error).toContain("not found"); }); }); describe("MCP Category Tools", () => { test("list_categories returns array with Uncategorized", async () => { const { db, userId } = await createTestDb(); const tools = registerCategoryTools(db, userId); const result = await tools.list_categories(); const data = parseResult(result); expect(Array.isArray(data)).toBe(true); expect(data.length).toBeGreaterThanOrEqual(1); expect(data.some((c: any) => c.name === "Uncategorized")).toBe(true); }); test("create_category creates a new category", async () => { const { db, userId } = await createTestDb(); const tools = registerCategoryTools(db, userId); const result = await tools.create_category({ name: "Shelter", icon: "tent", }); const data = parseResult(result); expect(data.name).toBe("Shelter"); expect(data.icon).toBe("tent"); }); }); describe("MCP Thread Tools", () => { test("create_thread starts a thread with status active", async () => { const { db, userId } = await createTestDb(); const tools = registerThreadTools(db, userId); const result = await tools.create_thread({ name: "Handlebar Bag", categoryId: 1, }); const data = parseResult(result); expect(data.name).toBe("Handlebar Bag"); expect(data.status).toBe("active"); }); test("add_candidate adds to thread", async () => { const { db, userId } = await createTestDb(); const tools = registerThreadTools(db, userId); const thread = parseResult( await tools.create_thread({ name: "Saddle Bag", categoryId: 1 }), ); const result = await tools.add_candidate({ threadId: thread.id, name: "Apidura Racing", categoryId: 1, priceCents: 8500, pros: "Lightweight", cons: "Expensive", }); const data = parseResult(result); expect(data.name).toBe("Apidura Racing"); expect(data.threadId).toBe(thread.id); expect(data.pros).toBe("Lightweight"); }); test("resolve_thread picks winner and creates item", async () => { const { db, userId } = await createTestDb(); const threadTools = registerThreadTools(db, userId); const itemTools = registerItemTools(db, userId); // Create thread with two candidates const thread = parseResult( await threadTools.create_thread({ name: "Frame Bag", categoryId: 1 }), ); const candidate1 = parseResult( await threadTools.add_candidate({ threadId: thread.id, name: "Revelate Tangle", categoryId: 1, priceCents: 12000, }), ); await threadTools.add_candidate({ threadId: thread.id, name: "Ortlieb Frame Pack", categoryId: 1, priceCents: 9000, }); // Resolve with first candidate const resolveResult = await threadTools.resolve_thread({ threadId: thread.id, candidateId: candidate1.id, }); const resolveData = parseResult(resolveResult); expect(resolveData.success).toBe(true); expect(resolveData.item.name).toBe("Revelate Tangle"); // Check item was added to collection const items = parseResult(await itemTools.list_items({})); expect(items.some((i: any) => i.name === "Revelate Tangle")).toBe(true); // Check thread is now resolved const threadList = parseResult( await threadTools.list_threads({ includeResolved: true }), ); const resolved = threadList.find((t: any) => t.id === thread.id); expect(resolved.status).toBe("resolved"); }); }); describe("MCP Setup Tools", () => { test("create_setup and list_setups", async () => { const { db, userId } = await createTestDb(); const tools = registerSetupTools(db, userId); await tools.create_setup({ name: "Weekend Trip" }); const result = await tools.list_setups(); const data = parseResult(result); expect(data.length).toBe(1); expect(data[0].name).toBe("Weekend Trip"); }); test("get_setup returns setup with items", async () => { const { db, userId } = await createTestDb(); const setupTools = registerSetupTools(db, userId); const itemTools = registerItemTools(db, userId); const setup = parseResult( await setupTools.create_setup({ name: "Overnighter" }), ); const item = parseResult( await itemTools.create_item({ name: "Bivvy", categoryId: 1 }), ); await setupTools.update_setup({ id: setup.id, itemIds: [item.id] }); const result = await setupTools.get_setup({ id: setup.id }); const data = parseResult(result); expect(data.name).toBe("Overnighter"); expect(data.items.length).toBe(1); expect(data.items[0].name).toBe("Bivvy"); }); }); describe("MCP Collection Summary Resource", () => { test("returns overview with correct counts", async () => { const { db, userId } = await createTestDb(); const summary = await getCollectionSummary(db, userId); expect(summary.overview).toBeDefined(); expect(summary.overview.totalItems).toBe(0); expect(summary.overview.categoryCount).toBe(1); // Uncategorized expect(summary.itemsByCategory).toBeDefined(); expect(summary.activeThreads).toBeDefined(); expect(Array.isArray(summary.activeThreads)).toBe(true); }); test("reflects items and threads after creation", async () => { const { db, userId } = await createTestDb(); const itemTools = registerItemTools(db, userId); const threadTools = registerThreadTools(db, userId); await itemTools.create_item({ name: "Tent", categoryId: 1, weightGrams: 1500, }); await itemTools.create_item({ name: "Sleeping Pad", categoryId: 1, weightGrams: 500, }); await threadTools.create_thread({ name: "Cook System", categoryId: 1, }); const summary = await getCollectionSummary(db, userId); expect(summary.overview.totalItems).toBe(2); expect(summary.overview.totalWeightGrams).toBe(2000); expect(summary.overview.activeThreadCount).toBe(1); expect(summary.itemsByCategory.Uncategorized).toBe(2); expect(summary.activeThreads.length).toBe(1); expect(summary.activeThreads[0].name).toBe("Cook System"); }); }); describe("MCP Cross-User Isolation", () => { test("user 2 cannot see user 1's items via MCP tools", async () => { const { db, userId } = await createTestDb(); const userId2 = await createSecondTestUser(db); const user1Tools = registerItemTools(db, userId); const user2Tools = registerItemTools(db, userId2); // User 1 creates an item await user1Tools.create_item({ name: "User 1 Tent", categoryId: 1, weightGrams: 1200, }); // User 2 creates an item await user2Tools.create_item({ name: "User 2 Sleeping Bag", categoryId: 1, weightGrams: 800, }); // Each user only sees their own items const user1Items = parseResult(await user1Tools.list_items({})); const user2Items = parseResult(await user2Tools.list_items({})); expect(user1Items).toHaveLength(1); expect(user1Items[0].name).toBe("User 1 Tent"); expect(user2Items).toHaveLength(1); expect(user2Items[0].name).toBe("User 2 Sleeping Bag"); }); test("user 2 cannot access user 1's item by ID", async () => { const { db, userId } = await createTestDb(); const userId2 = await createSecondTestUser(db); const user1Tools = registerItemTools(db, userId); const user2Tools = registerItemTools(db, userId2); const created = parseResult( await user1Tools.create_item({ name: "Private Item", categoryId: 1, }), ); // User 2 tries to get user 1's item const result = await user2Tools.get_item({ id: created.id }); const data = parseResult(result); expect(data.error).toContain("not found"); }); test("user 2 cannot see user 1's threads via MCP tools", async () => { const { db, userId } = await createTestDb(); const userId2 = await createSecondTestUser(db); const user1Tools = registerThreadTools(db, userId); const user2Tools = registerThreadTools(db, userId2); await user1Tools.create_thread({ name: "User 1 Thread", categoryId: 1, }); const user1Threads = parseResult( await user1Tools.list_threads({ includeResolved: false }), ); const user2Threads = parseResult( await user2Tools.list_threads({ includeResolved: false }), ); expect(user1Threads).toHaveLength(1); expect(user1Threads[0].name).toBe("User 1 Thread"); expect(user2Threads).toHaveLength(0); }); test("collection summary is scoped to user", async () => { const { db, userId } = await createTestDb(); const userId2 = await createSecondTestUser(db); const user1Tools = registerItemTools(db, userId); await user1Tools.create_item({ name: "User 1 Item", categoryId: 1, weightGrams: 500, }); const user1Summary = await getCollectionSummary(db, userId); const user2Summary = await getCollectionSummary(db, userId2); expect(user1Summary.overview.totalItems).toBe(1); expect(user2Summary.overview.totalItems).toBe(0); }); });