/**
* postgres-mcp - Text Tools Unit Tests
*
* Tests for PostgreSQL text processing tools with focus on
* full-text search, trigrams, fuzzy matching, and sentiment analysis.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getTextTools } from "../text.js";
import type { PostgresAdapter } from "../../PostgresAdapter.js";
import {
createMockPostgresAdapter,
createMockRequestContext,
} from "../../../../__tests__/mocks/index.js";
describe("getTextTools", () => {
let adapter: PostgresAdapter;
let tools: ReturnType<typeof getTextTools>;
beforeEach(() => {
vi.clearAllMocks();
adapter = createMockPostgresAdapter() as unknown as PostgresAdapter;
tools = getTextTools(adapter);
});
it("should return 13 text tools", () => {
expect(tools).toHaveLength(13);
});
it("should have all expected tool names", () => {
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("pg_text_search");
expect(toolNames).toContain("pg_text_rank");
expect(toolNames).toContain("pg_trigram_similarity");
expect(toolNames).toContain("pg_fuzzy_match");
expect(toolNames).toContain("pg_regexp_match");
expect(toolNames).toContain("pg_like_search");
expect(toolNames).toContain("pg_text_headline");
expect(toolNames).toContain("pg_create_fts_index");
expect(toolNames).toContain("pg_text_normalize");
expect(toolNames).toContain("pg_text_sentiment");
expect(toolNames).toContain("pg_text_to_vector");
expect(toolNames).toContain("pg_text_to_query");
expect(toolNames).toContain("pg_text_search_config");
});
it("should have group set to text for all tools", () => {
for (const tool of tools) {
expect(tool.group).toBe("text");
}
});
});
describe("pg_text_search", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should perform full-text search", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ title: "PostgreSQL Guide", rank: 0.9 }],
});
const tool = tools.find((t) => t.name === "pg_text_search")!;
const result = (await tool.handler(
{
table: "articles",
columns: ["title", "body"],
query: "postgres",
},
mockContext,
)) as {
rows: unknown[];
count: number;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("to_tsvector"),
["postgres"],
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("plainto_tsquery"),
expect.any(Array),
);
expect(result.count).toBe(1);
});
it("should use custom config", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_text_search")!;
await tool.handler(
{
table: "articles",
columns: ["title"],
query: "test",
config: "german",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("to_tsvector('german'"),
expect.any(Array),
);
});
it("should use custom select columns when provided (line 53 branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ id: 1, title: "Test", rank: 0.9 }],
});
const tool = tools.find((t) => t.name === "pg_text_search")!;
await tool.handler(
{
table: "articles",
columns: ["title", "body"],
query: "postgres",
select: ["id", "title"],
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining('SELECT "id", "title"'),
expect.any(Array),
);
});
it("should use * when select is empty array (line 53 branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_text_search")!;
await tool.handler(
{
table: "articles",
columns: ["title"],
query: "test",
select: [],
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("SELECT *"),
expect.any(Array),
);
});
it("should add LIMIT clause when limit > 0 (line 55 branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_text_search")!;
await tool.handler(
{
table: "articles",
columns: ["title"],
query: "test",
limit: 10,
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("LIMIT 10"),
expect.any(Array),
);
});
it("should not add LIMIT clause when limit is 0 (line 55 branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_text_search")!;
await tool.handler(
{
table: "articles",
columns: ["title"],
query: "test",
limit: 0,
},
mockContext,
);
const sql = mockAdapter.executeQuery.mock.calls[0]?.[0] as string;
expect(sql).not.toContain("LIMIT");
});
});
describe("pg_text_rank", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should rank text results", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ id: 1, rank: 0.85 }],
});
const tool = tools.find((t) => t.name === "pg_text_rank")!;
const result = (await tool.handler(
{
table: "documents",
column: "content",
query: "database",
},
mockContext,
)) as {
rows: unknown[];
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("ts_rank_cd"),
["database"],
);
expect(result.rows).toHaveLength(1);
});
});
describe("pg_trigram_similarity", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should find similar strings", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ name: "PostgreSQL", similarity: 0.8 }],
});
const tool = tools.find((t) => t.name === "pg_trigram_similarity")!;
const result = (await tool.handler(
{
table: "products",
column: "name",
value: "PostgreS",
},
mockContext,
)) as {
rows: unknown[];
count: number;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("similarity("),
["PostgreS"],
);
expect(result.count).toBe(1);
});
it("should respect threshold", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_trigram_similarity")!;
await tool.handler(
{
table: "products",
column: "name",
value: "test",
threshold: 0.5,
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("> 0.5"),
expect.any(Array),
);
});
});
describe("pg_fuzzy_match", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should use levenshtein by default", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ name: "Smith", distance: 1 }],
});
const tool = tools.find((t) => t.name === "pg_fuzzy_match")!;
await tool.handler(
{
table: "users",
column: "name",
value: "Smyth",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("levenshtein"),
["Smyth"],
);
});
it("should use soundex when specified", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_fuzzy_match")!;
await tool.handler(
{
table: "users",
column: "name",
value: "Smith",
method: "soundex",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("soundex"),
["Smith"],
);
});
it("should use metaphone when specified", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_fuzzy_match")!;
await tool.handler(
{
table: "users",
column: "name",
value: "Smith",
method: "metaphone",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("metaphone"),
["Smith"],
);
});
});
describe("pg_regexp_match", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should match using regex", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ email: "test@example.com" }],
});
const tool = tools.find((t) => t.name === "pg_regexp_match")!;
const result = (await tool.handler(
{
table: "users",
column: "email",
pattern: "^[a-z]+@",
},
mockContext,
)) as {
rows: unknown[];
count: number;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining(" ~ $1"),
["^[a-z]+@"],
);
expect(result.count).toBe(1);
});
it("should use case-insensitive match with i flag", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_regexp_match")!;
await tool.handler(
{
table: "users",
column: "email",
pattern: "TEST",
flags: "i",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining(" ~* $1"),
expect.any(Array),
);
});
it("should use custom select columns when provided (line 180 branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_regexp_match")!;
await tool.handler(
{
table: "users",
column: "email",
pattern: "^test",
select: ["id", "name"],
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining('SELECT "id", "name"'),
expect.any(Array),
);
});
});
describe("pg_like_search", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should search with LIKE", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ title: "PostgreSQL Tutorial" }],
});
const tool = tools.find((t) => t.name === "pg_like_search")!;
const result = (await tool.handler(
{
table: "articles",
column: "title",
pattern: "%PostgreSQL%",
},
mockContext,
)) as {
rows: unknown[];
count: number;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("LIKE $1"),
["%PostgreSQL%"],
);
expect(result.count).toBe(1);
});
it("should use ILIKE when case-insensitive", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_like_search")!;
await tool.handler(
{
table: "articles",
column: "title",
pattern: "%test%",
caseInsensitive: true,
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("ILIKE $1"),
expect.any(Array),
);
});
it("should use custom select columns when provided (line 210 branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_like_search")!;
await tool.handler(
{
table: "articles",
column: "title",
pattern: "%test%",
select: ["id", "title"],
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining('SELECT "id", "title"'),
expect.any(Array),
);
});
it("should add LIMIT clause when limit > 0 (line 212 branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_like_search")!;
await tool.handler(
{
table: "articles",
column: "title",
pattern: "%test%",
limit: 25,
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("LIMIT 25"),
expect.any(Array),
);
});
it("should use select columns with limit together (combined branch)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_like_search")!;
await tool.handler(
{
table: "articles",
column: "title",
pattern: "%test%",
select: ["id"],
limit: 10,
},
mockContext,
);
const sql = mockAdapter.executeQuery.mock.calls[0]?.[0] as string;
expect(sql).toContain('SELECT "id"');
expect(sql).toContain("LIMIT 10");
});
});
describe("pg_text_headline", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should generate headlines with highlights", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ headline: "Learn <b>PostgreSQL</b> basics" }],
});
const tool = tools.find((t) => t.name === "pg_text_headline")!;
const result = (await tool.handler(
{
table: "articles",
column: "content",
query: "PostgreSQL",
},
mockContext,
)) as {
rows: Array<{ headline: string }>;
count: number;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("ts_headline"),
["PostgreSQL"],
);
expect(result.rows[0].headline).toContain("<b>PostgreSQL</b>");
expect(result.count).toBe(1);
});
});
describe("pg_create_fts_index", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should create GIN index for FTS", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_create_fts_index")!;
const result = (await tool.handler(
{
table: "articles",
column: "body",
},
mockContext,
)) as {
success: boolean;
index: string;
config: string;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("CREATE INDEX"),
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("USING gin"),
);
expect(result.success).toBe(true);
expect(result.index).toContain("fts");
});
});
describe("pg_text_normalize", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should normalize text using unaccent", async () => {
mockAdapter.executeQuery
.mockResolvedValueOnce({ rows: [] }) // CREATE EXTENSION call
.mockResolvedValueOnce({ rows: [{ normalized: "cafe" }] }); // unaccent query
const tool = tools.find((t) => t.name === "pg_text_normalize")!;
const result = (await tool.handler(
{
text: "café",
},
mockContext,
)) as {
normalized: string;
};
expect(mockAdapter.executeQuery).toHaveBeenNthCalledWith(
1,
"CREATE EXTENSION IF NOT EXISTS unaccent",
);
expect(mockAdapter.executeQuery).toHaveBeenNthCalledWith(
2,
"SELECT unaccent($1) as normalized",
["café"],
);
expect(result.normalized).toBe("cafe");
});
});
describe("pg_text_sentiment", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should analyze positive sentiment", async () => {
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "This product is amazing and wonderful!",
},
mockContext,
)) as {
sentiment: string;
score: number;
positiveCount: number;
negativeCount: number;
};
expect(result.sentiment).toBe("positive");
expect(result.score).toBeGreaterThan(0);
expect(result.positiveCount).toBeGreaterThan(0);
});
it("should analyze negative sentiment", async () => {
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "This is terrible and horrible!",
},
mockContext,
)) as {
sentiment: string;
score: number;
negativeCount: number;
};
expect(result.sentiment).toContain("negative");
expect(result.score).toBeLessThan(0);
expect(result.negativeCount).toBeGreaterThan(0);
});
it("should return matched words when requested", async () => {
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "I love this great product!",
returnWords: true,
},
mockContext,
)) as {
matchedPositive: string[];
matchedNegative: string[];
};
expect(result.matchedPositive).toContain("love");
expect(result.matchedPositive).toContain("great");
expect(result.matchedNegative).toHaveLength(0);
});
it("should detect neutral sentiment", async () => {
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "The product arrived yesterday.",
},
mockContext,
)) as {
sentiment: string;
score: number;
};
expect(result.sentiment).toBe("neutral");
expect(result.score).toBe(0);
});
});
// =============================================================================
// Branch Coverage Tests - Sentiment Score Branches
// =============================================================================
describe("pg_text_sentiment score branches", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should detect very positive sentiment (score > 2)", async () => {
// Use many positive words to get score > 2
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "Amazing excellent fantastic wonderful great beautiful perfect awesome love brilliant",
},
mockContext,
)) as {
sentiment: string;
score: number;
positiveCount: number;
};
expect(result.sentiment).toBe("very_positive");
expect(result.score).toBeGreaterThan(2);
expect(result.positiveCount).toBeGreaterThan(3);
});
it("should detect very negative sentiment (score < -2)", async () => {
// Use many negative words to get score < -2
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "Terrible horrible awful bad disgusting ugly failure hate worst poor",
},
mockContext,
)) as {
sentiment: string;
score: number;
negativeCount: number;
};
expect(result.sentiment).toBe("very_negative");
expect(result.score).toBeLessThan(-2);
expect(result.negativeCount).toBeGreaterThan(3);
});
it("should detect high confidence with many matched words", async () => {
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "Amazing excellent fantastic wonderful beautiful perfect great",
},
mockContext,
)) as {
confidence: string;
positiveCount: number;
};
// With 7+ matched positive words, should be high confidence
expect(result.positiveCount).toBeGreaterThan(3);
expect(result.confidence).toBe("high");
});
it("should detect low confidence with single matched word", async () => {
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "This thing is good.",
},
mockContext,
)) as {
confidence: string;
positiveCount: number;
};
expect(result.positiveCount).toBe(1);
expect(result.confidence).toBe("low");
});
it("should detect medium confidence with 2-3 matched words", async () => {
const tool = tools.find((t) => t.name === "pg_text_sentiment")!;
const result = (await tool.handler(
{
text: "This product is good and great.",
},
mockContext,
)) as {
confidence: string;
positiveCount: number;
};
expect(result.positiveCount).toBe(2);
expect(result.confidence).toBe("medium");
});
});
// =============================================================================
// New Text Tools Tests
// =============================================================================
describe("pg_text_to_vector", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should convert text to tsvector with default config", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ vector: "'hello':1 'world':2" }],
});
const tool = tools.find((t) => t.name === "pg_text_to_vector")!;
const result = (await tool.handler(
{
text: "hello world",
},
mockContext,
)) as {
vector: string;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
"SELECT to_tsvector($1, $2) as vector",
["english", "hello world"],
);
expect(result.vector).toBe("'hello':1 'world':2");
});
it("should use custom config when specified", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ vector: "'hallo':1" }],
});
const tool = tools.find((t) => t.name === "pg_text_to_vector")!;
await tool.handler(
{
text: "hallo welt",
config: "german",
},
mockContext,
);
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
"SELECT to_tsvector($1, $2) as vector",
["german", "hallo welt"],
);
});
});
describe("pg_text_to_query", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should convert text to tsquery with plain mode (default)", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ query: "'hello' & 'world'" }],
});
const tool = tools.find((t) => t.name === "pg_text_to_query")!;
const result = (await tool.handler(
{
text: "hello world",
},
mockContext,
)) as {
query: string;
mode: string;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
"SELECT plainto_tsquery($1, $2) as query",
["english", "hello world"],
);
expect(result.mode).toBe("plain");
});
it("should use phrase mode when specified", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ query: "'hello' <-> 'world'" }],
});
const tool = tools.find((t) => t.name === "pg_text_to_query")!;
const result = (await tool.handler(
{
text: "hello world",
mode: "phrase",
},
mockContext,
)) as {
query: string;
mode: string;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
"SELECT phraseto_tsquery($1, $2) as query",
["english", "hello world"],
);
expect(result.mode).toBe("phrase");
});
it("should use websearch mode when specified", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [{ query: "'hello' | 'world'" }],
});
const tool = tools.find((t) => t.name === "pg_text_to_query")!;
const result = (await tool.handler(
{
text: "hello OR world",
mode: "websearch",
},
mockContext,
)) as {
query: string;
mode: string;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
"SELECT websearch_to_tsquery($1, $2) as query",
["english", "hello OR world"],
);
expect(result.mode).toBe("websearch");
});
});
describe("pg_text_search_config", () => {
let mockAdapter: ReturnType<typeof createMockPostgresAdapter>;
let tools: ReturnType<typeof getTextTools>;
let mockContext: ReturnType<typeof createMockRequestContext>;
beforeEach(() => {
vi.clearAllMocks();
mockAdapter = createMockPostgresAdapter();
tools = getTextTools(mockAdapter as unknown as PostgresAdapter);
mockContext = createMockRequestContext();
});
it("should list available search configurations", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({
rows: [
{ name: "english", schema: "pg_catalog", description: "English" },
{ name: "german", schema: "pg_catalog", description: "German" },
{ name: "simple", schema: "pg_catalog", description: "Simple" },
],
});
const tool = tools.find((t) => t.name === "pg_text_search_config")!;
const result = (await tool.handler({}, mockContext)) as {
configs: Array<{ name: string; schema: string; description: string }>;
count: number;
};
expect(mockAdapter.executeQuery).toHaveBeenCalledWith(
expect.stringContaining("pg_ts_config"),
);
expect(result.configs).toHaveLength(3);
expect(result.count).toBe(3);
expect(result.configs[0]?.name).toBe("english");
});
it("should return empty array when no configs found", async () => {
mockAdapter.executeQuery.mockResolvedValueOnce({ rows: [] });
const tool = tools.find((t) => t.name === "pg_text_search_config")!;
const result = (await tool.handler({}, mockContext)) as {
configs: unknown[];
count: number;
};
expect(result.configs).toEqual([]);
expect(result.count).toBe(0);
});
});