feat: expand API coverage with new tools, params, and 90% test threshold

This commit is contained in:
Deivid Soto 2026-02-12 15:45:08 +01:00
parent 8bb8e5507e
commit fa913d1561
21 changed files with 1573 additions and 88 deletions

View file

@ -0,0 +1,115 @@
import { describe, it, expect, vi } from "vitest";
import { createMockServer } from "../helpers.js";
import { registerAutocomplete } from "../../src/tools/autocomplete.js";
import { TorrentClawClient, ApiError } from "../../src/api-client.js";
function createMockClient(overrides: Partial<TorrentClawClient> = {}) {
return {
search: vi.fn(),
autocomplete: vi.fn(),
getPopular: vi.fn(),
getRecent: vi.fn(),
getWatchProviders: vi.fn(),
getCredits: vi.fn(),
getStats: vi.fn(),
getTorrentDownloadUrl: vi.fn(),
track: vi.fn(),
submitScanRequest: vi.fn(),
getScanStatus: vi.fn(),
...overrides,
} as unknown as TorrentClawClient;
}
describe("autocomplete tool", () => {
it("returns formatted suggestions", async () => {
const client = createMockClient({
autocomplete: vi.fn().mockResolvedValue({
suggestions: [
{
id: 1,
title: "Breaking Bad",
year: 2008,
contentType: "show",
posterUrl: null,
},
{
id: 2,
title: "The Break-Up",
year: 2006,
contentType: "movie",
posterUrl: null,
},
],
}),
});
const { server, getToolHandler } = createMockServer();
registerAutocomplete(server, client);
const handler = getToolHandler("autocomplete");
const result = await handler({ query: "break" });
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toContain("Breaking Bad");
expect(result.content[0].text).toContain("The Break-Up");
expect(result.content[0].text).toContain("[show]");
expect(result.content[0].text).toContain("[movie]");
});
it("returns message when no suggestions found", async () => {
const client = createMockClient({
autocomplete: vi.fn().mockResolvedValue({ suggestions: [] }),
});
const { server, getToolHandler } = createMockServer();
registerAutocomplete(server, client);
const handler = getToolHandler("autocomplete");
const result = await handler({ query: "zzzzz" });
expect(result.content[0].text).toContain("No suggestions");
expect(result.content[0].text).toContain("search_content");
});
it("returns isError on ApiError", async () => {
const client = createMockClient({
autocomplete: vi
.fn()
.mockRejectedValue(new ApiError(429, "Rate limited")),
});
const { server, getToolHandler } = createMockServer();
registerAutocomplete(server, client);
const handler = getToolHandler("autocomplete");
const result = await handler({ query: "test" });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("TorrentClaw API error (429)");
});
it("returns isError on generic error", async () => {
const client = createMockClient({
autocomplete: vi.fn().mockRejectedValue(new Error("Network timeout")),
});
const { server, getToolHandler } = createMockServer();
registerAutocomplete(server, client);
const handler = getToolHandler("autocomplete");
const result = await handler({ query: "test" });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Request failed: Network timeout");
});
it("handles non-Error throw", async () => {
const client = createMockClient({
autocomplete: vi.fn().mockRejectedValue("string error"),
});
const { server, getToolHandler } = createMockServer();
registerAutocomplete(server, client);
const handler = getToolHandler("autocomplete");
const result = await handler({ query: "test" });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Unknown error");
});
});

View file

@ -47,12 +47,12 @@ describe("get_popular tool", () => {
expect(result.content[0].text).toContain("200 clicks");
});
it("defaults limit to 10", async () => {
it("defaults limit to 12", async () => {
const getPopularMock = vi.fn().mockResolvedValue({
items: [],
total: 0,
page: 1,
pageSize: 10,
pageSize: 12,
});
const client = createMockClient({ getPopular: getPopularMock });
const { server, getToolHandler } = createMockServer();
@ -61,7 +61,7 @@ describe("get_popular tool", () => {
const handler = getToolHandler("get_popular");
await handler({});
expect(getPopularMock).toHaveBeenCalledWith(10, undefined);
expect(getPopularMock).toHaveBeenCalledWith(12, undefined, undefined);
});
it("returns isError on ApiError", async () => {

View file

@ -47,12 +47,12 @@ describe("get_recent tool", () => {
expect(result.content[0].text).toContain("[show]");
});
it("defaults limit to 10", async () => {
it("defaults limit to 12", async () => {
const getRecentMock = vi.fn().mockResolvedValue({
items: [],
total: 0,
page: 1,
pageSize: 10,
pageSize: 12,
});
const client = createMockClient({ getRecent: getRecentMock });
const { server, getToolHandler } = createMockServer();
@ -61,7 +61,7 @@ describe("get_recent tool", () => {
const handler = getToolHandler("get_recent");
await handler({});
expect(getRecentMock).toHaveBeenCalledWith(10, undefined);
expect(getRecentMock).toHaveBeenCalledWith(12, undefined, undefined);
});
it("returns isError on ApiError", async () => {

View file

@ -0,0 +1,190 @@
import { describe, it, expect, vi } from "vitest";
import { createMockServer } from "../helpers.js";
import { registerScanRequest } from "../../src/tools/scan-request.js";
import { TorrentClawClient, ApiError } from "../../src/api-client.js";
function createMockClient(overrides: Partial<TorrentClawClient> = {}) {
return {
search: vi.fn(),
autocomplete: vi.fn(),
getPopular: vi.fn(),
getRecent: vi.fn(),
getWatchProviders: vi.fn(),
getCredits: vi.fn(),
getStats: vi.fn(),
getTorrentDownloadUrl: vi.fn(),
track: vi.fn(),
submitScanRequest: vi.fn(),
getScanStatus: vi.fn(),
...overrides,
} as unknown as TorrentClawClient;
}
describe("submit_scan_request tool", () => {
it("submits scan request successfully", async () => {
const submitMock = vi.fn().mockResolvedValue({
status: "pending",
createdAt: "2026-01-01T00:00:00Z",
});
const client = createMockClient({ submitScanRequest: submitMock });
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("submit_scan_request");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
email: "test@example.com",
});
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toContain("Scan request submitted");
expect(result.content[0].text).toContain("pending");
expect(submitMock).toHaveBeenCalledWith(
"aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
"test@example.com",
);
});
it("returns isError on ApiError", async () => {
const client = createMockClient({
submitScanRequest: vi
.fn()
.mockRejectedValue(new ApiError(429, "Rate limited")),
});
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("submit_scan_request");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
email: "test@example.com",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("TorrentClaw API error (429)");
});
it("returns isError on generic error", async () => {
const client = createMockClient({
submitScanRequest: vi.fn().mockRejectedValue(new Error("Timeout")),
});
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("submit_scan_request");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
email: "test@example.com",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Request failed: Timeout");
});
it("returns isError on non-Error throw", async () => {
const client = createMockClient({
submitScanRequest: vi.fn().mockRejectedValue("string error"),
});
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("submit_scan_request");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
email: "test@example.com",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Unknown error");
});
});
describe("get_scan_status tool", () => {
it("returns scan status", async () => {
const statusMock = vi.fn().mockResolvedValue({
status: "completed",
source: "scan_request",
createdAt: "2026-01-01T00:00:00Z",
completedAt: "2026-01-01T00:05:00Z",
});
const client = createMockClient({ getScanStatus: statusMock });
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("get_scan_status");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
});
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toContain("completed");
expect(result.content[0].text).toContain("scan_request");
});
it("returns minimal status when fields are missing", async () => {
const statusMock = vi.fn().mockResolvedValue({
status: "not_scanned",
});
const client = createMockClient({ getScanStatus: statusMock });
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("get_scan_status");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
});
expect(result.content[0].text).toContain("not_scanned");
expect(result.content[0].text).not.toContain("Source:");
});
it("returns isError on ApiError", async () => {
const client = createMockClient({
getScanStatus: vi
.fn()
.mockRejectedValue(new ApiError(404, "Not found")),
});
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("get_scan_status");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("TorrentClaw API error (404)");
});
it("returns isError on generic error", async () => {
const client = createMockClient({
getScanStatus: vi.fn().mockRejectedValue(new Error("DNS failure")),
});
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("get_scan_status");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("DNS failure");
});
it("returns isError on non-Error throw", async () => {
const client = createMockClient({
getScanStatus: vi.fn().mockRejectedValue(42),
});
const { server, getToolHandler } = createMockServer();
registerScanRequest(server, client);
const handler = getToolHandler("get_scan_status");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Unknown error");
});
});

View file

@ -38,6 +38,7 @@ describe("search_content tool", () => {
genres: ["Action"],
ratingImdb: "8.8",
ratingTmdb: "8.4",
contentUrl: null,
hasTorrents: true,
torrents: [],
},
@ -79,6 +80,12 @@ describe("search_content tool", () => {
min_rating: 7,
quality: "1080p",
language: "es",
audio: "atmos",
hdr: "dolby_vision",
availability: "available",
locale: "es",
season: 1,
episode: 5,
sort: "seeders",
page: 2,
limit: 15,
@ -94,6 +101,12 @@ describe("search_content tool", () => {
min_rating: 7,
quality: "1080p",
language: "es",
audio: "atmos",
hdr: "dolby_vision",
availability: "available",
locale: "es",
season: 1,
episode: 5,
sort: "seeders",
page: 2,
limit: 15,
@ -101,11 +114,11 @@ describe("search_content tool", () => {
});
});
it("defaults limit to 10", async () => {
it("defaults limit to 20", async () => {
const searchMock = vi.fn().mockResolvedValue({
total: 0,
page: 1,
pageSize: 10,
pageSize: 20,
results: [],
});
const client = createMockClient({ search: searchMock });
@ -116,7 +129,7 @@ describe("search_content tool", () => {
await handler({ query: "test" });
expect(searchMock).toHaveBeenCalledWith(
expect.objectContaining({ limit: 10 }),
expect.objectContaining({ limit: 20 }),
);
});

View file

@ -0,0 +1,112 @@
import { describe, it, expect, vi } from "vitest";
import { createMockServer } from "../helpers.js";
import { registerTrackInteraction } from "../../src/tools/track-interaction.js";
import { TorrentClawClient, ApiError } from "../../src/api-client.js";
function createMockClient(overrides: Partial<TorrentClawClient> = {}) {
return {
search: vi.fn(),
autocomplete: vi.fn(),
getPopular: vi.fn(),
getRecent: vi.fn(),
getWatchProviders: vi.fn(),
getCredits: vi.fn(),
getStats: vi.fn(),
getTorrentDownloadUrl: vi.fn(),
track: vi.fn(),
submitScanRequest: vi.fn(),
getScanStatus: vi.fn(),
...overrides,
} as unknown as TorrentClawClient;
}
describe("track_interaction tool", () => {
it("tracks magnet interaction successfully", async () => {
const trackMock = vi.fn().mockResolvedValue({ ok: true });
const client = createMockClient({ track: trackMock });
const { server, getToolHandler } = createMockServer();
registerTrackInteraction(server, client);
const handler = getToolHandler("track_interaction");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
action: "magnet",
});
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toContain("Tracked magnet");
expect(trackMock).toHaveBeenCalledWith(
"aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
"magnet",
);
});
it("lowercases info_hash", async () => {
const trackMock = vi.fn().mockResolvedValue({ ok: true });
const client = createMockClient({ track: trackMock });
const { server, getToolHandler } = createMockServer();
registerTrackInteraction(server, client);
const handler = getToolHandler("track_interaction");
await handler({
info_hash: "AAF1E71C0A0E3B1C0F1A2B3C4D5E6F7A8B9C0D1E",
action: "copy",
});
expect(trackMock).toHaveBeenCalledWith(
"aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
"copy",
);
});
it("returns isError on ApiError", async () => {
const client = createMockClient({
track: vi.fn().mockRejectedValue(new ApiError(500, "Server error")),
});
const { server, getToolHandler } = createMockServer();
registerTrackInteraction(server, client);
const handler = getToolHandler("track_interaction");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
action: "torrent_download",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("TorrentClaw API error (500)");
});
it("returns isError on generic error", async () => {
const client = createMockClient({
track: vi.fn().mockRejectedValue(new Error("Network failure")),
});
const { server, getToolHandler } = createMockServer();
registerTrackInteraction(server, client);
const handler = getToolHandler("track_interaction");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
action: "magnet",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Request failed: Network failure");
});
it("returns isError on non-Error throw", async () => {
const client = createMockClient({
track: vi.fn().mockRejectedValue("string error"),
});
const { server, getToolHandler } = createMockServer();
registerTrackInteraction(server, client);
const handler = getToolHandler("track_interaction");
const result = await handler({
info_hash: "aaf1e71c0a0e3b1c0f1a2b3c4d5e6f7a8b9c0d1e",
action: "magnet",
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Unknown error");
});
});