/**
* Integration Tests: get-latest-posts tool
*
* Tests the get-latest-posts tool with real HackerNews API calls.
* Verifies correct behavior for retrieving latest posts by date.
*/
import { describe, expect, it } from "vitest";
import { getLatestPostsHandler } from "../../../src/tools/get-latest-posts.js";
describe("get-latest-posts integration", () => {
describe("All Types", () => {
it("should retrieve latest posts without filters", async () => {
const result = await getLatestPostsHandler({});
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(Array.isArray(data.hits)).toBe(true);
expect(data.hits.length).toBeGreaterThan(0);
expect(data.page).toBe(0);
}
});
it("should return posts sorted by date (most recent first)", async () => {
const result = await getLatestPostsHandler({});
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(1);
// Verify timestamps are in descending order (newest first)
const timestamps = data.hits
.filter((hit: { created_at_i?: number }) => hit.created_at_i)
.map((hit: { created_at_i: number }) => hit.created_at_i);
for (let i = 0; i < timestamps.length - 1; i++) {
expect(timestamps[i]).toBeGreaterThanOrEqual(timestamps[i + 1]);
}
}
});
});
describe("Filter by Tags", () => {
it("should filter by story tag", async () => {
const result = await getLatestPostsHandler({ 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 filter by comment tag", async () => {
const result = await getLatestPostsHandler({ tags: ["comment"] });
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 comments
for (const hit of data.hits as { _tags: string[] }[]) {
expect(hit._tags).toContain("comment");
}
}
});
it("should maintain date sorting with tag filter", async () => {
const result = await getLatestPostsHandler({ tags: ["story"] });
expect(result.isError).toBe(false);
const content = result.content[0];
if (content.type === "text") {
const data = JSON.parse(content.text);
// Verify timestamps are still in descending order
const timestamps = data.hits
.filter((hit: { created_at_i?: number }) => hit.created_at_i)
.map((hit: { created_at_i: number }) => hit.created_at_i);
for (let i = 0; i < timestamps.length - 1; i++) {
expect(timestamps[i]).toBeGreaterThanOrEqual(timestamps[i + 1]);
}
}
});
it("should handle multiple tags", async () => {
const result = await getLatestPostsHandler({
tags: ["story", "show_hn"],
});
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);
// Results should match one of the tags
for (const hit of data.hits as { _tags: string[] }[]) {
const hasMatchingTag = hit._tags.includes("story") || hit._tags.includes("show_hn");
expect(hasMatchingTag).toBe(true);
}
}
});
});
describe("Pagination", () => {
it("should respect custom hitsPerPage", async () => {
const result = await getLatestPostsHandler({ hitsPerPage: 10 });
expect(result.isError).toBe(false);
const content = result.content[0];
if (content.type === "text") {
const data = JSON.parse(content.text);
expect(data.hitsPerPage).toBe(10);
expect(data.hits.length).toBeLessThanOrEqual(10);
}
});
it("should return different results for different pages", async () => {
const page0 = await getLatestPostsHandler({ page: 0 });
const page1 = await getLatestPostsHandler({ 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 provide pagination metadata", async () => {
const result = await getLatestPostsHandler({});
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");
if (data.nbHits > 0) {
expect(data.nbPages).toBeGreaterThan(0);
}
}
});
});
describe("Error Handling", () => {
it("should return error for negative page number", async () => {
const result = await getLatestPostsHandler({ page: -1 });
expect(result.isError).toBe(true);
const content = result.content[0];
if (content.type === "text") {
expect(content.text).toBeTruthy();
}
});
it("should return error for invalid hitsPerPage", async () => {
const result = await getLatestPostsHandler({ hitsPerPage: 2000 });
expect(result.isError).toBe(true);
const content = result.content[0];
if (content.type === "text") {
expect(content.text).toBeTruthy();
}
});
it("should return error for hitsPerPage below minimum", async () => {
const result = await getLatestPostsHandler({ hitsPerPage: 0 });
expect(result.isError).toBe(true);
});
});
});