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");
});
});
|