From 7fc684c0f60f7da59fb5f2fdbf5a5ba0081ea674 Mon Sep 17 00:00:00 2001 From: Timor Morrien Date: Fri, 19 Sep 2025 17:37:09 +0200 Subject: [PATCH 1/4] Add internal stages that are hidden in the client --- .../iris/service/pyris/PyrisPipelineService.java | 4 ++-- .../service/pyris/dto/status/PyrisStageDTO.java | 13 +++++++------ .../app/iris/overview/services/iris-chat.service.ts | 8 ++++++-- .../iris/shared/entities/iris-stage-dto.model.ts | 2 ++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java index b612cf0ad68d..f372c17615cc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisPipelineService.java @@ -122,8 +122,8 @@ public void executePipeline(String name, String variant, Optional event, Consumer> statusUpdater) { // Define the preparation stages of pipeline execution with their initial states // There will be more stages added in Pyris later - var preparing = new PyrisStageDTO("Preparing", 10, null, null); - var executing = new PyrisStageDTO("Executing pipeline", 30, null, null); + var preparing = new PyrisStageDTO("Preparing", 10, null, null, false); + var executing = new PyrisStageDTO("Executing pipeline", 30, null, null, false); // Send initial status update indicating that the preparation stage is in progress statusUpdater.accept(List.of(preparing.inProgress(), executing.notStarted())); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/status/PyrisStageDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/status/PyrisStageDTO.java index 366895c2e7c5..4abb27b3f586 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/status/PyrisStageDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/status/PyrisStageDTO.java @@ -1,28 +1,29 @@ package de.tum.cit.aet.artemis.iris.service.pyris.dto.status; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PyrisStageDTO(String name, int weight, PyrisStageState state, String message) { +public record PyrisStageDTO(String name, int weight, PyrisStageState state, String message, @JsonProperty(defaultValue = "false") boolean internal) { public PyrisStageDTO notStarted() { - return new PyrisStageDTO(name, weight, PyrisStageState.NOT_STARTED, message); + return new PyrisStageDTO(name, weight, PyrisStageState.NOT_STARTED, message, internal); } public PyrisStageDTO inProgress() { - return new PyrisStageDTO(name, weight, PyrisStageState.IN_PROGRESS, message); + return new PyrisStageDTO(name, weight, PyrisStageState.IN_PROGRESS, message, internal); } public PyrisStageDTO error(String message) { - return new PyrisStageDTO(name, weight, PyrisStageState.ERROR, message); + return new PyrisStageDTO(name, weight, PyrisStageState.ERROR, message, internal); } public PyrisStageDTO done() { - return new PyrisStageDTO(name, weight, PyrisStageState.DONE, message); + return new PyrisStageDTO(name, weight, PyrisStageState.DONE, message, internal); } public PyrisStageDTO with(PyrisStageState state, String message) { - return new PyrisStageDTO(name, weight, state, message); + return new PyrisStageDTO(name, weight, state, message, internal); } } diff --git a/src/main/webapp/app/iris/overview/services/iris-chat.service.ts b/src/main/webapp/app/iris/overview/services/iris-chat.service.ts index abad5bd54eaf..02f3ac492271 100644 --- a/src/main/webapp/app/iris/overview/services/iris-chat.service.ts +++ b/src/main/webapp/app/iris/overview/services/iris-chat.service.ts @@ -406,11 +406,11 @@ export class IrisChatService implements OnDestroy { this.replaceOrAddMessage(payload.message); } if (payload.stages) { - this.stages.next(payload.stages); + this.stages.next(this.filterStages(payload.stages)); } break; case IrisChatWebsocketPayloadType.STATUS: - this.stages.next(payload.stages || []); + this.stages.next(this.filterStages(payload.stages || [])); if (payload.suggestions) { this.suggestions.next(payload.suggestions); } @@ -418,6 +418,10 @@ export class IrisChatService implements OnDestroy { } } + private filterStages(stages: IrisStageDTO[]): IrisStageDTO[] { + return stages.filter((stage) => !stage.internal); + } + protected close(): void { if (this.sessionId) { this.ws.unsubscribeFromSession(this.sessionId); diff --git a/src/main/webapp/app/iris/shared/entities/iris-stage-dto.model.ts b/src/main/webapp/app/iris/shared/entities/iris-stage-dto.model.ts index d65b2e9fca86..8345b570b025 100644 --- a/src/main/webapp/app/iris/shared/entities/iris-stage-dto.model.ts +++ b/src/main/webapp/app/iris/shared/entities/iris-stage-dto.model.ts @@ -3,6 +3,8 @@ export class IrisStageDTO { weight: number; state: IrisStageStateDTO; message: string; + // Internal stages are not shown in the UI and are hidden from the user + internal: boolean; lowerCaseState?: string; } From 1717a921e2c851deef3622b5c962ab7ee5e3612a Mon Sep 17 00:00:00 2001 From: Timor Morrien Date: Fri, 19 Sep 2025 18:13:17 +0200 Subject: [PATCH 2/4] Fix tests --- .../chat-status-bar.component.spec.ts | 12 +++++----- .../IrisChatTokenTrackingIntegrationTest.java | 4 ++-- ...isCompetencyGenerationIntegrationTest.java | 2 +- .../IrisTutorSuggestionIntegrationTest.java | 2 +- .../artemis/iris/MemirisIntegrationTest.java | 6 ++--- .../artemis/iris/PyrisFaqIngestionTest.java | 20 ++++++++-------- .../iris/PyrisLectureIngestionTest.java | 24 +++++++++---------- .../iris/PyrisRewritingIntegrationTest.java | 2 +- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/main/webapp/app/iris/overview/base-chatbot/chat-status-bar/chat-status-bar.component.spec.ts b/src/main/webapp/app/iris/overview/base-chatbot/chat-status-bar/chat-status-bar.component.spec.ts index bee330175ed7..88e1c7738649 100644 --- a/src/main/webapp/app/iris/overview/base-chatbot/chat-status-bar/chat-status-bar.component.spec.ts +++ b/src/main/webapp/app/iris/overview/base-chatbot/chat-status-bar/chat-status-bar.component.spec.ts @@ -23,7 +23,7 @@ describe('ChatStatusBarComponent', () => { }); it('should handle unfinished stages in ngOnChanges', () => { - component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' }]; + component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false }]; component.ngOnChanges(); expect(component.open).toBeTrue(); expect(component.activeStage).toEqual(component.stages[0]); @@ -31,7 +31,7 @@ describe('ChatStatusBarComponent', () => { }); it('should handle all stages finished in ngOnChanges', () => { - component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test' }]; + component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test', internal: false }]; component.ngOnChanges(); expect(component.open).toBeFalse(); expect(component.activeStage).toBeUndefined(); @@ -39,19 +39,19 @@ describe('ChatStatusBarComponent', () => { }); it('should return true for finished stages in isStageFinished', () => { - const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test' }; + const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.DONE, weight: 1, message: 'Test', internal: false }; expect(component.isStageFinished(stage)).toBeTrue(); stage.state = IrisStageStateDTO.SKIPPED; expect(component.isStageFinished(stage)).toBeTrue(); }); it('should return false for unfinished stages in isStageFinished', () => { - const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' }; + const stage: IrisStageDTO = { name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false }; expect(component.isStageFinished(stage)).toBeFalse(); }); it('should render progress bar when stages are present', () => { - component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' }]; + component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false }]; fixture.detectChanges(); const progressBar = fixture.debugElement.query(By.css('.progress-bar')); expect(progressBar).toBeTruthy(); @@ -65,7 +65,7 @@ describe('ChatStatusBarComponent', () => { }); it('should render stage name when stages are present', () => { - component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test' }]; + component.stages = [{ name: 'Test Stage', state: IrisStageStateDTO.IN_PROGRESS, weight: 1, message: 'Test', internal: false }]; component.ngOnChanges(); fixture.detectChanges(); const stageName = fixture.debugElement.query(By.css('.display')).nativeElement.textContent; diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java index 9be6168213e6..f077e8ca7cbd 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java @@ -129,7 +129,7 @@ void testTokenTrackingHandledExerciseChat() throws Exception { IrisMessage messageToSend = IrisMessageFactory.createIrisMessageForSessionWithContent(irisSession); var tokens = getMockLLMCosts("IRIS_CHAT_EXERCISE_MESSAGE"); List doneStage = new ArrayList<>(); - doneStage.add(new PyrisStageDTO("DoneTest", 10, PyrisStageState.DONE, "Done")); + doneStage.add(new PyrisStageDTO("DoneTest", 10, PyrisStageState.DONE, "Done", false)); irisRequestMockProvider.mockProgrammingExerciseChatResponse(dto -> { assertThat(dto.settings().authenticationToken()).isNotNull(); assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World", doneStage, tokens)); @@ -181,7 +181,7 @@ void testTokenTrackingExerciseChatWithPipelineFail() throws Exception { IrisMessage messageToSend = IrisMessageFactory.createIrisMessageForSessionWithContent(irisSession); var tokens = getMockLLMCosts("IRIS_CHAT_EXERCISE_MESSAGE"); List failedStages = new ArrayList<>(); - failedStages.add(new PyrisStageDTO("TestTokenFail", 10, PyrisStageState.ERROR, "Failed running pipeline")); + failedStages.add(new PyrisStageDTO("TestTokenFail", 10, PyrisStageState.ERROR, "Failed running pipeline", false)); irisRequestMockProvider.mockProgrammingExerciseChatResponse(dto -> { assertThat(dto.settings().authenticationToken()).isNotNull(); assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), null, failedStages, tokens)); diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java index c657f3cef738..58abcceed92d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java @@ -64,7 +64,7 @@ void generateCompetencies_asEditor_shouldSucceed() throws Exception { PyrisCompetencyRecommendationDTO expected = new PyrisCompetencyRecommendationDTO("test title", "test description", CompetencyTaxonomy.UNDERSTAND); List recommendations = List.of(expected, expected, expected); - List stages = List.of(new PyrisStageDTO("Generating Competencies", 10, PyrisStageState.DONE, null)); + List stages = List.of(new PyrisStageDTO("Generating Competencies", 10, PyrisStageState.DONE, null, false)); // In the real system, this would be triggered by Pyris via a REST call to the Artemis server String jobId = "testJobId"; diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisTutorSuggestionIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisTutorSuggestionIntegrationTest.java index 9799d8505712..6ff28fbc4503 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisTutorSuggestionIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisTutorSuggestionIntegrationTest.java @@ -306,7 +306,7 @@ void testTutorSuggestionStatusUpdateShouldBeHandled() throws Exception { var job = pyrisJobService.getAndAuthenticateJobFromHeaderElseThrow(mockRequest, de.tum.cit.aet.artemis.iris.service.pyris.job.TutorSuggestionJob.class); assertThat(job).isNotNull(); assertThat(job.jobId()).isEqualTo(token); - List stages = List.of(new PyrisStageDTO("Test stage", 0, PyrisStageState.DONE, "Done")); + List stages = List.of(new PyrisStageDTO("Test stage", 0, PyrisStageState.DONE, "Done", false)); var statusUpdate = new TutorSuggestionStatusUpdateDTO("Test suggestion", "Test result", stages, null); var mockRequestForStatusUpdate = new org.springframework.mock.web.MockHttpServletRequest(); mockRequestForStatusUpdate.addHeader(HttpHeaders.AUTHORIZATION, Constants.BEARER_PREFIX + token); diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/MemirisIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/MemirisIntegrationTest.java index 48f71f178204..d028d02bbb31 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/MemirisIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/MemirisIntegrationTest.java @@ -130,8 +130,8 @@ void courseChat_IntermediateAccessedMemoriesThenCreatedMemories() throws Excepti // Build non-terminal and terminal stage lists var preparingDone = stagesRef.get().getFirst(); - var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null); - var executingDone = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.DONE, null); + var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null, false); + var executingDone = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.DONE, null, false); // Send intermediate status with accessed memories only (no result yet) and non-terminal stages sendCourseStatus(jobIdRef.get(), null, List.of(preparingDone, executingInProgress), null, @@ -190,7 +190,7 @@ void courseChat_CreatedMemoriesUpdateAssistantMessage() throws Exception { await().until(() -> jobIdRef.get() != null && stagesRef.get() != null); var preparingDone = stagesRef.get().getFirst(); - var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null); + var executingInProgress = new PyrisStageDTO("Executing pipeline", 30, de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState.IN_PROGRESS, null, false); // First: send assistant result to create assistant message and set assistantMessageId on the job (keep job running with non-terminal stages) sendCourseStatus(jobIdRef.get(), "Initial Answer", List.of(preparingDone, executingInProgress), null, null, null); diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisFaqIngestionTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisFaqIngestionTest.java index dc0c84a5965b..d14609ddcc69 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisFaqIngestionTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisFaqIngestionTest.java @@ -139,7 +139,7 @@ void testAllStagesDoneIngestionStateDone() throws Exception { activateIrisFor(faq1.getCourse()); irisRequestMockProvider.mockFaqIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String jobToken = pyrisWebhookService.addFaq(faq1); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(doneStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/faqs/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); @@ -152,7 +152,7 @@ void testAllStagesDoneRemovesDeletionIngestionJob() throws Exception { activateIrisFor(faq1.getCourse()); irisRequestMockProvider.mockFaqDeletionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String jobToken = pyrisWebhookService.deleteFaq(faq1); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(doneStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/faqs/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); @@ -165,8 +165,8 @@ void testStageNotDoneKeepsAdditionIngestionJob() throws Exception { activateIrisFor(faq1.getCourse()); irisRequestMockProvider.mockFaqIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String jobToken = pyrisWebhookService.addFaq(faq1); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); - PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); + PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(doneStage, inProgressStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/faqs/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); @@ -180,8 +180,8 @@ void testStageNotDoneKeepsDeletionIngestionJob() throws Exception { irisRequestMockProvider.mockFaqDeletionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String jobToken = pyrisWebhookService.deleteFaq(faq1); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); - PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); + PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(doneStage, inProgressStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/faqs/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); @@ -195,7 +195,7 @@ void testErrorStageRemovesDeletionIngestionJob() throws Exception { irisRequestMockProvider.mockFaqDeletionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String jobToken = pyrisWebhookService.deleteFaq(faq1); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(errorStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/faqs/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); @@ -208,7 +208,7 @@ void testErrorStageRemovesAdditionIngestionJob() throws Exception { activateIrisFor(faq1.getCourse()); irisRequestMockProvider.mockFaqIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String jobToken = pyrisWebhookService.addFaq(faq1); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(errorStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/faqs/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); @@ -222,7 +222,7 @@ void testRunIdIngestionJob() throws Exception { irisRequestMockProvider.mockFaqIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String newJobToken = pyrisJobService.addFaqIngestionWebhookJob(123L, faq1.getId()); String chatJobToken = pyrisJobService.addCourseChatJob(123L, 123L, 123L); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(errorStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + chatJobToken)))); MockHttpServletResponse response = request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/runs/" + newJobToken + "/status", statusUpdate, @@ -238,7 +238,7 @@ void testIngestionJobDone() throws Exception { irisRequestMockProvider.mockFaqIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String newJobToken = pyrisJobService.addFaqIngestionWebhookJob(123L, faq1.getId()); String chatJobToken = pyrisJobService.addCourseChatJob(123L, 123L, 123L); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisFaqIngestionStatusUpdateDTO statusUpdate = new PyrisFaqIngestionStatusUpdateDTO("Success", List.of(errorStage), faq1.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + chatJobToken)))); MockHttpServletResponse response = request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/runs/" + newJobToken + "/status", statusUpdate, diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisLectureIngestionTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisLectureIngestionTest.java index 03c2a5c22535..b493802bce7e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisLectureIngestionTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisLectureIngestionTest.java @@ -167,7 +167,7 @@ void testAllStagesDoneIngestionStateDone() throws Exception { irisRequestMockProvider.mockIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); if (lecture1.getLectureUnits().getFirst() instanceof AttachmentVideoUnit unit) { String jobToken = pyrisWebhookService.addLectureUnitToPyrisDB(unit); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(doneStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); @@ -182,7 +182,7 @@ void testAllStagesDoneRemovesDeletionIngestionJob() throws Exception { irisRequestMockProvider.mockDeletionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); if (lecture1.getLectureUnits().getFirst() instanceof AttachmentVideoUnit unit) { String jobToken = pyrisWebhookService.deleteLectureFromPyrisDB(List.of(unit)); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(doneStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); @@ -197,8 +197,8 @@ void testStageNotDoneKeepsAdditionIngestionJob() throws Exception { irisRequestMockProvider.mockIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); if (lecture1.getLectureUnits().getFirst() instanceof AttachmentVideoUnit unit) { String jobToken = pyrisWebhookService.addLectureUnitToPyrisDB(unit); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); - PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); + PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(doneStage, inProgressStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); @@ -213,8 +213,8 @@ void testStageNotDoneKeepsDeletionIngestionJob() throws Exception { irisRequestMockProvider.mockDeletionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); if (lecture1.getLectureUnits().getFirst() instanceof AttachmentVideoUnit unit) { String jobToken = pyrisWebhookService.deleteLectureFromPyrisDB(List.of(unit)); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); - PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); + PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage completed successfully.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(doneStage, inProgressStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); @@ -229,7 +229,7 @@ void testErrorStageRemovesDeletionIngestionJob() throws Exception { irisRequestMockProvider.mockDeletionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); if (lecture1.getLectureUnits().getFirst() instanceof AttachmentVideoUnit unit) { String jobToken = pyrisWebhookService.deleteLectureFromPyrisDB(List.of(unit)); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(errorStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); @@ -244,7 +244,7 @@ void testErrorStageRemovesAdditionIngestionJob() throws Exception { irisRequestMockProvider.mockIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); if (lecture1.getLectureUnits().getFirst() instanceof AttachmentVideoUnit unit) { String jobToken = pyrisWebhookService.addLectureUnitToPyrisDB(unit); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(errorStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); @@ -259,7 +259,7 @@ void testRunIdIngestionJob() throws Exception { irisRequestMockProvider.mockIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String newJobToken = pyrisJobService.addLectureIngestionWebhookJob(123L, lecture1.getId(), lecture1.getLectureUnits().getFirst().getId()); String chatJobToken = pyrisJobService.addCourseChatJob(123L, 123L, 123L); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(errorStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + chatJobToken)))); MockHttpServletResponse response = request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/runs/" + newJobToken + "/status", statusUpdate, @@ -275,7 +275,7 @@ void testIngestionJobDone() throws Exception { irisRequestMockProvider.mockIngestionWebhookRunResponse(dto -> assertThat(dto.settings().authenticationToken()).isNotNull()); String newJobToken = pyrisJobService.addLectureIngestionWebhookJob(123L, lecture1.getId(), lecture1.getLectureUnits().getFirst().getId()); String chatJobToken = pyrisJobService.addCourseChatJob(123L, 123L, 123L); - PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error."); + PyrisStageDTO errorStage = new PyrisStageDTO("error", 1, PyrisStageState.ERROR, "Stage not broke due to error.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(errorStage), lecture1.getLectureUnits().getFirst().getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + chatJobToken)))); MockHttpServletResponse response = request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/runs/" + newJobToken + "/status", statusUpdate, @@ -353,7 +353,7 @@ void testDeleteLectureUnitWithTranscriptionFromPyrisDB() throws Exception { String jobToken = pyrisWebhookService.deleteLectureFromPyrisDB(List.of(unitWithTranscription)); assertThat(jobToken).isNotNull(); - PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully."); + PyrisStageDTO doneStage = new PyrisStageDTO("done", 1, PyrisStageState.DONE, "Stage completed successfully.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(doneStage), unitWithTranscription.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); @@ -382,7 +382,7 @@ void testDeleteLectureUnitWithTranscriptionKeepsJobIfNotDone() throws Exception String jobToken = pyrisWebhookService.deleteLectureFromPyrisDB(List.of(unitWithTranscription)); assertThat(jobToken).isNotNull(); - PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage läuft noch."); + PyrisStageDTO inProgressStage = new PyrisStageDTO("inProgressStage", 1, PyrisStageState.IN_PROGRESS, "Stage is still running.", false); PyrisLectureIngestionStatusUpdateDTO statusUpdate = new PyrisLectureIngestionStatusUpdateDTO("Success", List.of(inProgressStage), unitWithTranscription.getId()); var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of(HttpHeaders.AUTHORIZATION, List.of(Constants.BEARER_PREFIX + jobToken)))); request.postWithoutResponseBody("/api/iris/public/pyris/webhooks/ingestion/runs/" + jobToken + "/status", statusUpdate, HttpStatus.OK, headers); diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisRewritingIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisRewritingIntegrationTest.java index 7f5c4d548113..5b4931411465 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisRewritingIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisRewritingIntegrationTest.java @@ -62,7 +62,7 @@ void callRewritingPipeline_shouldSucceed() throws Exception { Long userId = userUtilService.getUserByLogin(userLogin).getId(); RewritingJob job = new RewritingJob(jobId, course.getId(), userId); - List stages = List.of(new PyrisStageDTO("Generating Rewriting", 10, PyrisStageState.DONE, null)); + List stages = List.of(new PyrisStageDTO("Generating Rewriting", 10, PyrisStageState.DONE, null, false)); List tokens = getMockLLMCosts("IRIS_CHAT_EXERCISE_MESSAGE"); String result = "result"; From a6c3419f30396bf1934990aa075385077a6d97e1 Mon Sep 17 00:00:00 2001 From: Phoebe Morrien Date: Wed, 29 Oct 2025 14:13:19 +0100 Subject: [PATCH 3/4] Add client test --- .../overview/services/iris-chat.service.spec.ts | 13 +++++++++++++ .../spec/helpers/sample/iris-sample-data.ts | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts b/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts index 9470246d3078..05b1d5e49984 100644 --- a/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts +++ b/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts @@ -22,6 +22,7 @@ import { mockUserMessageWithContent, mockWebsocketServerMessage, mockWebsocketStatusMessage, + mockWebsocketStatusMessageWithInteralStage, } from 'test/helpers/sample/iris-sample-data'; import { IrisMessage, IrisUserMessage } from 'app/iris/shared/entities/iris-message.model'; import 'app/shared/util/array.extension'; @@ -233,6 +234,18 @@ describe('IrisChatService', () => { tick(); })); + it('should handle websocket status message with internal stages', fakeAsync(() => { + jest.spyOn(httpService, 'getCurrentSessionOrCreateIfNotExists').mockReturnValueOnce(of(mockServerSessionHttpResponseWithId(id))); + jest.spyOn(httpService, 'getChatSessions').mockReturnValue(of([])); + jest.spyOn(wsMock, 'subscribeToSession').mockReturnValueOnce(of(mockWebsocketStatusMessageWithInteralStage)); + service.switchTo(ChatServiceMode.PROGRAMMING_EXERCISE, id); + + service.currentStages().subscribe((stages) => { + expect(stages).toEqual(mockWebsocketStatusMessageWithInteralStage.stages.filter((stage) => !stage.internal)); + }); + tick(); + })); + it('should handle websocket message', fakeAsync(() => { jest.spyOn(httpService, 'getCurrentSessionOrCreateIfNotExists').mockReturnValueOnce(of(mockServerSessionHttpResponseWithId(id))); jest.spyOn(httpService, 'getChatSessions').mockReturnValue(of([])); diff --git a/src/test/javascript/spec/helpers/sample/iris-sample-data.ts b/src/test/javascript/spec/helpers/sample/iris-sample-data.ts index b603918a13db..667ab4fede9e 100644 --- a/src/test/javascript/spec/helpers/sample/iris-sample-data.ts +++ b/src/test/javascript/spec/helpers/sample/iris-sample-data.ts @@ -89,6 +89,21 @@ export const mockWebsocketStatusMessage = { ], } as IrisChatWebsocketDTO; +export const mockWebsocketStatusMessageWithInteralStage = { + type: IrisChatWebsocketPayloadType.STATUS, + stages: [ + { + name: 'Stage 1', + state: IrisStageStateDTO.IN_PROGRESS, + }, + { + name: 'Internal Stage', + state: IrisStageStateDTO.IN_PROGRESS, + internal: true, + }, + ], +} as IrisChatWebsocketDTO; + export const mockConversation = { id: 1, exercise: irisExercise, From aa1087d0f93de175bc2a4b1cad1c95fe9095c6be Mon Sep 17 00:00:00 2001 From: Phoebe Morrien Date: Wed, 29 Oct 2025 14:14:26 +0100 Subject: [PATCH 4/4] Fix type errors --- .../app/iris/overview/services/iris-chat.service.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts b/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts index 05b1d5e49984..2d9b7bddb1c9 100644 --- a/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts +++ b/src/main/webapp/app/iris/overview/services/iris-chat.service.spec.ts @@ -28,6 +28,7 @@ import { IrisMessage, IrisUserMessage } from 'app/iris/shared/entities/iris-mess import 'app/shared/util/array.extension'; import { Router } from '@angular/router'; import { IrisSessionDTO } from 'app/iris/shared/entities/iris-session-dto.model'; +import { IrisStageDTO } from 'app/iris/shared/entities/iris-stage-dto.model'; describe('IrisChatService', () => { let service: IrisChatService; @@ -241,7 +242,7 @@ describe('IrisChatService', () => { service.switchTo(ChatServiceMode.PROGRAMMING_EXERCISE, id); service.currentStages().subscribe((stages) => { - expect(stages).toEqual(mockWebsocketStatusMessageWithInteralStage.stages.filter((stage) => !stage.internal)); + expect(stages).toEqual(mockWebsocketStatusMessageWithInteralStage.stages?.filter((stage: IrisStageDTO) => !stage.internal)); }); tick(); }));