summaryrefslogtreecommitdiffhomepage
path: root/packages/tool-youtube-transcript/src/client.test.ts
blob: a33f44a449636b1a0b6f9295bff9e5d5b5639cb4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { describe, expect, it } from "vitest";
import { createTranscriptClient, type FetchLike } from "./client.js";

function jsonResponse(body: unknown, status = 200): Response {
	return new Response(JSON.stringify(body), {
		status,
		headers: { "Content-Type": "application/json" },
	});
}

interface CapturedCall {
	url: string;
	method?: string | undefined;
}

/** Builds a fake fetch that returns scripted responses in order, capturing each call. */
function makeFetch(responses: Response[]): { fetchFn: FetchLike; calls: CapturedCall[] } {
	const calls: CapturedCall[] = [];
	let i = 0;
	const fetchFn: FetchLike = (async (input: string | URL | Request, init?: RequestInit) => {
		const url =
			typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
		calls.push({ url, method: init?.method });
		return responses[i++] ?? jsonResponse({});
	}) as unknown as FetchLike;
	return { fetchFn, calls };
}

const BASE = "http://test-transcriber.local";
const signal = (): AbortSignal => new AbortController().signal;

describe("createTranscriptClient.getTranscript", () => {
	it("sends GET /api/transcript?url=...", async () => {
		const { fetchFn, calls } = makeFetch([
			jsonResponse({
				status: "completed",
				video_id: "v1",
				full_text: "",
				segments: [],
			}),
		]);
		const client = createTranscriptClient({ baseUrl: BASE, fetchFn });
		await client.getTranscript("https://youtu.be/v1", signal());

		const call = calls[0];
		if (!call) throw new Error("no call captured");
		expect(call.url).toBe(
			`${BASE}/api/transcript?url=${encodeURIComponent("https://youtu.be/v1")}`,
		);
		expect(call.method).toBe("GET");
	});

	it("returns completed response", async () => {
		const body = {
			status: "completed" as const,
			video_id: "v1",
			full_text: "hi",
			segments: [{ text: "hi", start: 0, duration: 1 }],
		};
		const { fetchFn } = makeFetch([jsonResponse(body)]);
		const client = createTranscriptClient({ baseUrl: BASE, fetchFn });
		const result = await client.getTranscript("https://youtu.be/v1", signal());
		expect(result).toEqual(body);
	});

	it("returns queued response", async () => {
		const body = {
			status: "queued" as const,
			video_id: "v1",
			position: 2,
			estimated_seconds: 60,
		};
		const { fetchFn } = makeFetch([jsonResponse(body)]);
		const client = createTranscriptClient({ baseUrl: BASE, fetchFn });
		const result = await client.getTranscript("https://youtu.be/v1", signal());
		expect(result).toEqual(body);
	});

	it("returns failed response", async () => {
		const body = {
			status: "failed" as const,
			video_id: "v1",
			error: "boom",
			error_type: "DownloadError",
		};
		const { fetchFn } = makeFetch([jsonResponse(body)]);
		const client = createTranscriptClient({ baseUrl: BASE, fetchFn });
		const result = await client.getTranscript("https://youtu.be/v1", signal());
		expect(result).toEqual(body);
	});

	it("throws on HTTP error", async () => {
		const { fetchFn } = makeFetch([
			new Response("not found", { status: 404, statusText: "Not Found" }),
		]);
		const client = createTranscriptClient({ baseUrl: BASE, fetchFn });
		await expect(client.getTranscript("https://youtu.be/v1", signal())).rejects.toThrow("HTTP 404");
	});

	it("throws on timeout", async () => {
		const fetchFn: FetchLike = ((_input: string | URL | Request, init?: RequestInit) =>
			new Promise<Response>((_resolve, reject) => {
				const sig = init?.signal;
				if (!sig) return;
				sig.addEventListener("abort", () => {
					const err = new Error("aborted");
					err.name = "AbortError";
					reject(err);
				});
			})) as unknown as FetchLike;
		const client = createTranscriptClient({ baseUrl: BASE, fetchFn, timeoutMs: 10 });
		await expect(client.getTranscript("https://youtu.be/v1", signal())).rejects.toThrow(
			"timed out",
		);
	});

	it("respects abort signal", async () => {
		const controller = new AbortController();
		const fetchFn: FetchLike = ((_input: string | URL | Request, init?: RequestInit) =>
			new Promise<Response>((_resolve, reject) => {
				const sig = init?.signal;
				if (!sig) return;
				sig.addEventListener("abort", () => {
					const err = new Error("aborted");
					err.name = "AbortError";
					reject(err);
				});
			})) as unknown as FetchLike;
		const client = createTranscriptClient({ baseUrl: BASE, fetchFn, timeoutMs: 30_000 });
		const promise = client.getTranscript("https://youtu.be/v1", controller.signal);
		controller.abort();
		await expect(promise).rejects.toThrow("aborted");
	});
});