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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
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");
});
it("writes full transcript to /tmp/dispatch/youtube-transcribe/{video_id}.txt when truncated", async () => {
const longText = "x".repeat(60_000);
const client = makeStubClient(async () => ({
status: "completed",
video_id: "vid5",
full_text: longText,
segments: [],
}));
let writtenPath = "";
let writtenContent = "";
const tool = createYoutubeTranscriptTool({
client,
outputCap: 1000,
writeFile: (path, content) => {
writtenPath = path;
writtenContent = content;
},
});
const result = await tool.execute({ url: "https://youtu.be/vid5" }, stubCtx());
expect(writtenPath).toBe("/tmp/dispatch/youtube-transcribe/vid5.txt");
expect(writtenContent).toContain("x".repeat(60_000));
expect(result.content).toContain("/tmp/dispatch/youtube-transcribe/vid5.txt");
expect(result.content).toContain("use read_file to access it");
expect(result.content.length).toBeLessThan(writtenContent.length);
});
it("writes transcript to file even when not truncated", async () => {
const client = makeStubClient(async () => ({
status: "completed",
video_id: "vid6",
full_text: "short transcript",
segments: [{ text: "short transcript", start: 0, duration: 2 }],
}));
let writtenPath = "";
let writtenContent = "";
const tool = createYoutubeTranscriptTool({
client,
writeFile: (path, content) => {
writtenPath = path;
writtenContent = content;
},
});
const result = await tool.execute({ url: "https://youtu.be/vid6" }, stubCtx());
expect(writtenPath).toBe("/tmp/dispatch/youtube-transcribe/vid6.txt");
expect(writtenContent).toBe(result.content);
});
});
|