Skip to main content
Glama
wei
by wei
search-posts.test.ts11.4 kB
/** * Integration Tests: search-posts tool * * Tests the search-posts tool with real HackerNews API calls. * Verifies correct behavior for searches, filters, pagination, and error handling. */ import { describe, expect, it } from "vitest"; import { searchPostsTool } from "../../../src/tools/search-posts.js"; describe("search-posts integration", () => { describe("Basic Search", () => { it("should search for posts with keyword", async () => { const result = await searchPostsTool({ query: "Python" }); expect(result.isError).toBe(false); expect(result.content).toHaveLength(1); const content = result.content[0]; expect(content.type).toBe("text"); if (content.type === "text") { const data = JSON.parse(content.text); expect(data).toHaveProperty("hits"); expect(data).toHaveProperty("nbHits"); expect(data).toHaveProperty("page"); expect(data).toHaveProperty("nbPages"); expect(data).toHaveProperty("hitsPerPage"); expect(data).toHaveProperty("query"); expect(Array.isArray(data.hits)).toBe(true); expect(data.query).toBe("Python"); expect(data.page).toBe(0); } }); it("should return results containing search keyword", async () => { const result = await searchPostsTool({ query: "JavaScript" }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); expect(data.hits.length).toBeGreaterThan(0); // At least some results should contain the keyword const hasKeyword = data.hits.some( (hit: { title?: string; story_text?: string; comment_text?: string }) => hit.title?.toLowerCase().includes("javascript") || hit.story_text?.toLowerCase().includes("javascript") || hit.comment_text?.toLowerCase().includes("javascript") ); expect(hasKeyword).toBe(true); } }); }); describe("Search with Filters", () => { it("should filter by story tag", async () => { const result = await searchPostsTool({ query: "TypeScript", tags: ["story"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); expect(data.hits.length).toBeGreaterThan(0); // All results should be stories for (const hit of data.hits as { _tags: string[] }[]) { expect(hit._tags).toContain("story"); } } }); it("should apply numeric filter for points", async () => { const result = await searchPostsTool({ query: "startup", numericFilters: ["points>=100"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { // All results should have 100+ points for (const hit of data.hits as { points: number | null }[]) { if (hit.points !== null) { expect(hit.points).toBeGreaterThanOrEqual(100); } } } } }); it("should combine tags and numeric filters", async () => { const result = await searchPostsTool({ query: "AI", tags: ["story"], numericFilters: ["points>=50"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { for (const hit of data.hits as { _tags: string[]; points: number | null }[]) { expect(hit._tags).toContain("story"); if (hit.points !== null) { expect(hit.points).toBeGreaterThanOrEqual(50); } } } } }); it("should filter by multiple numeric conditions", async () => { const result = await searchPostsTool({ query: "programming", numericFilters: ["points>=100", "num_comments>=20"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { for (const hit of data.hits as { points: number | null; num_comments: number | null }[]) { if (hit.points !== null) { expect(hit.points).toBeGreaterThanOrEqual(100); } if (hit.num_comments !== null) { expect(hit.num_comments).toBeGreaterThanOrEqual(20); } } } } }); }); describe("Pagination", () => { it("should return first page by default", async () => { const result = await searchPostsTool({ query: "React" }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); expect(data.page).toBe(0); } }); it("should return different results for different pages", async () => { const page0 = await searchPostsTool({ query: "Node.js", page: 0 }); const page1 = await searchPostsTool({ query: "Node.js", page: 1 }); expect(page0.isError).toBe(false); expect(page1.isError).toBe(false); const content0 = page0.content[0]; const content1 = page1.content[0]; if (content0.type === "text" && content1.type === "text") { const data0 = JSON.parse(content0.text); const data1 = JSON.parse(content1.text); // If there are results on both pages, they should be different if (data0.hits.length > 0 && data1.hits.length > 0) { const firstId0 = data0.hits[0].objectID; const firstId1 = data1.hits[0].objectID; expect(firstId0).not.toBe(firstId1); } expect(data0.page).toBe(0); expect(data1.page).toBe(1); } }); it("should respect custom hitsPerPage", async () => { const result = await searchPostsTool({ query: "Docker", hitsPerPage: 5, }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); expect(data.hitsPerPage).toBe(5); if (data.hits.length > 0) { expect(data.hits.length).toBeLessThanOrEqual(5); } } }); it("should provide pagination metadata", async () => { const result = await searchPostsTool({ query: "web development" }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); expect(typeof data.nbHits).toBe("number"); expect(typeof data.nbPages).toBe("number"); expect(typeof data.page).toBe("number"); expect(typeof data.hitsPerPage).toBe("number"); // Verify pagination metadata is present and logical if (data.nbHits > 0) { expect(data.nbPages).toBeGreaterThan(0); // HackerNews API may cap the number of pages, so just verify it's reasonable expect(data.nbPages).toBeLessThanOrEqual(Math.ceil(data.nbHits / data.hitsPerPage)); } } }); }); describe("Error Handling", () => { it("should return validation error for empty query", async () => { const result = await searchPostsTool({ query: "" }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === "text") { expect(content.text).toContain("Validation error"); expect(content.text).toContain("query"); } }); it("should return validation error for invalid page number", async () => { const result = await searchPostsTool({ query: "test", page: -1 }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === "text") { expect(content.text).toContain("Validation error"); } }); it("should return validation error for invalid hitsPerPage", async () => { const result = await searchPostsTool({ query: "test", hitsPerPage: 2000, }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === "text") { expect(content.text).toContain("Validation error"); } }); it("should handle missing query parameter", async () => { const result = await searchPostsTool({}); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === "text") { expect(content.text).toContain("Validation error"); expect(content.text).toContain("query"); } }); }); describe("Advanced Filtering (User Story 6)", () => { it("should filter by points threshold", async () => { const result = await searchPostsTool({ query: "AI", numericFilters: ["points>=100"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { // All results should have 100+ points for (const hit of data.hits as { points: number | null }[]) { if (hit.points !== null) { expect(hit.points).toBeGreaterThanOrEqual(100); } } } } }); it("should filter by comment count threshold", async () => { const result = await searchPostsTool({ query: "startup", numericFilters: ["num_comments>=50"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { // All results should have 50+ comments for (const hit of data.hits as { num_comments: number | null }[]) { if (hit.num_comments !== null) { expect(hit.num_comments).toBeGreaterThanOrEqual(50); } } } } }); it("should combine multiple numeric filters (AND logic)", async () => { const result = await searchPostsTool({ query: "technology", numericFilters: ["points>=100", "num_comments>=20"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { // All results should match both conditions for (const hit of data.hits as { points: number | null; num_comments: number | null }[]) { if (hit.points !== null) { expect(hit.points).toBeGreaterThanOrEqual(100); } if (hit.num_comments !== null) { expect(hit.num_comments).toBeGreaterThanOrEqual(20); } } } } }); it("should filter by author using tags", async () => { const result = await searchPostsTool({ query: "startup", tags: ["author_pg"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { // All results should be by author "pg" for (const hit of data.hits as { author: string }[]) { expect(hit.author).toBe("pg"); } } } }); it("should support OR logic with tags (story or poll)", async () => { const result = await searchPostsTool({ query: "software", tags: ["(story,poll)"], }); expect(result.isError).toBe(false); const content = result.content[0]; if (content.type === "text") { const data = JSON.parse(content.text); if (data.hits.length > 0) { // All results should be either stories or polls for (const hit of data.hits as { _tags: string[] }[]) { const isStoryOrPoll = hit._tags.includes("story") || hit._tags.includes("poll"); expect(isStoryOrPoll).toBe(true); } } } }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wei/hn-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server