From 0b4715b80c98c9b15f5eb7ce75b7abfd29797633 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 18 Apr 2026 16:30:11 +0200 Subject: [PATCH] fix: update all tests and MCP catalog tool for manufacturerId schema migration --- src/server/mcp/tools/catalog.ts | 8 ++--- tests/mcp/tools.test.ts | 40 +++++++++++++++------ tests/routes/discovery.test.ts | 23 +++++++++--- tests/routes/global-items.test.ts | 45 ++++++++++++++++-------- tests/services/discovery.service.test.ts | 20 +++++++++-- tests/services/item.service.test.ts | 20 +++++++++-- tests/services/thread.service.test.ts | 20 +++++++++-- 7 files changed, 135 insertions(+), 41 deletions(-) diff --git a/src/server/mcp/tools/catalog.ts b/src/server/mcp/tools/catalog.ts index 4ff2647..2e64bcf 100644 --- a/src/server/mcp/tools/catalog.ts +++ b/src/server/mcp/tools/catalog.ts @@ -22,10 +22,10 @@ function errorResult(message: string): ToolResult { } const catalogItemInputSchema = { - brand: z.string().describe("Brand or manufacturer name"), + manufacturerSlug: z.string().describe("Manufacturer slug (e.g. 'revelate-designs', 'apidura')"), model: z .string() - .describe("Model name — combined with brand forms the unique identifier"), + .describe("Model name — combined with manufacturerSlug forms the unique identifier"), category: z .string() .optional() @@ -80,7 +80,7 @@ export const catalogToolDefinitions = [ export function registerCatalogTools(db: Db) { return { upsert_catalog_item: async (args: { - brand: string; + manufacturerSlug: string; model: string; category?: string; weightGrams?: number; @@ -105,7 +105,7 @@ export function registerCatalogTools(db: Db) { bulk_upsert_catalog: async (args: { items: Array<{ - brand: string; + manufacturerSlug: string; model: string; category?: string; weightGrams?: number; diff --git a/tests/mcp/tools.test.ts b/tests/mcp/tools.test.ts index 8152e45..e1ab48a 100644 --- a/tests/mcp/tools.test.ts +++ b/tests/mcp/tools.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { manufacturers } from "../../src/db/schema.ts"; import { getCollectionSummary } from "../../src/server/mcp/resources/collection.ts"; import { registerCatalogTools } from "../../src/server/mcp/tools/catalog.ts"; import { registerCategoryTools } from "../../src/server/mcp/tools/categories.ts"; @@ -7,6 +8,16 @@ 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"; +async function insertManufacturer(db: any, name: string) { + const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + const [row] = await db + .insert(manufacturers) + .values({ name, slug, website: `https://${slug}.com` }) + .onConflictDoUpdate({ target: manufacturers.slug, set: { name } }) + .returning(); + return row!; +} + function parseResult(result: { content: Array<{ type: string; text: string }>; }) { @@ -256,15 +267,15 @@ describe("MCP Collection Summary Resource", () => { describe("MCP Catalog Tools", () => { test("upsert_catalog_item creates a new global item with created=true", async () => { const { db } = await createTestDb(); + await insertManufacturer(db, "Revelate Designs"); const tools = registerCatalogTools(db); const result = await tools.upsert_catalog_item({ - brand: "Revelate Designs", + manufacturerSlug: "revelate-designs", model: "Terrapin System", weightGrams: 235, priceCents: 16500, }); const data = parseResult(result); - expect(data.brand).toBe("Revelate Designs"); expect(data.model).toBe("Terrapin System"); expect(data.created).toBe(true); expect(data.id).toBeDefined(); @@ -272,17 +283,18 @@ describe("MCP Catalog Tools", () => { test("upsert_catalog_item updates existing item on brand+model match", async () => { const { db } = await createTestDb(); + await insertManufacturer(db, "Apidura"); const tools = registerCatalogTools(db); // Create initial item await tools.upsert_catalog_item({ - brand: "Apidura", + manufacturerSlug: "apidura", model: "Handlebar Pack", }); // Update it const result = await tools.upsert_catalog_item({ - brand: "Apidura", + manufacturerSlug: "apidura", model: "Handlebar Pack", description: "Updated description", weightGrams: 120, @@ -295,10 +307,11 @@ describe("MCP Catalog Tools", () => { test("upsert_catalog_item includes attribution fields in result (SEED-03)", async () => { const { db } = await createTestDb(); + await insertManufacturer(db, "MSR"); const tools = registerCatalogTools(db); const result = await tools.upsert_catalog_item({ - brand: "MSR", + manufacturerSlug: "msr", model: "PocketRocket 2", sourceUrl: "https://www.cascadedesigns.com/msr/pocket-rocket-2", imageCredit: "MSR Photography", @@ -317,13 +330,16 @@ describe("MCP Catalog Tools", () => { test("bulk_upsert_catalog processes array and returns created/updated counts", async () => { const { db } = await createTestDb(); + await insertManufacturer(db, "Revelate Designs"); + await insertManufacturer(db, "Apidura"); + await insertManufacturer(db, "MSR"); const tools = registerCatalogTools(db); const result = await tools.bulk_upsert_catalog({ items: [ - { brand: "Revelate Designs", model: "Terrapin System" }, - { brand: "Apidura", model: "Handlebar Pack" }, - { brand: "MSR", model: "PocketRocket 2" }, + { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, + { manufacturerSlug: "apidura", model: "Handlebar Pack" }, + { manufacturerSlug: "msr", model: "PocketRocket 2" }, ], }); const data = parseResult(result); @@ -335,18 +351,20 @@ describe("MCP Catalog Tools", () => { test("bulk_upsert_catalog returns totalProcessed matching input length", async () => { const { db } = await createTestDb(); + await insertManufacturer(db, "Revelate Designs"); + await insertManufacturer(db, "Apidura"); const tools = registerCatalogTools(db); // Pre-create one item await tools.upsert_catalog_item({ - brand: "Revelate Designs", + manufacturerSlug: "revelate-designs", model: "Terrapin System", }); const result = await tools.bulk_upsert_catalog({ items: [ - { brand: "Revelate Designs", model: "Terrapin System" }, - { brand: "Apidura", model: "Handlebar Pack" }, + { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, + { manufacturerSlug: "apidura", model: "Handlebar Pack" }, ], }); const data = parseResult(result); diff --git a/tests/routes/discovery.test.ts b/tests/routes/discovery.test.ts index f6e29dc..d2d5208 100644 --- a/tests/routes/discovery.test.ts +++ b/tests/routes/discovery.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from "bun:test"; import { Hono } from "hono"; -import { globalItems, setups } from "../../src/db/schema.ts"; +import { globalItems, manufacturers, setups } from "../../src/db/schema.ts"; import { discoveryRoutes } from "../../src/server/routes/discovery.ts"; import { createTestDb } from "../helpers/db.ts"; @@ -20,17 +20,28 @@ async function createTestApp() { return { app, db, userId }; } +async function insertManufacturer(db: TestDb["db"], name: string) { + const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + const [row] = await db + .insert(manufacturers) + .values({ name, slug, website: `https://${slug}.com` }) + .onConflictDoUpdate({ target: manufacturers.slug, set: { name } }) + .returning(); + return row!; +} + async function insertGlobalItem( db: TestDb["db"], brand: string, model: string, category?: string, ) { + const m = await insertManufacturer(db, brand); const [row] = await db .insert(globalItems) - .values({ brand, model, category: category ?? "bags" }) + .values({ manufacturerId: m.id, model, category: category ?? "bags" }) .returning(); - return row; + return row!; } async function insertPublicSetup( @@ -142,14 +153,16 @@ describe("Discovery Routes", () => { const olderTime = new Date("2024-01-01T00:00:00Z"); const newerTime = new Date("2024-06-01T00:00:00Z"); + const mA = await insertManufacturer(db, "Brand A"); + const mB = await insertManufacturer(db, "Brand B"); await db.insert(globalItems).values({ - brand: "Brand A", + manufacturerId: mA.id, model: "Model A", category: "bags", createdAt: olderTime, }); await db.insert(globalItems).values({ - brand: "Brand B", + manufacturerId: mB.id, model: "Model B", category: "bags", createdAt: newerTime, diff --git a/tests/routes/global-items.test.ts b/tests/routes/global-items.test.ts index 67f7bd9..2919aa8 100644 --- a/tests/routes/global-items.test.ts +++ b/tests/routes/global-items.test.ts @@ -4,6 +4,7 @@ import { globalItems, globalItemTags, items, + manufacturers, tags, } from "../../src/db/schema.ts"; import { globalItemRoutes } from "../../src/server/routes/global-items.ts"; @@ -25,16 +26,27 @@ async function createTestApp() { return { app, db, userId }; } +async function insertManufacturer(db: TestDb["db"], name: string) { + const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + const [row] = await db + .insert(manufacturers) + .values({ name, slug, website: `https://${slug}.com` }) + .onConflictDoUpdate({ target: manufacturers.slug, set: { name } }) + .returning(); + return row!; +} + async function insertGlobalItem( db: TestDb["db"], brand: string, model: string, ) { + const m = await insertManufacturer(db, brand); const [row] = await db .insert(globalItems) - .values({ brand, model, category: "bags" }) + .values({ manufacturerId: m.id, model, category: "bags" }) .returning(); - return row; + return row!; } async function insertItem( @@ -113,18 +125,18 @@ describe("Global Item Routes", () => { describe("POST /api/global-items", () => { it("returns 200 with item and created=true on new item", async () => { + await insertManufacturer(db, "Revelate Designs"); const res = await app.request("/api/global-items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - brand: "Revelate Designs", + manufacturerSlug: "revelate-designs", model: "Terrapin System", }), }); expect(res.status).toBe(200); const body = await res.json(); - expect(body.item.brand).toBe("Revelate Designs"); expect(body.item.model).toBe("Terrapin System"); expect(body.created).toBe(true); }); @@ -136,7 +148,7 @@ describe("Global Item Routes", () => { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - brand: "Revelate Designs", + manufacturerSlug: "revelate-designs", model: "Terrapin System", description: "Updated description", }), @@ -148,7 +160,7 @@ describe("Global Item Routes", () => { expect(body.item.description).toBe("Updated description"); }); - it("returns 400 when brand is missing", async () => { + it("returns 400 when manufacturerSlug is missing", async () => { const res = await app.request("/api/global-items", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -161,7 +173,7 @@ describe("Global Item Routes", () => { const res = await app.request("/api/global-items", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ brand: "Revelate Designs" }), + body: JSON.stringify({ manufacturerSlug: "revelate-designs" }), }); expect(res.status).toBe(400); }); @@ -169,13 +181,15 @@ describe("Global Item Routes", () => { describe("POST /api/global-items/bulk", () => { it("returns 200 with created/updated counts", async () => { + await insertManufacturer(db, "Revelate Designs"); + await insertManufacturer(db, "Apidura"); const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ - { brand: "Revelate Designs", model: "Terrapin System" }, - { brand: "Apidura", model: "Handlebar Pack" }, + { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, + { manufacturerSlug: "apidura", model: "Handlebar Pack" }, ], }), }); @@ -189,14 +203,15 @@ describe("Global Item Routes", () => { it("returns correct counts for mix of new and existing items", async () => { await insertGlobalItem(db, "Revelate Designs", "Terrapin System"); + await insertManufacturer(db, "Apidura"); const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ - { brand: "Revelate Designs", model: "Terrapin System" }, - { brand: "Apidura", model: "Handlebar Pack" }, + { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, + { manufacturerSlug: "apidura", model: "Handlebar Pack" }, ], }), }); @@ -218,7 +233,7 @@ describe("Global Item Routes", () => { it("returns 400 when items array exceeds 100", async () => { const items = Array.from({ length: 101 }, (_, i) => ({ - brand: `Brand${i}`, + manufacturerSlug: `brand${i}`, model: `Model${i}`, })); const res = await app.request("/api/global-items/bulk", { @@ -229,14 +244,14 @@ describe("Global Item Routes", () => { expect(res.status).toBe(400); }); - it("returns 400 for invalid item in array (missing brand)", async () => { + it("returns 400 for invalid item in array (missing manufacturerSlug)", async () => { const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ - { brand: "Revelate Designs", model: "Terrapin System" }, - { model: "Invalid Item without brand" }, + { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, + { model: "Invalid Item without manufacturerSlug" }, ], }), }); diff --git a/tests/services/discovery.service.test.ts b/tests/services/discovery.service.test.ts index f046a99..13f1dbc 100644 --- a/tests/services/discovery.service.test.ts +++ b/tests/services/discovery.service.test.ts @@ -3,6 +3,7 @@ import { eq } from "drizzle-orm"; import { globalItems, items, + manufacturers, setupItems, setups, users, @@ -16,19 +17,34 @@ import { createTestDb } from "../helpers/db.ts"; type TestDb = Awaited>; +async function insertManufacturer(db: TestDb["db"], name: string) { + const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + const [existing] = await db + .select() + .from(manufacturers) + .where(eq(manufacturers.slug, slug)); + if (existing) return existing; + const [row] = await db + .insert(manufacturers) + .values({ name, slug, website: `https://${slug}.com` }) + .returning(); + return row!; +} + async function insertGlobalItem( db: TestDb["db"], data: { brand: string; model: string; category?: string }, ) { + const m = await insertManufacturer(db, data.brand); const [row] = await db .insert(globalItems) .values({ - brand: data.brand, + manufacturerId: m.id, model: data.model, category: data.category ?? null, }) .returning(); - return row; + return row!; } async function insertItem(db: TestDb["db"], userId: number, categoryId = 1) { diff --git a/tests/services/item.service.test.ts b/tests/services/item.service.test.ts index e38960b..000ea67 100644 --- a/tests/services/item.service.test.ts +++ b/tests/services/item.service.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from "bun:test"; -import { globalItems } from "../../src/db/schema.ts"; +import { globalItems, manufacturers } from "../../src/db/schema.ts"; import { createItem, deleteItem, @@ -170,6 +170,15 @@ describe("Item Service", () => { }); describe("reference items (globalItemId)", () => { + async function insertManufacturer(testDb: any, name: string) { + const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + const [row] = await testDb + .insert(manufacturers) + .values({ name, slug, website: `https://${slug}.com` }) + .returning(); + return row; + } + async function insertGlobalItem( testDb: any, data: { @@ -180,7 +189,14 @@ describe("Item Service", () => { imageUrl?: string; }, ) { - const [row] = await testDb.insert(globalItems).values(data).returning(); + const m = await insertManufacturer(testDb, data.brand); + const [row] = await testDb.insert(globalItems).values({ + manufacturerId: m.id, + model: data.model, + weightGrams: data.weightGrams ?? null, + priceCents: data.priceCents ?? null, + imageUrl: data.imageUrl ?? null, + }).returning(); return row; } diff --git a/tests/services/thread.service.test.ts b/tests/services/thread.service.test.ts index 1bd2300..c92ee0f 100644 --- a/tests/services/thread.service.test.ts +++ b/tests/services/thread.service.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from "bun:test"; -import { globalItems } from "../../src/db/schema.ts"; +import { globalItems, manufacturers } from "../../src/db/schema.ts"; import { createCandidate, createThread, @@ -618,6 +618,15 @@ describe("Thread Service", () => { }); describe("catalog-linked candidates (globalItemId)", () => { + async function insertManufacturer(testDb: any, name: string) { + const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + const [row] = await testDb + .insert(manufacturers) + .values({ name, slug, website: `https://${slug}.com` }) + .returning(); + return row; + } + async function insertGlobalItem( testDb: any, data: { @@ -628,7 +637,14 @@ describe("Thread Service", () => { imageUrl?: string; }, ) { - const [row] = await testDb.insert(globalItems).values(data).returning(); + const m = await insertManufacturer(testDb, data.brand); + const [row] = await testDb.insert(globalItems).values({ + manufacturerId: m.id, + model: data.model, + weightGrams: data.weightGrams ?? null, + priceCents: data.priceCents ?? null, + imageUrl: data.imageUrl ?? null, + }).returning(); return row; }