From dfb3a61afa545b67b85dbefe6b217affd14c16a7 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 21 Jun 2026 14:58:38 +0900 Subject: feat(tool-youtube-transcript): YouTube transcription tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../tool-youtube-transcript/src/format.test.ts | 121 +++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/tool-youtube-transcript/src/format.test.ts (limited to 'packages/tool-youtube-transcript/src/format.test.ts') 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"); + }); +}); -- cgit v1.2.3