import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("./qdrant/client.js");
vi.mock("./embeddings/openai.js");
vi.mock("./logger.js", () => ({
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
fatal: vi.fn(),
trace: vi.fn(),
child: vi.fn().mockReturnThis(),
},
}));
describe("MCP Server Tool Schemas", () => {
describe("CreateCollectionSchema", () => {
it("should validate correct collection creation input", async () => {
const { z } = await import("zod");
const CreateCollectionSchema = z.object({
name: z.string(),
distance: z.enum(["Cosine", "Euclid", "Dot"]).optional(),
});
const validInput = { name: "test-collection" };
expect(() => CreateCollectionSchema.parse(validInput)).not.toThrow();
const validInputWithDistance = {
name: "test-collection",
distance: "Cosine" as const,
};
expect(() =>
CreateCollectionSchema.parse(validInputWithDistance),
).not.toThrow();
});
it("should reject invalid distance metric", async () => {
const { z } = await import("zod");
const CreateCollectionSchema = z.object({
name: z.string(),
distance: z.enum(["Cosine", "Euclid", "Dot"]).optional(),
});
const invalidInput = { name: "test", distance: "Invalid" };
expect(() => CreateCollectionSchema.parse(invalidInput)).toThrow();
});
it("should require name field", async () => {
const { z } = await import("zod");
const CreateCollectionSchema = z.object({
name: z.string(),
distance: z.enum(["Cosine", "Euclid", "Dot"]).optional(),
});
const invalidInput = { distance: "Cosine" };
expect(() => CreateCollectionSchema.parse(invalidInput)).toThrow();
});
});
describe("AddDocumentsSchema", () => {
it("should validate correct document addition input", async () => {
const { z } = await import("zod");
const AddDocumentsSchema = z.object({
collection: z.string(),
documents: z.array(
z.object({
id: z.union([z.string(), z.number()]),
text: z.string(),
metadata: z.record(z.string(), z.any()).optional(),
}),
),
});
const validInput = {
collection: "test-collection",
documents: [
{ id: 1, text: "test document" },
{ id: "doc-2", text: "another document", metadata: { type: "test" } },
],
};
expect(() => AddDocumentsSchema.parse(validInput)).not.toThrow();
});
it("should accept both string and number IDs", async () => {
const { z } = await import("zod");
const AddDocumentsSchema = z.object({
collection: z.string(),
documents: z.array(
z.object({
id: z.union([z.string(), z.number()]),
text: z.string(),
metadata: z.record(z.string(), z.any()).optional(),
}),
),
});
const stringIdInput = {
collection: "test",
documents: [{ id: "string-id", text: "test" }],
};
expect(() => AddDocumentsSchema.parse(stringIdInput)).not.toThrow();
const numberIdInput = {
collection: "test",
documents: [{ id: 123, text: "test" }],
};
expect(() => AddDocumentsSchema.parse(numberIdInput)).not.toThrow();
});
it("should require text field in documents", async () => {
const { z } = await import("zod");
const AddDocumentsSchema = z.object({
collection: z.string(),
documents: z.array(
z.object({
id: z.union([z.string(), z.number()]),
text: z.string(),
metadata: z.record(z.string(), z.any()).optional(),
}),
),
});
const invalidInput = {
collection: "test",
documents: [{ id: 1, metadata: {} }],
};
expect(() => AddDocumentsSchema.parse(invalidInput)).toThrow();
});
});
describe("SemanticSearchSchema", () => {
it("should validate correct search input", async () => {
const { z } = await import("zod");
const SemanticSearchSchema = z.object({
collection: z.string(),
query: z.string(),
limit: z.number().optional(),
filter: z.record(z.string(), z.any()).optional(),
});
const validInput = {
collection: "test-collection",
query: "search query",
limit: 10,
filter: { category: "test" },
};
expect(() => SemanticSearchSchema.parse(validInput)).not.toThrow();
});
it("should work with minimal input", async () => {
const { z } = await import("zod");
const SemanticSearchSchema = z.object({
collection: z.string(),
query: z.string(),
limit: z.number().optional(),
filter: z.record(z.string(), z.any()).optional(),
});
const minimalInput = {
collection: "test",
query: "search",
};
expect(() => SemanticSearchSchema.parse(minimalInput)).not.toThrow();
});
it("should require collection and query", async () => {
const { z } = await import("zod");
const SemanticSearchSchema = z.object({
collection: z.string(),
query: z.string(),
limit: z.number().optional(),
filter: z.record(z.string(), z.any()).optional(),
});
const missingQuery = { collection: "test", limit: 5 };
expect(() => SemanticSearchSchema.parse(missingQuery)).toThrow();
const missingCollection = { query: "test", limit: 5 };
expect(() => SemanticSearchSchema.parse(missingCollection)).toThrow();
});
});
describe("DeleteCollectionSchema", () => {
it("should validate correct delete input", async () => {
const { z } = await import("zod");
const DeleteCollectionSchema = z.object({
name: z.string(),
});
const validInput = { name: "test-collection" };
expect(() => DeleteCollectionSchema.parse(validInput)).not.toThrow();
});
it("should require name field", async () => {
const { z } = await import("zod");
const DeleteCollectionSchema = z.object({
name: z.string(),
});
expect(() => DeleteCollectionSchema.parse({})).toThrow();
});
});
describe("GetCollectionInfoSchema", () => {
it("should validate correct input", async () => {
const { z } = await import("zod");
const GetCollectionInfoSchema = z.object({
name: z.string(),
});
const validInput = { name: "test-collection" };
expect(() => GetCollectionInfoSchema.parse(validInput)).not.toThrow();
});
});
describe("DeleteDocumentsSchema", () => {
it("should validate correct delete documents input", async () => {
const { z } = await import("zod");
const DeleteDocumentsSchema = z.object({
collection: z.string(),
ids: z.array(z.union([z.string(), z.number()])),
});
const validInput = {
collection: "test-collection",
ids: [1, "doc-2", 3],
};
expect(() => DeleteDocumentsSchema.parse(validInput)).not.toThrow();
});
it("should accept string and number IDs", async () => {
const { z } = await import("zod");
const DeleteDocumentsSchema = z.object({
collection: z.string(),
ids: z.array(z.union([z.string(), z.number()])),
});
const stringIds = { collection: "test", ids: ["a", "b", "c"] };
expect(() => DeleteDocumentsSchema.parse(stringIds)).not.toThrow();
const numberIds = { collection: "test", ids: [1, 2, 3] };
expect(() => DeleteDocumentsSchema.parse(numberIds)).not.toThrow();
const mixedIds = { collection: "test", ids: [1, "b", 3] };
expect(() => DeleteDocumentsSchema.parse(mixedIds)).not.toThrow();
});
it("should require both collection and ids", async () => {
const { z } = await import("zod");
const DeleteDocumentsSchema = z.object({
collection: z.string(),
ids: z.array(z.union([z.string(), z.number()])),
});
const missingIds = { collection: "test" };
expect(() => DeleteDocumentsSchema.parse(missingIds)).toThrow();
const missingCollection = { ids: [1, 2, 3] };
expect(() => DeleteDocumentsSchema.parse(missingCollection)).toThrow();
});
});
});
describe("MCP Server Resource URIs", () => {
it("should match collections URI pattern", () => {
const collectionsUri = "qdrant://collections";
expect(collectionsUri).toMatch(/^qdrant:\/\/collections$/);
});
it("should match collection detail URI pattern", () => {
const collectionUri = "qdrant://collection/my-collection";
const match = collectionUri.match(/^qdrant:\/\/collection\/(.+)$/);
expect(match).not.toBeNull();
expect(match![1]).toBe("my-collection");
});
it("should extract collection name from URI", () => {
const testCases = [
{ uri: "qdrant://collection/test", expected: "test" },
{ uri: "qdrant://collection/my-docs", expected: "my-docs" },
{ uri: "qdrant://collection/collection-123", expected: "collection-123" },
];
testCases.forEach(({ uri, expected }) => {
const match = uri.match(/^qdrant:\/\/collection\/(.+)$/);
expect(match![1]).toBe(expected);
});
});
it("should not match invalid URIs", () => {
const invalidUris = [
"qdrant://invalid",
"http://collections",
"qdrant://collection/",
"qdrant:collections",
];
invalidUris.forEach((uri) => {
const collectionsMatch = uri.match(/^qdrant:\/\/collections$/);
const collectionMatch = uri.match(/^qdrant:\/\/collection\/(.+)$/);
expect(collectionsMatch || collectionMatch).toBeFalsy();
});
});
});