From 6c8afa253dd603b8a52b3205a35bad57fda8fc08 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 06:47:29 +0000 Subject: [PATCH 1/2] fix: save RunAgent workspaceId on rooms that have no messages yet RunAgent only overlaid the workspace on the room for the length of a run, so a room started by RunAgent never got ROOM.WORKSPACE_ID or options.workspace saved, unlike a room created with CreateRoom. A run that starts an empty room now saves both, after checking the workspace is active and viewable by the user. Rooms that already have messages keep their saved workspace and still get the per-run overlay. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015nRQJ4qQU5EHJvmuq96eDc --- src/prerna/reactor/agent/AgentRunner.java | 46 +++++ .../AgentRunnerWorkspacePersistenceTest.java | 176 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java diff --git a/src/prerna/reactor/agent/AgentRunner.java b/src/prerna/reactor/agent/AgentRunner.java index 4c785d2c04..4deb0f983d 100644 --- a/src/prerna/reactor/agent/AgentRunner.java +++ b/src/prerna/reactor/agent/AgentRunner.java @@ -278,6 +278,11 @@ private static AgentHarnessResult run(String roomId, String input, String engine AgentConfig agentConfig = AgentConfigLoader.load(room, filePath, modelId, params, agentParams, maxTurns, maxReflections, explicitWorkspaceId); + // A run that starts an empty room claims it for the agent, as CreateRoom would. + if (!resumeMode) { + persistWorkspaceOnEmptyRoom(room, insight, explicitWorkspaceId); + } + try { SkillStager.stage(filePath, agentConfig.getSkills()); } catch (Exception e) { @@ -410,6 +415,47 @@ private static final class WorkspaceOverlay { } } + /** + * Saves {@code workspaceId} on a room that has no messages yet, both as the + * {@code ROOM.WORKSPACE_ID} column and as {@code options.workspace}, so a room + * started by RunAgent matches one created with {@code CreateRoom(workspaceId)}. + * Rooms with messages keep their saved workspace; the run only overlays it. + * Skipped when the workspace is missing, disabled, or not viewable by the + * user. + */ + static void persistWorkspaceOnEmptyRoom(Room room, Insight insight, String workspaceId) { + if (workspaceId == null || workspaceId.trim().isEmpty()) { + return; + } + List messages = room.getMessages(); + if (messages != null && !messages.isEmpty()) { + return; + } + try { + Map ws = ModelInferenceLogsUtils.getWorkspaceEntry(workspaceId); + if (ws == null || !Boolean.TRUE.equals(ws.get("is_active")) || insight.getUser() == null + || !SecurityProjectUtils.userCanViewProject(insight.getUser(), workspaceId)) { + return; + } + + Map workspace = new HashMap<>(); + workspace.put("workspace_id", workspaceId); + if (ws.get("name") != null) { + workspace.put("name", String.valueOf(ws.get("name"))); + } + Map opts = new HashMap<>(room.getOptionsMap()); + opts.put("workspace", workspace); + + ModelInferenceLogsUtils.setRoomWorkspaceId(room.getId(), room.getUserId(), workspaceId); + ModelInferenceLogsUtils.setRoomOptions(room.getId(), room.getUserId(), opts); + room.setOptionsMap(opts); + logger.info("AgentRunner: saved workspace '{}' on empty room '{}'", workspaceId, room.getId()); + } catch (Exception e) { + logger.warn("AgentRunner: could not save workspace '{}' on room '{}': {}", workspaceId, room.getId(), + e.getMessage(), e); + } + } + /** * Applies an in-memory workspace override for one run when needed. */ diff --git a/test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java b/test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java new file mode 100644 index 0000000000..a2d5fe211c --- /dev/null +++ b/test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.agent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import prerna.auth.User; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.engine.impl.model.Room; +import prerna.engine.impl.model.inferencetracking.ModelInferenceLogsUtils; +import prerna.engine.impl.model.message.AbstractMessage; +import prerna.om.Insight; + +/** + * Verifies {@link AgentRunner#persistWorkspaceOnEmptyRoom}: a RunAgent + * workspaceId is saved on a room only while the room has no messages, and only + * for an active workspace the user can view. + */ +class AgentRunnerWorkspacePersistenceTest { + + private static final String ROOM_ID = "room-ws-1"; + private static final String USER_ID = "user-1"; + private static final String WORKSPACE_ID = "ws-1"; + + private Room room; + private Insight insight; + private User user; + private Map options; + private List messages; + + @BeforeEach + void setUp() { + room = mock(Room.class); + insight = mock(Insight.class); + user = mock(User.class); + options = new HashMap<>(); + messages = new ArrayList<>(); + when(room.getId()).thenReturn(ROOM_ID); + when(room.getUserId()).thenReturn(USER_ID); + when(room.getOptionsMap()).thenReturn(options); + when(room.getMessages()).thenReturn(messages); + when(insight.getUser()).thenReturn(user); + } + + private static Map workspaceRow(boolean active) { + Map row = new HashMap<>(); + row.put("name", "My Agent"); + row.put("is_active", active); + return row; + } + + @Test + void savesWorkspaceOnEmptyRoom() { + try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); + MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { + logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(true)); + security.when(() -> SecurityProjectUtils.userCanViewProject(user, WORKSPACE_ID)).thenReturn(true); + + AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); + + logs.verify(() -> ModelInferenceLogsUtils.setRoomWorkspaceId(ROOM_ID, USER_ID, WORKSPACE_ID)); + Map expectedWorkspace = new HashMap<>(); + expectedWorkspace.put("workspace_id", WORKSPACE_ID); + expectedWorkspace.put("name", "My Agent"); + Map expectedOptions = Collections.singletonMap("workspace", expectedWorkspace); + logs.verify(() -> ModelInferenceLogsUtils.setRoomOptions(ROOM_ID, USER_ID, expectedOptions)); + Mockito.verify(room).setOptionsMap(expectedOptions); + } + } + + @Test + void leavesRoomWithMessagesUnchanged() { + messages.add(mock(AbstractMessage.class)); + try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); + MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { + logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(true)); + security.when(() -> SecurityProjectUtils.userCanViewProject(user, WORKSPACE_ID)).thenReturn(true); + + AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); + + assertNothingSaved(logs); + } + } + + @Test + void skipsWhenNoWorkspaceIdGiven() { + try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class)) { + AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, null); + AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, " "); + + assertNothingSaved(logs); + } + } + + @Test + void skipsMissingOrDisabledWorkspace() { + try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); + MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { + security.when(() -> SecurityProjectUtils.userCanViewProject(any(User.class), anyString())) + .thenReturn(true); + + logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(null); + AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); + + logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(false)); + AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); + + assertNothingSaved(logs); + } + } + + @Test + void skipsWorkspaceUserCannotView() { + try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); + MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { + logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(true)); + security.when(() -> SecurityProjectUtils.userCanViewProject(user, WORKSPACE_ID)).thenReturn(false); + + AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); + + assertNothingSaved(logs); + } + } + + private void assertNothingSaved(MockedStatic logs) { + logs.verify(() -> ModelInferenceLogsUtils.setRoomWorkspaceId(anyString(), anyString(), anyString()), + Mockito.never()); + logs.verify(() -> ModelInferenceLogsUtils.setRoomOptions(anyString(), anyString(), anyMap()), + Mockito.never()); + Mockito.verify(room, Mockito.never()).setOptionsMap(any()); + assertFalse(options.containsKey("workspace")); + assertEquals(0, options.size()); + } +} From ab835a3beefabf52947fbeb5856b76cbb20f9e3d Mon Sep 17 00:00:00 2001 From: Kelly Date: Sat, 3 Oct 2026 12:03:38 -0700 Subject: [PATCH 2/2] chore: remove unit test file --- .../AgentRunnerWorkspacePersistenceTest.java | 176 ------------------ 1 file changed, 176 deletions(-) delete mode 100644 test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java diff --git a/test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java b/test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java deleted file mode 100644 index a2d5fe211c..0000000000 --- a/test/prerna/reactor/agent/AgentRunnerWorkspacePersistenceTest.java +++ /dev/null @@ -1,176 +0,0 @@ -/******************************************************************************* - * Copyright 2015 Defense Health Agency (DHA) - * - * If your use of this software does not include any GPLv2 components: - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ---------------------------------------------------------------------------- - * If your use of this software includes any GPLv2 components: - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *******************************************************************************/ -package prerna.reactor.agent; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - -import prerna.auth.User; -import prerna.auth.utils.SecurityProjectUtils; -import prerna.engine.impl.model.Room; -import prerna.engine.impl.model.inferencetracking.ModelInferenceLogsUtils; -import prerna.engine.impl.model.message.AbstractMessage; -import prerna.om.Insight; - -/** - * Verifies {@link AgentRunner#persistWorkspaceOnEmptyRoom}: a RunAgent - * workspaceId is saved on a room only while the room has no messages, and only - * for an active workspace the user can view. - */ -class AgentRunnerWorkspacePersistenceTest { - - private static final String ROOM_ID = "room-ws-1"; - private static final String USER_ID = "user-1"; - private static final String WORKSPACE_ID = "ws-1"; - - private Room room; - private Insight insight; - private User user; - private Map options; - private List messages; - - @BeforeEach - void setUp() { - room = mock(Room.class); - insight = mock(Insight.class); - user = mock(User.class); - options = new HashMap<>(); - messages = new ArrayList<>(); - when(room.getId()).thenReturn(ROOM_ID); - when(room.getUserId()).thenReturn(USER_ID); - when(room.getOptionsMap()).thenReturn(options); - when(room.getMessages()).thenReturn(messages); - when(insight.getUser()).thenReturn(user); - } - - private static Map workspaceRow(boolean active) { - Map row = new HashMap<>(); - row.put("name", "My Agent"); - row.put("is_active", active); - return row; - } - - @Test - void savesWorkspaceOnEmptyRoom() { - try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); - MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { - logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(true)); - security.when(() -> SecurityProjectUtils.userCanViewProject(user, WORKSPACE_ID)).thenReturn(true); - - AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); - - logs.verify(() -> ModelInferenceLogsUtils.setRoomWorkspaceId(ROOM_ID, USER_ID, WORKSPACE_ID)); - Map expectedWorkspace = new HashMap<>(); - expectedWorkspace.put("workspace_id", WORKSPACE_ID); - expectedWorkspace.put("name", "My Agent"); - Map expectedOptions = Collections.singletonMap("workspace", expectedWorkspace); - logs.verify(() -> ModelInferenceLogsUtils.setRoomOptions(ROOM_ID, USER_ID, expectedOptions)); - Mockito.verify(room).setOptionsMap(expectedOptions); - } - } - - @Test - void leavesRoomWithMessagesUnchanged() { - messages.add(mock(AbstractMessage.class)); - try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); - MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { - logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(true)); - security.when(() -> SecurityProjectUtils.userCanViewProject(user, WORKSPACE_ID)).thenReturn(true); - - AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); - - assertNothingSaved(logs); - } - } - - @Test - void skipsWhenNoWorkspaceIdGiven() { - try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class)) { - AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, null); - AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, " "); - - assertNothingSaved(logs); - } - } - - @Test - void skipsMissingOrDisabledWorkspace() { - try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); - MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { - security.when(() -> SecurityProjectUtils.userCanViewProject(any(User.class), anyString())) - .thenReturn(true); - - logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(null); - AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); - - logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(false)); - AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); - - assertNothingSaved(logs); - } - } - - @Test - void skipsWorkspaceUserCannotView() { - try (MockedStatic logs = Mockito.mockStatic(ModelInferenceLogsUtils.class); - MockedStatic security = Mockito.mockStatic(SecurityProjectUtils.class)) { - logs.when(() -> ModelInferenceLogsUtils.getWorkspaceEntry(WORKSPACE_ID)).thenReturn(workspaceRow(true)); - security.when(() -> SecurityProjectUtils.userCanViewProject(user, WORKSPACE_ID)).thenReturn(false); - - AgentRunner.persistWorkspaceOnEmptyRoom(room, insight, WORKSPACE_ID); - - assertNothingSaved(logs); - } - } - - private void assertNothingSaved(MockedStatic logs) { - logs.verify(() -> ModelInferenceLogsUtils.setRoomWorkspaceId(anyString(), anyString(), anyString()), - Mockito.never()); - logs.verify(() -> ModelInferenceLogsUtils.setRoomOptions(anyString(), anyString(), anyMap()), - Mockito.never()); - Mockito.verify(room, Mockito.never()).setOptionsMap(any()); - assertFalse(options.containsKey("workspace")); - assertEquals(0, options.size()); - } -}