vidpipe 1.0.0
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/README.md +243 -0
- package/assets/fonts/Montserrat-Bold.ttf +0 -0
- package/assets/fonts/Montserrat-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/dist/__tests__/agents.test.d.ts +2 -0
- package/dist/__tests__/agents.test.d.ts.map +1 -0
- package/dist/__tests__/agents.test.js +434 -0
- package/dist/__tests__/agents.test.js.map +1 -0
- package/dist/__tests__/aspectRatio.test.d.ts +2 -0
- package/dist/__tests__/aspectRatio.test.d.ts.map +1 -0
- package/dist/__tests__/aspectRatio.test.js +406 -0
- package/dist/__tests__/aspectRatio.test.js.map +1 -0
- package/dist/__tests__/captionGenerator.test.d.ts +2 -0
- package/dist/__tests__/captionGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/captionGenerator.test.js +435 -0
- package/dist/__tests__/captionGenerator.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +81 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/faceDetection.test.d.ts +2 -0
- package/dist/__tests__/faceDetection.test.d.ts.map +1 -0
- package/dist/__tests__/faceDetection.test.js +372 -0
- package/dist/__tests__/faceDetection.test.js.map +1 -0
- package/dist/__tests__/ffmpegTools.test.d.ts +2 -0
- package/dist/__tests__/ffmpegTools.test.d.ts.map +1 -0
- package/dist/__tests__/ffmpegTools.test.js +464 -0
- package/dist/__tests__/ffmpegTools.test.js.map +1 -0
- package/dist/__tests__/integration/captionBurn.test.d.ts +2 -0
- package/dist/__tests__/integration/captionBurn.test.d.ts.map +1 -0
- package/dist/__tests__/integration/captionBurn.test.js +103 -0
- package/dist/__tests__/integration/captionBurn.test.js.map +1 -0
- package/dist/__tests__/integration/clipComposite.test.d.ts +2 -0
- package/dist/__tests__/integration/clipComposite.test.d.ts.map +1 -0
- package/dist/__tests__/integration/clipComposite.test.js +56 -0
- package/dist/__tests__/integration/clipComposite.test.js.map +1 -0
- package/dist/__tests__/integration/faceDetection.test.d.ts +2 -0
- package/dist/__tests__/integration/faceDetection.test.d.ts.map +1 -0
- package/dist/__tests__/integration/faceDetection.test.js +85 -0
- package/dist/__tests__/integration/faceDetection.test.js.map +1 -0
- package/dist/__tests__/integration/ffmpegPipeline.test.d.ts +2 -0
- package/dist/__tests__/integration/ffmpegPipeline.test.d.ts.map +1 -0
- package/dist/__tests__/integration/ffmpegPipeline.test.js +88 -0
- package/dist/__tests__/integration/ffmpegPipeline.test.js.map +1 -0
- package/dist/__tests__/integration/fixture.d.ts +19 -0
- package/dist/__tests__/integration/fixture.d.ts.map +1 -0
- package/dist/__tests__/integration/fixture.js +112 -0
- package/dist/__tests__/integration/fixture.js.map +1 -0
- package/dist/__tests__/integration/fixture.test.d.ts +2 -0
- package/dist/__tests__/integration/fixture.test.d.ts.map +1 -0
- package/dist/__tests__/integration/fixture.test.js +27 -0
- package/dist/__tests__/integration/fixture.test.js.map +1 -0
- package/dist/__tests__/integration/realCaptions.test.d.ts +2 -0
- package/dist/__tests__/integration/realCaptions.test.d.ts.map +1 -0
- package/dist/__tests__/integration/realCaptions.test.js +226 -0
- package/dist/__tests__/integration/realCaptions.test.js.map +1 -0
- package/dist/__tests__/integration/realPipeline.test.d.ts +2 -0
- package/dist/__tests__/integration/realPipeline.test.d.ts.map +1 -0
- package/dist/__tests__/integration/realPipeline.test.js +210 -0
- package/dist/__tests__/integration/realPipeline.test.js.map +1 -0
- package/dist/__tests__/integration/silenceRemoval.test.d.ts +2 -0
- package/dist/__tests__/integration/silenceRemoval.test.d.ts.map +1 -0
- package/dist/__tests__/integration/silenceRemoval.test.js +93 -0
- package/dist/__tests__/integration/silenceRemoval.test.js.map +1 -0
- package/dist/__tests__/pipeline.test.d.ts +2 -0
- package/dist/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline.test.js +434 -0
- package/dist/__tests__/pipeline.test.js.map +1 -0
- package/dist/__tests__/services.test.d.ts +2 -0
- package/dist/__tests__/services.test.d.ts.map +1 -0
- package/dist/__tests__/services.test.js +655 -0
- package/dist/__tests__/services.test.js.map +1 -0
- package/dist/__tests__/silenceRemoval.test.d.ts +2 -0
- package/dist/__tests__/silenceRemoval.test.d.ts.map +1 -0
- package/dist/__tests__/silenceRemoval.test.js +266 -0
- package/dist/__tests__/silenceRemoval.test.js.map +1 -0
- package/dist/__tests__/singlePassEdit.test.d.ts +2 -0
- package/dist/__tests__/singlePassEdit.test.d.ts.map +1 -0
- package/dist/__tests__/singlePassEdit.test.js +321 -0
- package/dist/__tests__/singlePassEdit.test.js.map +1 -0
- package/dist/__tests__/smoke.test.d.ts +2 -0
- package/dist/__tests__/smoke.test.d.ts.map +1 -0
- package/dist/__tests__/smoke.test.js +8 -0
- package/dist/__tests__/smoke.test.js.map +1 -0
- package/dist/__tests__/utilities.test.d.ts +2 -0
- package/dist/__tests__/utilities.test.d.ts.map +1 -0
- package/dist/__tests__/utilities.test.js +268 -0
- package/dist/__tests__/utilities.test.js.map +1 -0
- package/dist/agents/BaseAgent.d.ts +52 -0
- package/dist/agents/BaseAgent.d.ts.map +1 -0
- package/dist/agents/BaseAgent.js +108 -0
- package/dist/agents/BaseAgent.js.map +1 -0
- package/dist/agents/BlogAgent.d.ts +3 -0
- package/dist/agents/BlogAgent.d.ts.map +1 -0
- package/dist/agents/BlogAgent.js +163 -0
- package/dist/agents/BlogAgent.js.map +1 -0
- package/dist/agents/ChapterAgent.d.ts +11 -0
- package/dist/agents/ChapterAgent.d.ts.map +1 -0
- package/dist/agents/ChapterAgent.js +191 -0
- package/dist/agents/ChapterAgent.js.map +1 -0
- package/dist/agents/MediumVideoAgent.d.ts +3 -0
- package/dist/agents/MediumVideoAgent.d.ts.map +1 -0
- package/dist/agents/MediumVideoAgent.js +219 -0
- package/dist/agents/MediumVideoAgent.js.map +1 -0
- package/dist/agents/ShortsAgent.d.ts +3 -0
- package/dist/agents/ShortsAgent.d.ts.map +1 -0
- package/dist/agents/ShortsAgent.js +243 -0
- package/dist/agents/ShortsAgent.js.map +1 -0
- package/dist/agents/SilenceRemovalAgent.d.ts +9 -0
- package/dist/agents/SilenceRemovalAgent.d.ts.map +1 -0
- package/dist/agents/SilenceRemovalAgent.js +208 -0
- package/dist/agents/SilenceRemovalAgent.js.map +1 -0
- package/dist/agents/SocialMediaAgent.d.ts +4 -0
- package/dist/agents/SocialMediaAgent.d.ts.map +1 -0
- package/dist/agents/SocialMediaAgent.js +248 -0
- package/dist/agents/SocialMediaAgent.js.map +1 -0
- package/dist/agents/SummaryAgent.d.ts +11 -0
- package/dist/agents/SummaryAgent.d.ts.map +1 -0
- package/dist/agents/SummaryAgent.js +333 -0
- package/dist/agents/SummaryAgent.js.map +1 -0
- package/dist/config/brand.d.ts +29 -0
- package/dist/config/brand.d.ts.map +1 -0
- package/dist/config/brand.js +83 -0
- package/dist/config/brand.js.map +1 -0
- package/dist/config/environment.d.ts +36 -0
- package/dist/config/environment.d.ts.map +1 -0
- package/dist/config/environment.js +44 -0
- package/dist/config/environment.js.map +1 -0
- package/dist/config/logger.d.ts +5 -0
- package/dist/config/logger.d.ts.map +1 -0
- package/dist/config/logger.js +13 -0
- package/dist/config/logger.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline.d.ts +57 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +287 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/services/captionGeneration.d.ts +7 -0
- package/dist/services/captionGeneration.d.ts.map +1 -0
- package/dist/services/captionGeneration.js +29 -0
- package/dist/services/captionGeneration.js.map +1 -0
- package/dist/services/fileWatcher.d.ts +19 -0
- package/dist/services/fileWatcher.d.ts.map +1 -0
- package/dist/services/fileWatcher.js +120 -0
- package/dist/services/fileWatcher.js.map +1 -0
- package/dist/services/gitOperations.d.ts +3 -0
- package/dist/services/gitOperations.d.ts.map +1 -0
- package/dist/services/gitOperations.js +43 -0
- package/dist/services/gitOperations.js.map +1 -0
- package/dist/services/socialPosting.d.ts +38 -0
- package/dist/services/socialPosting.d.ts.map +1 -0
- package/dist/services/socialPosting.js +102 -0
- package/dist/services/socialPosting.js.map +1 -0
- package/dist/services/transcription.d.ts +3 -0
- package/dist/services/transcription.d.ts.map +1 -0
- package/dist/services/transcription.js +100 -0
- package/dist/services/transcription.js.map +1 -0
- package/dist/services/videoIngestion.d.ts +3 -0
- package/dist/services/videoIngestion.d.ts.map +1 -0
- package/dist/services/videoIngestion.js +103 -0
- package/dist/services/videoIngestion.js.map +1 -0
- package/dist/tools/captions/captionGenerator.d.ts +84 -0
- package/dist/tools/captions/captionGenerator.d.ts.map +1 -0
- package/dist/tools/captions/captionGenerator.js +390 -0
- package/dist/tools/captions/captionGenerator.js.map +1 -0
- package/dist/tools/ffmpeg/aspectRatio.d.ts +101 -0
- package/dist/tools/ffmpeg/aspectRatio.d.ts.map +1 -0
- package/dist/tools/ffmpeg/aspectRatio.js +338 -0
- package/dist/tools/ffmpeg/aspectRatio.js.map +1 -0
- package/dist/tools/ffmpeg/audioExtraction.d.ts +16 -0
- package/dist/tools/ffmpeg/audioExtraction.d.ts.map +1 -0
- package/dist/tools/ffmpeg/audioExtraction.js +86 -0
- package/dist/tools/ffmpeg/audioExtraction.js.map +1 -0
- package/dist/tools/ffmpeg/captionBurning.d.ts +8 -0
- package/dist/tools/ffmpeg/captionBurning.d.ts.map +1 -0
- package/dist/tools/ffmpeg/captionBurning.js +71 -0
- package/dist/tools/ffmpeg/captionBurning.js.map +1 -0
- package/dist/tools/ffmpeg/clipExtraction.d.ts +23 -0
- package/dist/tools/ffmpeg/clipExtraction.d.ts.map +1 -0
- package/dist/tools/ffmpeg/clipExtraction.js +178 -0
- package/dist/tools/ffmpeg/clipExtraction.js.map +1 -0
- package/dist/tools/ffmpeg/faceDetection.d.ts +127 -0
- package/dist/tools/ffmpeg/faceDetection.d.ts.map +1 -0
- package/dist/tools/ffmpeg/faceDetection.js +500 -0
- package/dist/tools/ffmpeg/faceDetection.js.map +1 -0
- package/dist/tools/ffmpeg/frameCapture.d.ts +10 -0
- package/dist/tools/ffmpeg/frameCapture.d.ts.map +1 -0
- package/dist/tools/ffmpeg/frameCapture.js +48 -0
- package/dist/tools/ffmpeg/frameCapture.js.map +1 -0
- package/dist/tools/ffmpeg/silenceDetection.d.ts +10 -0
- package/dist/tools/ffmpeg/silenceDetection.d.ts.map +1 -0
- package/dist/tools/ffmpeg/silenceDetection.js +55 -0
- package/dist/tools/ffmpeg/silenceDetection.js.map +1 -0
- package/dist/tools/ffmpeg/singlePassEdit.d.ts +25 -0
- package/dist/tools/ffmpeg/singlePassEdit.d.ts.map +1 -0
- package/dist/tools/ffmpeg/singlePassEdit.js +123 -0
- package/dist/tools/ffmpeg/singlePassEdit.js.map +1 -0
- package/dist/tools/search/exaClient.d.ts +8 -0
- package/dist/tools/search/exaClient.d.ts.map +1 -0
- package/dist/tools/search/exaClient.js +38 -0
- package/dist/tools/search/exaClient.js.map +1 -0
- package/dist/tools/whisper/whisperClient.d.ts +3 -0
- package/dist/tools/whisper/whisperClient.d.ts.map +1 -0
- package/dist/tools/whisper/whisperClient.js +77 -0
- package/dist/tools/whisper/whisperClient.js.map +1 -0
- package/dist/types/index.d.ts +305 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +44 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const { mockExecFile, mockMkdtemp, mockCopyFile, mockReaddir, mockUnlink, mockRmdir } = vi.hoisted(() => ({
|
|
3
|
+
mockExecFile: vi.fn(),
|
|
4
|
+
mockMkdtemp: vi.fn(),
|
|
5
|
+
mockCopyFile: vi.fn(),
|
|
6
|
+
mockReaddir: vi.fn(),
|
|
7
|
+
mockUnlink: vi.fn(),
|
|
8
|
+
mockRmdir: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('child_process', () => ({
|
|
11
|
+
execFile: mockExecFile,
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('fs', async (importOriginal) => {
|
|
14
|
+
const original = await importOriginal();
|
|
15
|
+
return {
|
|
16
|
+
...original,
|
|
17
|
+
promises: {
|
|
18
|
+
...original.promises,
|
|
19
|
+
mkdtemp: mockMkdtemp,
|
|
20
|
+
copyFile: mockCopyFile,
|
|
21
|
+
readdir: mockReaddir,
|
|
22
|
+
unlink: mockUnlink,
|
|
23
|
+
rmdir: mockRmdir,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
vi.mock('../config/logger.js', () => ({
|
|
28
|
+
default: {
|
|
29
|
+
info: vi.fn(),
|
|
30
|
+
warn: vi.fn(),
|
|
31
|
+
error: vi.fn(),
|
|
32
|
+
debug: vi.fn(),
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
import { buildFilterComplex, singlePassEdit, singlePassEditAndCaption } from '../tools/ffmpeg/singlePassEdit.js';
|
|
36
|
+
describe('buildFilterComplex', () => {
|
|
37
|
+
describe('basic filter generation', () => {
|
|
38
|
+
it('produces correct trim+setpts+concat for 2 segments', () => {
|
|
39
|
+
const segments = [
|
|
40
|
+
{ start: 0, end: 5.5 },
|
|
41
|
+
{ start: 8.2, end: 12.0 },
|
|
42
|
+
];
|
|
43
|
+
const result = buildFilterComplex(segments);
|
|
44
|
+
const lines = result.split(';\n');
|
|
45
|
+
// 2 segments × 2 (video + audio) + 1 concat = 5 filter parts
|
|
46
|
+
expect(lines).toHaveLength(5);
|
|
47
|
+
// Video trim for segment 0
|
|
48
|
+
expect(lines[0]).toBe('[0:v]trim=start=0.000:end=5.500,setpts=PTS-STARTPTS[v0]');
|
|
49
|
+
// Audio trim for segment 0
|
|
50
|
+
expect(lines[1]).toBe('[0:a]atrim=start=0.000:end=5.500,asetpts=PTS-STARTPTS[a0]');
|
|
51
|
+
// Video trim for segment 1
|
|
52
|
+
expect(lines[2]).toBe('[0:v]trim=start=8.200:end=12.000,setpts=PTS-STARTPTS[v1]');
|
|
53
|
+
// Audio trim for segment 1
|
|
54
|
+
expect(lines[3]).toBe('[0:a]atrim=start=8.200:end=12.000,asetpts=PTS-STARTPTS[a1]');
|
|
55
|
+
// Concat
|
|
56
|
+
expect(lines[4]).toBe('[v0][a0][v1][a1]concat=n=2:v=1:a=1[outv][outa]');
|
|
57
|
+
});
|
|
58
|
+
it('formats timestamps to 3 decimal places', () => {
|
|
59
|
+
const segments = [{ start: 1.1, end: 2.22 }];
|
|
60
|
+
const result = buildFilterComplex(segments);
|
|
61
|
+
expect(result).toContain('start=1.100');
|
|
62
|
+
expect(result).toContain('end=2.220');
|
|
63
|
+
});
|
|
64
|
+
it('each segment has paired video and audio trim filters', () => {
|
|
65
|
+
const segments = [
|
|
66
|
+
{ start: 0, end: 3 },
|
|
67
|
+
{ start: 5, end: 10 },
|
|
68
|
+
{ start: 15, end: 20 },
|
|
69
|
+
];
|
|
70
|
+
const result = buildFilterComplex(segments);
|
|
71
|
+
const lines = result.split(';\n');
|
|
72
|
+
for (let i = 0; i < segments.length; i++) {
|
|
73
|
+
const videoLine = lines[i * 2];
|
|
74
|
+
const audioLine = lines[i * 2 + 1];
|
|
75
|
+
expect(videoLine).toContain(`[0:v]trim=start=${segments[i].start.toFixed(3)}:end=${segments[i].end.toFixed(3)}`);
|
|
76
|
+
expect(videoLine).toContain(`setpts=PTS-STARTPTS[v${i}]`);
|
|
77
|
+
expect(audioLine).toContain(`[0:a]atrim=start=${segments[i].start.toFixed(3)}:end=${segments[i].end.toFixed(3)}`);
|
|
78
|
+
expect(audioLine).toContain(`asetpts=PTS-STARTPTS[a${i}]`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it('concat n= matches segment count', () => {
|
|
82
|
+
const segments = [
|
|
83
|
+
{ start: 0, end: 1 },
|
|
84
|
+
{ start: 2, end: 3 },
|
|
85
|
+
{ start: 4, end: 5 },
|
|
86
|
+
{ start: 6, end: 7 },
|
|
87
|
+
];
|
|
88
|
+
const result = buildFilterComplex(segments);
|
|
89
|
+
expect(result).toContain('concat=n=4:v=1:a=1');
|
|
90
|
+
});
|
|
91
|
+
it('concat inputs list all segment pairs in order', () => {
|
|
92
|
+
const segments = [
|
|
93
|
+
{ start: 0, end: 1 },
|
|
94
|
+
{ start: 2, end: 3 },
|
|
95
|
+
{ start: 4, end: 5 },
|
|
96
|
+
];
|
|
97
|
+
const result = buildFilterComplex(segments);
|
|
98
|
+
expect(result).toContain('[v0][a0][v1][a1][v2][a2]concat=n=3');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('with captions', () => {
|
|
102
|
+
it('appends ASS subtitle filter after concat', () => {
|
|
103
|
+
const segments = [
|
|
104
|
+
{ start: 0, end: 5 },
|
|
105
|
+
{ start: 8, end: 12 },
|
|
106
|
+
];
|
|
107
|
+
const result = buildFilterComplex(segments, { assFilename: 'captions.ass' });
|
|
108
|
+
const lines = result.split(';\n');
|
|
109
|
+
// Last line should be the ASS filter
|
|
110
|
+
expect(lines[lines.length - 1]).toContain('ass=captions.ass');
|
|
111
|
+
expect(lines[lines.length - 1]).toContain('[outv]');
|
|
112
|
+
});
|
|
113
|
+
it('uses intermediate labels [cv][ca] for concat when captions enabled', () => {
|
|
114
|
+
const segments = [{ start: 0, end: 5 }];
|
|
115
|
+
const result = buildFilterComplex(segments, { assFilename: 'subs.ass' });
|
|
116
|
+
expect(result).toContain('concat=n=1:v=1:a=1[cv][ca]');
|
|
117
|
+
expect(result).not.toContain('[outv][outa]');
|
|
118
|
+
});
|
|
119
|
+
it('sets fontsdir parameter correctly', () => {
|
|
120
|
+
const segments = [{ start: 0, end: 5 }];
|
|
121
|
+
const result = buildFilterComplex(segments, {
|
|
122
|
+
assFilename: 'captions.ass',
|
|
123
|
+
fontsdir: '/tmp/fonts',
|
|
124
|
+
});
|
|
125
|
+
expect(result).toContain('fontsdir=/tmp/fonts');
|
|
126
|
+
});
|
|
127
|
+
it('defaults fontsdir to "." when not specified', () => {
|
|
128
|
+
const segments = [{ start: 0, end: 5 }];
|
|
129
|
+
const result = buildFilterComplex(segments, { assFilename: 'captions.ass' });
|
|
130
|
+
expect(result).toContain('fontsdir=.');
|
|
131
|
+
});
|
|
132
|
+
it('without captions uses [outv][outa] labels directly', () => {
|
|
133
|
+
const segments = [{ start: 0, end: 5 }];
|
|
134
|
+
const result = buildFilterComplex(segments);
|
|
135
|
+
expect(result).toContain('[outv][outa]');
|
|
136
|
+
expect(result).not.toContain('[cv]');
|
|
137
|
+
expect(result).not.toContain('[ca]');
|
|
138
|
+
expect(result).not.toContain('ass=');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('edge cases', () => {
|
|
142
|
+
it('single segment produces valid filter', () => {
|
|
143
|
+
const segments = [{ start: 0, end: 60 }];
|
|
144
|
+
const result = buildFilterComplex(segments);
|
|
145
|
+
const lines = result.split(';\n');
|
|
146
|
+
expect(lines).toHaveLength(3); // video trim, audio trim, concat
|
|
147
|
+
expect(result).toContain('concat=n=1:v=1:a=1[outv][outa]');
|
|
148
|
+
expect(result).toContain('[v0][a0]');
|
|
149
|
+
});
|
|
150
|
+
it('handles 10+ segments correctly', () => {
|
|
151
|
+
const segments = Array.from({ length: 12 }, (_, i) => ({
|
|
152
|
+
start: i * 10,
|
|
153
|
+
end: i * 10 + 8,
|
|
154
|
+
}));
|
|
155
|
+
const result = buildFilterComplex(segments);
|
|
156
|
+
const lines = result.split(';\n');
|
|
157
|
+
// 12 segments × 2 + 1 concat = 25 lines
|
|
158
|
+
expect(lines).toHaveLength(25);
|
|
159
|
+
expect(result).toContain('concat=n=12:v=1:a=1');
|
|
160
|
+
// Check double-digit indices
|
|
161
|
+
expect(result).toContain('[v10]');
|
|
162
|
+
expect(result).toContain('[a11]');
|
|
163
|
+
});
|
|
164
|
+
it('handles very short segments without negative durations', () => {
|
|
165
|
+
const segments = [
|
|
166
|
+
{ start: 5.001, end: 5.002 },
|
|
167
|
+
];
|
|
168
|
+
const result = buildFilterComplex(segments);
|
|
169
|
+
expect(result).toContain('start=5.001:end=5.002');
|
|
170
|
+
// No negative values in the output
|
|
171
|
+
expect(result).not.toMatch(/-\d+\.\d+/);
|
|
172
|
+
});
|
|
173
|
+
it('handles segments starting at 0', () => {
|
|
174
|
+
const segments = [{ start: 0, end: 1 }];
|
|
175
|
+
const result = buildFilterComplex(segments);
|
|
176
|
+
expect(result).toContain('start=0.000:end=1.000');
|
|
177
|
+
});
|
|
178
|
+
it('handles high-precision floating point timestamps', () => {
|
|
179
|
+
const segments = [{ start: 1.23456789, end: 9.87654321 }];
|
|
180
|
+
const result = buildFilterComplex(segments);
|
|
181
|
+
// toFixed(3) rounds correctly
|
|
182
|
+
expect(result).toContain('start=1.235');
|
|
183
|
+
expect(result).toContain('end=9.877');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('input validation', () => {
|
|
187
|
+
it('throws on empty segments array', () => {
|
|
188
|
+
expect(() => buildFilterComplex([])).toThrow('keepSegments must not be empty');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('singlePassEdit', () => {
|
|
193
|
+
const segments = [
|
|
194
|
+
{ start: 0, end: 5 },
|
|
195
|
+
{ start: 10, end: 15 },
|
|
196
|
+
];
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
vi.clearAllMocks();
|
|
199
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
|
|
200
|
+
cb(null, '', '');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
it('calls execFile with correct input and output paths', async () => {
|
|
204
|
+
await singlePassEdit('/input.mp4', segments, '/output.mp4');
|
|
205
|
+
expect(mockExecFile).toHaveBeenCalledOnce();
|
|
206
|
+
const [cmd, args] = mockExecFile.mock.calls[0];
|
|
207
|
+
expect(cmd).toMatch(/ffmpeg/);
|
|
208
|
+
expect(args).toContain('/input.mp4');
|
|
209
|
+
expect(args[args.length - 1]).toBe('/output.mp4');
|
|
210
|
+
});
|
|
211
|
+
it('includes -filter_complex matching buildFilterComplex output', async () => {
|
|
212
|
+
await singlePassEdit('/input.mp4', segments, '/output.mp4');
|
|
213
|
+
const args = mockExecFile.mock.calls[0][1];
|
|
214
|
+
const fcIdx = args.indexOf('-filter_complex');
|
|
215
|
+
expect(fcIdx).toBeGreaterThan(-1);
|
|
216
|
+
expect(args[fcIdx + 1]).toBe(buildFilterComplex(segments));
|
|
217
|
+
});
|
|
218
|
+
it('uses -preset ultrafast and -threads 4', async () => {
|
|
219
|
+
await singlePassEdit('/input.mp4', segments, '/output.mp4');
|
|
220
|
+
const args = mockExecFile.mock.calls[0][1];
|
|
221
|
+
expect(args).toContain('-preset');
|
|
222
|
+
expect(args[args.indexOf('-preset') + 1]).toBe('ultrafast');
|
|
223
|
+
expect(args).toContain('-threads');
|
|
224
|
+
expect(args[args.indexOf('-threads') + 1]).toBe('4');
|
|
225
|
+
});
|
|
226
|
+
it('maps [outv] and [outa]', async () => {
|
|
227
|
+
await singlePassEdit('/input.mp4', segments, '/output.mp4');
|
|
228
|
+
const args = mockExecFile.mock.calls[0][1];
|
|
229
|
+
expect(args).toContain('[outv]');
|
|
230
|
+
expect(args).toContain('[outa]');
|
|
231
|
+
});
|
|
232
|
+
it('returns output path on success', async () => {
|
|
233
|
+
const result = await singlePassEdit('/input.mp4', segments, '/output.mp4');
|
|
234
|
+
expect(result).toBe('/output.mp4');
|
|
235
|
+
});
|
|
236
|
+
it('rejects with error on FFmpeg failure', async () => {
|
|
237
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
|
|
238
|
+
cb(new Error('ffmpeg crash'), '', 'some stderr');
|
|
239
|
+
});
|
|
240
|
+
await expect(singlePassEdit('/input.mp4', segments, '/output.mp4'))
|
|
241
|
+
.rejects.toThrow('Single-pass edit failed: ffmpeg crash');
|
|
242
|
+
});
|
|
243
|
+
it('passes maxBuffer option to execFile', async () => {
|
|
244
|
+
await singlePassEdit('/input.mp4', segments, '/output.mp4');
|
|
245
|
+
const opts = mockExecFile.mock.calls[0][2];
|
|
246
|
+
expect(opts.maxBuffer).toBe(50 * 1024 * 1024);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('singlePassEditAndCaption', () => {
|
|
250
|
+
const segments = [
|
|
251
|
+
{ start: 0, end: 5 },
|
|
252
|
+
{ start: 10, end: 15 },
|
|
253
|
+
];
|
|
254
|
+
const tempDir = '/tmp/caption-abc123';
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
vi.clearAllMocks();
|
|
257
|
+
mockMkdtemp.mockResolvedValue(tempDir);
|
|
258
|
+
mockCopyFile.mockResolvedValue(undefined);
|
|
259
|
+
mockReaddir.mockResolvedValue(['Montserrat-Bold.ttf', 'readme.txt']);
|
|
260
|
+
mockUnlink.mockResolvedValue(undefined);
|
|
261
|
+
mockRmdir.mockResolvedValue(undefined);
|
|
262
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
|
|
263
|
+
mockReaddir.mockResolvedValueOnce(['captions.ass', 'Montserrat-Bold.ttf']);
|
|
264
|
+
cb(null, '', '');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
it('creates a temp directory via mkdtemp', async () => {
|
|
268
|
+
await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
269
|
+
expect(mockMkdtemp).toHaveBeenCalledOnce();
|
|
270
|
+
});
|
|
271
|
+
it('copies ASS file to temp directory', async () => {
|
|
272
|
+
await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
273
|
+
const copyArgs = mockCopyFile.mock.calls.map((c) => [c[0], c[1]]);
|
|
274
|
+
const assCopy = copyArgs.find((c) => c[0] === '/captions.ass');
|
|
275
|
+
expect(assCopy).toBeDefined();
|
|
276
|
+
expect(assCopy[1]).toContain('captions.ass');
|
|
277
|
+
});
|
|
278
|
+
it('copies .ttf font files to temp directory but skips non-font files', async () => {
|
|
279
|
+
await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
280
|
+
const copyDests = mockCopyFile.mock.calls.map((c) => c[1]);
|
|
281
|
+
expect(copyDests.some((d) => d.includes('Montserrat-Bold.ttf'))).toBe(true);
|
|
282
|
+
expect(copyDests.some((d) => d.includes('readme.txt'))).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
it('builds filter_complex with ass filter and fontsdir', async () => {
|
|
285
|
+
await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
286
|
+
const args = mockExecFile.mock.calls[0][1];
|
|
287
|
+
const fcIdx = args.indexOf('-filter_complex');
|
|
288
|
+
const fc = args[fcIdx + 1];
|
|
289
|
+
expect(fc).toContain('ass=captions.ass');
|
|
290
|
+
expect(fc).toContain('fontsdir=.');
|
|
291
|
+
});
|
|
292
|
+
it('maps [ca] instead of [outa] for captioned output', async () => {
|
|
293
|
+
await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
294
|
+
const args = mockExecFile.mock.calls[0][1];
|
|
295
|
+
expect(args).toContain('[ca]');
|
|
296
|
+
expect(args).not.toContain('[outa]');
|
|
297
|
+
});
|
|
298
|
+
it('runs execFile with cwd set to temp directory', async () => {
|
|
299
|
+
await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
300
|
+
const opts = mockExecFile.mock.calls[0][2];
|
|
301
|
+
expect(opts.cwd).toBe(tempDir);
|
|
302
|
+
});
|
|
303
|
+
it('returns output path on success', async () => {
|
|
304
|
+
const result = await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
305
|
+
expect(result).toBe('/output.mp4');
|
|
306
|
+
});
|
|
307
|
+
it('cleans up temp directory after completion', async () => {
|
|
308
|
+
await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
|
|
309
|
+
expect(mockRmdir).toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
it('rejects with error on FFmpeg failure and still cleans up', async () => {
|
|
312
|
+
mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
|
|
313
|
+
mockReaddir.mockResolvedValueOnce(['captions.ass']);
|
|
314
|
+
cb(new Error('encode failed'), '', 'error details');
|
|
315
|
+
});
|
|
316
|
+
await expect(singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4'))
|
|
317
|
+
.rejects.toThrow('Single-pass edit failed: encode failed');
|
|
318
|
+
expect(mockRmdir).toHaveBeenCalled();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
//# sourceMappingURL=singlePassEdit.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singlePassEdit.test.js","sourceRoot":"","sources":["../../src/__tests__/singlePassEdit.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smoke.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/smoke.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
describe('Smoke Test', () => {
|
|
3
|
+
it('should import environment config', async () => {
|
|
4
|
+
const mod = await import('../config/environment.js');
|
|
5
|
+
expect(mod).toBeDefined();
|
|
6
|
+
});
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=smoke.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smoke.test.js","sourceRoot":"","sources":["../../src/__tests__/smoke.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QAChD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;QACrD,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utilities.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/utilities.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// ── Mocks must be declared before imports ──────────────────────────────
|
|
3
|
+
// Mock logger to suppress console output in tests
|
|
4
|
+
vi.mock('../config/logger.js', () => {
|
|
5
|
+
const mockLogger = {
|
|
6
|
+
info: vi.fn(),
|
|
7
|
+
warn: vi.fn(),
|
|
8
|
+
error: vi.fn(),
|
|
9
|
+
debug: vi.fn(),
|
|
10
|
+
level: 'info',
|
|
11
|
+
};
|
|
12
|
+
return { default: mockLogger, setVerbose: vi.fn() };
|
|
13
|
+
});
|
|
14
|
+
// Mock environment config
|
|
15
|
+
vi.mock('../config/environment.js', () => ({
|
|
16
|
+
getConfig: vi.fn().mockReturnValue({
|
|
17
|
+
BRAND_PATH: '/fake/brand.json',
|
|
18
|
+
OPENAI_API_KEY: 'test-key',
|
|
19
|
+
EXA_API_KEY: 'test-exa-key',
|
|
20
|
+
}),
|
|
21
|
+
initConfig: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
// Mock fs for brand.ts and whisperClient.ts
|
|
24
|
+
vi.mock('fs', async () => {
|
|
25
|
+
const actual = await vi.importActual('fs');
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
default: {
|
|
29
|
+
...actual,
|
|
30
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
31
|
+
readFileSync: vi.fn().mockReturnValue('{}'),
|
|
32
|
+
statSync: vi.fn().mockReturnValue({ size: 1024 * 1024 }),
|
|
33
|
+
createReadStream: vi.fn().mockReturnValue('fake-stream'),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
// Mock openai for whisperClient.ts
|
|
38
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
39
|
+
text: 'Hello world',
|
|
40
|
+
language: 'en',
|
|
41
|
+
duration: 5.0,
|
|
42
|
+
segments: [{ id: 0, start: 0, end: 1, text: 'Hello world' }],
|
|
43
|
+
words: [
|
|
44
|
+
{ word: 'Hello', start: 0, end: 0.5 },
|
|
45
|
+
{ word: 'world', start: 0.6, end: 1.0 },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
vi.mock('openai', () => ({
|
|
49
|
+
default: class MockOpenAI {
|
|
50
|
+
audio = {
|
|
51
|
+
transcriptions: {
|
|
52
|
+
create: mockCreate,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
// Mock exa-js for exaClient.ts
|
|
58
|
+
const mockSearchAndContents = vi.fn().mockResolvedValue({
|
|
59
|
+
results: [
|
|
60
|
+
{ title: 'Test Result', url: 'https://test.com', text: 'test content' },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
vi.mock('exa-js', () => ({
|
|
64
|
+
default: class MockExa {
|
|
65
|
+
searchAndContents = mockSearchAndContents;
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
// ── Imports (after mocks) ──────────────────────────────────────────────
|
|
69
|
+
import fs from 'fs';
|
|
70
|
+
import { getConfig } from '../config/environment.js';
|
|
71
|
+
// ========================================================================
|
|
72
|
+
// 1. brand.ts
|
|
73
|
+
// ========================================================================
|
|
74
|
+
describe('brand.ts', () => {
|
|
75
|
+
const fakeBrand = {
|
|
76
|
+
name: 'TestBrand',
|
|
77
|
+
handle: '@test',
|
|
78
|
+
tagline: 'Test tagline',
|
|
79
|
+
voice: { tone: 'casual', personality: 'fun', style: 'brief' },
|
|
80
|
+
advocacy: { primary: ['a11y'], interests: ['tech'], avoids: ['spam'] },
|
|
81
|
+
customVocabulary: ['Copilot', 'TypeScript', 'FFmpeg'],
|
|
82
|
+
hashtags: { always: ['#test'], preferred: ['#dev'], platforms: {} },
|
|
83
|
+
contentGuidelines: {
|
|
84
|
+
shortsFocus: 'key moments',
|
|
85
|
+
blogFocus: 'education',
|
|
86
|
+
socialFocus: 'engagement',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.resetModules();
|
|
91
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
92
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(fakeBrand));
|
|
93
|
+
});
|
|
94
|
+
it('getBrandConfig() returns parsed brand config', async () => {
|
|
95
|
+
const { getBrandConfig } = await import('../config/brand.js');
|
|
96
|
+
const config = getBrandConfig();
|
|
97
|
+
expect(config.name).toBe('TestBrand');
|
|
98
|
+
expect(config.handle).toBe('@test');
|
|
99
|
+
expect(config.customVocabulary).toContain('Copilot');
|
|
100
|
+
});
|
|
101
|
+
it('getBrandConfig() returns defaults when file not found', async () => {
|
|
102
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
103
|
+
const { getBrandConfig } = await import('../config/brand.js');
|
|
104
|
+
const config = getBrandConfig();
|
|
105
|
+
expect(config.name).toBe('Creator');
|
|
106
|
+
expect(config.handle).toBe('@creator');
|
|
107
|
+
});
|
|
108
|
+
it('getBrandConfig() caches — second call does not re-read', async () => {
|
|
109
|
+
const { getBrandConfig } = await import('../config/brand.js');
|
|
110
|
+
getBrandConfig();
|
|
111
|
+
const callsAfterFirst = vi.mocked(fs.readFileSync).mock.calls.length;
|
|
112
|
+
getBrandConfig();
|
|
113
|
+
const callsAfterSecond = vi.mocked(fs.readFileSync).mock.calls.length;
|
|
114
|
+
// Second call should not trigger another read
|
|
115
|
+
expect(callsAfterSecond).toBe(callsAfterFirst);
|
|
116
|
+
});
|
|
117
|
+
it('getWhisperPrompt() includes vocabulary words', async () => {
|
|
118
|
+
const { getWhisperPrompt } = await import('../config/brand.js');
|
|
119
|
+
const prompt = getWhisperPrompt();
|
|
120
|
+
expect(prompt).toContain('Copilot');
|
|
121
|
+
expect(prompt).toContain('TypeScript');
|
|
122
|
+
expect(prompt).toContain('FFmpeg');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// ========================================================================
|
|
126
|
+
// 2. logger.ts
|
|
127
|
+
// ========================================================================
|
|
128
|
+
describe('logger.ts', () => {
|
|
129
|
+
it('default export has expected logging methods', async () => {
|
|
130
|
+
// Import the REAL logger (not the mock) for interface checks
|
|
131
|
+
const loggerMod = await import('../config/logger.js');
|
|
132
|
+
const log = loggerMod.default;
|
|
133
|
+
expect(typeof log.info).toBe('function');
|
|
134
|
+
expect(typeof log.warn).toBe('function');
|
|
135
|
+
expect(typeof log.error).toBe('function');
|
|
136
|
+
expect(typeof log.debug).toBe('function');
|
|
137
|
+
});
|
|
138
|
+
it('setVerbose is exported as a function', async () => {
|
|
139
|
+
const { setVerbose } = await import('../config/logger.js');
|
|
140
|
+
expect(typeof setVerbose).toBe('function');
|
|
141
|
+
});
|
|
142
|
+
it('real logger: setVerbose changes level to debug', async () => {
|
|
143
|
+
// Temporarily import real logger by resetting modules
|
|
144
|
+
vi.resetModules();
|
|
145
|
+
// Dynamically import without the mock to test real behaviour
|
|
146
|
+
const realLogger = await vi.importActual('../config/logger.js');
|
|
147
|
+
realLogger.setVerbose();
|
|
148
|
+
expect(realLogger.default.level).toBe('debug');
|
|
149
|
+
});
|
|
150
|
+
it('real logger is a winston Logger instance', async () => {
|
|
151
|
+
vi.resetModules();
|
|
152
|
+
const realLogger = await vi.importActual('../config/logger.js');
|
|
153
|
+
// Winston loggers have a `transports` array
|
|
154
|
+
expect(realLogger.default).toHaveProperty('transports');
|
|
155
|
+
expect(Array.isArray(realLogger.default.transports)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
// ========================================================================
|
|
159
|
+
// 3. whisperClient.ts
|
|
160
|
+
// ========================================================================
|
|
161
|
+
describe('whisperClient.ts', () => {
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
vi.resetModules();
|
|
164
|
+
mockCreate.mockClear();
|
|
165
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
166
|
+
vi.mocked(fs.statSync).mockReturnValue({ size: 1024 * 1024 });
|
|
167
|
+
vi.mocked(fs.createReadStream).mockReturnValue('fake-stream');
|
|
168
|
+
vi.mocked(getConfig).mockReturnValue({
|
|
169
|
+
OPENAI_API_KEY: 'test-key',
|
|
170
|
+
BRAND_PATH: '/fake/brand.json',
|
|
171
|
+
EXA_API_KEY: '',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
it('transcribeAudio() returns correct transcript structure', async () => {
|
|
175
|
+
const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
|
|
176
|
+
const result = await transcribeAudio('/fake/audio.mp3');
|
|
177
|
+
expect(result.text).toBe('Hello world');
|
|
178
|
+
expect(result.language).toBe('en');
|
|
179
|
+
expect(result.duration).toBe(5.0);
|
|
180
|
+
expect(result.segments).toHaveLength(1);
|
|
181
|
+
expect(result.words).toHaveLength(2);
|
|
182
|
+
});
|
|
183
|
+
it('transcribeAudio() parses words correctly', async () => {
|
|
184
|
+
const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
|
|
185
|
+
const result = await transcribeAudio('/fake/audio.mp3');
|
|
186
|
+
expect(result.words[0]).toEqual({ word: 'Hello', start: 0, end: 0.5 });
|
|
187
|
+
expect(result.words[1]).toEqual({ word: 'world', start: 0.6, end: 1.0 });
|
|
188
|
+
});
|
|
189
|
+
it('transcribeAudio() throws when file not found', async () => {
|
|
190
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
191
|
+
const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
|
|
192
|
+
await expect(transcribeAudio('/missing/audio.mp3')).rejects.toThrow('Audio file not found');
|
|
193
|
+
});
|
|
194
|
+
it('transcribeAudio() throws when file exceeds 25MB', async () => {
|
|
195
|
+
vi.mocked(fs.statSync).mockReturnValue({
|
|
196
|
+
size: 30 * 1024 * 1024,
|
|
197
|
+
});
|
|
198
|
+
const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
|
|
199
|
+
await expect(transcribeAudio('/fake/large.mp3')).rejects.toThrow("exceeds Whisper's 25MB limit");
|
|
200
|
+
});
|
|
201
|
+
it('transcribeAudio() handles API 401 error', async () => {
|
|
202
|
+
mockCreate.mockRejectedValueOnce(Object.assign(new Error('Unauthorized'), { status: 401 }));
|
|
203
|
+
const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
|
|
204
|
+
await expect(transcribeAudio('/fake/audio.mp3')).rejects.toThrow('OpenAI API authentication failed');
|
|
205
|
+
});
|
|
206
|
+
it('transcribeAudio() handles API 429 rate limit', async () => {
|
|
207
|
+
mockCreate.mockRejectedValueOnce(Object.assign(new Error('Rate limited'), { status: 429 }));
|
|
208
|
+
const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
|
|
209
|
+
await expect(transcribeAudio('/fake/audio.mp3')).rejects.toThrow('rate limit exceeded');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
// ========================================================================
|
|
213
|
+
// 4. exaClient.ts
|
|
214
|
+
// ========================================================================
|
|
215
|
+
describe('exaClient.ts', () => {
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
vi.resetModules();
|
|
218
|
+
mockSearchAndContents.mockClear();
|
|
219
|
+
mockSearchAndContents.mockResolvedValue({
|
|
220
|
+
results: [
|
|
221
|
+
{ title: 'Test Result', url: 'https://test.com', text: 'test content' },
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
vi.mocked(getConfig).mockReturnValue({
|
|
225
|
+
EXA_API_KEY: 'test-exa-key',
|
|
226
|
+
OPENAI_API_KEY: '',
|
|
227
|
+
BRAND_PATH: '',
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
it('searchWeb() returns results', async () => {
|
|
231
|
+
const { searchWeb } = await import('../tools/search/exaClient.js');
|
|
232
|
+
const results = await searchWeb('TypeScript');
|
|
233
|
+
expect(results).toHaveLength(1);
|
|
234
|
+
expect(results[0].title).toBe('Test Result');
|
|
235
|
+
expect(results[0].url).toBe('https://test.com');
|
|
236
|
+
expect(results[0].snippet).toBe('test content');
|
|
237
|
+
});
|
|
238
|
+
it('searchWeb() returns empty array when EXA_API_KEY not set', async () => {
|
|
239
|
+
vi.mocked(getConfig).mockReturnValue({
|
|
240
|
+
EXA_API_KEY: '',
|
|
241
|
+
OPENAI_API_KEY: '',
|
|
242
|
+
BRAND_PATH: '',
|
|
243
|
+
});
|
|
244
|
+
const { searchWeb } = await import('../tools/search/exaClient.js');
|
|
245
|
+
const results = await searchWeb('test');
|
|
246
|
+
expect(results).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
it('searchWeb() returns empty array on API error', async () => {
|
|
249
|
+
mockSearchAndContents.mockRejectedValueOnce(new Error('API failure'));
|
|
250
|
+
const { searchWeb } = await import('../tools/search/exaClient.js');
|
|
251
|
+
const results = await searchWeb('test');
|
|
252
|
+
expect(results).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
it('searchTopics() returns map of topic → results', async () => {
|
|
255
|
+
const { searchTopics } = await import('../tools/search/exaClient.js');
|
|
256
|
+
const map = await searchTopics(['topic1', 'topic2']);
|
|
257
|
+
expect(map).toBeInstanceOf(Map);
|
|
258
|
+
expect(map.size).toBe(2);
|
|
259
|
+
expect(map.get('topic1')).toHaveLength(1);
|
|
260
|
+
expect(map.get('topic2')).toHaveLength(1);
|
|
261
|
+
});
|
|
262
|
+
it('searchTopics() passes numResults=3 per topic', async () => {
|
|
263
|
+
const { searchTopics } = await import('../tools/search/exaClient.js');
|
|
264
|
+
await searchTopics(['single-topic']);
|
|
265
|
+
expect(mockSearchAndContents).toHaveBeenCalledWith('single-topic', expect.objectContaining({ numResults: 3 }));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
//# sourceMappingURL=utilities.test.js.map
|