@quatrain/worker 1.2.5 → 1.2.7
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/dist/FileSystem.js +3 -2
- package/dist/FileSystem.test.d.ts +1 -0
- package/dist/FileSystem.test.js +285 -0
- package/dist/Worker.test.js +108 -0
- package/package.json +3 -3
- package/src/FileSystem.test.ts +295 -0
- package/src/FileSystem.ts +3 -2
- package/src/Worker.test.ts +144 -0
package/dist/FileSystem.js
CHANGED
|
@@ -135,9 +135,10 @@ class FileSystem {
|
|
|
135
135
|
* @param mime
|
|
136
136
|
* @returns Promise
|
|
137
137
|
*/
|
|
138
|
-
static uploadFile(filename, meta, mime = 'video/mp4') {
|
|
138
|
+
static async uploadFile(filename, meta, mime = 'video/mp4') {
|
|
139
139
|
// try to get more file metadata
|
|
140
|
-
|
|
140
|
+
const info = await FileSystem.getInfo(filename);
|
|
141
|
+
meta = { ...meta, ...info };
|
|
141
142
|
Worker_1.Worker.info(`Uploading file ${filename} to ${meta.uploadUrl}`);
|
|
142
143
|
const { size } = node_fs_1.default.statSync(filename);
|
|
143
144
|
if (size < 32 * 1024) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
40
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
41
|
+
const FileSystem_1 = require("./FileSystem");
|
|
42
|
+
const axios_1 = __importDefault(require("axios"));
|
|
43
|
+
const node_fetch_native_1 = __importDefault(require("node-fetch-native"));
|
|
44
|
+
const ffmpeg = __importStar(require("fluent-ffmpeg"));
|
|
45
|
+
const node_stream_1 = require("node:stream");
|
|
46
|
+
jest.mock('axios');
|
|
47
|
+
jest.mock('node-fetch-native');
|
|
48
|
+
jest.mock('fluent-ffmpeg', () => ({
|
|
49
|
+
ffprobe: jest.fn()
|
|
50
|
+
}));
|
|
51
|
+
// Mock @quatrain/log globally to completely silence all logging calls across the monorepo graph during worker tests.
|
|
52
|
+
// Returns a valid dummy logger object to satisfy Core.addLogger and Worker.addLogger definitions.
|
|
53
|
+
jest.mock('@quatrain/log', () => {
|
|
54
|
+
const dummyLoggerInstance = {
|
|
55
|
+
log: jest.fn(),
|
|
56
|
+
info: jest.fn(),
|
|
57
|
+
debug: jest.fn(),
|
|
58
|
+
error: jest.fn(),
|
|
59
|
+
warn: jest.fn()
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
Log: {
|
|
63
|
+
info: jest.fn(),
|
|
64
|
+
debug: jest.fn(),
|
|
65
|
+
error: jest.fn(),
|
|
66
|
+
warn: jest.fn(),
|
|
67
|
+
addLogger: jest.fn().mockReturnValue(dummyLoggerInstance)
|
|
68
|
+
},
|
|
69
|
+
LogLevel: {
|
|
70
|
+
INFO: 1,
|
|
71
|
+
DEBUG: 0,
|
|
72
|
+
ERROR: 3,
|
|
73
|
+
WARN: 2
|
|
74
|
+
},
|
|
75
|
+
DefaultLoggerAdapter: jest.fn().mockImplementation(() => dummyLoggerInstance)
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const originalExistsSync = node_fs_1.default.existsSync;
|
|
79
|
+
describe('FileSystem Utilities (Worker)', () => {
|
|
80
|
+
const testBaseDir = node_path_1.default.resolve(__dirname, 'temp_test_dir');
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
// Mock createWriteStream and createReadStream to return in-memory streams.
|
|
83
|
+
// This completely prevents physical file descriptor locks and background ENOENT races.
|
|
84
|
+
jest.spyOn(node_fs_1.default, 'createWriteStream').mockImplementation(() => {
|
|
85
|
+
return new node_stream_1.PassThrough();
|
|
86
|
+
});
|
|
87
|
+
jest.spyOn(node_fs_1.default, 'createReadStream').mockImplementation(() => {
|
|
88
|
+
return node_stream_1.Readable.from(Buffer.alloc(35 * 1024));
|
|
89
|
+
});
|
|
90
|
+
jest.spyOn(node_fs_1.default, 'existsSync').mockImplementation((p) => {
|
|
91
|
+
if (typeof p === 'string' && (p.endsWith('test.txt') || p.endsWith('small.mp4') || p.endsWith('large.mp4') || p.endsWith('large-fail.mp4'))) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return originalExistsSync(p);
|
|
95
|
+
});
|
|
96
|
+
jest.spyOn(node_fs_1.default, 'statSync').mockImplementation((p) => {
|
|
97
|
+
if (typeof p === 'string') {
|
|
98
|
+
if (p.endsWith('small.mp4')) {
|
|
99
|
+
return { size: 13 };
|
|
100
|
+
}
|
|
101
|
+
if (p.endsWith('large.mp4') || p.endsWith('large-fail.mp4')) {
|
|
102
|
+
return { size: 35 * 1024 };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return (jest.requireActual('node:fs').statSync)(p);
|
|
106
|
+
});
|
|
107
|
+
// Setup base temp folder if needed
|
|
108
|
+
if (originalExistsSync(testBaseDir)) {
|
|
109
|
+
node_fs_1.default.rmSync(testBaseDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
// Clean up base temp folder
|
|
114
|
+
if (originalExistsSync(testBaseDir)) {
|
|
115
|
+
node_fs_1.default.rmSync(testBaseDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
jest.clearAllMocks();
|
|
118
|
+
jest.restoreAllMocks();
|
|
119
|
+
});
|
|
120
|
+
describe('prepare and Directory Actions', () => {
|
|
121
|
+
it('should prepare standard workspace layouts', () => {
|
|
122
|
+
// Restore real fs methods for actual directory testing
|
|
123
|
+
jest.restoreAllMocks();
|
|
124
|
+
FileSystem_1.FileSystem.prepare(testBaseDir);
|
|
125
|
+
expect(node_fs_1.default.existsSync(testBaseDir)).toBe(true);
|
|
126
|
+
expect(node_fs_1.default.existsSync(node_path_1.default.join(testBaseDir, 'images'))).toBe(true);
|
|
127
|
+
expect(node_fs_1.default.existsSync(node_path_1.default.join(testBaseDir, 'vecto'))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('should create directories using makeFolder', () => {
|
|
130
|
+
jest.restoreAllMocks();
|
|
131
|
+
node_fs_1.default.mkdirSync(testBaseDir);
|
|
132
|
+
const subDir = node_path_1.default.join(testBaseDir, 'custom');
|
|
133
|
+
FileSystem_1.FileSystem.makeFolder(subDir);
|
|
134
|
+
expect(node_fs_1.default.existsSync(subDir)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
it('should recursively clear directories', () => {
|
|
137
|
+
jest.restoreAllMocks();
|
|
138
|
+
FileSystem_1.FileSystem.prepare(testBaseDir);
|
|
139
|
+
const sub = node_path_1.default.join(testBaseDir, 'images', 'nested');
|
|
140
|
+
node_fs_1.default.mkdirSync(sub);
|
|
141
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(sub, 'file.txt'), 'hello');
|
|
142
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(testBaseDir, 'rootfile.txt'), 'world');
|
|
143
|
+
expect(node_fs_1.default.existsSync(sub)).toBe(true);
|
|
144
|
+
FileSystem_1.FileSystem.removeFolder(testBaseDir, true);
|
|
145
|
+
expect(node_fs_1.default.existsSync(testBaseDir)).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
it('should throw an error on non-recursive deletion containing directories', () => {
|
|
148
|
+
jest.restoreAllMocks();
|
|
149
|
+
node_fs_1.default.mkdirSync(testBaseDir);
|
|
150
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(testBaseDir, 'nested'));
|
|
151
|
+
expect(() => FileSystem_1.FileSystem.removeFolder(testBaseDir, false)).toThrow('Folder contains folder');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('downloadFile', () => {
|
|
155
|
+
it('should download files to target local paths', async () => {
|
|
156
|
+
const mockStream = {
|
|
157
|
+
pipe: jest.fn((writer) => {
|
|
158
|
+
process.nextTick(() => {
|
|
159
|
+
writer.end();
|
|
160
|
+
});
|
|
161
|
+
})
|
|
162
|
+
};
|
|
163
|
+
axios_1.default.get.mockResolvedValue({
|
|
164
|
+
data: mockStream
|
|
165
|
+
});
|
|
166
|
+
const filepath = node_path_1.default.join(testBaseDir, 'test.txt');
|
|
167
|
+
const result = await FileSystem_1.FileSystem.downloadFile('https://example.com/test.txt', filepath);
|
|
168
|
+
expect(result).toBe(true);
|
|
169
|
+
expect(axios_1.default.get).toHaveBeenCalledWith('https://example.com/test.txt', {
|
|
170
|
+
responseType: 'stream'
|
|
171
|
+
});
|
|
172
|
+
expect(node_fs_1.default.existsSync(filepath)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
it('should catch and propagate download failures', async () => {
|
|
175
|
+
axios_1.default.get.mockRejectedValue(new Error('Network error'));
|
|
176
|
+
const filepath = node_path_1.default.join(testBaseDir, 'test.txt');
|
|
177
|
+
await expect(FileSystem_1.FileSystem.downloadFile('https://example.com/test.txt', filepath)).rejects.toThrow('Network error');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('safeString', () => {
|
|
181
|
+
it('should convert spaces to underscores', () => {
|
|
182
|
+
expect(FileSystem_1.FileSystem.safeString('hello world test')).toBe('hello_world_test');
|
|
183
|
+
expect(FileSystem_1.FileSystem.safeString('clean-name')).toBe('clean-name');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('getInfo (Media probe)', () => {
|
|
187
|
+
it('should resolve metadata from video extensions', async () => {
|
|
188
|
+
const mockMetadata = {
|
|
189
|
+
streams: [
|
|
190
|
+
{
|
|
191
|
+
width: 1920,
|
|
192
|
+
height: 1080,
|
|
193
|
+
duration: '10.5',
|
|
194
|
+
bit_rate: '5000',
|
|
195
|
+
nb_frames: '300'
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
};
|
|
199
|
+
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
200
|
+
callback(null, mockMetadata);
|
|
201
|
+
});
|
|
202
|
+
const info = await FileSystem_1.FileSystem.getInfo('video.mp4');
|
|
203
|
+
expect(info).toEqual({
|
|
204
|
+
width: 1920,
|
|
205
|
+
height: 1080,
|
|
206
|
+
framerate: 29,
|
|
207
|
+
duration: 10,
|
|
208
|
+
bitrate: '5000'
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it('should bypass non-video extensions silently', async () => {
|
|
212
|
+
const info = await FileSystem_1.FileSystem.getInfo('document.pdf');
|
|
213
|
+
expect(info).toEqual({});
|
|
214
|
+
});
|
|
215
|
+
it('should bubble up probe errors', async () => {
|
|
216
|
+
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
217
|
+
callback(new Error('Probe failed'));
|
|
218
|
+
});
|
|
219
|
+
await expect(FileSystem_1.FileSystem.getInfo('video.mp4')).rejects.toThrow('Probe failed');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('uploadFile', () => {
|
|
223
|
+
it('should upload small files (< 32KB) using full read buffers', async () => {
|
|
224
|
+
const testFile = node_path_1.default.join(testBaseDir, 'small.mp4');
|
|
225
|
+
const mockMetadata = {
|
|
226
|
+
streams: [{ width: 320, height: 240, duration: '1', bit_rate: '100', nb_frames: '10' }]
|
|
227
|
+
};
|
|
228
|
+
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
229
|
+
callback(null, mockMetadata);
|
|
230
|
+
});
|
|
231
|
+
node_fetch_native_1.default.mockResolvedValue({
|
|
232
|
+
ok: true
|
|
233
|
+
});
|
|
234
|
+
// Stub readFileSync for small content read
|
|
235
|
+
jest.spyOn(node_fs_1.default, 'readFileSync').mockReturnValue(Buffer.from('small content'));
|
|
236
|
+
const meta = {
|
|
237
|
+
bucket: 'mock-bucket',
|
|
238
|
+
ref: 'remote/small.mp4',
|
|
239
|
+
uploadUrl: 'https://upload.com/small.mp4'
|
|
240
|
+
};
|
|
241
|
+
const result = await FileSystem_1.FileSystem.uploadFile(testFile, meta);
|
|
242
|
+
expect(result.size).toBe(13);
|
|
243
|
+
expect(result.width).toBe(320);
|
|
244
|
+
expect(result.uploadUrl).toBeUndefined();
|
|
245
|
+
expect(node_fetch_native_1.default).toHaveBeenCalledWith('https://upload.com/small.mp4', expect.any(Object));
|
|
246
|
+
});
|
|
247
|
+
it('should upload large files (>= 32KB) using readable streams', async () => {
|
|
248
|
+
const testFile = node_path_1.default.join(testBaseDir, 'large.mp4');
|
|
249
|
+
const mockMetadata = {
|
|
250
|
+
streams: [{ width: 1280, height: 720, duration: '5', bit_rate: '2000', nb_frames: '150' }]
|
|
251
|
+
};
|
|
252
|
+
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
253
|
+
callback(null, mockMetadata);
|
|
254
|
+
});
|
|
255
|
+
node_fetch_native_1.default.mockResolvedValue({
|
|
256
|
+
ok: true
|
|
257
|
+
});
|
|
258
|
+
const meta = {
|
|
259
|
+
bucket: 'mock-bucket',
|
|
260
|
+
ref: 'remote/large.mp4',
|
|
261
|
+
uploadUrl: 'https://upload.com/large.mp4'
|
|
262
|
+
};
|
|
263
|
+
const result = await FileSystem_1.FileSystem.uploadFile(testFile, meta);
|
|
264
|
+
expect(result.size).toBe(35 * 1024);
|
|
265
|
+
expect(result.uploadUrl).toBeUndefined();
|
|
266
|
+
expect(node_fetch_native_1.default).toHaveBeenCalledWith('https://upload.com/large.mp4', expect.any(Object));
|
|
267
|
+
});
|
|
268
|
+
it('should reject large uploads if HTTP request fails', async () => {
|
|
269
|
+
const testFile = node_path_1.default.join(testBaseDir, 'large-fail.mp4');
|
|
270
|
+
const mockMetadata = {
|
|
271
|
+
streams: [{ width: 1280, height: 720, duration: '5', bit_rate: '2000', nb_frames: '150' }]
|
|
272
|
+
};
|
|
273
|
+
ffmpeg.ffprobe.mockImplementation((file, callback) => {
|
|
274
|
+
callback(null, mockMetadata);
|
|
275
|
+
});
|
|
276
|
+
node_fetch_native_1.default.mockRejectedValue(new Error('Connection abort'));
|
|
277
|
+
const meta = {
|
|
278
|
+
bucket: 'mock-bucket',
|
|
279
|
+
ref: 'remote/large-fail.mp4',
|
|
280
|
+
uploadUrl: 'https://upload.com/large-fail.mp4'
|
|
281
|
+
};
|
|
282
|
+
await expect(FileSystem_1.FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Connection abort');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
package/dist/Worker.test.js
CHANGED
|
@@ -9,6 +9,18 @@ const node_child_process_1 = require("node:child_process");
|
|
|
9
9
|
// Mock dependencies
|
|
10
10
|
jest.mock('axios');
|
|
11
11
|
jest.mock('node:child_process');
|
|
12
|
+
jest.mock('@quatrain/queue', () => {
|
|
13
|
+
const mockQueueInstance = {
|
|
14
|
+
listen: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
Queue: {
|
|
18
|
+
addQueue: jest.fn(),
|
|
19
|
+
getQueue: jest.fn(() => mockQueueInstance),
|
|
20
|
+
info: jest.fn(),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}, { virtual: true });
|
|
12
24
|
const mockedAxios = axios_1.default;
|
|
13
25
|
const mockedSpawn = node_child_process_1.spawn;
|
|
14
26
|
describe('Worker', () => {
|
|
@@ -77,6 +89,25 @@ describe('Worker', () => {
|
|
|
77
89
|
metadata: { size: 1024 },
|
|
78
90
|
}));
|
|
79
91
|
});
|
|
92
|
+
it('should execute then block on success in pushEvent', async () => {
|
|
93
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
94
|
+
mockedAxios.patch.mockResolvedValue({
|
|
95
|
+
statusText: 'OK',
|
|
96
|
+
data: { success: true },
|
|
97
|
+
});
|
|
98
|
+
Worker_1.Worker.pushEvent('test-event');
|
|
99
|
+
// Allow microtasks to run so .then is executed
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
101
|
+
expect(mockedAxios.patch).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
it('should handle axios error in pushEvent', async () => {
|
|
104
|
+
Worker_1.Worker.endpoint = 'https://api.example.com/events';
|
|
105
|
+
mockedAxios.patch.mockRejectedValue(new Error('Network error'));
|
|
106
|
+
Worker_1.Worker.pushEvent('test-event');
|
|
107
|
+
// Allow microtasks to run so .catch is executed
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
109
|
+
expect(mockedAxios.patch).toHaveBeenCalled();
|
|
110
|
+
});
|
|
80
111
|
});
|
|
81
112
|
describe('pushEventAsync', () => {
|
|
82
113
|
it('should return false when endpoint is not set', async () => {
|
|
@@ -213,6 +244,83 @@ describe('Worker', () => {
|
|
|
213
244
|
shell: false,
|
|
214
245
|
});
|
|
215
246
|
});
|
|
247
|
+
it('should throw an error when spawn itself throws', async () => {
|
|
248
|
+
mockedSpawn.mockImplementationOnce(() => {
|
|
249
|
+
throw new Error('Spawn failure');
|
|
250
|
+
});
|
|
251
|
+
await expect(Worker_1.Worker.execPromise('cmd')).rejects.toThrow('Spawn failure');
|
|
252
|
+
});
|
|
253
|
+
it('should throw and log when an error occurs before the promise constructor', async () => {
|
|
254
|
+
const spyInfo = jest.spyOn(Worker_1.Worker, 'info').mockImplementationOnce(() => {
|
|
255
|
+
throw new Error('Before promise error');
|
|
256
|
+
});
|
|
257
|
+
await expect(() => Worker_1.Worker.execPromise('cmd')).toThrow('Before promise error');
|
|
258
|
+
spyInfo.mockRestore();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
describe('handler', () => {
|
|
262
|
+
let originalExit;
|
|
263
|
+
beforeAll(() => {
|
|
264
|
+
originalExit = process.exit;
|
|
265
|
+
process.exit = jest.fn();
|
|
266
|
+
});
|
|
267
|
+
afterAll(() => {
|
|
268
|
+
process.exit = originalExit;
|
|
269
|
+
});
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
jest.clearAllMocks();
|
|
272
|
+
delete process.env.JSON;
|
|
273
|
+
});
|
|
274
|
+
it('should listen to queue in queue mode', async () => {
|
|
275
|
+
const messageHandler = jest.fn();
|
|
276
|
+
const config = {
|
|
277
|
+
mode: 'queue',
|
|
278
|
+
topic: 'test-topic',
|
|
279
|
+
queueAdapter: 'test-adapter',
|
|
280
|
+
concurrency: 5,
|
|
281
|
+
gpu: false,
|
|
282
|
+
};
|
|
283
|
+
const { Queue } = require('@quatrain/queue');
|
|
284
|
+
await Worker_1.Worker.handler(messageHandler, config);
|
|
285
|
+
expect(Queue.addQueue).toHaveBeenCalledWith('test-adapter', 'default', true);
|
|
286
|
+
expect(Queue.getQueue().listen).toHaveBeenCalledWith('test-topic', messageHandler, {
|
|
287
|
+
concurrency: 5,
|
|
288
|
+
gpu: false,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
it('should handle test mode and call messageHandler', async () => {
|
|
292
|
+
const messageHandler = jest.fn();
|
|
293
|
+
const config = {
|
|
294
|
+
mode: 'test',
|
|
295
|
+
};
|
|
296
|
+
await Worker_1.Worker.handler(messageHandler, config);
|
|
297
|
+
expect(messageHandler).toHaveBeenCalledWith(expect.objectContaining({ dummy: 'test-data' }));
|
|
298
|
+
});
|
|
299
|
+
it('should handle cli mode with environment variable', async () => {
|
|
300
|
+
const messageHandler = jest.fn();
|
|
301
|
+
process.env.JSON = JSON.stringify({ cli: 'data' });
|
|
302
|
+
const config = {
|
|
303
|
+
mode: 'cli',
|
|
304
|
+
};
|
|
305
|
+
await Worker_1.Worker.handler(messageHandler, config);
|
|
306
|
+
expect(messageHandler).toHaveBeenCalledWith(JSON.stringify({ cli: 'data' }));
|
|
307
|
+
});
|
|
308
|
+
it('should throw when cli mode has no environment variable', async () => {
|
|
309
|
+
const messageHandler = jest.fn();
|
|
310
|
+
const config = {
|
|
311
|
+
mode: 'cli',
|
|
312
|
+
};
|
|
313
|
+
await Worker_1.Worker.handler(messageHandler, config);
|
|
314
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
315
|
+
});
|
|
316
|
+
it('should exit when mode is unknown', async () => {
|
|
317
|
+
const messageHandler = jest.fn();
|
|
318
|
+
const config = {
|
|
319
|
+
mode: 'unknown',
|
|
320
|
+
};
|
|
321
|
+
await Worker_1.Worker.handler(messageHandler, config);
|
|
322
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
323
|
+
});
|
|
216
324
|
});
|
|
217
325
|
describe('logger', () => {
|
|
218
326
|
it('should have a logger instance', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quatrain/worker",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Container Worker helpers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"typescript": "^5.1.5"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@quatrain/core": "^1.2.
|
|
37
|
+
"@quatrain/core": "^1.2.14",
|
|
38
38
|
"@quatrain/queue": "^1.2.3",
|
|
39
|
-
"@quatrain/storage": "^1.2.
|
|
39
|
+
"@quatrain/storage": "^1.2.9",
|
|
40
40
|
"axios": "^1.7.7",
|
|
41
41
|
"fluent-ffmpeg": "^2.1.2",
|
|
42
42
|
"fs-extra": "^11.2.0",
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { FileSystem } from './FileSystem'
|
|
4
|
+
import axios from 'axios'
|
|
5
|
+
import fetch from 'node-fetch-native'
|
|
6
|
+
import * as ffmpeg from 'fluent-ffmpeg'
|
|
7
|
+
import { PassThrough, Readable } from 'node:stream'
|
|
8
|
+
|
|
9
|
+
jest.mock('axios')
|
|
10
|
+
jest.mock('node-fetch-native')
|
|
11
|
+
jest.mock('fluent-ffmpeg', () => ({
|
|
12
|
+
ffprobe: jest.fn()
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
// Mock @quatrain/log globally to completely silence all logging calls across the monorepo graph during worker tests.
|
|
16
|
+
// Returns a valid dummy logger object to satisfy Core.addLogger and Worker.addLogger definitions.
|
|
17
|
+
jest.mock('@quatrain/log', () => {
|
|
18
|
+
const dummyLoggerInstance = {
|
|
19
|
+
log: jest.fn(),
|
|
20
|
+
info: jest.fn(),
|
|
21
|
+
debug: jest.fn(),
|
|
22
|
+
error: jest.fn(),
|
|
23
|
+
warn: jest.fn()
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
Log: {
|
|
27
|
+
info: jest.fn(),
|
|
28
|
+
debug: jest.fn(),
|
|
29
|
+
error: jest.fn(),
|
|
30
|
+
warn: jest.fn(),
|
|
31
|
+
addLogger: jest.fn().mockReturnValue(dummyLoggerInstance)
|
|
32
|
+
},
|
|
33
|
+
LogLevel: {
|
|
34
|
+
INFO: 1,
|
|
35
|
+
DEBUG: 0,
|
|
36
|
+
ERROR: 3,
|
|
37
|
+
WARN: 2
|
|
38
|
+
},
|
|
39
|
+
DefaultLoggerAdapter: jest.fn().mockImplementation(() => dummyLoggerInstance)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const originalExistsSync = fs.existsSync
|
|
44
|
+
|
|
45
|
+
describe('FileSystem Utilities (Worker)', () => {
|
|
46
|
+
const testBaseDir = path.resolve(__dirname, 'temp_test_dir')
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
// Mock createWriteStream and createReadStream to return in-memory streams.
|
|
50
|
+
// This completely prevents physical file descriptor locks and background ENOENT races.
|
|
51
|
+
jest.spyOn(fs, 'createWriteStream').mockImplementation(() => {
|
|
52
|
+
return new PassThrough() as any
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
jest.spyOn(fs, 'createReadStream').mockImplementation(() => {
|
|
56
|
+
return Readable.from(Buffer.alloc(35 * 1024)) as any
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
jest.spyOn(fs, 'existsSync').mockImplementation((p) => {
|
|
60
|
+
if (typeof p === 'string' && (p.endsWith('test.txt') || p.endsWith('small.mp4') || p.endsWith('large.mp4') || p.endsWith('large-fail.mp4'))) {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
return originalExistsSync(p)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
jest.spyOn(fs, 'statSync').mockImplementation((p) => {
|
|
67
|
+
if (typeof p === 'string') {
|
|
68
|
+
if (p.endsWith('small.mp4')) {
|
|
69
|
+
return { size: 13 } as any
|
|
70
|
+
}
|
|
71
|
+
if (p.endsWith('large.mp4') || p.endsWith('large-fail.mp4')) {
|
|
72
|
+
return { size: 35 * 1024 } as any
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return (jest.requireActual('node:fs').statSync)(p)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Setup base temp folder if needed
|
|
79
|
+
if (originalExistsSync(testBaseDir)) {
|
|
80
|
+
fs.rmSync(testBaseDir, { recursive: true, force: true })
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
// Clean up base temp folder
|
|
86
|
+
if (originalExistsSync(testBaseDir)) {
|
|
87
|
+
fs.rmSync(testBaseDir, { recursive: true, force: true })
|
|
88
|
+
}
|
|
89
|
+
jest.clearAllMocks()
|
|
90
|
+
jest.restoreAllMocks()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('prepare and Directory Actions', () => {
|
|
94
|
+
it('should prepare standard workspace layouts', () => {
|
|
95
|
+
// Restore real fs methods for actual directory testing
|
|
96
|
+
jest.restoreAllMocks()
|
|
97
|
+
FileSystem.prepare(testBaseDir)
|
|
98
|
+
|
|
99
|
+
expect(fs.existsSync(testBaseDir)).toBe(true)
|
|
100
|
+
expect(fs.existsSync(path.join(testBaseDir, 'images'))).toBe(true)
|
|
101
|
+
expect(fs.existsSync(path.join(testBaseDir, 'vecto'))).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should create directories using makeFolder', () => {
|
|
105
|
+
jest.restoreAllMocks()
|
|
106
|
+
fs.mkdirSync(testBaseDir)
|
|
107
|
+
const subDir = path.join(testBaseDir, 'custom')
|
|
108
|
+
FileSystem.makeFolder(subDir)
|
|
109
|
+
expect(fs.existsSync(subDir)).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should recursively clear directories', () => {
|
|
113
|
+
jest.restoreAllMocks()
|
|
114
|
+
FileSystem.prepare(testBaseDir)
|
|
115
|
+
const sub = path.join(testBaseDir, 'images', 'nested')
|
|
116
|
+
fs.mkdirSync(sub)
|
|
117
|
+
fs.writeFileSync(path.join(sub, 'file.txt'), 'hello')
|
|
118
|
+
fs.writeFileSync(path.join(testBaseDir, 'rootfile.txt'), 'world')
|
|
119
|
+
|
|
120
|
+
expect(fs.existsSync(sub)).toBe(true)
|
|
121
|
+
|
|
122
|
+
FileSystem.removeFolder(testBaseDir, true)
|
|
123
|
+
expect(fs.existsSync(testBaseDir)).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should throw an error on non-recursive deletion containing directories', () => {
|
|
127
|
+
jest.restoreAllMocks()
|
|
128
|
+
fs.mkdirSync(testBaseDir)
|
|
129
|
+
fs.mkdirSync(path.join(testBaseDir, 'nested'))
|
|
130
|
+
|
|
131
|
+
expect(() => FileSystem.removeFolder(testBaseDir, false)).toThrow('Folder contains folder')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('downloadFile', () => {
|
|
136
|
+
it('should download files to target local paths', async () => {
|
|
137
|
+
const mockStream = {
|
|
138
|
+
pipe: jest.fn((writer) => {
|
|
139
|
+
process.nextTick(() => {
|
|
140
|
+
writer.end()
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
(axios.get as jest.Mock).mockResolvedValue({
|
|
146
|
+
data: mockStream
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const filepath = path.join(testBaseDir, 'test.txt')
|
|
150
|
+
|
|
151
|
+
const result = await FileSystem.downloadFile('https://example.com/test.txt', filepath)
|
|
152
|
+
expect(result).toBe(true)
|
|
153
|
+
expect(axios.get).toHaveBeenCalledWith('https://example.com/test.txt', {
|
|
154
|
+
responseType: 'stream'
|
|
155
|
+
})
|
|
156
|
+
expect(fs.existsSync(filepath)).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should catch and propagate download failures', async () => {
|
|
160
|
+
(axios.get as jest.Mock).mockRejectedValue(new Error('Network error'))
|
|
161
|
+
|
|
162
|
+
const filepath = path.join(testBaseDir, 'test.txt')
|
|
163
|
+
|
|
164
|
+
await expect(FileSystem.downloadFile('https://example.com/test.txt', filepath)).rejects.toThrow('Network error')
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('safeString', () => {
|
|
169
|
+
it('should convert spaces to underscores', () => {
|
|
170
|
+
expect(FileSystem.safeString('hello world test')).toBe('hello_world_test')
|
|
171
|
+
expect(FileSystem.safeString('clean-name')).toBe('clean-name')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('getInfo (Media probe)', () => {
|
|
176
|
+
it('should resolve metadata from video extensions', async () => {
|
|
177
|
+
const mockMetadata = {
|
|
178
|
+
streams: [
|
|
179
|
+
{
|
|
180
|
+
width: 1920,
|
|
181
|
+
height: 1080,
|
|
182
|
+
duration: '10.5',
|
|
183
|
+
bit_rate: '5000',
|
|
184
|
+
nb_frames: '300'
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
(ffmpeg.ffprobe as any).mockImplementation((file: string, callback: any) => {
|
|
190
|
+
callback(null, mockMetadata)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const info = await FileSystem.getInfo('video.mp4')
|
|
194
|
+
expect(info).toEqual({
|
|
195
|
+
width: 1920,
|
|
196
|
+
height: 1080,
|
|
197
|
+
framerate: 29,
|
|
198
|
+
duration: 10,
|
|
199
|
+
bitrate: '5000'
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should bypass non-video extensions silently', async () => {
|
|
204
|
+
const info = await FileSystem.getInfo('document.pdf')
|
|
205
|
+
expect(info).toEqual({})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should bubble up probe errors', async () => {
|
|
209
|
+
(ffmpeg.ffprobe as any).mockImplementation((file: string, callback: any) => {
|
|
210
|
+
callback(new Error('Probe failed'))
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await expect(FileSystem.getInfo('video.mp4')).rejects.toThrow('Probe failed')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('uploadFile', () => {
|
|
218
|
+
it('should upload small files (< 32KB) using full read buffers', async () => {
|
|
219
|
+
const testFile = path.join(testBaseDir, 'small.mp4')
|
|
220
|
+
|
|
221
|
+
const mockMetadata = {
|
|
222
|
+
streams: [{ width: 320, height: 240, duration: '1', bit_rate: '100', nb_frames: '10' }]
|
|
223
|
+
};
|
|
224
|
+
(ffmpeg.ffprobe as any).mockImplementation((file: string, callback: any) => {
|
|
225
|
+
callback(null, mockMetadata)
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
(fetch as any).mockResolvedValue({
|
|
229
|
+
ok: true
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Stub readFileSync for small content read
|
|
233
|
+
jest.spyOn(fs, 'readFileSync').mockReturnValue(Buffer.from('small content'))
|
|
234
|
+
|
|
235
|
+
const meta = {
|
|
236
|
+
bucket: 'mock-bucket',
|
|
237
|
+
ref: 'remote/small.mp4',
|
|
238
|
+
uploadUrl: 'https://upload.com/small.mp4'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result = await FileSystem.uploadFile(testFile, meta)
|
|
242
|
+
expect(result.size).toBe(13)
|
|
243
|
+
expect(result.width).toBe(320)
|
|
244
|
+
expect(result.uploadUrl).toBeUndefined()
|
|
245
|
+
expect(fetch).toHaveBeenCalledWith('https://upload.com/small.mp4', expect.any(Object))
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should upload large files (>= 32KB) using readable streams', async () => {
|
|
249
|
+
const testFile = path.join(testBaseDir, 'large.mp4')
|
|
250
|
+
|
|
251
|
+
const mockMetadata = {
|
|
252
|
+
streams: [{ width: 1280, height: 720, duration: '5', bit_rate: '2000', nb_frames: '150' }]
|
|
253
|
+
};
|
|
254
|
+
(ffmpeg.ffprobe as any).mockImplementation((file: string, callback: any) => {
|
|
255
|
+
callback(null, mockMetadata)
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
(fetch as any).mockResolvedValue({
|
|
259
|
+
ok: true
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const meta = {
|
|
263
|
+
bucket: 'mock-bucket',
|
|
264
|
+
ref: 'remote/large.mp4',
|
|
265
|
+
uploadUrl: 'https://upload.com/large.mp4'
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await FileSystem.uploadFile(testFile, meta)
|
|
269
|
+
expect(result.size).toBe(35 * 1024)
|
|
270
|
+
expect(result.uploadUrl).toBeUndefined()
|
|
271
|
+
expect(fetch).toHaveBeenCalledWith('https://upload.com/large.mp4', expect.any(Object))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should reject large uploads if HTTP request fails', async () => {
|
|
275
|
+
const testFile = path.join(testBaseDir, 'large-fail.mp4')
|
|
276
|
+
|
|
277
|
+
const mockMetadata = {
|
|
278
|
+
streams: [{ width: 1280, height: 720, duration: '5', bit_rate: '2000', nb_frames: '150' }]
|
|
279
|
+
};
|
|
280
|
+
(ffmpeg.ffprobe as any).mockImplementation((file: string, callback: any) => {
|
|
281
|
+
callback(null, mockMetadata)
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
(fetch as any).mockRejectedValue(new Error('Connection abort'))
|
|
285
|
+
|
|
286
|
+
const meta = {
|
|
287
|
+
bucket: 'mock-bucket',
|
|
288
|
+
ref: 'remote/large-fail.mp4',
|
|
289
|
+
uploadUrl: 'https://upload.com/large-fail.mp4'
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await expect(FileSystem.uploadFile(testFile, meta)).rejects.toThrow('Connection abort')
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
})
|
package/src/FileSystem.ts
CHANGED
|
@@ -105,13 +105,14 @@ export class FileSystem {
|
|
|
105
105
|
* @param mime
|
|
106
106
|
* @returns Promise
|
|
107
107
|
*/
|
|
108
|
-
static uploadFile(
|
|
108
|
+
static async uploadFile(
|
|
109
109
|
filename: string,
|
|
110
110
|
meta: FileType,
|
|
111
111
|
mime = 'video/mp4'
|
|
112
112
|
): Promise<FileType> {
|
|
113
113
|
// try to get more file metadata
|
|
114
|
-
|
|
114
|
+
const info = await FileSystem.getInfo(filename)
|
|
115
|
+
meta = { ...meta, ...info }
|
|
115
116
|
Worker.info(`Uploading file ${filename} to ${meta.uploadUrl}`)
|
|
116
117
|
const { size } = fs.statSync(filename)
|
|
117
118
|
|
package/src/Worker.test.ts
CHANGED
|
@@ -5,6 +5,18 @@ import { spawn } from 'node:child_process'
|
|
|
5
5
|
// Mock dependencies
|
|
6
6
|
jest.mock('axios')
|
|
7
7
|
jest.mock('node:child_process')
|
|
8
|
+
jest.mock('@quatrain/queue', () => {
|
|
9
|
+
const mockQueueInstance = {
|
|
10
|
+
listen: jest.fn(),
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
Queue: {
|
|
14
|
+
addQueue: jest.fn(),
|
|
15
|
+
getQueue: jest.fn(() => mockQueueInstance),
|
|
16
|
+
info: jest.fn(),
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}, { virtual: true })
|
|
8
20
|
|
|
9
21
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
|
10
22
|
const mockedSpawn = spawn as jest.MockedFunction<typeof spawn>
|
|
@@ -101,6 +113,35 @@ describe('Worker', () => {
|
|
|
101
113
|
})
|
|
102
114
|
)
|
|
103
115
|
})
|
|
116
|
+
|
|
117
|
+
it('should execute then block on success in pushEvent', async () => {
|
|
118
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
119
|
+
|
|
120
|
+
mockedAxios.patch.mockResolvedValue({
|
|
121
|
+
statusText: 'OK',
|
|
122
|
+
data: { success: true },
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
Worker.pushEvent('test-event')
|
|
126
|
+
|
|
127
|
+
// Allow microtasks to run so .then is executed
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
129
|
+
|
|
130
|
+
expect(mockedAxios.patch).toHaveBeenCalled()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should handle axios error in pushEvent', async () => {
|
|
134
|
+
Worker.endpoint = 'https://api.example.com/events'
|
|
135
|
+
|
|
136
|
+
mockedAxios.patch.mockRejectedValue(new Error('Network error'))
|
|
137
|
+
|
|
138
|
+
Worker.pushEvent('test-event')
|
|
139
|
+
|
|
140
|
+
// Allow microtasks to run so .catch is executed
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
142
|
+
|
|
143
|
+
expect(mockedAxios.patch).toHaveBeenCalled()
|
|
144
|
+
})
|
|
104
145
|
})
|
|
105
146
|
|
|
106
147
|
describe('pushEventAsync', () => {
|
|
@@ -282,6 +323,109 @@ describe('Worker', () => {
|
|
|
282
323
|
shell: false,
|
|
283
324
|
})
|
|
284
325
|
})
|
|
326
|
+
|
|
327
|
+
it('should throw an error when spawn itself throws', async () => {
|
|
328
|
+
mockedSpawn.mockImplementationOnce(() => {
|
|
329
|
+
throw new Error('Spawn failure')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
await expect(Worker.execPromise('cmd')).rejects.toThrow('Spawn failure')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('should throw and log when an error occurs before the promise constructor', async () => {
|
|
336
|
+
const spyInfo = jest.spyOn(Worker, 'info').mockImplementationOnce(() => {
|
|
337
|
+
throw new Error('Before promise error')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
await expect(() => Worker.execPromise('cmd')).toThrow('Before promise error')
|
|
341
|
+
spyInfo.mockRestore()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
describe('handler', () => {
|
|
346
|
+
let originalExit: any
|
|
347
|
+
|
|
348
|
+
beforeAll(() => {
|
|
349
|
+
originalExit = process.exit
|
|
350
|
+
process.exit = jest.fn() as any
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
afterAll(() => {
|
|
354
|
+
process.exit = originalExit
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
jest.clearAllMocks()
|
|
359
|
+
delete process.env.JSON
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('should listen to queue in queue mode', async () => {
|
|
363
|
+
const messageHandler = jest.fn()
|
|
364
|
+
const config = {
|
|
365
|
+
mode: 'queue' as const,
|
|
366
|
+
topic: 'test-topic',
|
|
367
|
+
queueAdapter: 'test-adapter' as any,
|
|
368
|
+
concurrency: 5,
|
|
369
|
+
gpu: false,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const { Queue } = require('@quatrain/queue')
|
|
373
|
+
|
|
374
|
+
await Worker.handler(messageHandler, config)
|
|
375
|
+
|
|
376
|
+
expect(Queue.addQueue).toHaveBeenCalledWith('test-adapter', 'default', true)
|
|
377
|
+
expect(Queue.getQueue().listen).toHaveBeenCalledWith('test-topic', messageHandler, {
|
|
378
|
+
concurrency: 5,
|
|
379
|
+
gpu: false,
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('should handle test mode and call messageHandler', async () => {
|
|
384
|
+
const messageHandler = jest.fn()
|
|
385
|
+
const config = {
|
|
386
|
+
mode: 'test' as const,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
await Worker.handler(messageHandler, config)
|
|
390
|
+
|
|
391
|
+
expect(messageHandler).toHaveBeenCalledWith(
|
|
392
|
+
expect.objectContaining({ dummy: 'test-data' })
|
|
393
|
+
)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('should handle cli mode with environment variable', async () => {
|
|
397
|
+
const messageHandler = jest.fn()
|
|
398
|
+
process.env.JSON = JSON.stringify({ cli: 'data' })
|
|
399
|
+
const config = {
|
|
400
|
+
mode: 'cli' as const,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await Worker.handler(messageHandler, config)
|
|
404
|
+
|
|
405
|
+
expect(messageHandler).toHaveBeenCalledWith(JSON.stringify({ cli: 'data' }))
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should throw when cli mode has no environment variable', async () => {
|
|
409
|
+
const messageHandler = jest.fn()
|
|
410
|
+
const config = {
|
|
411
|
+
mode: 'cli' as const,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await Worker.handler(messageHandler, config)
|
|
415
|
+
|
|
416
|
+
expect(process.exit).toHaveBeenCalledWith(1)
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('should exit when mode is unknown', async () => {
|
|
420
|
+
const messageHandler = jest.fn()
|
|
421
|
+
const config = {
|
|
422
|
+
mode: 'unknown' as any,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await Worker.handler(messageHandler, config)
|
|
426
|
+
|
|
427
|
+
expect(process.exit).toHaveBeenCalledWith(1)
|
|
428
|
+
})
|
|
285
429
|
})
|
|
286
430
|
|
|
287
431
|
describe('logger', () => {
|