import { beforeEach, describe, expect, it, vi } from "vitest";
import { EventBusService } from "../events/EventBusService";
import { EventType } from "../events/types";
import { PipelineClient } from "./PipelineClient";
// Mock tRPC client factory
const mockClient: any = {
ping: { query: vi.fn() },
enqueueScrapeJob: { mutate: vi.fn() },
enqueueRefreshJob: { mutate: vi.fn() },
getJob: { query: vi.fn() },
getJobs: { query: vi.fn() },
cancelJob: { mutate: vi.fn() },
clearCompletedJobs: { mutate: vi.fn() },
};
// Mock WebSocket client
const mockWsClient = {
close: vi.fn(),
};
vi.mock("@trpc/client", () => {
return {
createTRPCProxyClient: () => mockClient,
httpBatchLink: vi.fn(),
createWSClient: vi.fn(() => mockWsClient),
wsLink: vi.fn(),
splitLink: vi.fn(),
} as any;
});
describe("PipelineClient", () => {
let client: PipelineClient;
let eventBus: EventBusService;
const serverUrl = "http://localhost:8080";
beforeEach(() => {
vi.resetAllMocks();
// Reset default mock behaviors
mockClient.ping.query.mockResolvedValue({ status: "ok" });
mockClient.enqueueScrapeJob.mutate.mockResolvedValue({ jobId: "job-123" });
mockClient.enqueueRefreshJob.mutate.mockResolvedValue({ jobId: "job-456" });
mockClient.getJob.query.mockResolvedValue(undefined);
mockClient.getJobs.query.mockResolvedValue({ jobs: [] });
mockClient.cancelJob.mutate.mockResolvedValue({ success: true });
mockClient.clearCompletedJobs.mutate.mockResolvedValue({ count: 5 });
eventBus = new EventBusService();
client = new PipelineClient(serverUrl, eventBus);
});
describe("start", () => {
it("should succeed when external worker is healthy", async () => {
await expect(client.start()).resolves.toBeUndefined();
expect(mockClient.ping.query).toHaveBeenCalled();
});
it("should fail when external worker is unreachable", async () => {
mockClient.ping.query.mockRejectedValueOnce(new Error("Connection refused"));
await expect(client.start()).rejects.toThrow(
"Failed to connect to external worker",
);
});
});
describe("enqueueScrapeJob", () => {
it("should delegate job creation to external API", async () => {
const mockJobId = "job-123";
mockClient.enqueueScrapeJob.mutate.mockResolvedValueOnce({ jobId: mockJobId });
const jobId = await client.enqueueScrapeJob("react", "18.0.0", {
url: "https://react.dev",
library: "react",
version: "18.0.0",
});
expect(jobId).toBe(mockJobId);
expect(mockClient.enqueueScrapeJob.mutate).toHaveBeenCalledWith({
library: "react",
version: "18.0.0",
options: {
url: "https://react.dev",
library: "react",
version: "18.0.0",
},
});
});
it("should handle API errors gracefully", async () => {
mockClient.enqueueScrapeJob.mutate.mockRejectedValueOnce(new Error("Bad request"));
await expect(client.enqueueScrapeJob("invalid", null, {} as any)).rejects.toThrow(
"Failed to enqueue job: Bad request",
);
});
});
describe("waitForJobCompletion", () => {
it("should resolve when job completes successfully via event bus", async () => {
const jobId = "job-123";
// Start waiting
const waitPromise = client.waitForJobCompletion(jobId);
// Simulate event bus emitting status change
setTimeout(() => {
eventBus.emit(EventType.JOB_STATUS_CHANGE, {
id: jobId,
status: "completed",
library: "test",
version: null,
} as any);
}, 10);
await expect(waitPromise).resolves.toBeUndefined();
});
it("should throw error when job fails via event bus", async () => {
const jobId = "job-123";
// Start waiting
const waitPromise = client.waitForJobCompletion(jobId);
// Simulate event bus emitting failure
setTimeout(() => {
eventBus.emit(EventType.JOB_STATUS_CHANGE, {
id: jobId,
status: "failed",
library: "test",
version: null,
error: { message: "Scraping failed" },
} as any);
}, 10);
await expect(waitPromise).rejects.toThrow("Scraping failed");
});
it("should ignore events for other jobs", async () => {
const jobId = "job-123";
// Start waiting
const waitPromise = client.waitForJobCompletion(jobId);
// Emit events for different job (should be ignored)
setTimeout(() => {
eventBus.emit(EventType.JOB_STATUS_CHANGE, {
id: "other-job",
status: "completed",
library: "test",
version: null,
} as any);
}, 10);
// Emit event for our job
setTimeout(() => {
eventBus.emit(EventType.JOB_STATUS_CHANGE, {
id: jobId,
status: "completed",
library: "test",
version: null,
} as any);
}, 20);
await expect(waitPromise).resolves.toBeUndefined();
});
});
describe("getJob", () => {
it("should return undefined for non-existent job", async () => {
mockClient.getJob.query.mockResolvedValueOnce(undefined);
const result = await client.getJob("non-existent");
expect(result).toBeUndefined();
});
it("should return job data for existing job", async () => {
// Mock returns a Date object (simulating superjson deserialization)
const mockJob = {
id: "job-123",
status: "completed",
createdAt: new Date("2023-01-01T00:00:00.000Z"),
startedAt: null,
finishedAt: null,
updatedAt: undefined,
};
mockClient.getJob.query.mockResolvedValueOnce(mockJob);
const result = await client.getJob("job-123");
expect(result).toEqual(mockJob);
});
});
});