@xynogen/pix-9router 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/data.test.ts +0 -122
- package/src/format.test.ts +0 -441
- package/src/provider.test.ts +0 -213
- package/src/render.test.ts +0 -218
- package/src/tool-schema.test.ts +0 -60
- package/src/transcribe.test.ts +0 -517
package/src/transcribe.test.ts
DELETED
|
@@ -1,517 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { chmod, readFile, rm } from "node:fs/promises";
|
|
4
|
-
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
|
|
7
|
-
import registerTranscribe, {
|
|
8
|
-
buildTranscriptionResult,
|
|
9
|
-
mimeType,
|
|
10
|
-
parseTranscriptionResponse,
|
|
11
|
-
resolveOutputPath,
|
|
12
|
-
validateOutputPath,
|
|
13
|
-
writeTranscriptionFile,
|
|
14
|
-
} from "./transcribe.js";
|
|
15
|
-
|
|
16
|
-
// ── mimeType ─────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
describe("mimeType", () => {
|
|
19
|
-
it("returns audio/mpeg for .mp3", () => {
|
|
20
|
-
expect(mimeType("recording.mp3")).toBe("audio/mpeg");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("returns audio/mpeg for .mpga", () => {
|
|
24
|
-
expect(mimeType("recording.mpga")).toBe("audio/mpeg");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("returns audio/wav for .wav", () => {
|
|
28
|
-
expect(mimeType("recording.wav")).toBe("audio/wav");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("returns audio/flac for .flac", () => {
|
|
32
|
-
expect(mimeType("music.flac")).toBe("audio/flac");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("returns audio/ogg for .ogg", () => {
|
|
36
|
-
expect(mimeType("voice.ogg")).toBe("audio/ogg");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns audio/mp4 for .m4a", () => {
|
|
40
|
-
expect(mimeType("podcast.m4a")).toBe("audio/mp4");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns audio/webm for .webm", () => {
|
|
44
|
-
expect(mimeType("clip.webm")).toBe("audio/webm");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("returns audio/mp4 for .mp4", () => {
|
|
48
|
-
expect(mimeType("video.mp4")).toBe("audio/mp4");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("is case-insensitive on extension", () => {
|
|
52
|
-
expect(mimeType("LOUD.MP3")).toBe("audio/mpeg");
|
|
53
|
-
expect(mimeType("file.WAV")).toBe("audio/wav");
|
|
54
|
-
expect(mimeType("track.Flac")).toBe("audio/flac");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("returns application/octet-stream for unknown extension", () => {
|
|
58
|
-
expect(mimeType("archive.zip")).toBe("application/octet-stream");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("returns application/octet-stream for no extension", () => {
|
|
62
|
-
expect(mimeType("noext")).toBe("application/octet-stream");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("handles paths with directories", () => {
|
|
66
|
-
expect(mimeType("/home/user/audio/meeting.mp3")).toBe("audio/mpeg");
|
|
67
|
-
expect(mimeType("./recordings/call.wav")).toBe("audio/wav");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("handles dotfiles with audio extensions", () => {
|
|
71
|
-
expect(mimeType(".hidden.mp3")).toBe("audio/mpeg");
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// ── parseTranscriptionResponse ───────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
describe("parseTranscriptionResponse", () => {
|
|
78
|
-
it("extracts text from JSON response", () => {
|
|
79
|
-
const raw = JSON.stringify({ text: "Hello, world!" });
|
|
80
|
-
expect(parseTranscriptionResponse(raw)).toBe("Hello, world!");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("extracts text from JSON with extra fields", () => {
|
|
84
|
-
const raw = JSON.stringify({
|
|
85
|
-
text: "Transcribed content",
|
|
86
|
-
duration: 5.2,
|
|
87
|
-
language: "en",
|
|
88
|
-
});
|
|
89
|
-
expect(parseTranscriptionResponse(raw)).toBe("Transcribed content");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("returns raw JSON string when text field is missing", () => {
|
|
93
|
-
const raw = JSON.stringify({ result: "no text field" });
|
|
94
|
-
expect(parseTranscriptionResponse(raw)).toBe(raw);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("returns plain text as-is when not JSON", () => {
|
|
98
|
-
expect(parseTranscriptionResponse("Just plain text")).toBe("Just plain text");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("returns empty string from JSON with empty text", () => {
|
|
102
|
-
const raw = JSON.stringify({ text: "" });
|
|
103
|
-
// ?? only triggers on null/undefined, not empty string — so "" is returned
|
|
104
|
-
expect(parseTranscriptionResponse(raw)).toBe("");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("returns raw when text is null", () => {
|
|
108
|
-
const raw = JSON.stringify({ text: null });
|
|
109
|
-
// null ?? raw → raw
|
|
110
|
-
expect(parseTranscriptionResponse(raw)).toBe(raw);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("returns raw when text is undefined (absent from parsed object)", () => {
|
|
114
|
-
const raw = JSON.stringify({ other: "stuff" });
|
|
115
|
-
// parsed.text is undefined → undefined ?? raw → raw
|
|
116
|
-
expect(parseTranscriptionResponse(raw)).toBe(raw);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("handles multiline transcription", () => {
|
|
120
|
-
const text = "Line one.\nLine two.\nLine three.";
|
|
121
|
-
const raw = JSON.stringify({ text });
|
|
122
|
-
expect(parseTranscriptionResponse(raw)).toBe(text);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("handles unicode in transcription", () => {
|
|
126
|
-
const text = "日本語のテスト 🎙️";
|
|
127
|
-
const raw = JSON.stringify({ text });
|
|
128
|
-
expect(parseTranscriptionResponse(raw)).toBe(text);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("handles broken JSON gracefully", () => {
|
|
132
|
-
expect(parseTranscriptionResponse("{broken")).toBe("{broken");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("handles empty string input", () => {
|
|
136
|
-
expect(parseTranscriptionResponse("")).toBe("");
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ── resolveOutputPath ────────────────────────────────────────────────────────
|
|
141
|
-
|
|
142
|
-
describe("resolveOutputPath", () => {
|
|
143
|
-
it("keeps absolute paths as-is", () => {
|
|
144
|
-
const abs = "/tmp/foo/bar.txt";
|
|
145
|
-
expect(resolveOutputPath(abs)).toBe(abs);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("resolves relative paths against cwd", () => {
|
|
149
|
-
const rel = "transcripts/out.txt";
|
|
150
|
-
const result = resolveOutputPath(rel);
|
|
151
|
-
expect(result.endsWith("transcripts/out.txt")).toBe(true);
|
|
152
|
-
expect(result.startsWith(process.cwd())).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// ── writeTranscriptionFile ───────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
describe("writeTranscriptionFile", () => {
|
|
159
|
-
const tmpRoot = mkdtempSync(join(tmpdir(), "pix-transcribe-test-"));
|
|
160
|
-
|
|
161
|
-
it("writes text to the given file path", async () => {
|
|
162
|
-
const file = join(tmpRoot, "simple.txt");
|
|
163
|
-
const abs = await writeTranscriptionFile(file, "hello world");
|
|
164
|
-
expect(abs).toBe(file);
|
|
165
|
-
expect(await readFile(file, "utf-8")).toBe("hello world");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("creates parent directories recursively", async () => {
|
|
169
|
-
const file = join(tmpRoot, "deep", "nested", "dir", "out.txt");
|
|
170
|
-
const abs = await writeTranscriptionFile(file, "deep content");
|
|
171
|
-
expect(abs).toBe(file);
|
|
172
|
-
expect(await readFile(file, "utf-8")).toBe("deep content");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("preserves full unicode without truncation", async () => {
|
|
176
|
-
const text = `日本語のテスト\n${"x".repeat(100_000)}`;
|
|
177
|
-
const file = join(tmpRoot, "huge.txt");
|
|
178
|
-
await writeTranscriptionFile(file, text);
|
|
179
|
-
const got = await readFile(file, "utf-8");
|
|
180
|
-
expect(got.length).toBe(text.length);
|
|
181
|
-
expect(got).toBe(text);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("overwrites an existing file", async () => {
|
|
185
|
-
const file = join(tmpRoot, "overwrite.txt");
|
|
186
|
-
await writeTranscriptionFile(file, "first");
|
|
187
|
-
await writeTranscriptionFile(file, "second");
|
|
188
|
-
expect(await readFile(file, "utf-8")).toBe("second");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// cleanup tmp root
|
|
192
|
-
it("cleanup", async () => {
|
|
193
|
-
await rm(tmpRoot, { recursive: true, force: true });
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// ── buildTranscriptionResult ─────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
describe("buildTranscriptionResult", () => {
|
|
200
|
-
const tmpRoot = mkdtempSync(join(tmpdir(), "pix-transcribe-result-"));
|
|
201
|
-
|
|
202
|
-
it("returns inline text (truncated at 50_000) when no output_file is set", async () => {
|
|
203
|
-
const text = "a".repeat(60_000);
|
|
204
|
-
const result = await buildTranscriptionResult(text, "dg/nova-3", "api", undefined);
|
|
205
|
-
expect(result.content).toHaveLength(1);
|
|
206
|
-
expect(result.content[0]?.type).toBe("text");
|
|
207
|
-
expect(result.content[0]?.text.length).toBe(50_000);
|
|
208
|
-
expect(result.details).toMatchObject({
|
|
209
|
-
_type: "transcribeResult",
|
|
210
|
-
outcome: "success",
|
|
211
|
-
model: "dg/nova-3",
|
|
212
|
-
source: "api",
|
|
213
|
-
chars: 60_000,
|
|
214
|
-
truncated: true,
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("returns inline text (untruncated) when short and no output_file", async () => {
|
|
219
|
-
const text = "short transcript";
|
|
220
|
-
const result = await buildTranscriptionResult(text, "dg/nova-3", "api", undefined);
|
|
221
|
-
expect(result.content[0]?.text).toBe("short transcript");
|
|
222
|
-
expect(result.details.chars).toBe(text.length);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("writes full text to file and returns short path summary when output_file is set", async () => {
|
|
226
|
-
const text = "a".repeat(100_000); // way over 50k
|
|
227
|
-
const file = join(tmpRoot, "result-a.txt");
|
|
228
|
-
const result = await buildTranscriptionResult(text, "dg/nova-3", "api", file);
|
|
229
|
-
|
|
230
|
-
// content is a short summary, not the full text
|
|
231
|
-
expect(result.content).toHaveLength(1);
|
|
232
|
-
const summary = result.content[0]?.text ?? "";
|
|
233
|
-
expect(summary.length).toBeLessThan(200);
|
|
234
|
-
expect(summary).toContain("100000");
|
|
235
|
-
expect(summary).toContain(file);
|
|
236
|
-
|
|
237
|
-
// full text was written verbatim
|
|
238
|
-
const onDisk = await readFile(file, "utf-8");
|
|
239
|
-
expect(onDisk.length).toBe(100_000);
|
|
240
|
-
expect(onDisk).toBe(text);
|
|
241
|
-
|
|
242
|
-
// details includes resolved absolute path
|
|
243
|
-
expect(result.details.output_path).toBe(file);
|
|
244
|
-
expect(result.details.chars).toBe(100_000);
|
|
245
|
-
expect(result.details.source).toBe("api");
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("passes through curl-fallback source label", async () => {
|
|
249
|
-
const text = "hi";
|
|
250
|
-
const result = await buildTranscriptionResult(text, "dg/nova-3", "curl-fallback", undefined);
|
|
251
|
-
expect(result.details.source).toBe("curl-fallback");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("relative output_file is resolved against cwd and created", async () => {
|
|
255
|
-
const text = "relative path content";
|
|
256
|
-
const relDir = join(tmpRoot, "rel", "sub");
|
|
257
|
-
const relFile = join(relDir, "out.txt");
|
|
258
|
-
const result = await buildTranscriptionResult(text, "dg/nova-3", "api", relFile);
|
|
259
|
-
|
|
260
|
-
expect(result.details.output_path).toBe(relFile);
|
|
261
|
-
expect(await readFile(relFile, "utf-8")).toBe("relative path content");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("cleanup", async () => {
|
|
265
|
-
await rm(tmpRoot, { recursive: true, force: true });
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// ── validateOutputPath ───────────────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
describe("validateOutputPath", () => {
|
|
272
|
-
const tmpRoot = mkdtempSync(join(tmpdir(), "pix-validate-"));
|
|
273
|
-
|
|
274
|
-
it("accepts a fresh path under a writable parent", async () => {
|
|
275
|
-
const result = await validateOutputPath(join(tmpRoot, "fresh.txt"));
|
|
276
|
-
expect(result.ok).toBe(true);
|
|
277
|
-
if (result.ok) expect(result.path).toBe(join(tmpRoot, "fresh.txt"));
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("accepts a fresh path several levels deep", async () => {
|
|
281
|
-
const result = await validateOutputPath(join(tmpRoot, "a", "b", "c", "deep.txt"));
|
|
282
|
-
expect(result.ok).toBe(true);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("rejects null bytes", async () => {
|
|
286
|
-
const result = await validateOutputPath(join(tmpRoot, "x\0y.txt"));
|
|
287
|
-
expect(result.ok).toBe(false);
|
|
288
|
-
if (!result.ok) expect(result.reason).toContain("null byte");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it.each([
|
|
292
|
-
"/etc",
|
|
293
|
-
"/etc/passwd",
|
|
294
|
-
"/etc/cron.daily/x",
|
|
295
|
-
"/proc/cpuinfo",
|
|
296
|
-
"/sys/kernel/x",
|
|
297
|
-
"/boot/efi/x",
|
|
298
|
-
`${homedir()}/.ssh/authorized_keys`,
|
|
299
|
-
`${homedir()}/.aws/credentials`,
|
|
300
|
-
`${homedir()}/.gnupg/gpg.conf`,
|
|
301
|
-
`${homedir()}/.config/gh/hosts.yml`,
|
|
302
|
-
])("rejects sensitive prefix %s", async (bad) => {
|
|
303
|
-
const result = await validateOutputPath(bad);
|
|
304
|
-
expect(result.ok).toBe(false);
|
|
305
|
-
if (!result.ok) {
|
|
306
|
-
expect(result.reason).toMatch(/refusing to write/);
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("rejects when no ancestor directory exists at all", async () => {
|
|
311
|
-
// Deep path under a non-existent tree with no existing ancestor
|
|
312
|
-
const result = await validateOutputPath("/no-such-root-xyz/abc/def/x.txt");
|
|
313
|
-
expect(result.ok).toBe(false);
|
|
314
|
-
if (!result.ok) expect(result.reason).toMatch(/no existing ancestor/);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("rejects when parent is not writable", async () => {
|
|
318
|
-
const readOnlyParent = join(tmpRoot, "ro");
|
|
319
|
-
mkdirSync(readOnlyParent);
|
|
320
|
-
await chmod(readOnlyParent, 0o555);
|
|
321
|
-
try {
|
|
322
|
-
const result = await validateOutputPath(join(readOnlyParent, "x.txt"));
|
|
323
|
-
expect(result.ok).toBe(false);
|
|
324
|
-
if (!result.ok) expect(result.reason).toMatch(/no writable ancestor/);
|
|
325
|
-
} finally {
|
|
326
|
-
// restore so cleanup can rm -rf
|
|
327
|
-
await chmod(readOnlyParent, 0o755);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("rejects when target is a symlink", async () => {
|
|
332
|
-
const real = join(tmpRoot, "real.txt");
|
|
333
|
-
writeFileSync(real, "x");
|
|
334
|
-
const link = join(tmpRoot, "link.txt");
|
|
335
|
-
symlinkSync(real, link);
|
|
336
|
-
const result = await validateOutputPath(link);
|
|
337
|
-
expect(result.ok).toBe(false);
|
|
338
|
-
if (!result.ok) expect(result.reason).toMatch(/symlink/);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("rejects when target is an existing directory", async () => {
|
|
342
|
-
const dir = join(tmpRoot, "isadir");
|
|
343
|
-
mkdirSync(dir);
|
|
344
|
-
const result = await validateOutputPath(dir);
|
|
345
|
-
expect(result.ok).toBe(false);
|
|
346
|
-
if (!result.ok) expect(result.reason).toMatch(/directory/);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("rejects when a parent in the chain is a symlink", async () => {
|
|
350
|
-
const realSubdir = join(tmpRoot, "real-sub");
|
|
351
|
-
mkdirSync(realSubdir);
|
|
352
|
-
const symlinkedParent = join(tmpRoot, "fake-parent");
|
|
353
|
-
symlinkSync(realSubdir, symlinkedParent);
|
|
354
|
-
const result = await validateOutputPath(join(symlinkedParent, "x.txt"));
|
|
355
|
-
expect(result.ok).toBe(false);
|
|
356
|
-
if (!result.ok) expect(result.reason).toMatch(/parent is a symlink/);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it("accepts a path inside an existing file's parent (overwrite OK)", async () => {
|
|
360
|
-
const existing = join(tmpRoot, "exists.txt");
|
|
361
|
-
writeFileSync(existing, "old");
|
|
362
|
-
const result = await validateOutputPath(existing);
|
|
363
|
-
expect(result.ok).toBe(true);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("cleanup", async () => {
|
|
367
|
-
await rm(tmpRoot, { recursive: true, force: true });
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// ── writeTranscriptionFile — rejection propagation ───────────────────────────
|
|
372
|
-
|
|
373
|
-
describe("writeTranscriptionFile — rejection propagation", () => {
|
|
374
|
-
it("throws on sensitive prefix", async () => {
|
|
375
|
-
await expect(writeTranscriptionFile("/etc/some-file", "x")).rejects.toThrow(
|
|
376
|
-
/refusing to write under \/etc/,
|
|
377
|
-
);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it("throws on null byte", async () => {
|
|
381
|
-
await expect(writeTranscriptionFile("/tmp/pix-test-\0x.txt", "x")).rejects.toThrow(/null byte/);
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
// ── buildTranscriptionResult — write failure path ───────────────────────────
|
|
386
|
-
|
|
387
|
-
describe("buildTranscriptionResult — write failure path", () => {
|
|
388
|
-
it("returns isError + inline fallback when output_file is rejected", async () => {
|
|
389
|
-
const text = "the actual transcription that was successfully produced";
|
|
390
|
-
const result = await buildTranscriptionResult(
|
|
391
|
-
text,
|
|
392
|
-
"dg/nova-3",
|
|
393
|
-
"api",
|
|
394
|
-
"/etc/passwd",
|
|
395
|
-
"meeting.mp3",
|
|
396
|
-
);
|
|
397
|
-
expect(result.isError).toBe(true);
|
|
398
|
-
expect(result.details).toMatchObject({
|
|
399
|
-
_type: "transcribeResult",
|
|
400
|
-
outcome: "error",
|
|
401
|
-
file: "meeting.mp3",
|
|
402
|
-
write_error: expect.stringMatching(/refusing to write/),
|
|
403
|
-
});
|
|
404
|
-
expect(result.details.output_path).toBeUndefined();
|
|
405
|
-
// both blocks, including the recovered transcript, remain available to the model
|
|
406
|
-
expect(result.content).toHaveLength(2);
|
|
407
|
-
const joined = result.content.map((c) => c.text).join("\n");
|
|
408
|
-
expect(joined).toContain(text);
|
|
409
|
-
expect(joined).toContain("/etc/passwd");
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
const theme = {
|
|
414
|
-
fg: (token: string, text: string) => `[${token}]${text}`,
|
|
415
|
-
bold: (text: string) => `*${text}*`,
|
|
416
|
-
} as never;
|
|
417
|
-
|
|
418
|
-
function captureTranscribeRenderer() {
|
|
419
|
-
let tool: Record<string, unknown> | undefined;
|
|
420
|
-
registerTranscribe({
|
|
421
|
-
registerTool(value: Record<string, unknown>) {
|
|
422
|
-
tool = value;
|
|
423
|
-
},
|
|
424
|
-
} as never);
|
|
425
|
-
if (!tool) throw new Error("transcribe tool was not registered");
|
|
426
|
-
return tool;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function renderTranscribe(
|
|
430
|
-
result: Record<string, unknown>,
|
|
431
|
-
expanded = false,
|
|
432
|
-
isError = false,
|
|
433
|
-
): string {
|
|
434
|
-
let text = "";
|
|
435
|
-
const component = {
|
|
436
|
-
setText(value: string) {
|
|
437
|
-
text = value;
|
|
438
|
-
},
|
|
439
|
-
render: () => [],
|
|
440
|
-
invalidate: () => {},
|
|
441
|
-
};
|
|
442
|
-
const tool = captureTranscribeRenderer();
|
|
443
|
-
const renderResult = tool.renderResult as ((...args: never[]) => unknown) | undefined;
|
|
444
|
-
renderResult?.(result as never, { expanded, isPartial: false } as never, theme, {
|
|
445
|
-
lastComponent: component,
|
|
446
|
-
isError,
|
|
447
|
-
state: { collapsed: true },
|
|
448
|
-
expanded,
|
|
449
|
-
invalidate: () => {},
|
|
450
|
-
} as never);
|
|
451
|
-
return text;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
describe("transcribe compact renderer", () => {
|
|
455
|
-
it("uses the self-rendered shell so the status mark has no leading box padding", () => {
|
|
456
|
-
expect(captureTranscribeRenderer().renderShell).toBe("self");
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it("summarizes inline and written transcripts", () => {
|
|
460
|
-
const inline = renderTranscribe({
|
|
461
|
-
content: [{ type: "text", text: "full transcript" }],
|
|
462
|
-
details: {
|
|
463
|
-
_type: "transcribeResult",
|
|
464
|
-
outcome: "success",
|
|
465
|
-
file: "/recordings/meeting.mp3",
|
|
466
|
-
model: "dg/nova-3",
|
|
467
|
-
source: "api",
|
|
468
|
-
chars: 12_400,
|
|
469
|
-
truncated: false,
|
|
470
|
-
},
|
|
471
|
-
});
|
|
472
|
-
expect(inline).toContain("✓");
|
|
473
|
-
expect(inline).toContain("meeting.mp3");
|
|
474
|
-
expect(inline).toContain("12.4K chars · dg/nova-3");
|
|
475
|
-
|
|
476
|
-
const written = renderTranscribe({
|
|
477
|
-
content: [{ type: "text", text: "Transcribed 12400 chars → /tmp/notes.md" }],
|
|
478
|
-
details: {
|
|
479
|
-
_type: "transcribeResult",
|
|
480
|
-
outcome: "success",
|
|
481
|
-
file: "/recordings/meeting.mp3",
|
|
482
|
-
model: "dg/nova-3",
|
|
483
|
-
source: "api",
|
|
484
|
-
chars: 12_400,
|
|
485
|
-
truncated: false,
|
|
486
|
-
output_path: "/tmp/notes.md",
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
expect(written).toContain("12.4K chars · wrote notes.md");
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it("summarizes write failure and restores both exact blocks when expanded", () => {
|
|
493
|
-
const result = {
|
|
494
|
-
content: [
|
|
495
|
-
{ type: "text", text: "Writing failed: permission denied" },
|
|
496
|
-
{ type: "text", text: "recovered transcript" },
|
|
497
|
-
],
|
|
498
|
-
details: {
|
|
499
|
-
_type: "transcribeResult",
|
|
500
|
-
outcome: "error",
|
|
501
|
-
file: "/recordings/meeting.mp3",
|
|
502
|
-
model: "dg/nova-3",
|
|
503
|
-
source: "api",
|
|
504
|
-
chars: 20,
|
|
505
|
-
truncated: false,
|
|
506
|
-
write_error: "permission denied",
|
|
507
|
-
},
|
|
508
|
-
};
|
|
509
|
-
const compact = renderTranscribe(result, false, true);
|
|
510
|
-
expect(compact).toContain("✗");
|
|
511
|
-
expect(compact).toContain("write failed · transcript preserved inline");
|
|
512
|
-
|
|
513
|
-
const expanded = renderTranscribe(result, true, true);
|
|
514
|
-
expect(expanded).toContain("Writing failed: permission denied");
|
|
515
|
-
expect(expanded).toContain("recovered transcript");
|
|
516
|
-
});
|
|
517
|
-
});
|