summaryrefslogtreecommitdiffhomepage
path: root/packages/tool-youtube-transcript/src/format.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 14:58:38 +0900
committerAdam Malczewski <[email protected]>2026-06-21 14:58:38 +0900
commitdfb3a61afa545b67b85dbefe6b217affd14c16a7 (patch)
treefbe0d18323136cc19d971e18f0801428bcd2e4a7 /packages/tool-youtube-transcript/src/format.test.ts
parentd56fe9cf64719bb330c17b2daee58c0bafa057c9 (diff)
downloaddispatch-dfb3a61afa545b67b85dbefe6b217affd14c16a7.tar.gz
dispatch-dfb3a61afa545b67b85dbefe6b217affd14c16a7.zip
feat(tool-youtube-transcript): YouTube transcription tool
New standard tool extension backed by a self-hosted transcriber service (http://100.102.55.49:41090, Tailscale, no API key). One tool youtube_transcript — fetches transcripts for YouTube videos. Returns completed (full text + timestamped segments), queued/processing (position + ETA + .youtube_subtitles_pending retry convention), or failed (error). Pure core: validateUrl + format* functions + truncateOutput. Injected edge: TranscriptClient (injectable fetchFn, AbortSignal.any for cancellation). concurrencySafe true, capabilities network. 30 tests. Verified: tsc EXIT 0, 1152 vitest, biome clean (327 files). Boot smoke clean.
Diffstat (limited to 'packages/tool-youtube-transcript/src/format.test.ts')
-rw-r--r--packages/tool-youtube-transcript/src/format.test.ts121
1 files changed, 121 insertions, 0 deletions
diff --git a/packages/tool-youtube-transcript/src/format.test.ts b/packages/tool-youtube-transcript/src/format.test.ts
new file mode 100644
index 0000000..00b5d05
--- /dev/null
+++ b/packages/tool-youtube-transcript/src/format.test.ts
@@ -0,0 +1,121 @@
+import { describe, expect, it } from "vitest";
+import {
+ type CompletedResponse,
+ type FailedResponse,
+ formatCompleted,
+ formatFailed,
+ formatQueued,
+ formatTimestamp,
+ type QueuedResponse,
+ truncateOutput,
+} from "./format.js";
+
+describe("formatTimestamp", () => {
+ it("formats seconds as m:ss", () => {
+ expect(formatTimestamp(65)).toBe("1:05");
+ expect(formatTimestamp(723)).toBe("12:03");
+ });
+
+ it("handles zero", () => {
+ expect(formatTimestamp(0)).toBe("0:00");
+ });
+
+ it("handles minutes over 60", () => {
+ // 3700s = 61m40s; 4530s = 75m30s.
+ expect(formatTimestamp(3700)).toBe("61:40");
+ expect(formatTimestamp(4530)).toBe("75:30");
+ });
+});
+
+describe("formatCompleted", () => {
+ it("formats markdown with full text + segments", () => {
+ const data: CompletedResponse = {
+ status: "completed",
+ video_id: "vid123",
+ full_text: "Hello world.",
+ segments: [
+ { text: "Hello world.", start: 0, duration: 2.5 },
+ { text: "Second line.", start: 65, duration: 1.0 },
+ ],
+ };
+ const out = formatCompleted("https://youtu.be/vid123", data);
+ const expected = [
+ "## Transcript for https://youtu.be/vid123",
+ "**Video ID:** vid123",
+ "",
+ "### Full text",
+ "",
+ "Hello world.",
+ "",
+ "### Timestamped segments",
+ "",
+ "[0:00] Hello world.",
+ "[1:05] Second line.",
+ ].join("\n");
+ expect(out).toBe(expected);
+ });
+});
+
+describe("formatQueued", () => {
+ it("returns status + position + estimated time + pending file instruction", () => {
+ const data: QueuedResponse = {
+ status: "queued",
+ video_id: "vid456",
+ position: 3,
+ estimated_seconds: 120,
+ };
+ const now = () => 1_000_000_000_000;
+ const out = formatQueued("https://youtu.be/vid456", data, now);
+ const expectedTime = new Date(1_000_000_000_000 + 120_000).toISOString();
+ const expected =
+ `Transcript not yet available (status: queued, queue position: 3).\n` +
+ `Estimated available at: ${expectedTime} (in ~120s).\n` +
+ `You must append this video URL to .youtube_subtitles_pending in the current ` +
+ `working directory: https://youtu.be/vid456`;
+ expect(out).toBe(expected);
+ });
+
+ it("includes the processing status + pending-file instruction", () => {
+ const data: QueuedResponse = {
+ status: "processing",
+ video_id: "vid457",
+ position: 0,
+ estimated_seconds: 45.5,
+ };
+ const out = formatQueued("https://youtu.be/vid457", data, () => 0);
+ expect(out).toContain("status: processing");
+ expect(out).toContain("queue position: 0");
+ expect(out).toContain("(in ~46s)");
+ expect(out).toContain(".youtube_subtitles_pending");
+ expect(out).toContain("https://youtu.be/vid457");
+ });
+});
+
+describe("formatFailed", () => {
+ it("returns error type + details", () => {
+ const data: FailedResponse = {
+ status: "failed",
+ video_id: "vid789",
+ error: "Video unavailable",
+ error_type: "NotFoundError",
+ };
+ expect(formatFailed(data)).toBe(
+ "Transcript fetch failed. Error type: NotFoundError. Details: Video unavailable",
+ );
+ });
+});
+
+describe("truncateOutput", () => {
+ it("truncates with notice when over cap", () => {
+ const output = "a".repeat(100);
+ const result = truncateOutput(output, 50);
+ expect(result).toContain("a".repeat(50));
+ expect(result).toContain("[Output truncated: exceeded 50 characters]");
+ expect(result.length).toBeLessThan(output.length + 100);
+ });
+
+ it("returns as-is when under cap", () => {
+ expect(truncateOutput("short", 100)).toBe("short");
+ expect(truncateOutput("exact", 5)).toBe("exact");
+ });
+});