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