Completed
Push — master ( 0ba6c7...700f4d )
by
unknown
39:21 queued 19:33
created
tests/lib/TaskProcessing/TaskProcessingTest.php 1 patch
Indentation   +1372 added lines, -1372 removed lines patch added patch discarded remove patch
@@ -60,1436 +60,1436 @@
 block discarded – undo
60 60
 use Test\BackgroundJob\DummyJobList;
61 61
 
62 62
 class AudioToImage implements ITaskType {
63
-	public const ID = 'test:audiotoimage';
64
-
65
-	public function getId(): string {
66
-		return self::ID;
67
-	}
68
-
69
-	public function getName(): string {
70
-		return self::class;
71
-	}
72
-
73
-	public function getDescription(): string {
74
-		return self::class;
75
-	}
76
-
77
-	public function getInputShape(): array {
78
-		return [
79
-			'audio' => new ShapeDescriptor('Audio', 'The audio', EShapeType::Audio),
80
-		];
81
-	}
82
-
83
-	public function getOutputShape(): array {
84
-		return [
85
-			'spectrogram' => new ShapeDescriptor('Spectrogram', 'The audio spectrogram', EShapeType::Image),
86
-		];
87
-	}
63
+    public const ID = 'test:audiotoimage';
64
+
65
+    public function getId(): string {
66
+        return self::ID;
67
+    }
68
+
69
+    public function getName(): string {
70
+        return self::class;
71
+    }
72
+
73
+    public function getDescription(): string {
74
+        return self::class;
75
+    }
76
+
77
+    public function getInputShape(): array {
78
+        return [
79
+            'audio' => new ShapeDescriptor('Audio', 'The audio', EShapeType::Audio),
80
+        ];
81
+    }
82
+
83
+    public function getOutputShape(): array {
84
+        return [
85
+            'spectrogram' => new ShapeDescriptor('Spectrogram', 'The audio spectrogram', EShapeType::Image),
86
+        ];
87
+    }
88 88
 }
89 89
 
90 90
 class AsyncProvider implements IProvider {
91
-	public function getId(): string {
92
-		return 'test:sync:success';
93
-	}
94
-
95
-	public function getName(): string {
96
-		return self::class;
97
-	}
98
-
99
-	public function getTaskTypeId(): string {
100
-		return AudioToImage::ID;
101
-	}
102
-
103
-	public function getExpectedRuntime(): int {
104
-		return 10;
105
-	}
106
-
107
-	public function getOptionalInputShape(): array {
108
-		return [
109
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
110
-		];
111
-	}
112
-
113
-	public function getOptionalOutputShape(): array {
114
-		return [
115
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
116
-		];
117
-	}
118
-
119
-	public function getInputShapeEnumValues(): array {
120
-		return [];
121
-	}
122
-
123
-	public function getInputShapeDefaults(): array {
124
-		return [];
125
-	}
126
-
127
-	public function getOptionalInputShapeEnumValues(): array {
128
-		return [];
129
-	}
130
-
131
-	public function getOptionalInputShapeDefaults(): array {
132
-		return [];
133
-	}
134
-
135
-	public function getOutputShapeEnumValues(): array {
136
-		return [];
137
-	}
138
-
139
-	public function getOptionalOutputShapeEnumValues(): array {
140
-		return [];
141
-	}
91
+    public function getId(): string {
92
+        return 'test:sync:success';
93
+    }
94
+
95
+    public function getName(): string {
96
+        return self::class;
97
+    }
98
+
99
+    public function getTaskTypeId(): string {
100
+        return AudioToImage::ID;
101
+    }
102
+
103
+    public function getExpectedRuntime(): int {
104
+        return 10;
105
+    }
106
+
107
+    public function getOptionalInputShape(): array {
108
+        return [
109
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
110
+        ];
111
+    }
112
+
113
+    public function getOptionalOutputShape(): array {
114
+        return [
115
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
116
+        ];
117
+    }
118
+
119
+    public function getInputShapeEnumValues(): array {
120
+        return [];
121
+    }
122
+
123
+    public function getInputShapeDefaults(): array {
124
+        return [];
125
+    }
126
+
127
+    public function getOptionalInputShapeEnumValues(): array {
128
+        return [];
129
+    }
130
+
131
+    public function getOptionalInputShapeDefaults(): array {
132
+        return [];
133
+    }
134
+
135
+    public function getOutputShapeEnumValues(): array {
136
+        return [];
137
+    }
138
+
139
+    public function getOptionalOutputShapeEnumValues(): array {
140
+        return [];
141
+    }
142 142
 }
143 143
 
144 144
 class SuccessfulSyncProvider implements IProvider, ISynchronousProvider {
145
-	public const ID = 'test:sync:success';
146
-
147
-	public function getId(): string {
148
-		return self::ID;
149
-	}
150
-
151
-	public function getName(): string {
152
-		return self::class;
153
-	}
154
-
155
-	public function getTaskTypeId(): string {
156
-		return TextToText::ID;
157
-	}
158
-
159
-	public function getExpectedRuntime(): int {
160
-		return 10;
161
-	}
162
-
163
-	public function getOptionalInputShape(): array {
164
-		return [
165
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
166
-		];
167
-	}
168
-
169
-	public function getOptionalOutputShape(): array {
170
-		return [
171
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
172
-		];
173
-	}
174
-
175
-	public function process(?string $userId, array $input, callable $reportProgress): array {
176
-		return ['output' => $input['input']];
177
-	}
178
-
179
-	public function getInputShapeEnumValues(): array {
180
-		return [];
181
-	}
182
-
183
-	public function getInputShapeDefaults(): array {
184
-		return [];
185
-	}
186
-
187
-	public function getOptionalInputShapeEnumValues(): array {
188
-		return [];
189
-	}
190
-
191
-	public function getOptionalInputShapeDefaults(): array {
192
-		return [];
193
-	}
194
-
195
-	public function getOutputShapeEnumValues(): array {
196
-		return [];
197
-	}
198
-
199
-	public function getOptionalOutputShapeEnumValues(): array {
200
-		return [];
201
-	}
145
+    public const ID = 'test:sync:success';
146
+
147
+    public function getId(): string {
148
+        return self::ID;
149
+    }
150
+
151
+    public function getName(): string {
152
+        return self::class;
153
+    }
154
+
155
+    public function getTaskTypeId(): string {
156
+        return TextToText::ID;
157
+    }
158
+
159
+    public function getExpectedRuntime(): int {
160
+        return 10;
161
+    }
162
+
163
+    public function getOptionalInputShape(): array {
164
+        return [
165
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
166
+        ];
167
+    }
168
+
169
+    public function getOptionalOutputShape(): array {
170
+        return [
171
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
172
+        ];
173
+    }
174
+
175
+    public function process(?string $userId, array $input, callable $reportProgress): array {
176
+        return ['output' => $input['input']];
177
+    }
178
+
179
+    public function getInputShapeEnumValues(): array {
180
+        return [];
181
+    }
182
+
183
+    public function getInputShapeDefaults(): array {
184
+        return [];
185
+    }
186
+
187
+    public function getOptionalInputShapeEnumValues(): array {
188
+        return [];
189
+    }
190
+
191
+    public function getOptionalInputShapeDefaults(): array {
192
+        return [];
193
+    }
194
+
195
+    public function getOutputShapeEnumValues(): array {
196
+        return [];
197
+    }
198
+
199
+    public function getOptionalOutputShapeEnumValues(): array {
200
+        return [];
201
+    }
202 202
 }
203 203
 
204 204
 
205 205
 
206 206
 class FailingSyncProvider implements IProvider, ISynchronousProvider {
207
-	public const ERROR_MESSAGE = 'Failure';
208
-	public function getId(): string {
209
-		return 'test:sync:fail';
210
-	}
211
-
212
-	public function getName(): string {
213
-		return self::class;
214
-	}
215
-
216
-	public function getTaskTypeId(): string {
217
-		return TextToText::ID;
218
-	}
219
-
220
-	public function getExpectedRuntime(): int {
221
-		return 10;
222
-	}
223
-
224
-	public function getOptionalInputShape(): array {
225
-		return [
226
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
227
-		];
228
-	}
229
-
230
-	public function getOptionalOutputShape(): array {
231
-		return [
232
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
233
-		];
234
-	}
235
-
236
-	public function process(?string $userId, array $input, callable $reportProgress): array {
237
-		throw new ProcessingException(self::ERROR_MESSAGE);
238
-	}
239
-
240
-	public function getInputShapeEnumValues(): array {
241
-		return [];
242
-	}
243
-
244
-	public function getInputShapeDefaults(): array {
245
-		return [];
246
-	}
247
-
248
-	public function getOptionalInputShapeEnumValues(): array {
249
-		return [];
250
-	}
251
-
252
-	public function getOptionalInputShapeDefaults(): array {
253
-		return [];
254
-	}
255
-
256
-	public function getOutputShapeEnumValues(): array {
257
-		return [];
258
-	}
259
-
260
-	public function getOptionalOutputShapeEnumValues(): array {
261
-		return [];
262
-	}
207
+    public const ERROR_MESSAGE = 'Failure';
208
+    public function getId(): string {
209
+        return 'test:sync:fail';
210
+    }
211
+
212
+    public function getName(): string {
213
+        return self::class;
214
+    }
215
+
216
+    public function getTaskTypeId(): string {
217
+        return TextToText::ID;
218
+    }
219
+
220
+    public function getExpectedRuntime(): int {
221
+        return 10;
222
+    }
223
+
224
+    public function getOptionalInputShape(): array {
225
+        return [
226
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
227
+        ];
228
+    }
229
+
230
+    public function getOptionalOutputShape(): array {
231
+        return [
232
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
233
+        ];
234
+    }
235
+
236
+    public function process(?string $userId, array $input, callable $reportProgress): array {
237
+        throw new ProcessingException(self::ERROR_MESSAGE);
238
+    }
239
+
240
+    public function getInputShapeEnumValues(): array {
241
+        return [];
242
+    }
243
+
244
+    public function getInputShapeDefaults(): array {
245
+        return [];
246
+    }
247
+
248
+    public function getOptionalInputShapeEnumValues(): array {
249
+        return [];
250
+    }
251
+
252
+    public function getOptionalInputShapeDefaults(): array {
253
+        return [];
254
+    }
255
+
256
+    public function getOutputShapeEnumValues(): array {
257
+        return [];
258
+    }
259
+
260
+    public function getOptionalOutputShapeEnumValues(): array {
261
+        return [];
262
+    }
263 263
 }
264 264
 
265 265
 
266 266
 class FailingSyncProviderWithUserFacingError implements IProvider, ISynchronousProvider {
267
-	public const ERROR_MESSAGE = 'Failure';
268
-	public const USER_FACING_ERROR_MESSAGE = 'User-facing Failure';
269
-	public function getId(): string {
270
-		return 'test:sync:fail:user-facing';
271
-	}
272
-
273
-	public function getName(): string {
274
-		return self::class;
275
-	}
276
-
277
-	public function getTaskTypeId(): string {
278
-		return TextToText::ID;
279
-	}
280
-
281
-	public function getExpectedRuntime(): int {
282
-		return 10;
283
-	}
284
-
285
-	public function getOptionalInputShape(): array {
286
-		return [
287
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
288
-		];
289
-	}
290
-
291
-	public function getOptionalOutputShape(): array {
292
-		return [
293
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
294
-		];
295
-	}
296
-
297
-	public function process(?string $userId, array $input, callable $reportProgress): array {
298
-		throw new UserFacingProcessingException(self::ERROR_MESSAGE, userFacingMessage: self::USER_FACING_ERROR_MESSAGE);
299
-	}
300
-
301
-	public function getInputShapeEnumValues(): array {
302
-		return [];
303
-	}
304
-
305
-	public function getInputShapeDefaults(): array {
306
-		return [];
307
-	}
308
-
309
-	public function getOptionalInputShapeEnumValues(): array {
310
-		return [];
311
-	}
312
-
313
-	public function getOptionalInputShapeDefaults(): array {
314
-		return [];
315
-	}
316
-
317
-	public function getOutputShapeEnumValues(): array {
318
-		return [];
319
-	}
320
-
321
-	public function getOptionalOutputShapeEnumValues(): array {
322
-		return [];
323
-	}
267
+    public const ERROR_MESSAGE = 'Failure';
268
+    public const USER_FACING_ERROR_MESSAGE = 'User-facing Failure';
269
+    public function getId(): string {
270
+        return 'test:sync:fail:user-facing';
271
+    }
272
+
273
+    public function getName(): string {
274
+        return self::class;
275
+    }
276
+
277
+    public function getTaskTypeId(): string {
278
+        return TextToText::ID;
279
+    }
280
+
281
+    public function getExpectedRuntime(): int {
282
+        return 10;
283
+    }
284
+
285
+    public function getOptionalInputShape(): array {
286
+        return [
287
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
288
+        ];
289
+    }
290
+
291
+    public function getOptionalOutputShape(): array {
292
+        return [
293
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
294
+        ];
295
+    }
296
+
297
+    public function process(?string $userId, array $input, callable $reportProgress): array {
298
+        throw new UserFacingProcessingException(self::ERROR_MESSAGE, userFacingMessage: self::USER_FACING_ERROR_MESSAGE);
299
+    }
300
+
301
+    public function getInputShapeEnumValues(): array {
302
+        return [];
303
+    }
304
+
305
+    public function getInputShapeDefaults(): array {
306
+        return [];
307
+    }
308
+
309
+    public function getOptionalInputShapeEnumValues(): array {
310
+        return [];
311
+    }
312
+
313
+    public function getOptionalInputShapeDefaults(): array {
314
+        return [];
315
+    }
316
+
317
+    public function getOutputShapeEnumValues(): array {
318
+        return [];
319
+    }
320
+
321
+    public function getOptionalOutputShapeEnumValues(): array {
322
+        return [];
323
+    }
324 324
 }
325 325
 
326 326
 class BrokenSyncProvider implements IProvider, ISynchronousProvider {
327
-	public function getId(): string {
328
-		return 'test:sync:broken-output';
329
-	}
330
-
331
-	public function getName(): string {
332
-		return self::class;
333
-	}
334
-
335
-	public function getTaskTypeId(): string {
336
-		return TextToText::ID;
337
-	}
338
-
339
-	public function getExpectedRuntime(): int {
340
-		return 10;
341
-	}
342
-
343
-	public function getOptionalInputShape(): array {
344
-		return [
345
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
346
-		];
347
-	}
348
-
349
-	public function getOptionalOutputShape(): array {
350
-		return [
351
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
352
-		];
353
-	}
354
-
355
-	public function process(?string $userId, array $input, callable $reportProgress): array {
356
-		return [];
357
-	}
358
-
359
-	public function getInputShapeEnumValues(): array {
360
-		return [];
361
-	}
362
-
363
-	public function getInputShapeDefaults(): array {
364
-		return [];
365
-	}
366
-
367
-	public function getOptionalInputShapeEnumValues(): array {
368
-		return [];
369
-	}
370
-
371
-	public function getOptionalInputShapeDefaults(): array {
372
-		return [];
373
-	}
374
-
375
-	public function getOutputShapeEnumValues(): array {
376
-		return [];
377
-	}
378
-
379
-	public function getOptionalOutputShapeEnumValues(): array {
380
-		return [];
381
-	}
327
+    public function getId(): string {
328
+        return 'test:sync:broken-output';
329
+    }
330
+
331
+    public function getName(): string {
332
+        return self::class;
333
+    }
334
+
335
+    public function getTaskTypeId(): string {
336
+        return TextToText::ID;
337
+    }
338
+
339
+    public function getExpectedRuntime(): int {
340
+        return 10;
341
+    }
342
+
343
+    public function getOptionalInputShape(): array {
344
+        return [
345
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
346
+        ];
347
+    }
348
+
349
+    public function getOptionalOutputShape(): array {
350
+        return [
351
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
352
+        ];
353
+    }
354
+
355
+    public function process(?string $userId, array $input, callable $reportProgress): array {
356
+        return [];
357
+    }
358
+
359
+    public function getInputShapeEnumValues(): array {
360
+        return [];
361
+    }
362
+
363
+    public function getInputShapeDefaults(): array {
364
+        return [];
365
+    }
366
+
367
+    public function getOptionalInputShapeEnumValues(): array {
368
+        return [];
369
+    }
370
+
371
+    public function getOptionalInputShapeDefaults(): array {
372
+        return [];
373
+    }
374
+
375
+    public function getOutputShapeEnumValues(): array {
376
+        return [];
377
+    }
378
+
379
+    public function getOptionalOutputShapeEnumValues(): array {
380
+        return [];
381
+    }
382 382
 }
383 383
 
384 384
 class SuccessfulTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider {
385
-	public bool $ran = false;
385
+    public bool $ran = false;
386 386
 
387
-	public function getName(): string {
388
-		return 'TEST Vanilla LLM Provider';
389
-	}
387
+    public function getName(): string {
388
+        return 'TEST Vanilla LLM Provider';
389
+    }
390 390
 
391
-	public function process(string $prompt): string {
392
-		$this->ran = true;
393
-		return $prompt . ' Summarize';
394
-	}
391
+    public function process(string $prompt): string {
392
+        $this->ran = true;
393
+        return $prompt . ' Summarize';
394
+    }
395 395
 
396
-	public function getTaskType(): string {
397
-		return SummaryTaskType::class;
398
-	}
396
+    public function getTaskType(): string {
397
+        return SummaryTaskType::class;
398
+    }
399 399
 }
400 400
 
401 401
 class FailingTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider {
402
-	public bool $ran = false;
402
+    public bool $ran = false;
403 403
 
404
-	public function getName(): string {
405
-		return 'TEST Vanilla LLM Provider';
406
-	}
404
+    public function getName(): string {
405
+        return 'TEST Vanilla LLM Provider';
406
+    }
407 407
 
408
-	public function process(string $prompt): string {
409
-		$this->ran = true;
410
-		throw new \Exception('ERROR');
411
-	}
408
+    public function process(string $prompt): string {
409
+        $this->ran = true;
410
+        throw new \Exception('ERROR');
411
+    }
412 412
 
413
-	public function getTaskType(): string {
414
-		return SummaryTaskType::class;
415
-	}
413
+    public function getTaskType(): string {
414
+        return SummaryTaskType::class;
415
+    }
416 416
 }
417 417
 
418 418
 class SuccessfulTextToImageProvider implements \OCP\TextToImage\IProvider {
419
-	public bool $ran = false;
420
-
421
-	public function getId(): string {
422
-		return 'test:successful';
423
-	}
424
-
425
-	public function getName(): string {
426
-		return 'TEST Provider';
427
-	}
428
-
429
-	public function generate(string $prompt, array $resources): void {
430
-		$this->ran = true;
431
-		foreach ($resources as $resource) {
432
-			fwrite($resource, 'test');
433
-		}
434
-	}
435
-
436
-	public function getExpectedRuntime(): int {
437
-		return 1;
438
-	}
419
+    public bool $ran = false;
420
+
421
+    public function getId(): string {
422
+        return 'test:successful';
423
+    }
424
+
425
+    public function getName(): string {
426
+        return 'TEST Provider';
427
+    }
428
+
429
+    public function generate(string $prompt, array $resources): void {
430
+        $this->ran = true;
431
+        foreach ($resources as $resource) {
432
+            fwrite($resource, 'test');
433
+        }
434
+    }
435
+
436
+    public function getExpectedRuntime(): int {
437
+        return 1;
438
+    }
439 439
 }
440 440
 
441 441
 class FailingTextToImageProvider implements \OCP\TextToImage\IProvider {
442
-	public bool $ran = false;
442
+    public bool $ran = false;
443 443
 
444
-	public function getId(): string {
445
-		return 'test:failing';
446
-	}
444
+    public function getId(): string {
445
+        return 'test:failing';
446
+    }
447 447
 
448
-	public function getName(): string {
449
-		return 'TEST Provider';
450
-	}
448
+    public function getName(): string {
449
+        return 'TEST Provider';
450
+    }
451 451
 
452
-	public function generate(string $prompt, array $resources): void {
453
-		$this->ran = true;
454
-		throw new \RuntimeException('ERROR');
455
-	}
452
+    public function generate(string $prompt, array $resources): void {
453
+        $this->ran = true;
454
+        throw new \RuntimeException('ERROR');
455
+    }
456 456
 
457
-	public function getExpectedRuntime(): int {
458
-		return 1;
459
-	}
457
+    public function getExpectedRuntime(): int {
458
+        return 1;
459
+    }
460 460
 }
461 461
 
462 462
 class ExternalProvider implements IProvider {
463
-	public const ID = 'event:external:provider';
464
-	public const TASK_TYPE_ID = 'event:external:tasktype';
465
-
466
-	public function getId(): string {
467
-		return self::ID;
468
-	}
469
-	public function getName(): string {
470
-		return 'External Provider via Event';
471
-	}
472
-	public function getTaskTypeId(): string {
473
-		return self::TASK_TYPE_ID;
474
-	}
475
-	public function getExpectedRuntime(): int {
476
-		return 5;
477
-	}
478
-	public function getOptionalInputShape(): array {
479
-		return [];
480
-	}
481
-	public function getOptionalOutputShape(): array {
482
-		return [];
483
-	}
484
-	public function getInputShapeEnumValues(): array {
485
-		return [];
486
-	}
487
-	public function getInputShapeDefaults(): array {
488
-		return [];
489
-	}
490
-	public function getOptionalInputShapeEnumValues(): array {
491
-		return [];
492
-	}
493
-	public function getOptionalInputShapeDefaults(): array {
494
-		return [];
495
-	}
496
-	public function getOutputShapeEnumValues(): array {
497
-		return [];
498
-	}
499
-	public function getOptionalOutputShapeEnumValues(): array {
500
-		return [];
501
-	}
463
+    public const ID = 'event:external:provider';
464
+    public const TASK_TYPE_ID = 'event:external:tasktype';
465
+
466
+    public function getId(): string {
467
+        return self::ID;
468
+    }
469
+    public function getName(): string {
470
+        return 'External Provider via Event';
471
+    }
472
+    public function getTaskTypeId(): string {
473
+        return self::TASK_TYPE_ID;
474
+    }
475
+    public function getExpectedRuntime(): int {
476
+        return 5;
477
+    }
478
+    public function getOptionalInputShape(): array {
479
+        return [];
480
+    }
481
+    public function getOptionalOutputShape(): array {
482
+        return [];
483
+    }
484
+    public function getInputShapeEnumValues(): array {
485
+        return [];
486
+    }
487
+    public function getInputShapeDefaults(): array {
488
+        return [];
489
+    }
490
+    public function getOptionalInputShapeEnumValues(): array {
491
+        return [];
492
+    }
493
+    public function getOptionalInputShapeDefaults(): array {
494
+        return [];
495
+    }
496
+    public function getOutputShapeEnumValues(): array {
497
+        return [];
498
+    }
499
+    public function getOptionalOutputShapeEnumValues(): array {
500
+        return [];
501
+    }
502 502
 }
503 503
 
504 504
 
505 505
 class ExternalTriggerableProvider implements ITriggerableProvider {
506
-	public const ID = 'event:external:provider:triggerable';
507
-	public const TASK_TYPE_ID = TextToText::ID;
508
-
509
-	public function getId(): string {
510
-		return self::ID;
511
-	}
512
-	public function getName(): string {
513
-		return 'External Triggerable Provider via Event';
514
-	}
515
-
516
-	public function getTaskTypeId(): string {
517
-		return self::TASK_TYPE_ID;
518
-	}
519
-
520
-	public function trigger(): void {
521
-	}
522
-	public function getExpectedRuntime(): int {
523
-		return 5;
524
-	}
525
-	public function getOptionalInputShape(): array {
526
-		return [];
527
-	}
528
-	public function getOptionalOutputShape(): array {
529
-		return [];
530
-	}
531
-	public function getInputShapeEnumValues(): array {
532
-		return [];
533
-	}
534
-	public function getInputShapeDefaults(): array {
535
-		return [];
536
-	}
537
-	public function getOptionalInputShapeEnumValues(): array {
538
-		return [];
539
-	}
540
-	public function getOptionalInputShapeDefaults(): array {
541
-		return [];
542
-	}
543
-	public function getOutputShapeEnumValues(): array {
544
-		return [];
545
-	}
546
-	public function getOptionalOutputShapeEnumValues(): array {
547
-		return [];
548
-	}
506
+    public const ID = 'event:external:provider:triggerable';
507
+    public const TASK_TYPE_ID = TextToText::ID;
508
+
509
+    public function getId(): string {
510
+        return self::ID;
511
+    }
512
+    public function getName(): string {
513
+        return 'External Triggerable Provider via Event';
514
+    }
515
+
516
+    public function getTaskTypeId(): string {
517
+        return self::TASK_TYPE_ID;
518
+    }
519
+
520
+    public function trigger(): void {
521
+    }
522
+    public function getExpectedRuntime(): int {
523
+        return 5;
524
+    }
525
+    public function getOptionalInputShape(): array {
526
+        return [];
527
+    }
528
+    public function getOptionalOutputShape(): array {
529
+        return [];
530
+    }
531
+    public function getInputShapeEnumValues(): array {
532
+        return [];
533
+    }
534
+    public function getInputShapeDefaults(): array {
535
+        return [];
536
+    }
537
+    public function getOptionalInputShapeEnumValues(): array {
538
+        return [];
539
+    }
540
+    public function getOptionalInputShapeDefaults(): array {
541
+        return [];
542
+    }
543
+    public function getOutputShapeEnumValues(): array {
544
+        return [];
545
+    }
546
+    public function getOptionalOutputShapeEnumValues(): array {
547
+        return [];
548
+    }
549 549
 }
550 550
 
551 551
 class ConflictingExternalProvider implements IProvider {
552
-	// Same ID as SuccessfulSyncProvider
553
-	public const ID = 'test:sync:success';
554
-	public const TASK_TYPE_ID = 'event:external:tasktype'; // Can be different task type
555
-
556
-	public function getId(): string {
557
-		return self::ID;
558
-	}
559
-	public function getName(): string {
560
-		return 'Conflicting External Provider';
561
-	}
562
-	public function getTaskTypeId(): string {
563
-		return self::TASK_TYPE_ID;
564
-	}
565
-	public function getExpectedRuntime(): int {
566
-		return 50;
567
-	}
568
-	public function getOptionalInputShape(): array {
569
-		return [];
570
-	}
571
-	public function getOptionalOutputShape(): array {
572
-		return [];
573
-	}
574
-	public function getInputShapeEnumValues(): array {
575
-		return [];
576
-	}
577
-	public function getInputShapeDefaults(): array {
578
-		return [];
579
-	}
580
-	public function getOptionalInputShapeEnumValues(): array {
581
-		return [];
582
-	}
583
-	public function getOptionalInputShapeDefaults(): array {
584
-		return [];
585
-	}
586
-	public function getOutputShapeEnumValues(): array {
587
-		return [];
588
-	}
589
-	public function getOptionalOutputShapeEnumValues(): array {
590
-		return [];
591
-	}
552
+    // Same ID as SuccessfulSyncProvider
553
+    public const ID = 'test:sync:success';
554
+    public const TASK_TYPE_ID = 'event:external:tasktype'; // Can be different task type
555
+
556
+    public function getId(): string {
557
+        return self::ID;
558
+    }
559
+    public function getName(): string {
560
+        return 'Conflicting External Provider';
561
+    }
562
+    public function getTaskTypeId(): string {
563
+        return self::TASK_TYPE_ID;
564
+    }
565
+    public function getExpectedRuntime(): int {
566
+        return 50;
567
+    }
568
+    public function getOptionalInputShape(): array {
569
+        return [];
570
+    }
571
+    public function getOptionalOutputShape(): array {
572
+        return [];
573
+    }
574
+    public function getInputShapeEnumValues(): array {
575
+        return [];
576
+    }
577
+    public function getInputShapeDefaults(): array {
578
+        return [];
579
+    }
580
+    public function getOptionalInputShapeEnumValues(): array {
581
+        return [];
582
+    }
583
+    public function getOptionalInputShapeDefaults(): array {
584
+        return [];
585
+    }
586
+    public function getOutputShapeEnumValues(): array {
587
+        return [];
588
+    }
589
+    public function getOptionalOutputShapeEnumValues(): array {
590
+        return [];
591
+    }
592 592
 }
593 593
 
594 594
 class ExternalTaskType implements ITaskType {
595
-	public const ID = 'event:external:tasktype';
596
-
597
-	public function getId(): string {
598
-		return self::ID;
599
-	}
600
-	public function getName(): string {
601
-		return 'External Task Type via Event';
602
-	}
603
-	public function getDescription(): string {
604
-		return 'A task type added via event';
605
-	}
606
-	public function getInputShape(): array {
607
-		return ['external_input' => new ShapeDescriptor('Ext In', '', EShapeType::Text)];
608
-	}
609
-	public function getOutputShape(): array {
610
-		return ['external_output' => new ShapeDescriptor('Ext Out', '', EShapeType::Text)];
611
-	}
595
+    public const ID = 'event:external:tasktype';
596
+
597
+    public function getId(): string {
598
+        return self::ID;
599
+    }
600
+    public function getName(): string {
601
+        return 'External Task Type via Event';
602
+    }
603
+    public function getDescription(): string {
604
+        return 'A task type added via event';
605
+    }
606
+    public function getInputShape(): array {
607
+        return ['external_input' => new ShapeDescriptor('Ext In', '', EShapeType::Text)];
608
+    }
609
+    public function getOutputShape(): array {
610
+        return ['external_output' => new ShapeDescriptor('Ext Out', '', EShapeType::Text)];
611
+    }
612 612
 }
613 613
 
614 614
 class ConflictingExternalTaskType implements ITaskType {
615
-	// Same ID as built-in TextToText
616
-	public const ID = TextToText::ID;
617
-
618
-	public function getId(): string {
619
-		return self::ID;
620
-	}
621
-	public function getName(): string {
622
-		return 'Conflicting External Task Type';
623
-	}
624
-	public function getDescription(): string {
625
-		return 'Overrides built-in TextToText';
626
-	}
627
-	public function getInputShape(): array {
628
-		return ['override_input' => new ShapeDescriptor('Override In', '', EShapeType::Number)];
629
-	}
630
-	public function getOutputShape(): array {
631
-		return ['override_output' => new ShapeDescriptor('Override Out', '', EShapeType::Number)];
632
-	}
615
+    // Same ID as built-in TextToText
616
+    public const ID = TextToText::ID;
617
+
618
+    public function getId(): string {
619
+        return self::ID;
620
+    }
621
+    public function getName(): string {
622
+        return 'Conflicting External Task Type';
623
+    }
624
+    public function getDescription(): string {
625
+        return 'Overrides built-in TextToText';
626
+    }
627
+    public function getInputShape(): array {
628
+        return ['override_input' => new ShapeDescriptor('Override In', '', EShapeType::Number)];
629
+    }
630
+    public function getOutputShape(): array {
631
+        return ['override_output' => new ShapeDescriptor('Override Out', '', EShapeType::Number)];
632
+    }
633 633
 }
634 634
 
635 635
 #[\PHPUnit\Framework\Attributes\Group('DB')]
636 636
 class TaskProcessingTest extends \Test\TestCase {
637
-	private Coordinator&MockObject $coordinator;
638
-	private IServerContainer&MockObject $serverContainer;
639
-	private IEventDispatcher&MockObject $eventDispatcher;
640
-	private IJobList&MockObject $jobList;
641
-	private IUserMountCache&MockObject $userMountCache;
642
-	private RegistrationContext&MockObject $registrationContext;
643
-
644
-	/** @var array<class-string, IProvider> */
645
-	private array $providers;
646
-	private IAppConfig $appConfig;
647
-	private IConfig $config;
648
-	private IRootFolder $rootFolder;
649
-	private TaskMapper $taskMapper;
650
-	private IManager $manager;
651
-
652
-	public const TEST_USER = 'testuser';
653
-
654
-	protected function setUp(): void {
655
-		parent::setUp();
656
-
657
-		$this->providers = [
658
-			SuccessfulSyncProvider::class => new SuccessfulSyncProvider(),
659
-			FailingSyncProvider::class => new FailingSyncProvider(),
660
-			FailingSyncProviderWithUserFacingError::class => new FailingSyncProviderWithUserFacingError(),
661
-			BrokenSyncProvider::class => new BrokenSyncProvider(),
662
-			AsyncProvider::class => new AsyncProvider(),
663
-			AudioToImage::class => new AudioToImage(),
664
-			SuccessfulTextProcessingSummaryProvider::class => new SuccessfulTextProcessingSummaryProvider(),
665
-			FailingTextProcessingSummaryProvider::class => new FailingTextProcessingSummaryProvider(),
666
-			SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(),
667
-			FailingTextToImageProvider::class => new FailingTextToImageProvider(),
668
-			ExternalProvider::class => new ExternalProvider(),
669
-			ExternalTriggerableProvider::class => new ExternalTriggerableProvider(),
670
-			ConflictingExternalProvider::class => new ConflictingExternalProvider(),
671
-			ExternalTaskType::class => new ExternalTaskType(),
672
-			ConflictingExternalTaskType::class => new ConflictingExternalTaskType(),
673
-		];
674
-
675
-		$userManager = Server::get(IUserManager::class);
676
-		if (!$userManager->userExists(self::TEST_USER)) {
677
-			$userManager->createUser(self::TEST_USER, 'test');
678
-		}
679
-
680
-		$this->serverContainer = $this->createMock(IServerContainer::class);
681
-		$this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) {
682
-			return $this->providers[$class];
683
-		});
684
-
685
-		$this->registrationContext = $this->createMock(RegistrationContext::class);
686
-		$this->coordinator = $this->createMock(Coordinator::class);
687
-		$this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext);
688
-
689
-		$this->rootFolder = Server::get(IRootFolder::class);
690
-		$this->taskMapper = Server::get(TaskMapper::class);
691
-
692
-		$this->jobList = $this->createPartialMock(DummyJobList::class, ['add']);
693
-		$this->jobList->expects($this->any())->method('add')->willReturnCallback(function (): void {
694
-		});
695
-
696
-		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
697
-		$this->configureEventDispatcherMock();
698
-
699
-		$text2imageManager = new \OC\TextToImage\Manager(
700
-			$this->serverContainer,
701
-			$this->coordinator,
702
-			Server::get(LoggerInterface::class),
703
-			$this->jobList,
704
-			Server::get(\OC\TextToImage\Db\TaskMapper::class),
705
-			Server::get(IConfig::class),
706
-			Server::get(IAppDataFactory::class),
707
-		);
708
-
709
-		$this->userMountCache = $this->createMock(IUserMountCache::class);
710
-		$this->config = Server::get(IConfig::class);
711
-		$this->appConfig = Server::get(IAppConfig::class);
712
-		$this->manager = new Manager(
713
-			$this->appConfig,
714
-			$this->coordinator,
715
-			$this->serverContainer,
716
-			Server::get(LoggerInterface::class),
717
-			$this->taskMapper,
718
-			$this->jobList,
719
-			$this->eventDispatcher,
720
-			Server::get(IAppDataFactory::class),
721
-			Server::get(IRootFolder::class),
722
-			$text2imageManager,
723
-			$this->userMountCache,
724
-			Server::get(IClientService::class),
725
-			Server::get(IAppManager::class),
726
-			$userManager,
727
-			Server::get(IUserSession::class),
728
-			Server::get(ICacheFactory::class),
729
-			Server::get(IFactory::class),
730
-		);
731
-	}
732
-
733
-	private function getFile(string $name, string $content): File {
734
-		$folder = $this->rootFolder->getUserFolder(self::TEST_USER);
735
-		$file = $folder->newFile($name, $content);
736
-		return $file;
737
-	}
738
-
739
-	public function testShouldNotHaveAnyProviders(): void {
740
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
741
-		self::assertCount(0, $this->manager->getAvailableTaskTypes());
742
-		self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
743
-		self::assertFalse($this->manager->hasProviders());
744
-		self::expectException(PreConditionNotMetException::class);
745
-		$this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
746
-	}
747
-
748
-	public function testProviderShouldBeRegisteredAndTaskTypeDisabled(): void {
749
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
750
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
751
-		]);
752
-		$taskProcessingTypeSettings = [
753
-			TextToText::ID => false,
754
-		];
755
-		$this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
756
-		self::assertCount(0, $this->manager->getAvailableTaskTypes());
757
-		self::assertCount(1, $this->manager->getAvailableTaskTypes(true));
758
-		self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
759
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds(true));
760
-		self::assertTrue($this->manager->hasProviders());
761
-		self::expectException(PreConditionNotMetException::class);
762
-		$this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
763
-	}
764
-
765
-
766
-	public function testProviderShouldBeRegisteredAndTaskFailValidation(): void {
767
-		$this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
768
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
769
-			new ServiceRegistration('test', BrokenSyncProvider::class)
770
-		]);
771
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
772
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
773
-		self::assertTrue($this->manager->hasProviders());
774
-		$task = new Task(TextToText::ID, ['wrongInputKey' => 'Hello'], 'test', null);
775
-		self::assertNull($task->getId());
776
-		self::expectException(ValidationException::class);
777
-		$this->manager->scheduleTask($task);
778
-	}
779
-
780
-	public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation(): void {
781
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
782
-			new ServiceRegistration('test', AudioToImage::class)
783
-		]);
784
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
785
-			new ServiceRegistration('test', AsyncProvider::class)
786
-		]);
787
-		$user = $this->createMock(IUser::class);
788
-		$user->expects($this->any())->method('getUID')->willReturn(null);
789
-		$mount = $this->createMock(ICachedMountInfo::class);
790
-		$mount->expects($this->any())->method('getUser')->willReturn($user);
791
-		$this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
792
-
793
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
794
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
795
-		self::assertTrue($this->manager->hasProviders());
796
-
797
-		$audioId = $this->getFile('audioInput', 'Hello')->getId();
798
-		$task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null);
799
-		self::assertNull($task->getId());
800
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
801
-		self::expectException(UnauthorizedException::class);
802
-		$this->manager->scheduleTask($task);
803
-	}
804
-
805
-	public function testProviderShouldBeRegisteredAndFail(): void {
806
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
807
-			new ServiceRegistration('test', FailingSyncProvider::class)
808
-		]);
809
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
810
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
811
-		self::assertTrue($this->manager->hasProviders());
812
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
813
-		self::assertNull($task->getId());
814
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
815
-		$this->manager->scheduleTask($task);
816
-		self::assertNotNull($task->getId());
817
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
818
-
819
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
820
-
821
-		$backgroundJob = new SynchronousBackgroundJob(
822
-			Server::get(ITimeFactory::class),
823
-			$this->manager,
824
-			$this->jobList,
825
-			Server::get(LoggerInterface::class),
826
-		);
827
-		$backgroundJob->start($this->jobList);
828
-
829
-		$task = $this->manager->getTask($task->getId());
830
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
831
-		self::assertEquals(FailingSyncProvider::ERROR_MESSAGE, $task->getErrorMessage());
832
-	}
833
-
834
-	public function testProviderShouldBeRegisteredAndFailWithUserFacingMessage(): void {
835
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
836
-			new ServiceRegistration('test', FailingSyncProviderWithUserFacingError::class)
837
-		]);
838
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
839
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
840
-		self::assertTrue($this->manager->hasProviders());
841
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
842
-		self::assertNull($task->getId());
843
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
844
-		$this->manager->scheduleTask($task);
845
-		self::assertNotNull($task->getId());
846
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
847
-
848
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
849
-
850
-		$backgroundJob = new SynchronousBackgroundJob(
851
-			Server::get(ITimeFactory::class),
852
-			$this->manager,
853
-			$this->jobList,
854
-			Server::get(LoggerInterface::class),
855
-		);
856
-		$backgroundJob->start($this->jobList);
857
-
858
-		$task = $this->manager->getTask($task->getId());
859
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
860
-		self::assertEquals(FailingSyncProviderWithUserFacingError::ERROR_MESSAGE, $task->getErrorMessage());
861
-		self::assertEquals(FailingSyncProviderWithUserFacingError::USER_FACING_ERROR_MESSAGE, $task->getUserFacingErrorMessage());
862
-	}
863
-
864
-	public function testProviderShouldBeRegisteredAndFailOutputValidation(): void {
865
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
866
-			new ServiceRegistration('test', BrokenSyncProvider::class)
867
-		]);
868
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
869
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
870
-		self::assertTrue($this->manager->hasProviders());
871
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
872
-		self::assertNull($task->getId());
873
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
874
-		$this->manager->scheduleTask($task);
875
-		self::assertNotNull($task->getId());
876
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
877
-
878
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
879
-
880
-		$backgroundJob = new SynchronousBackgroundJob(
881
-			Server::get(ITimeFactory::class),
882
-			$this->manager,
883
-			$this->jobList,
884
-			Server::get(LoggerInterface::class),
885
-		);
886
-		$backgroundJob->start($this->jobList);
887
-
888
-		$task = $this->manager->getTask($task->getId());
889
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
890
-		self::assertEquals('The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec', $task->getErrorMessage());
891
-	}
892
-
893
-	public function testProviderShouldBeRegisteredAndRun(): void {
894
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
895
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
896
-		]);
897
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
898
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
899
-		$taskTypeStruct = $this->manager->getAvailableTaskTypes()[array_keys($this->manager->getAvailableTaskTypes())[0]];
900
-		self::assertTrue(isset($taskTypeStruct['inputShape']['input']));
901
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['inputShape']['input']->getShapeType());
902
-		self::assertTrue(isset($taskTypeStruct['optionalInputShape']['optionalKey']));
903
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalInputShape']['optionalKey']->getShapeType());
904
-		self::assertTrue(isset($taskTypeStruct['outputShape']['output']));
905
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['outputShape']['output']->getShapeType());
906
-		self::assertTrue(isset($taskTypeStruct['optionalOutputShape']['optionalKey']));
907
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalOutputShape']['optionalKey']->getShapeType());
908
-
909
-		self::assertTrue($this->manager->hasProviders());
910
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
911
-		self::assertNull($task->getId());
912
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
913
-		$this->manager->scheduleTask($task);
914
-		self::assertNotNull($task->getId());
915
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
916
-
917
-		// Task object retrieved from db is up-to-date
918
-		$task2 = $this->manager->getTask($task->getId());
919
-		self::assertEquals($task->getId(), $task2->getId());
920
-		self::assertEquals(['input' => 'Hello'], $task2->getInput());
921
-		self::assertNull($task2->getOutput());
922
-		self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
923
-
924
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
925
-
926
-		$backgroundJob = new SynchronousBackgroundJob(
927
-			Server::get(ITimeFactory::class),
928
-			$this->manager,
929
-			$this->jobList,
930
-			Server::get(LoggerInterface::class),
931
-		);
932
-		$backgroundJob->start($this->jobList);
933
-
934
-		$task = $this->manager->getTask($task->getId());
935
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
936
-		self::assertEquals(['output' => 'Hello'], $task->getOutput());
937
-		self::assertEquals(1, $task->getProgress());
938
-	}
939
-
940
-	public function testTaskTypeExplicitlyEnabled(): void {
941
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
942
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
943
-		]);
944
-
945
-		$taskProcessingTypeSettings = [
946
-			TextToText::ID => true,
947
-		];
948
-		$this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
949
-
950
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
951
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
952
-
953
-		self::assertTrue($this->manager->hasProviders());
954
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
955
-		self::assertNull($task->getId());
956
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
957
-		$this->manager->scheduleTask($task);
958
-		self::assertNotNull($task->getId());
959
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
960
-
961
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
962
-
963
-		$backgroundJob = new SynchronousBackgroundJob(
964
-			Server::get(ITimeFactory::class),
965
-			$this->manager,
966
-			$this->jobList,
967
-			Server::get(LoggerInterface::class),
968
-		);
969
-		$backgroundJob->start($this->jobList);
970
-
971
-		$task = $this->manager->getTask($task->getId());
972
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
973
-		self::assertEquals(['output' => 'Hello'], $task->getOutput());
974
-		self::assertEquals(1, $task->getProgress());
975
-	}
976
-
977
-	public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData(): void {
978
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
979
-			new ServiceRegistration('test', AudioToImage::class)
980
-		]);
981
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
982
-			new ServiceRegistration('test', AsyncProvider::class)
983
-		]);
984
-
985
-		$user = $this->createMock(IUser::class);
986
-		$user->expects($this->any())->method('getUID')->willReturn('testuser');
987
-		$mount = $this->createMock(ICachedMountInfo::class);
988
-		$mount->expects($this->any())->method('getUser')->willReturn($user);
989
-		$this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
990
-
991
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
992
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
993
-
994
-		self::assertTrue($this->manager->hasProviders());
995
-		$audioId = $this->getFile('audioInput', 'Hello')->getId();
996
-		$task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
997
-		self::assertNull($task->getId());
998
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
999
-		$this->manager->scheduleTask($task);
1000
-		self::assertNotNull($task->getId());
1001
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1002
-
1003
-		// Task object retrieved from db is up-to-date
1004
-		$task2 = $this->manager->getTask($task->getId());
1005
-		self::assertEquals($task->getId(), $task2->getId());
1006
-		self::assertEquals(['audio' => $audioId], $task2->getInput());
1007
-		self::assertNull($task2->getOutput());
1008
-		self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1009
-
1010
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1011
-
1012
-		$this->manager->setTaskProgress($task2->getId(), 0.1);
1013
-		$input = $this->manager->prepareInputData($task2);
1014
-		self::assertTrue(isset($input['audio']));
1015
-		self::assertInstanceOf(File::class, $input['audio']);
1016
-		self::assertEquals($audioId, $input['audio']->getId());
1017
-
1018
-		$this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => 'World']);
1019
-
1020
-		$task = $this->manager->getTask($task->getId());
1021
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1022
-		self::assertEquals(1, $task->getProgress());
1023
-		self::assertTrue(isset($task->getOutput()['spectrogram']));
1024
-		$node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1025
-		self::assertNotNull($node);
1026
-		self::assertInstanceOf(File::class, $node);
1027
-		self::assertEquals('World', $node->getContent());
1028
-	}
1029
-
1030
-	public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds(): void {
1031
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1032
-			new ServiceRegistration('test', AudioToImage::class)
1033
-		]);
1034
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1035
-			new ServiceRegistration('test', AsyncProvider::class)
1036
-		]);
1037
-		$user = $this->createMock(IUser::class);
1038
-		$user->expects($this->any())->method('getUID')->willReturn('testuser');
1039
-		$mount = $this->createMock(ICachedMountInfo::class);
1040
-		$mount->expects($this->any())->method('getUser')->willReturn($user);
1041
-		$this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
1042
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
1043
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1044
-
1045
-		self::assertTrue($this->manager->hasProviders());
1046
-		$audioId = $this->getFile('audioInput', 'Hello')->getId();
1047
-		$task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
1048
-		self::assertNull($task->getId());
1049
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
1050
-		$this->manager->scheduleTask($task);
1051
-		self::assertNotNull($task->getId());
1052
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1053
-
1054
-		// Task object retrieved from db is up-to-date
1055
-		$task2 = $this->manager->getTask($task->getId());
1056
-		self::assertEquals($task->getId(), $task2->getId());
1057
-		self::assertEquals(['audio' => $audioId], $task2->getInput());
1058
-		self::assertNull($task2->getOutput());
1059
-		self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1060
-
1061
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1062
-
1063
-		$this->manager->setTaskProgress($task2->getId(), 0.1);
1064
-		$input = $this->manager->prepareInputData($task2);
1065
-		self::assertTrue(isset($input['audio']));
1066
-		self::assertInstanceOf(File::class, $input['audio']);
1067
-		self::assertEquals($audioId, $input['audio']->getId());
1068
-
1069
-		$outputFileId = $this->getFile('audioOutput', 'World')->getId();
1070
-
1071
-		$this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true);
1072
-
1073
-		$task = $this->manager->getTask($task->getId());
1074
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1075
-		self::assertEquals(1, $task->getProgress());
1076
-		self::assertTrue(isset($task->getOutput()['spectrogram']));
1077
-		$node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']);
1078
-		self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']);
1079
-		self::assertInstanceOf(File::class, $node);
1080
-		self::assertEquals('World', $node->getContent());
1081
-	}
1082
-
1083
-	public function testNonexistentTask(): void {
1084
-		$this->expectException(NotFoundException::class);
1085
-		$this->manager->getTask(2147483646);
1086
-	}
1087
-
1088
-	public function testOldTasksShouldBeCleanedUp(): void {
1089
-		$currentTime = new \DateTime('now');
1090
-		$timeFactory = $this->createMock(ITimeFactory::class);
1091
-		$timeFactory->expects($this->any())->method('getDateTime')->willReturnCallback(fn () => $currentTime);
1092
-		$timeFactory->expects($this->any())->method('getTime')->willReturnCallback(fn () => $currentTime->getTimestamp());
1093
-
1094
-		$this->taskMapper = new TaskMapper(
1095
-			Server::get(IDBConnection::class),
1096
-			$timeFactory,
1097
-		);
1098
-
1099
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1100
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
1101
-		]);
1102
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
1103
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1104
-		self::assertTrue($this->manager->hasProviders());
1105
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
1106
-		$this->manager->scheduleTask($task);
1107
-
1108
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1109
-
1110
-		$backgroundJob = new SynchronousBackgroundJob(
1111
-			Server::get(ITimeFactory::class),
1112
-			$this->manager,
1113
-			$this->jobList,
1114
-			Server::get(LoggerInterface::class),
1115
-		);
1116
-		$backgroundJob->start($this->jobList);
1117
-
1118
-		$task = $this->manager->getTask($task->getId());
1119
-
1120
-		$currentTime = $currentTime->add(new \DateInterval('P1Y'));
1121
-		// run background job
1122
-		$bgJob = new RemoveOldTasksBackgroundJob(
1123
-			$timeFactory,
1124
-			$this->manager,
1125
-			$this->taskMapper,
1126
-			Server::get(LoggerInterface::class),
1127
-			Server::get(IAppDataFactory::class),
1128
-		);
1129
-		$bgJob->setArgument([]);
1130
-		$bgJob->start($this->jobList);
1131
-
1132
-		$this->expectException(NotFoundException::class);
1133
-		$this->manager->getTask($task->getId());
1134
-	}
1135
-
1136
-	public function testShouldTransparentlyHandleTextProcessingProviders(): void {
1137
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1138
-			new ServiceRegistration('test', SuccessfulTextProcessingSummaryProvider::class)
1139
-		]);
1140
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1141
-		]);
1142
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1143
-		self::assertCount(1, $taskTypes);
1144
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1145
-		self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1146
-		self::assertTrue($this->manager->hasProviders());
1147
-		$task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1148
-		$this->manager->scheduleTask($task);
1149
-
1150
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1151
-
1152
-		$backgroundJob = new SynchronousBackgroundJob(
1153
-			Server::get(ITimeFactory::class),
1154
-			$this->manager,
1155
-			$this->jobList,
1156
-			Server::get(LoggerInterface::class),
1157
-		);
1158
-		$backgroundJob->start($this->jobList);
1159
-
1160
-		$task = $this->manager->getTask($task->getId());
1161
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1162
-		self::assertIsArray($task->getOutput());
1163
-		self::assertTrue(isset($task->getOutput()['output']));
1164
-		self::assertEquals('Hello Summarize', $task->getOutput()['output']);
1165
-		self::assertTrue($this->providers[SuccessfulTextProcessingSummaryProvider::class]->ran);
1166
-	}
1167
-
1168
-	public function testShouldTransparentlyHandleFailingTextProcessingProviders(): void {
1169
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1170
-			new ServiceRegistration('test', FailingTextProcessingSummaryProvider::class)
1171
-		]);
1172
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1173
-		]);
1174
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1175
-		self::assertCount(1, $taskTypes);
1176
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1177
-		self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1178
-		self::assertTrue($this->manager->hasProviders());
1179
-		$task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1180
-		$this->manager->scheduleTask($task);
1181
-
1182
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1183
-
1184
-		$backgroundJob = new SynchronousBackgroundJob(
1185
-			Server::get(ITimeFactory::class),
1186
-			$this->manager,
1187
-			$this->jobList,
1188
-			Server::get(LoggerInterface::class),
1189
-		);
1190
-		$backgroundJob->start($this->jobList);
1191
-
1192
-		$task = $this->manager->getTask($task->getId());
1193
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1194
-		self::assertTrue($task->getOutput() === null);
1195
-		self::assertEquals('ERROR', $task->getErrorMessage());
1196
-		self::assertTrue($this->providers[FailingTextProcessingSummaryProvider::class]->ran);
1197
-	}
1198
-
1199
-	public function testShouldTransparentlyHandleText2ImageProviders(): void {
1200
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1201
-			new ServiceRegistration('test', SuccessfulTextToImageProvider::class)
1202
-		]);
1203
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1204
-		]);
1205
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1206
-		self::assertCount(1, $taskTypes);
1207
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1208
-		self::assertTrue(isset($taskTypes[TextToImage::ID]));
1209
-		self::assertTrue($this->manager->hasProviders());
1210
-		$task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1211
-		$this->manager->scheduleTask($task);
1212
-
1213
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1214
-
1215
-		$backgroundJob = new SynchronousBackgroundJob(
1216
-			Server::get(ITimeFactory::class),
1217
-			$this->manager,
1218
-			$this->jobList,
1219
-			Server::get(LoggerInterface::class),
1220
-		);
1221
-		$backgroundJob->start($this->jobList);
1222
-
1223
-		$task = $this->manager->getTask($task->getId());
1224
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1225
-		self::assertIsArray($task->getOutput());
1226
-		self::assertTrue(isset($task->getOutput()['images']));
1227
-		self::assertIsArray($task->getOutput()['images']);
1228
-		self::assertCount(3, $task->getOutput()['images']);
1229
-		self::assertTrue($this->providers[SuccessfulTextToImageProvider::class]->ran);
1230
-		$node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['images'][0], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1231
-		self::assertNotNull($node);
1232
-		self::assertInstanceOf(File::class, $node);
1233
-		self::assertEquals('test', $node->getContent());
1234
-	}
1235
-
1236
-	public function testShouldTransparentlyHandleFailingText2ImageProviders(): void {
1237
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1238
-			new ServiceRegistration('test', FailingTextToImageProvider::class)
1239
-		]);
1240
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1241
-		]);
1242
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1243
-		self::assertCount(1, $taskTypes);
1244
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1245
-		self::assertTrue(isset($taskTypes[TextToImage::ID]));
1246
-		self::assertTrue($this->manager->hasProviders());
1247
-		$task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1248
-		$this->manager->scheduleTask($task);
1249
-
1250
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1251
-
1252
-		$backgroundJob = new SynchronousBackgroundJob(
1253
-			Server::get(ITimeFactory::class),
1254
-			$this->manager,
1255
-			$this->jobList,
1256
-			Server::get(LoggerInterface::class),
1257
-		);
1258
-		$backgroundJob->start($this->jobList);
1259
-
1260
-		$task = $this->manager->getTask($task->getId());
1261
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1262
-		self::assertTrue($task->getOutput() === null);
1263
-		self::assertEquals('ERROR', $task->getErrorMessage());
1264
-		self::assertTrue($this->providers[FailingTextToImageProvider::class]->ran);
1265
-	}
1266
-
1267
-	public function testMergeProvidersLocalAndEvent() {
1268
-		// Arrange: Local provider registered, DIFFERENT external provider via event
1269
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1270
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
1271
-		]);
1272
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1273
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1274
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1275
-
1276
-		$externalProvider = new ExternalProvider(); // ID = 'event:external:provider'
1277
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1278
-		$this->manager = $this->createManagerInstance();
1279
-
1280
-		// Act
1281
-		$providers = $this->manager->getProviders();
1282
-
1283
-		// Assert: Both providers should be present
1284
-		self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1285
-		self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1286
-		self::assertArrayHasKey(ExternalProvider::ID, $providers);
1287
-		self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1288
-		self::assertCount(2, $providers);
1289
-	}
1290
-
1291
-	public function testGetProvidersIncludesExternalViaEvent() {
1292
-		// Arrange: No local providers, one external provider via event
1293
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1294
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1295
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1296
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1297
-
1298
-
1299
-		$externalProvider = new ExternalProvider();
1300
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1301
-		$this->manager = $this->createManagerInstance(); // Create manager with configured mocks
1302
-
1303
-		// Act
1304
-		$providers = $this->manager->getProviders(); // Returns ID-indexed array
1305
-
1306
-		// Assert
1307
-		self::assertArrayHasKey(ExternalProvider::ID, $providers);
1308
-		self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1309
-		self::assertCount(1, $providers);
1310
-		self::assertTrue($this->manager->hasProviders());
1311
-	}
1312
-
1313
-	public function testGetAvailableTaskTypesIncludesExternalViaEvent() {
1314
-		// Arrange: No local types/providers, one external type and provider via event
1315
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1316
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([]);
1317
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1318
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1319
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1320
-
1321
-		$externalProvider = new ExternalProvider(); // Provides ExternalTaskType
1322
-		$externalTaskType = new ExternalTaskType();
1323
-		$this->configureEventDispatcherMock(
1324
-			providersToAdd: [$externalProvider],
1325
-			taskTypesToAdd: [$externalTaskType]
1326
-		);
1327
-		$this->manager = $this->createManagerInstance();
1328
-
1329
-		// Act
1330
-		$availableTypes = $this->manager->getAvailableTaskTypes();
1331
-
1332
-		// Assert
1333
-		self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1334
-		self::assertContains(ExternalTaskType::ID, $this->manager->getAvailableTaskTypeIds());
1335
-		self::assertEquals(ExternalTaskType::ID, $externalProvider->getTaskTypeId(), 'Test Sanity: Provider must handle the Task Type');
1336
-		self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1337
-		// Check if shapes match the external type/provider
1338
-		self::assertArrayHasKey('external_input', $availableTypes[ExternalTaskType::ID]['inputShape']);
1339
-		self::assertArrayHasKey('external_output', $availableTypes[ExternalTaskType::ID]['outputShape']);
1340
-		self::assertEmpty($availableTypes[ExternalTaskType::ID]['optionalInputShape']); // From ExternalProvider
1341
-	}
1342
-
1343
-	public function testLocalProviderWinsConflictWithEvent() {
1344
-		// Arrange: Local provider registered, conflicting external provider via event
1345
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1346
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
1347
-		]);
1348
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1349
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1350
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1351
-
1352
-		$conflictingExternalProvider = new ConflictingExternalProvider(); // ID = 'test:sync:success'
1353
-		$this->configureEventDispatcherMock(providersToAdd: [$conflictingExternalProvider]);
1354
-		$this->manager = $this->createManagerInstance();
1355
-
1356
-		// Act
1357
-		$providers = $this->manager->getProviders();
1358
-
1359
-		// Assert: Only the local provider should be present for the conflicting ID
1360
-		self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1361
-		self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1362
-		self::assertCount(1, $providers); // Ensure no extra provider was added
1363
-	}
1364
-
1365
-	public function testTriggerableProviderWithNoOtherRunningTasks() {
1366
-		// Arrange: Local provider registered, conflicting external provider via event
1367
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1368
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1369
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1370
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1371
-
1372
-		$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1373
-		$externalProvider->expects($this->once())->method('trigger');
1374
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1375
-		$this->manager = $this->createManagerInstance();
1376
-
1377
-		// Act
1378
-		$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1379
-		$this->manager->scheduleTask($task);
1380
-	}
1381
-
1382
-	public function testTriggerableProviderWithOtherRunningTasks() {
1383
-		// Arrange: Local provider registered, conflicting external provider via event
1384
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1385
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1386
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1387
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1388
-
1389
-		$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1390
-		$externalProvider->expects($this->once())->method('trigger');
1391
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1392
-		$this->manager = $this->createManagerInstance();
1393
-
1394
-		$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1395
-		$this->manager->scheduleTask($task);
1396
-		$this->manager->lockTask($task);
1397
-
1398
-		// Act
1399
-		$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1400
-		$this->manager->scheduleTask($task);
1401
-	}
1402
-
1403
-	public function testMergeTaskTypesLocalAndEvent() {
1404
-		// Arrange: Local type registered, DIFFERENT external type via event
1405
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1406
-			new ServiceRegistration('test', AsyncProvider::class)
1407
-		]);
1408
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1409
-			new ServiceRegistration('test', AudioToImage::class)
1410
-		]);
1411
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1412
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1413
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1414
-
1415
-		$externalTaskType = new ExternalTaskType(); // ID = 'event:external:tasktype'
1416
-		$externalProvider = new ExternalProvider(); // Handles 'event:external:tasktype'
1417
-		$this->configureEventDispatcherMock(
1418
-			providersToAdd: [$externalProvider],
1419
-			taskTypesToAdd: [$externalTaskType]
1420
-		);
1421
-		$this->manager = $this->createManagerInstance();
1422
-
1423
-		// Act
1424
-		$availableTypes = $this->manager->getAvailableTaskTypes();
1425
-		$availableTypeIds = $this->manager->getAvailableTaskTypeIds();
1426
-
1427
-		// Assert: Both task types should be available
1428
-		self::assertContains(AudioToImage::ID, $availableTypeIds);
1429
-		self::assertArrayHasKey(AudioToImage::ID, $availableTypes);
1430
-		self::assertEquals(AudioToImage::class, $availableTypes[AudioToImage::ID]['name']);
1431
-
1432
-		self::assertContains(ExternalTaskType::ID, $availableTypeIds);
1433
-		self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1434
-		self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1435
-
1436
-		self::assertCount(2, $availableTypes);
1437
-	}
1438
-
1439
-	private function createManagerInstance(): Manager {
1440
-		// Clear potentially cached config values if needed
1441
-		$this->appConfig->deleteKey('core', 'ai.taskprocessing_type_preferences');
1442
-
1443
-		// Re-create Text2ImageManager if its state matters or mocks change
1444
-		$text2imageManager = new \OC\TextToImage\Manager(
1445
-			$this->serverContainer,
1446
-			$this->coordinator,
1447
-			Server::get(LoggerInterface::class),
1448
-			$this->jobList,
1449
-			Server::get(\OC\TextToImage\Db\TaskMapper::class),
1450
-			$this->config, // Use the shared config mock
1451
-			Server::get(IAppDataFactory::class),
1452
-		);
1453
-
1454
-		return new Manager(
1455
-			$this->appConfig,
1456
-			$this->coordinator,
1457
-			$this->serverContainer,
1458
-			Server::get(LoggerInterface::class),
1459
-			$this->taskMapper,
1460
-			$this->jobList,
1461
-			$this->eventDispatcher, // Use the potentially reconfigured mock
1462
-			Server::get(IAppDataFactory::class),
1463
-			$this->rootFolder,
1464
-			$text2imageManager,
1465
-			$this->userMountCache,
1466
-			Server::get(IClientService::class),
1467
-			Server::get(IAppManager::class),
1468
-			Server::get(IUserManager::class),
1469
-			Server::get(IUserSession::class),
1470
-			Server::get(ICacheFactory::class),
1471
-			Server::get(IFactory::class),
1472
-		);
1473
-	}
1474
-
1475
-	private function configureEventDispatcherMock(
1476
-		array $providersToAdd = [],
1477
-		array $taskTypesToAdd = [],
1478
-		?int $expectedCalls = null,
1479
-	): void {
1480
-		$dispatchExpectation = $expectedCalls === null ? $this->any() : $this->exactly($expectedCalls);
1481
-
1482
-		$this->eventDispatcher->expects($dispatchExpectation)
1483
-			->method('dispatchTyped')
1484
-			->willReturnCallback(function (object $event) use ($providersToAdd, $taskTypesToAdd): void {
1485
-				if ($event instanceof GetTaskProcessingProvidersEvent) {
1486
-					foreach ($providersToAdd as $providerInstance) {
1487
-						$event->addProvider($providerInstance);
1488
-					}
1489
-					foreach ($taskTypesToAdd as $taskTypeInstance) {
1490
-						$event->addTaskType($taskTypeInstance);
1491
-					}
1492
-				}
1493
-			});
1494
-	}
637
+    private Coordinator&MockObject $coordinator;
638
+    private IServerContainer&MockObject $serverContainer;
639
+    private IEventDispatcher&MockObject $eventDispatcher;
640
+    private IJobList&MockObject $jobList;
641
+    private IUserMountCache&MockObject $userMountCache;
642
+    private RegistrationContext&MockObject $registrationContext;
643
+
644
+    /** @var array<class-string, IProvider> */
645
+    private array $providers;
646
+    private IAppConfig $appConfig;
647
+    private IConfig $config;
648
+    private IRootFolder $rootFolder;
649
+    private TaskMapper $taskMapper;
650
+    private IManager $manager;
651
+
652
+    public const TEST_USER = 'testuser';
653
+
654
+    protected function setUp(): void {
655
+        parent::setUp();
656
+
657
+        $this->providers = [
658
+            SuccessfulSyncProvider::class => new SuccessfulSyncProvider(),
659
+            FailingSyncProvider::class => new FailingSyncProvider(),
660
+            FailingSyncProviderWithUserFacingError::class => new FailingSyncProviderWithUserFacingError(),
661
+            BrokenSyncProvider::class => new BrokenSyncProvider(),
662
+            AsyncProvider::class => new AsyncProvider(),
663
+            AudioToImage::class => new AudioToImage(),
664
+            SuccessfulTextProcessingSummaryProvider::class => new SuccessfulTextProcessingSummaryProvider(),
665
+            FailingTextProcessingSummaryProvider::class => new FailingTextProcessingSummaryProvider(),
666
+            SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(),
667
+            FailingTextToImageProvider::class => new FailingTextToImageProvider(),
668
+            ExternalProvider::class => new ExternalProvider(),
669
+            ExternalTriggerableProvider::class => new ExternalTriggerableProvider(),
670
+            ConflictingExternalProvider::class => new ConflictingExternalProvider(),
671
+            ExternalTaskType::class => new ExternalTaskType(),
672
+            ConflictingExternalTaskType::class => new ConflictingExternalTaskType(),
673
+        ];
674
+
675
+        $userManager = Server::get(IUserManager::class);
676
+        if (!$userManager->userExists(self::TEST_USER)) {
677
+            $userManager->createUser(self::TEST_USER, 'test');
678
+        }
679
+
680
+        $this->serverContainer = $this->createMock(IServerContainer::class);
681
+        $this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) {
682
+            return $this->providers[$class];
683
+        });
684
+
685
+        $this->registrationContext = $this->createMock(RegistrationContext::class);
686
+        $this->coordinator = $this->createMock(Coordinator::class);
687
+        $this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext);
688
+
689
+        $this->rootFolder = Server::get(IRootFolder::class);
690
+        $this->taskMapper = Server::get(TaskMapper::class);
691
+
692
+        $this->jobList = $this->createPartialMock(DummyJobList::class, ['add']);
693
+        $this->jobList->expects($this->any())->method('add')->willReturnCallback(function (): void {
694
+        });
695
+
696
+        $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
697
+        $this->configureEventDispatcherMock();
698
+
699
+        $text2imageManager = new \OC\TextToImage\Manager(
700
+            $this->serverContainer,
701
+            $this->coordinator,
702
+            Server::get(LoggerInterface::class),
703
+            $this->jobList,
704
+            Server::get(\OC\TextToImage\Db\TaskMapper::class),
705
+            Server::get(IConfig::class),
706
+            Server::get(IAppDataFactory::class),
707
+        );
708
+
709
+        $this->userMountCache = $this->createMock(IUserMountCache::class);
710
+        $this->config = Server::get(IConfig::class);
711
+        $this->appConfig = Server::get(IAppConfig::class);
712
+        $this->manager = new Manager(
713
+            $this->appConfig,
714
+            $this->coordinator,
715
+            $this->serverContainer,
716
+            Server::get(LoggerInterface::class),
717
+            $this->taskMapper,
718
+            $this->jobList,
719
+            $this->eventDispatcher,
720
+            Server::get(IAppDataFactory::class),
721
+            Server::get(IRootFolder::class),
722
+            $text2imageManager,
723
+            $this->userMountCache,
724
+            Server::get(IClientService::class),
725
+            Server::get(IAppManager::class),
726
+            $userManager,
727
+            Server::get(IUserSession::class),
728
+            Server::get(ICacheFactory::class),
729
+            Server::get(IFactory::class),
730
+        );
731
+    }
732
+
733
+    private function getFile(string $name, string $content): File {
734
+        $folder = $this->rootFolder->getUserFolder(self::TEST_USER);
735
+        $file = $folder->newFile($name, $content);
736
+        return $file;
737
+    }
738
+
739
+    public function testShouldNotHaveAnyProviders(): void {
740
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
741
+        self::assertCount(0, $this->manager->getAvailableTaskTypes());
742
+        self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
743
+        self::assertFalse($this->manager->hasProviders());
744
+        self::expectException(PreConditionNotMetException::class);
745
+        $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
746
+    }
747
+
748
+    public function testProviderShouldBeRegisteredAndTaskTypeDisabled(): void {
749
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
750
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
751
+        ]);
752
+        $taskProcessingTypeSettings = [
753
+            TextToText::ID => false,
754
+        ];
755
+        $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
756
+        self::assertCount(0, $this->manager->getAvailableTaskTypes());
757
+        self::assertCount(1, $this->manager->getAvailableTaskTypes(true));
758
+        self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
759
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds(true));
760
+        self::assertTrue($this->manager->hasProviders());
761
+        self::expectException(PreConditionNotMetException::class);
762
+        $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
763
+    }
764
+
765
+
766
+    public function testProviderShouldBeRegisteredAndTaskFailValidation(): void {
767
+        $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
768
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
769
+            new ServiceRegistration('test', BrokenSyncProvider::class)
770
+        ]);
771
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
772
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
773
+        self::assertTrue($this->manager->hasProviders());
774
+        $task = new Task(TextToText::ID, ['wrongInputKey' => 'Hello'], 'test', null);
775
+        self::assertNull($task->getId());
776
+        self::expectException(ValidationException::class);
777
+        $this->manager->scheduleTask($task);
778
+    }
779
+
780
+    public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation(): void {
781
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
782
+            new ServiceRegistration('test', AudioToImage::class)
783
+        ]);
784
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
785
+            new ServiceRegistration('test', AsyncProvider::class)
786
+        ]);
787
+        $user = $this->createMock(IUser::class);
788
+        $user->expects($this->any())->method('getUID')->willReturn(null);
789
+        $mount = $this->createMock(ICachedMountInfo::class);
790
+        $mount->expects($this->any())->method('getUser')->willReturn($user);
791
+        $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
792
+
793
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
794
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
795
+        self::assertTrue($this->manager->hasProviders());
796
+
797
+        $audioId = $this->getFile('audioInput', 'Hello')->getId();
798
+        $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null);
799
+        self::assertNull($task->getId());
800
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
801
+        self::expectException(UnauthorizedException::class);
802
+        $this->manager->scheduleTask($task);
803
+    }
804
+
805
+    public function testProviderShouldBeRegisteredAndFail(): void {
806
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
807
+            new ServiceRegistration('test', FailingSyncProvider::class)
808
+        ]);
809
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
810
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
811
+        self::assertTrue($this->manager->hasProviders());
812
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
813
+        self::assertNull($task->getId());
814
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
815
+        $this->manager->scheduleTask($task);
816
+        self::assertNotNull($task->getId());
817
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
818
+
819
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
820
+
821
+        $backgroundJob = new SynchronousBackgroundJob(
822
+            Server::get(ITimeFactory::class),
823
+            $this->manager,
824
+            $this->jobList,
825
+            Server::get(LoggerInterface::class),
826
+        );
827
+        $backgroundJob->start($this->jobList);
828
+
829
+        $task = $this->manager->getTask($task->getId());
830
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
831
+        self::assertEquals(FailingSyncProvider::ERROR_MESSAGE, $task->getErrorMessage());
832
+    }
833
+
834
+    public function testProviderShouldBeRegisteredAndFailWithUserFacingMessage(): void {
835
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
836
+            new ServiceRegistration('test', FailingSyncProviderWithUserFacingError::class)
837
+        ]);
838
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
839
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
840
+        self::assertTrue($this->manager->hasProviders());
841
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
842
+        self::assertNull($task->getId());
843
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
844
+        $this->manager->scheduleTask($task);
845
+        self::assertNotNull($task->getId());
846
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
847
+
848
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
849
+
850
+        $backgroundJob = new SynchronousBackgroundJob(
851
+            Server::get(ITimeFactory::class),
852
+            $this->manager,
853
+            $this->jobList,
854
+            Server::get(LoggerInterface::class),
855
+        );
856
+        $backgroundJob->start($this->jobList);
857
+
858
+        $task = $this->manager->getTask($task->getId());
859
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
860
+        self::assertEquals(FailingSyncProviderWithUserFacingError::ERROR_MESSAGE, $task->getErrorMessage());
861
+        self::assertEquals(FailingSyncProviderWithUserFacingError::USER_FACING_ERROR_MESSAGE, $task->getUserFacingErrorMessage());
862
+    }
863
+
864
+    public function testProviderShouldBeRegisteredAndFailOutputValidation(): void {
865
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
866
+            new ServiceRegistration('test', BrokenSyncProvider::class)
867
+        ]);
868
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
869
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
870
+        self::assertTrue($this->manager->hasProviders());
871
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
872
+        self::assertNull($task->getId());
873
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
874
+        $this->manager->scheduleTask($task);
875
+        self::assertNotNull($task->getId());
876
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
877
+
878
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
879
+
880
+        $backgroundJob = new SynchronousBackgroundJob(
881
+            Server::get(ITimeFactory::class),
882
+            $this->manager,
883
+            $this->jobList,
884
+            Server::get(LoggerInterface::class),
885
+        );
886
+        $backgroundJob->start($this->jobList);
887
+
888
+        $task = $this->manager->getTask($task->getId());
889
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
890
+        self::assertEquals('The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec', $task->getErrorMessage());
891
+    }
892
+
893
+    public function testProviderShouldBeRegisteredAndRun(): void {
894
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
895
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
896
+        ]);
897
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
898
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
899
+        $taskTypeStruct = $this->manager->getAvailableTaskTypes()[array_keys($this->manager->getAvailableTaskTypes())[0]];
900
+        self::assertTrue(isset($taskTypeStruct['inputShape']['input']));
901
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['inputShape']['input']->getShapeType());
902
+        self::assertTrue(isset($taskTypeStruct['optionalInputShape']['optionalKey']));
903
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalInputShape']['optionalKey']->getShapeType());
904
+        self::assertTrue(isset($taskTypeStruct['outputShape']['output']));
905
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['outputShape']['output']->getShapeType());
906
+        self::assertTrue(isset($taskTypeStruct['optionalOutputShape']['optionalKey']));
907
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalOutputShape']['optionalKey']->getShapeType());
908
+
909
+        self::assertTrue($this->manager->hasProviders());
910
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
911
+        self::assertNull($task->getId());
912
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
913
+        $this->manager->scheduleTask($task);
914
+        self::assertNotNull($task->getId());
915
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
916
+
917
+        // Task object retrieved from db is up-to-date
918
+        $task2 = $this->manager->getTask($task->getId());
919
+        self::assertEquals($task->getId(), $task2->getId());
920
+        self::assertEquals(['input' => 'Hello'], $task2->getInput());
921
+        self::assertNull($task2->getOutput());
922
+        self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
923
+
924
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
925
+
926
+        $backgroundJob = new SynchronousBackgroundJob(
927
+            Server::get(ITimeFactory::class),
928
+            $this->manager,
929
+            $this->jobList,
930
+            Server::get(LoggerInterface::class),
931
+        );
932
+        $backgroundJob->start($this->jobList);
933
+
934
+        $task = $this->manager->getTask($task->getId());
935
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
936
+        self::assertEquals(['output' => 'Hello'], $task->getOutput());
937
+        self::assertEquals(1, $task->getProgress());
938
+    }
939
+
940
+    public function testTaskTypeExplicitlyEnabled(): void {
941
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
942
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
943
+        ]);
944
+
945
+        $taskProcessingTypeSettings = [
946
+            TextToText::ID => true,
947
+        ];
948
+        $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
949
+
950
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
951
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
952
+
953
+        self::assertTrue($this->manager->hasProviders());
954
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
955
+        self::assertNull($task->getId());
956
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
957
+        $this->manager->scheduleTask($task);
958
+        self::assertNotNull($task->getId());
959
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
960
+
961
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
962
+
963
+        $backgroundJob = new SynchronousBackgroundJob(
964
+            Server::get(ITimeFactory::class),
965
+            $this->manager,
966
+            $this->jobList,
967
+            Server::get(LoggerInterface::class),
968
+        );
969
+        $backgroundJob->start($this->jobList);
970
+
971
+        $task = $this->manager->getTask($task->getId());
972
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
973
+        self::assertEquals(['output' => 'Hello'], $task->getOutput());
974
+        self::assertEquals(1, $task->getProgress());
975
+    }
976
+
977
+    public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData(): void {
978
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
979
+            new ServiceRegistration('test', AudioToImage::class)
980
+        ]);
981
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
982
+            new ServiceRegistration('test', AsyncProvider::class)
983
+        ]);
984
+
985
+        $user = $this->createMock(IUser::class);
986
+        $user->expects($this->any())->method('getUID')->willReturn('testuser');
987
+        $mount = $this->createMock(ICachedMountInfo::class);
988
+        $mount->expects($this->any())->method('getUser')->willReturn($user);
989
+        $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
990
+
991
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
992
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
993
+
994
+        self::assertTrue($this->manager->hasProviders());
995
+        $audioId = $this->getFile('audioInput', 'Hello')->getId();
996
+        $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
997
+        self::assertNull($task->getId());
998
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
999
+        $this->manager->scheduleTask($task);
1000
+        self::assertNotNull($task->getId());
1001
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1002
+
1003
+        // Task object retrieved from db is up-to-date
1004
+        $task2 = $this->manager->getTask($task->getId());
1005
+        self::assertEquals($task->getId(), $task2->getId());
1006
+        self::assertEquals(['audio' => $audioId], $task2->getInput());
1007
+        self::assertNull($task2->getOutput());
1008
+        self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1009
+
1010
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1011
+
1012
+        $this->manager->setTaskProgress($task2->getId(), 0.1);
1013
+        $input = $this->manager->prepareInputData($task2);
1014
+        self::assertTrue(isset($input['audio']));
1015
+        self::assertInstanceOf(File::class, $input['audio']);
1016
+        self::assertEquals($audioId, $input['audio']->getId());
1017
+
1018
+        $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => 'World']);
1019
+
1020
+        $task = $this->manager->getTask($task->getId());
1021
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1022
+        self::assertEquals(1, $task->getProgress());
1023
+        self::assertTrue(isset($task->getOutput()['spectrogram']));
1024
+        $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1025
+        self::assertNotNull($node);
1026
+        self::assertInstanceOf(File::class, $node);
1027
+        self::assertEquals('World', $node->getContent());
1028
+    }
1029
+
1030
+    public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds(): void {
1031
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1032
+            new ServiceRegistration('test', AudioToImage::class)
1033
+        ]);
1034
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1035
+            new ServiceRegistration('test', AsyncProvider::class)
1036
+        ]);
1037
+        $user = $this->createMock(IUser::class);
1038
+        $user->expects($this->any())->method('getUID')->willReturn('testuser');
1039
+        $mount = $this->createMock(ICachedMountInfo::class);
1040
+        $mount->expects($this->any())->method('getUser')->willReturn($user);
1041
+        $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
1042
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
1043
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1044
+
1045
+        self::assertTrue($this->manager->hasProviders());
1046
+        $audioId = $this->getFile('audioInput', 'Hello')->getId();
1047
+        $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
1048
+        self::assertNull($task->getId());
1049
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
1050
+        $this->manager->scheduleTask($task);
1051
+        self::assertNotNull($task->getId());
1052
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1053
+
1054
+        // Task object retrieved from db is up-to-date
1055
+        $task2 = $this->manager->getTask($task->getId());
1056
+        self::assertEquals($task->getId(), $task2->getId());
1057
+        self::assertEquals(['audio' => $audioId], $task2->getInput());
1058
+        self::assertNull($task2->getOutput());
1059
+        self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1060
+
1061
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1062
+
1063
+        $this->manager->setTaskProgress($task2->getId(), 0.1);
1064
+        $input = $this->manager->prepareInputData($task2);
1065
+        self::assertTrue(isset($input['audio']));
1066
+        self::assertInstanceOf(File::class, $input['audio']);
1067
+        self::assertEquals($audioId, $input['audio']->getId());
1068
+
1069
+        $outputFileId = $this->getFile('audioOutput', 'World')->getId();
1070
+
1071
+        $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true);
1072
+
1073
+        $task = $this->manager->getTask($task->getId());
1074
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1075
+        self::assertEquals(1, $task->getProgress());
1076
+        self::assertTrue(isset($task->getOutput()['spectrogram']));
1077
+        $node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']);
1078
+        self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']);
1079
+        self::assertInstanceOf(File::class, $node);
1080
+        self::assertEquals('World', $node->getContent());
1081
+    }
1082
+
1083
+    public function testNonexistentTask(): void {
1084
+        $this->expectException(NotFoundException::class);
1085
+        $this->manager->getTask(2147483646);
1086
+    }
1087
+
1088
+    public function testOldTasksShouldBeCleanedUp(): void {
1089
+        $currentTime = new \DateTime('now');
1090
+        $timeFactory = $this->createMock(ITimeFactory::class);
1091
+        $timeFactory->expects($this->any())->method('getDateTime')->willReturnCallback(fn () => $currentTime);
1092
+        $timeFactory->expects($this->any())->method('getTime')->willReturnCallback(fn () => $currentTime->getTimestamp());
1093
+
1094
+        $this->taskMapper = new TaskMapper(
1095
+            Server::get(IDBConnection::class),
1096
+            $timeFactory,
1097
+        );
1098
+
1099
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1100
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
1101
+        ]);
1102
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
1103
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1104
+        self::assertTrue($this->manager->hasProviders());
1105
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
1106
+        $this->manager->scheduleTask($task);
1107
+
1108
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1109
+
1110
+        $backgroundJob = new SynchronousBackgroundJob(
1111
+            Server::get(ITimeFactory::class),
1112
+            $this->manager,
1113
+            $this->jobList,
1114
+            Server::get(LoggerInterface::class),
1115
+        );
1116
+        $backgroundJob->start($this->jobList);
1117
+
1118
+        $task = $this->manager->getTask($task->getId());
1119
+
1120
+        $currentTime = $currentTime->add(new \DateInterval('P1Y'));
1121
+        // run background job
1122
+        $bgJob = new RemoveOldTasksBackgroundJob(
1123
+            $timeFactory,
1124
+            $this->manager,
1125
+            $this->taskMapper,
1126
+            Server::get(LoggerInterface::class),
1127
+            Server::get(IAppDataFactory::class),
1128
+        );
1129
+        $bgJob->setArgument([]);
1130
+        $bgJob->start($this->jobList);
1131
+
1132
+        $this->expectException(NotFoundException::class);
1133
+        $this->manager->getTask($task->getId());
1134
+    }
1135
+
1136
+    public function testShouldTransparentlyHandleTextProcessingProviders(): void {
1137
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1138
+            new ServiceRegistration('test', SuccessfulTextProcessingSummaryProvider::class)
1139
+        ]);
1140
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1141
+        ]);
1142
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1143
+        self::assertCount(1, $taskTypes);
1144
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1145
+        self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1146
+        self::assertTrue($this->manager->hasProviders());
1147
+        $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1148
+        $this->manager->scheduleTask($task);
1149
+
1150
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1151
+
1152
+        $backgroundJob = new SynchronousBackgroundJob(
1153
+            Server::get(ITimeFactory::class),
1154
+            $this->manager,
1155
+            $this->jobList,
1156
+            Server::get(LoggerInterface::class),
1157
+        );
1158
+        $backgroundJob->start($this->jobList);
1159
+
1160
+        $task = $this->manager->getTask($task->getId());
1161
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1162
+        self::assertIsArray($task->getOutput());
1163
+        self::assertTrue(isset($task->getOutput()['output']));
1164
+        self::assertEquals('Hello Summarize', $task->getOutput()['output']);
1165
+        self::assertTrue($this->providers[SuccessfulTextProcessingSummaryProvider::class]->ran);
1166
+    }
1167
+
1168
+    public function testShouldTransparentlyHandleFailingTextProcessingProviders(): void {
1169
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1170
+            new ServiceRegistration('test', FailingTextProcessingSummaryProvider::class)
1171
+        ]);
1172
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1173
+        ]);
1174
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1175
+        self::assertCount(1, $taskTypes);
1176
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1177
+        self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1178
+        self::assertTrue($this->manager->hasProviders());
1179
+        $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1180
+        $this->manager->scheduleTask($task);
1181
+
1182
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1183
+
1184
+        $backgroundJob = new SynchronousBackgroundJob(
1185
+            Server::get(ITimeFactory::class),
1186
+            $this->manager,
1187
+            $this->jobList,
1188
+            Server::get(LoggerInterface::class),
1189
+        );
1190
+        $backgroundJob->start($this->jobList);
1191
+
1192
+        $task = $this->manager->getTask($task->getId());
1193
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1194
+        self::assertTrue($task->getOutput() === null);
1195
+        self::assertEquals('ERROR', $task->getErrorMessage());
1196
+        self::assertTrue($this->providers[FailingTextProcessingSummaryProvider::class]->ran);
1197
+    }
1198
+
1199
+    public function testShouldTransparentlyHandleText2ImageProviders(): void {
1200
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1201
+            new ServiceRegistration('test', SuccessfulTextToImageProvider::class)
1202
+        ]);
1203
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1204
+        ]);
1205
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1206
+        self::assertCount(1, $taskTypes);
1207
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1208
+        self::assertTrue(isset($taskTypes[TextToImage::ID]));
1209
+        self::assertTrue($this->manager->hasProviders());
1210
+        $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1211
+        $this->manager->scheduleTask($task);
1212
+
1213
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1214
+
1215
+        $backgroundJob = new SynchronousBackgroundJob(
1216
+            Server::get(ITimeFactory::class),
1217
+            $this->manager,
1218
+            $this->jobList,
1219
+            Server::get(LoggerInterface::class),
1220
+        );
1221
+        $backgroundJob->start($this->jobList);
1222
+
1223
+        $task = $this->manager->getTask($task->getId());
1224
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1225
+        self::assertIsArray($task->getOutput());
1226
+        self::assertTrue(isset($task->getOutput()['images']));
1227
+        self::assertIsArray($task->getOutput()['images']);
1228
+        self::assertCount(3, $task->getOutput()['images']);
1229
+        self::assertTrue($this->providers[SuccessfulTextToImageProvider::class]->ran);
1230
+        $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['images'][0], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1231
+        self::assertNotNull($node);
1232
+        self::assertInstanceOf(File::class, $node);
1233
+        self::assertEquals('test', $node->getContent());
1234
+    }
1235
+
1236
+    public function testShouldTransparentlyHandleFailingText2ImageProviders(): void {
1237
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1238
+            new ServiceRegistration('test', FailingTextToImageProvider::class)
1239
+        ]);
1240
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1241
+        ]);
1242
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1243
+        self::assertCount(1, $taskTypes);
1244
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1245
+        self::assertTrue(isset($taskTypes[TextToImage::ID]));
1246
+        self::assertTrue($this->manager->hasProviders());
1247
+        $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1248
+        $this->manager->scheduleTask($task);
1249
+
1250
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1251
+
1252
+        $backgroundJob = new SynchronousBackgroundJob(
1253
+            Server::get(ITimeFactory::class),
1254
+            $this->manager,
1255
+            $this->jobList,
1256
+            Server::get(LoggerInterface::class),
1257
+        );
1258
+        $backgroundJob->start($this->jobList);
1259
+
1260
+        $task = $this->manager->getTask($task->getId());
1261
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1262
+        self::assertTrue($task->getOutput() === null);
1263
+        self::assertEquals('ERROR', $task->getErrorMessage());
1264
+        self::assertTrue($this->providers[FailingTextToImageProvider::class]->ran);
1265
+    }
1266
+
1267
+    public function testMergeProvidersLocalAndEvent() {
1268
+        // Arrange: Local provider registered, DIFFERENT external provider via event
1269
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1270
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
1271
+        ]);
1272
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1273
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1274
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1275
+
1276
+        $externalProvider = new ExternalProvider(); // ID = 'event:external:provider'
1277
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1278
+        $this->manager = $this->createManagerInstance();
1279
+
1280
+        // Act
1281
+        $providers = $this->manager->getProviders();
1282
+
1283
+        // Assert: Both providers should be present
1284
+        self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1285
+        self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1286
+        self::assertArrayHasKey(ExternalProvider::ID, $providers);
1287
+        self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1288
+        self::assertCount(2, $providers);
1289
+    }
1290
+
1291
+    public function testGetProvidersIncludesExternalViaEvent() {
1292
+        // Arrange: No local providers, one external provider via event
1293
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1294
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1295
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1296
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1297
+
1298
+
1299
+        $externalProvider = new ExternalProvider();
1300
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1301
+        $this->manager = $this->createManagerInstance(); // Create manager with configured mocks
1302
+
1303
+        // Act
1304
+        $providers = $this->manager->getProviders(); // Returns ID-indexed array
1305
+
1306
+        // Assert
1307
+        self::assertArrayHasKey(ExternalProvider::ID, $providers);
1308
+        self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1309
+        self::assertCount(1, $providers);
1310
+        self::assertTrue($this->manager->hasProviders());
1311
+    }
1312
+
1313
+    public function testGetAvailableTaskTypesIncludesExternalViaEvent() {
1314
+        // Arrange: No local types/providers, one external type and provider via event
1315
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1316
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([]);
1317
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1318
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1319
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1320
+
1321
+        $externalProvider = new ExternalProvider(); // Provides ExternalTaskType
1322
+        $externalTaskType = new ExternalTaskType();
1323
+        $this->configureEventDispatcherMock(
1324
+            providersToAdd: [$externalProvider],
1325
+            taskTypesToAdd: [$externalTaskType]
1326
+        );
1327
+        $this->manager = $this->createManagerInstance();
1328
+
1329
+        // Act
1330
+        $availableTypes = $this->manager->getAvailableTaskTypes();
1331
+
1332
+        // Assert
1333
+        self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1334
+        self::assertContains(ExternalTaskType::ID, $this->manager->getAvailableTaskTypeIds());
1335
+        self::assertEquals(ExternalTaskType::ID, $externalProvider->getTaskTypeId(), 'Test Sanity: Provider must handle the Task Type');
1336
+        self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1337
+        // Check if shapes match the external type/provider
1338
+        self::assertArrayHasKey('external_input', $availableTypes[ExternalTaskType::ID]['inputShape']);
1339
+        self::assertArrayHasKey('external_output', $availableTypes[ExternalTaskType::ID]['outputShape']);
1340
+        self::assertEmpty($availableTypes[ExternalTaskType::ID]['optionalInputShape']); // From ExternalProvider
1341
+    }
1342
+
1343
+    public function testLocalProviderWinsConflictWithEvent() {
1344
+        // Arrange: Local provider registered, conflicting external provider via event
1345
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1346
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
1347
+        ]);
1348
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1349
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1350
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1351
+
1352
+        $conflictingExternalProvider = new ConflictingExternalProvider(); // ID = 'test:sync:success'
1353
+        $this->configureEventDispatcherMock(providersToAdd: [$conflictingExternalProvider]);
1354
+        $this->manager = $this->createManagerInstance();
1355
+
1356
+        // Act
1357
+        $providers = $this->manager->getProviders();
1358
+
1359
+        // Assert: Only the local provider should be present for the conflicting ID
1360
+        self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1361
+        self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1362
+        self::assertCount(1, $providers); // Ensure no extra provider was added
1363
+    }
1364
+
1365
+    public function testTriggerableProviderWithNoOtherRunningTasks() {
1366
+        // Arrange: Local provider registered, conflicting external provider via event
1367
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1368
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1369
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1370
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1371
+
1372
+        $externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1373
+        $externalProvider->expects($this->once())->method('trigger');
1374
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1375
+        $this->manager = $this->createManagerInstance();
1376
+
1377
+        // Act
1378
+        $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1379
+        $this->manager->scheduleTask($task);
1380
+    }
1381
+
1382
+    public function testTriggerableProviderWithOtherRunningTasks() {
1383
+        // Arrange: Local provider registered, conflicting external provider via event
1384
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1385
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1386
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1387
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1388
+
1389
+        $externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1390
+        $externalProvider->expects($this->once())->method('trigger');
1391
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1392
+        $this->manager = $this->createManagerInstance();
1393
+
1394
+        $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1395
+        $this->manager->scheduleTask($task);
1396
+        $this->manager->lockTask($task);
1397
+
1398
+        // Act
1399
+        $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1400
+        $this->manager->scheduleTask($task);
1401
+    }
1402
+
1403
+    public function testMergeTaskTypesLocalAndEvent() {
1404
+        // Arrange: Local type registered, DIFFERENT external type via event
1405
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1406
+            new ServiceRegistration('test', AsyncProvider::class)
1407
+        ]);
1408
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1409
+            new ServiceRegistration('test', AudioToImage::class)
1410
+        ]);
1411
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1412
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1413
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1414
+
1415
+        $externalTaskType = new ExternalTaskType(); // ID = 'event:external:tasktype'
1416
+        $externalProvider = new ExternalProvider(); // Handles 'event:external:tasktype'
1417
+        $this->configureEventDispatcherMock(
1418
+            providersToAdd: [$externalProvider],
1419
+            taskTypesToAdd: [$externalTaskType]
1420
+        );
1421
+        $this->manager = $this->createManagerInstance();
1422
+
1423
+        // Act
1424
+        $availableTypes = $this->manager->getAvailableTaskTypes();
1425
+        $availableTypeIds = $this->manager->getAvailableTaskTypeIds();
1426
+
1427
+        // Assert: Both task types should be available
1428
+        self::assertContains(AudioToImage::ID, $availableTypeIds);
1429
+        self::assertArrayHasKey(AudioToImage::ID, $availableTypes);
1430
+        self::assertEquals(AudioToImage::class, $availableTypes[AudioToImage::ID]['name']);
1431
+
1432
+        self::assertContains(ExternalTaskType::ID, $availableTypeIds);
1433
+        self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1434
+        self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1435
+
1436
+        self::assertCount(2, $availableTypes);
1437
+    }
1438
+
1439
+    private function createManagerInstance(): Manager {
1440
+        // Clear potentially cached config values if needed
1441
+        $this->appConfig->deleteKey('core', 'ai.taskprocessing_type_preferences');
1442
+
1443
+        // Re-create Text2ImageManager if its state matters or mocks change
1444
+        $text2imageManager = new \OC\TextToImage\Manager(
1445
+            $this->serverContainer,
1446
+            $this->coordinator,
1447
+            Server::get(LoggerInterface::class),
1448
+            $this->jobList,
1449
+            Server::get(\OC\TextToImage\Db\TaskMapper::class),
1450
+            $this->config, // Use the shared config mock
1451
+            Server::get(IAppDataFactory::class),
1452
+        );
1453
+
1454
+        return new Manager(
1455
+            $this->appConfig,
1456
+            $this->coordinator,
1457
+            $this->serverContainer,
1458
+            Server::get(LoggerInterface::class),
1459
+            $this->taskMapper,
1460
+            $this->jobList,
1461
+            $this->eventDispatcher, // Use the potentially reconfigured mock
1462
+            Server::get(IAppDataFactory::class),
1463
+            $this->rootFolder,
1464
+            $text2imageManager,
1465
+            $this->userMountCache,
1466
+            Server::get(IClientService::class),
1467
+            Server::get(IAppManager::class),
1468
+            Server::get(IUserManager::class),
1469
+            Server::get(IUserSession::class),
1470
+            Server::get(ICacheFactory::class),
1471
+            Server::get(IFactory::class),
1472
+        );
1473
+    }
1474
+
1475
+    private function configureEventDispatcherMock(
1476
+        array $providersToAdd = [],
1477
+        array $taskTypesToAdd = [],
1478
+        ?int $expectedCalls = null,
1479
+    ): void {
1480
+        $dispatchExpectation = $expectedCalls === null ? $this->any() : $this->exactly($expectedCalls);
1481
+
1482
+        $this->eventDispatcher->expects($dispatchExpectation)
1483
+            ->method('dispatchTyped')
1484
+            ->willReturnCallback(function (object $event) use ($providersToAdd, $taskTypesToAdd): void {
1485
+                if ($event instanceof GetTaskProcessingProvidersEvent) {
1486
+                    foreach ($providersToAdd as $providerInstance) {
1487
+                        $event->addProvider($providerInstance);
1488
+                    }
1489
+                    foreach ($taskTypesToAdd as $taskTypeInstance) {
1490
+                        $event->addTaskType($taskTypeInstance);
1491
+                    }
1492
+                }
1493
+            });
1494
+    }
1495 1495
 }
Please login to merge, or discard this patch.