summaryrefslogtreecommitdiffhomepage
path: root/packages/tool-youtube-transcript/src/tool.test.ts
blob: 4fcb63e52f52ee6246a8ed8ad9d3be1a95e9853b (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
import { createLogger, type LogRecord, type ToolExecuteContext } from "@dispatch/kernel";
import { describe, expect, it } from "vitest";
import type { TranscriptClient } from "./client.js";
import type { TranscriptResponse } from "./format.js";
import { createYoutubeTranscriptTool } from "./tool.js";

function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext {
	return {
		toolCallId: "test-call-1",
		onOutput: () => {},
		signal: new AbortController().signal,
		log: createLogger(
			{ extensionId: "test" },
			{ emit: () => {} },
			{ now: () => 0, newId: () => "id" },
		),
		...overrides,
	};
}

function makeStubClient(
	responder: (url: string, signal: AbortSignal) => Promise<TranscriptResponse>,
): TranscriptClient {
	return { getTranscript: (url, signal) => responder(url, signal) };
}

describe("youtube_transcript", () => {
	it("returns formatted transcript on completed", async () => {
		const client = makeStubClient(async () => ({
			status: "completed",
			video_id: "vid1",
			full_text: "Hello world.",
			segments: [{ text: "Hello world.", start: 0, duration: 2 }],
		}));
		const tool = createYoutubeTranscriptTool({ client });
		const result = await tool.execute({ url: "https://youtu.be/vid1" }, stubCtx());
		expect(result.isError).toBe(undefined);
		expect(result.content).toContain("## Transcript for https://youtu.be/vid1");
		expect(result.content).toContain("**Video ID:** vid1");
		expect(result.content).toContain("Hello world.");
		expect(result.content).toContain("[0:00] Hello world.");
	});

	it("returns queued message with status and ETA", async () => {
		const client = makeStubClient(async () => ({
			status: "queued",
			video_id: "vid2",
			position: 1,
			estimated_seconds: 30,
		}));
		const tool = createYoutubeTranscriptTool({ client });
		const result = await tool.execute({ url: "https://youtu.be/vid2" }, stubCtx());
		expect(result.isError).toBe(undefined);
		expect(result.content).toContain("status: queued");
		expect(result.content).toContain("queue position: 1");
		expect(result.content).toContain("in ~30s");
		expect(result.content).toContain("https://youtu.be/vid2");
	});

	it("returns failed message", async () => {
		const client = makeStubClient(async () => ({
			status: "failed",
			video_id: "vid3",
			error: "Video unavailable",
			error_type: "NotFoundError",
		}));
		const tool = createYoutubeTranscriptTool({ client });
		const result = await tool.execute({ url: "https://youtu.be/vid3" }, stubCtx());
		expect(result.isError).toBe(undefined);
		expect(result.content).toContain("Error type: NotFoundError");
		expect(result.content).toContain("Details: Video unavailable");
	});

	it("validation error returns isError", async () => {
		const client = makeStubClient(async () => {
			throw new Error("should not be called");
		});
		const tool = createYoutubeTranscriptTool({ client });
		const result = await tool.execute({ url: "" }, stubCtx());
		expect(result.isError).toBe(true);
		expect(result.content).toContain("url");
	});

	it("uses conversationId from ctx (not required but passed through)", async () => {
		const records: LogRecord[] = [];
		const client = makeStubClient(async () => ({
			status: "completed",
			video_id: "vid4",
			full_text: "text",
			segments: [],
		}));
		const tool = createYoutubeTranscriptTool({ client });
		const ctx = stubCtx({
			conversationId: "conv-xyz",
			log: createLogger(
				{ extensionId: "test", conversationId: "conv-xyz" },
				{
					emit: (r) => {
						records.push(r);
					},
				},
				{ now: () => 0, newId: () => "id" },
			),
		});
		const result = await tool.execute({ url: "https://youtu.be/vid4" }, ctx);
		expect(result.isError).toBe(undefined);
		expect(result.content).toContain("## Transcript for");
		// The execute span flows through ctx.log, which carries conversationId.
		const spanOpen = records.find((r) => r.kind === "span-open");
		expect(spanOpen).toBeDefined();
		expect(spanOpen?.conversationId).toBe("conv-xyz");
	});
});