From dfbe1aa759ffec63f4333b133c2b885a02e3f287 Mon Sep 17 00:00:00 2001 From: WSHAPER <42714629+WSHAPER@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:15:33 +0100 Subject: [PATCH 1/9] fix: show names for task output files in result view Task output files rendered as anonymous chips in the assistant result view because FileDisplay never fetched file info for outputs, and the only existing info endpoint resolves files through the user folder, so it cannot find task outputs that live in appdata. Add a task-scoped output file info endpoint that authorizes through the same task ownership and file id checks as the existing output file preview and download routes, returning name and size. Output file chips now fetch their info through it when the task id is known. Assisted-by: opencode (zai/glm-5.3) Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com> --- appinfo/routes.php | 1 + lib/Controller/AssistantApiController.php | 26 +++ lib/Service/AssistantService.php | 17 ++ src/components/fields/FileDisplay.vue | 12 ++ tests/unit/Service/AssistantServiceTest.php | 182 ++++++++++++++++++++ 5 files changed, 238 insertions(+) create mode 100644 tests/unit/Service/AssistantServiceTest.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 3dc4df1d0..e49b7d1d4 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -34,6 +34,7 @@ ['name' => 'assistantApi#shareOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/file/{fileId}/share', 'verb' => 'POST', 'requirements' => $requirements], ['name' => 'assistantApi#saveOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/file/{fileId}/save', 'verb' => 'POST', 'requirements' => $requirements], ['name' => 'assistantApi#getOutputFilePreview', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/preview', 'verb' => 'GET', 'requirements' => $requirements], + ['name' => 'assistantApi#getOutputFileInfo', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/info', 'verb' => 'GET', 'requirements' => $requirements], ['name' => 'assistantApi#getOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/download', 'verb' => 'GET', 'requirements' => $requirements], ['name' => 'assistantApi#runFileAction', 'url' => '/api/{apiVersion}/file-action/{fileId}/{taskTypeId}', 'verb' => 'POST', 'requirements' => $requirements], ['name' => 'assistantApi#getAssistantFolderPath', 'url' => '/api/{apiVersion}/assistant-folder-path', 'verb' => 'GET', 'requirements' => $requirements], diff --git a/lib/Controller/AssistantApiController.php b/lib/Controller/AssistantApiController.php index d9953d3dd..8bbbe7346 100644 --- a/lib/Controller/AssistantApiController.php +++ b/lib/Controller/AssistantApiController.php @@ -421,6 +421,32 @@ public function getOutputFile(int $ocpTaskId, int $fileId): DataDownloadResponse } } + /** + * Get task output file info + * + * Get information about a task output file + * + * @param int $ocpTaskId The task ID + * @param int $fileId The task output file ID + * @return DataResponse|DataResponse + * + * 200: The file info is returned + * 404: The output file is not found + */ + #[NoAdminRequired] + #[NoCsrfRequired] + public function getOutputFileInfo(int $ocpTaskId, int $fileId): DataResponse { + try { + $fileInfo = $this->assistantService->getTaskOutputFileInfo($this->userId, $ocpTaskId, $fileId); + if ($fileInfo !== null) { + return new DataResponse($fileInfo); + } + } catch (Exception|Throwable $e) { + $this->logger->error('getOutputFileInfo error', ['exception' => $e]); + } + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + /** * Run a file action * diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php index 768b9f1ce..7cf65cecc 100644 --- a/lib/Service/AssistantService.php +++ b/lib/Service/AssistantService.php @@ -655,6 +655,23 @@ public function getTaskOutputFile(string $userId, int $ocpTaskId, int $fileId): return $node; } + /** + * @param string $userId + * @param int $ocpTaskId + * @param int $fileId + * @return array|null + * @throws Exception + * @throws NotFoundException + * @throws TaskProcessingException + */ + public function getTaskOutputFileInfo(string $userId, int $ocpTaskId, int $fileId): ?array { + $file = $this->getTaskOutputFile($userId, $ocpTaskId, $fileId); + return [ + 'name' => $file->getName(), + 'size' => $file->getSize(), + ]; + } + /** * @param string $userId * @param int $ocpTaskId diff --git a/src/components/fields/FileDisplay.vue b/src/components/fields/FileDisplay.vue index 31c5ee26c..aeae166f0 100644 --- a/src/components/fields/FileDisplay.vue +++ b/src/components/fields/FileDisplay.vue @@ -81,6 +81,8 @@ export default { console.debug('CURRENT TASK', this.myCurrentTaskId) if (!this.isOutput) { this.getFileInfo() + } else if (this.myCurrentTaskId !== null && this.myCurrentTaskId !== undefined) { + this.getOutputFileInfo() } }, @@ -92,6 +94,16 @@ export default { this.fileInfo = response.data.ocs.data }) }, + getOutputFileInfo() { + const url = generateOcsUrl('apps/assistant/api/v1/task/{taskId}/output-file/{fileId}/info', { + taskId: this.myCurrentTaskId, + fileId: this.fileId, + }) + axios.get(url) + .then((response) => { + this.fileInfo = response.data.ocs.data + }) + }, }, } diff --git a/tests/unit/Service/AssistantServiceTest.php b/tests/unit/Service/AssistantServiceTest.php new file mode 100644 index 000000000..3bb819be3 --- /dev/null +++ b/tests/unit/Service/AssistantServiceTest.php @@ -0,0 +1,182 @@ +taskProcessingManager = $this->createMock(ITaskProcessingManager::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + + $this->taskProcessingManager->method('getAvailableTaskTypes') + ->willReturn([ + 'test:tasktype' => [ + 'name' => 'Test task type', + 'description' => 'A test task type', + 'inputShape' => [], + 'optionalInputShape' => [], + 'outputShape' => [ + 'result' => new ShapeDescriptor('Result', 'The result file', EShapeType::File), + ], + 'optionalOutputShape' => [], + ], + ]); + + $this->service = new AssistantService( + $this->taskProcessingManager, + $this->createMock(TaskNotificationMapper::class), + $this->createMock(NotificationService::class), + $this->createMock(PreviewService::class), + $this->createMock(LoggerInterface::class), + $this->rootFolder, + $this->createMock(IL10N::class), + $this->createMock(ITempManager::class), + $this->createMock(IConfig::class), + $this->createMock(IAppConfig::class), + $this->createMock(IShareManager::class), + $this->createMock(SystemTagService::class), + ); + } + + private function getTask(string $userId): Task { + $task = new Task( + 'test:tasktype', + [], + 'assistant', + $userId, + ); + $task->setId(self::TASK_ID); + $task->setOutput(['result' => self::FILE_ID]); + return $task; + } + + private function getOutputFile(): File&MockObject { + $file = $this->createMock(File::class); + $file->method('getName')->willReturn('result.txt'); + $file->method('getSize')->willReturn(1024); + return $file; + } + + public function testGetTaskOutputFileInfo(): void { + $this->taskProcessingManager->method('getTask') + ->with(self::TASK_ID) + ->willReturn($this->getTask(self::USER_ID)); + $this->rootFolder->method('getFirstNodeById') + ->with(self::FILE_ID) + ->willReturn($this->getOutputFile()); + + $fileInfo = $this->service->getTaskOutputFileInfo(self::USER_ID, self::TASK_ID, self::FILE_ID); + + $this->assertSame([ + 'name' => 'result.txt', + 'size' => 1024, + ], $fileInfo); + } + + public function testGetTaskOutputFileInfoForAppdataFile(): void { + $this->taskProcessingManager->method('getTask') + ->with(self::TASK_ID) + ->willReturn($this->getTask(self::USER_ID)); + $this->rootFolder->method('getFirstNodeById') + ->with(self::FILE_ID) + ->willReturn(null); + $this->rootFolder->method('getAppDataDirectoryName') + ->willReturn('assistant'); + $this->rootFolder->method('getFirstNodeByIdInPath') + ->with(self::FILE_ID, '/assistant/') + ->willReturn($this->getOutputFile()); + + $fileInfo = $this->service->getTaskOutputFileInfo(self::USER_ID, self::TASK_ID, self::FILE_ID); + + $this->assertSame([ + 'name' => 'result.txt', + 'size' => 1024, + ], $fileInfo); + } + + public function testGetTaskOutputFileInfoWrongUser(): void { + $this->taskProcessingManager->method('getTask') + ->with(self::TASK_ID) + ->willReturn($this->getTask(self::USER_ID)); + + $this->expectException(DBException::class); + $this->expectExceptionMessage('Unauthorized'); + + $this->service->getTaskOutputFileInfo('another_user', self::TASK_ID, self::FILE_ID); + } + + public function testGetTaskOutputFileInfoUnknownFileId(): void { + $this->taskProcessingManager->method('getTask') + ->with(self::TASK_ID) + ->willReturn($this->getTask(self::USER_ID)); + + $this->expectException(DBException::class); + $this->expectExceptionMessage('Not found'); + + $this->service->getTaskOutputFileInfo(self::USER_ID, self::TASK_ID, 999999999); + } + + public function testGetTaskOutputFileInfoNotAFileNode(): void { + $this->taskProcessingManager->method('getTask') + ->with(self::TASK_ID) + ->willReturn($this->getTask(self::USER_ID)); + $this->rootFolder->method('getFirstNodeById') + ->with(self::FILE_ID) + ->willReturn(null); + $this->rootFolder->method('getAppDataDirectoryName') + ->willReturn('assistant'); + $this->rootFolder->method('getFirstNodeByIdInPath') + ->with(self::FILE_ID, '/assistant/') + ->willReturn(null); + + $this->expectException(TaskProcessingNotFoundException::class); + $this->expectExceptionMessage('Node is not a file'); + + $this->service->getTaskOutputFileInfo(self::USER_ID, self::TASK_ID, self::FILE_ID); + } +} From 186dcfb9f579a9ab6d8e46441e0a941797b5c4cf Mon Sep 17 00:00:00 2001 From: WSHAPER <42714629+WSHAPER@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:11:39 +0100 Subject: [PATCH 2/9] fix: keep 404 honest in output file info and survive one failed fetch Review feedback on the info endpoint: mapping every exception to 404 contradicted the documented contract, because a transient task manager or root folder failure looked identical to a missing output. Unauthorized and not found still share the 404 on purpose, so the response does not reveal whether another user's task exists; every other failure is logged and answered as a server error. The docblock now states both statuses. The frontend fetch had no failure handling, so one failed request left the chip unnamed until remount. It now retries once, which covers the transient case without a log-only catch. Also pins the URL contract between the registered route and the URL FileDisplay builds, covers the folder-node branch of the root lookup, and documents the test methods. Assisted-by: opencode (zai/glm-5.3) Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com> --- lib/Controller/AssistantApiController.php | 18 ++++++-- src/components/fields/FileDisplay.vue | 12 ++++- tests/unit/AppInfo/RoutesTest.php | 49 +++++++++++++++++++++ tests/unit/Service/AssistantServiceTest.php | 37 ++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 tests/unit/AppInfo/RoutesTest.php diff --git a/lib/Controller/AssistantApiController.php b/lib/Controller/AssistantApiController.php index 8bbbe7346..72f8119b2 100644 --- a/lib/Controller/AssistantApiController.php +++ b/lib/Controller/AssistantApiController.php @@ -27,6 +27,7 @@ use OCP\IL10N; use OCP\IRequest; use OCP\Lock\LockedException; +use OCP\TaskProcessing\Exception\NotFoundException as TaskProcessingNotFoundException; use OCP\TaskProcessing\Task; use OCP\TaskProcessing\TaskTypes\AudioToText; use OCP\TaskProcessing\TaskTypes\TextToTextSummary; @@ -428,10 +429,11 @@ public function getOutputFile(int $ocpTaskId, int $fileId): DataDownloadResponse * * @param int $ocpTaskId The task ID * @param int $fileId The task output file ID - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse * * 200: The file info is returned * 404: The output file is not found + * 500: The output file info could not be retrieved */ #[NoAdminRequired] #[NoCsrfRequired] @@ -441,10 +443,20 @@ public function getOutputFileInfo(int $ocpTaskId, int $fileId): DataResponse { if ($fileInfo !== null) { return new DataResponse($fileInfo); } - } catch (Exception|Throwable $e) { + return new DataResponse('', Http::STATUS_NOT_FOUND); + } catch (Exception $e) { + // Unauthorized and not found both map to 404 so the response does + // not reveal whether another user's task exists. + if (in_array($e->getCode(), [Http::STATUS_UNAUTHORIZED, Http::STATUS_NOT_FOUND], true)) { + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + $this->logger->error('getOutputFileInfo error', ['exception' => $e]); + } catch (TaskProcessingNotFoundException $e) { + return new DataResponse('', Http::STATUS_NOT_FOUND); + } catch (Throwable $e) { $this->logger->error('getOutputFileInfo error', ['exception' => $e]); } - return new DataResponse('', Http::STATUS_NOT_FOUND); + return new DataResponse('', Http::STATUS_INTERNAL_SERVER_ERROR); } /** diff --git a/src/components/fields/FileDisplay.vue b/src/components/fields/FileDisplay.vue index aeae166f0..d6cb35a5f 100644 --- a/src/components/fields/FileDisplay.vue +++ b/src/components/fields/FileDisplay.vue @@ -94,7 +94,12 @@ export default { this.fileInfo = response.data.ocs.data }) }, - getOutputFileInfo() { + /** + * Fetch the output file info through the task-scoped endpoint. + * Retries once so a transient failure does not leave the chip + * unnamed for the component's lifetime. + */ + getOutputFileInfo(retry = true) { const url = generateOcsUrl('apps/assistant/api/v1/task/{taskId}/output-file/{fileId}/info', { taskId: this.myCurrentTaskId, fileId: this.fileId, @@ -103,6 +108,11 @@ export default { .then((response) => { this.fileInfo = response.data.ocs.data }) + .catch(() => { + if (retry) { + this.getOutputFileInfo(false) + } + }) }, }, } diff --git a/tests/unit/AppInfo/RoutesTest.php b/tests/unit/AppInfo/RoutesTest.php new file mode 100644 index 000000000..b15a7b50a --- /dev/null +++ b/tests/unit/AppInfo/RoutesTest.php @@ -0,0 +1,49 @@ +taskProcessingManager->method('getTask') ->with(self::TASK_ID) @@ -118,6 +122,10 @@ public function testGetTaskOutputFileInfo(): void { ], $fileInfo); } + /** + * An output file stored in appdata instead of the user folder still + * resolves through the appdata path lookup. + */ public function testGetTaskOutputFileInfoForAppdataFile(): void { $this->taskProcessingManager->method('getTask') ->with(self::TASK_ID) @@ -139,6 +147,9 @@ public function testGetTaskOutputFileInfoForAppdataFile(): void { ], $fileInfo); } + /** + * A user cannot fetch output file info of another user's task. + */ public function testGetTaskOutputFileInfoWrongUser(): void { $this->taskProcessingManager->method('getTask') ->with(self::TASK_ID) @@ -150,6 +161,9 @@ public function testGetTaskOutputFileInfoWrongUser(): void { $this->service->getTaskOutputFileInfo('another_user', self::TASK_ID, self::FILE_ID); } + /** + * A file id that does not belong to the task is rejected as not found. + */ public function testGetTaskOutputFileInfoUnknownFileId(): void { $this->taskProcessingManager->method('getTask') ->with(self::TASK_ID) @@ -161,6 +175,9 @@ public function testGetTaskOutputFileInfoUnknownFileId(): void { $this->service->getTaskOutputFileInfo(self::USER_ID, self::TASK_ID, 999999999); } + /** + * A node id that resolves to nothing at all is rejected as not a file. + */ public function testGetTaskOutputFileInfoNotAFileNode(): void { $this->taskProcessingManager->method('getTask') ->with(self::TASK_ID) @@ -179,4 +196,24 @@ public function testGetTaskOutputFileInfoNotAFileNode(): void { $this->service->getTaskOutputFileInfo(self::USER_ID, self::TASK_ID, self::FILE_ID); } + + /** + * A node id that resolves to a non-file node (for example a folder) + * through the root lookup is rejected exactly like the missing case, + * so the instance check cannot be dropped without a failing test. + */ + public function testGetTaskOutputFileInfoNotAFolderNode(): void { + $folder = $this->createMock(Folder::class); + $this->taskProcessingManager->method('getTask') + ->with(self::TASK_ID) + ->willReturn($this->getTask(self::USER_ID)); + $this->rootFolder->method('getFirstNodeById') + ->with(self::FILE_ID) + ->willReturn($folder); + + $this->expectException(TaskProcessingNotFoundException::class); + $this->expectExceptionMessage('Node is not a file'); + + $this->service->getTaskOutputFileInfo(self::USER_ID, self::TASK_ID, self::FILE_ID); + } } From c3372bac30ea606e71b96cb299baa6369547fd6d Mon Sep 17 00:00:00 2001 From: WSHAPER <42714629+WSHAPER@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:16:56 +0100 Subject: [PATCH 3/9] chore: regenerate openapi spec for the output file info endpoint The spec was never regenerated when the endpoint landed, so the openapi CI failed. The generated entry covers the route, both success and error statuses including the 500 response. Assisted-by: opencode (zai/glm-5.3) Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com> --- openapi.json | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/openapi.json b/openapi.json index 1ed2389b1..d3afa4da3 100644 --- a/openapi.json +++ b/openapi.json @@ -2452,6 +2452,201 @@ } } }, + "/ocs/v2.php/apps/assistant/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/info": { + "get": { + "operationId": "assistant_api-get-output-file-info", + "summary": "Get task output file info", + "description": "Get information about a task output file", + "tags": [ + "assistant_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "ocpTaskId", + "in": "path", + "description": "The task ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "fileId", + "in": "path", + "description": "The task output file ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The file info is returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "name", + "size" + ], + "properties": { + "name": { + "type": "string" + }, + "size": { + "type": "integer", + "format": "int64" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "The output file is not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "string" + } + } + } + } + } + } + } + }, + "500": { + "description": "The output file info could not be retrieved", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "string" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/assistant/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/download": { "get": { "operationId": "assistant_api-get-output-file", From 336ede72e612a49a9309f32b58fe0b2b06a9b0d9 Mon Sep 17 00:00:00 2001 From: WSHAPER <42714629+WSHAPER@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:16:56 +0100 Subject: [PATCH 4/9] fix: show names for image outputs in result view Image outputs render through ImageDisplay, which never fetched file info, so generated images showed as bare previews with no name while file outputs got their name from the previous change. Image outputs now fetch their info through the same task-scoped endpoint, with the same single retry, and show the name as a caption under the image. Assisted-by: opencode (zai/glm-5.3) Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com> --- src/components/fields/ImageDisplay.vue | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/components/fields/ImageDisplay.vue b/src/components/fields/ImageDisplay.vue index 7d51b5cbb..fe48e1aa4 100644 --- a/src/components/fields/ImageDisplay.vue +++ b/src/components/fields/ImageDisplay.vue @@ -7,11 +7,17 @@ + + {{ fileName }} + @@ -87,6 +120,8 @@ export default { From d2a2d5938afc5c305faa3d20ba544da3130f0065 Mon Sep 17 00:00:00 2001 From: WSHAPER <42714629+WSHAPER@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:21:38 +0100 Subject: [PATCH 5/9] test: state plainly what the URL contract check asserts Review feedback: the docblock claimed the test keeps the route and the frontend request from drifting apart, but a source-text check cannot guarantee what axios.get receives; the substring could survive in a comment while the request URL changes. Split the test in two: the route registration half keeps its exact contract, the frontend half is named and documented as the source-text tripwire it actually is, and now also covers ImageDisplay, which builds the same URL. A behavioral frontend test would need test tooling this repository does not have yet. Assisted-by: opencode (zai/glm-5.3) Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com> --- tests/unit/AppInfo/RoutesTest.php | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/tests/unit/AppInfo/RoutesTest.php b/tests/unit/AppInfo/RoutesTest.php index b15a7b50a..ba15a2c98 100644 --- a/tests/unit/AppInfo/RoutesTest.php +++ b/tests/unit/AppInfo/RoutesTest.php @@ -13,16 +13,18 @@ /** * Pins the URL contract between the registered OCS route for output file - * info and the URL the frontend builds, so the two sides cannot drift - * apart silently. + * info and the URLs the display components build. The frontend half is a + * source-text check, not a behavioral test: the repository has no + * frontend test tooling yet, so it is a drift tripwire against accidental + * edits, not a guarantee of the URL actually requested. */ class RoutesTest extends TestCase { /** - * The output file info route stays registered with the URL shape the - * FileDisplay component builds its request from. + * The output file info route stays registered with the expected URL + * shape and verb. */ - public function testOutputFileInfoRouteMatchesFrontendUrl(): void { + public function testOutputFileInfoRouteIsRegisteredWithExpectedUrl(): void { $routes = require __DIR__ . '/../../../appinfo/routes.php'; self::assertArrayHasKey('ocs', $routes); @@ -37,13 +39,23 @@ public function testOutputFileInfoRouteMatchesFrontendUrl(): void { self::assertNotNull($match, 'the assistantApi#getOutputFileInfo route must stay registered'); self::assertSame('/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/info', $match['url']); self::assertSame('GET', $match['verb']); + } - $component = file_get_contents(__DIR__ . '/../../../src/components/fields/FileDisplay.vue'); - self::assertIsString($component); - self::assertStringContainsString( - 'task/{taskId}/output-file/{fileId}/info', - $component, - 'FileDisplay must keep building the output file info URL for the registered route' - ); + /** + * Source-text check: both display components keep building the output + * file info URL the registered route expects. This does not assert the + * argument passed to axios.get; a frontend unit test would, but the + * repository has no frontend test tooling yet. + */ + public function testDisplayComponentsBuildOutputFileInfoUrlInSource(): void { + foreach (['FileDisplay.vue', 'ImageDisplay.vue'] as $componentName) { + $component = file_get_contents(__DIR__ . '/../../../src/components/fields/' . $componentName); + self::assertIsString($component, $componentName . ' must exist'); + self::assertStringContainsString( + 'task/{taskId}/output-file/{fileId}/info', + $component, + $componentName . ' must keep building the output file info URL for the registered route' + ); + } } } From a7d6d96f17340298e56e733871f0bb4c216b7dc4 Mon Sep 17 00:00:00 2001 From: WSHAPER <42714629+WSHAPER@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:13:33 +0100 Subject: [PATCH 6/9] fix: hide image output names in the task list Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com> --- src/components/fields/ImageDisplay.vue | 17 ++++++++++++----- src/components/fields/MediaField.vue | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/fields/ImageDisplay.vue b/src/components/fields/ImageDisplay.vue index fe48e1aa4..bc018984c 100644 --- a/src/components/fields/ImageDisplay.vue +++ b/src/components/fields/ImageDisplay.vue @@ -3,11 +3,12 @@ - SPDX-License-Identifier: AGPL-3.0-or-later -->