diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..5421f58b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ '**' ] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Fetch Sources + uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + + - name: Run Tests + run: ./gradlew check + + - name: Collect Tests Result + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: tests-result + path: ${{ github.workspace }}/build/reports/tests + + - name: Run Plugin Verification tasks + run: ./gradlew runPluginVerifier \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 273db223..0855e810 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,49 +1,68 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + plugins { - id("java") - id("org.jetbrains.intellij") version "1.13.3" + id("java") + id("org.jetbrains.intellij") version "1.13.3" } group = "ee.carlrobert" version = "1.10.5" repositories { - mavenCentral() + mavenCentral() } intellij { - version.set("2022.2") - type.set("IC") - plugins.set(listOf()) + version.set("2022.2") + type.set("IC") + plugins.set(listOf()) } dependencies { - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.2") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.2") - implementation("com.fifesoft:rsyntaxtextarea:3.3.2") - implementation("com.vladsch.flexmark:flexmark-all:0.64.0") - implementation("org.apache.commons:commons-text:1.10.0") - implementation("ee.carlrobert:openai-client:1.0.14") - implementation("com.knuddels:jtokkit:0.2.0") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.2") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.2") + implementation("com.fifesoft:rsyntaxtextarea:3.3.2") + implementation("com.vladsch.flexmark:flexmark-all:0.64.0") + implementation("org.apache.commons:commons-text:1.10.0") + implementation("ee.carlrobert:openai-client:1.0.14") + implementation("com.knuddels:jtokkit:0.2.0") + + testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("org.awaitility:awaitility:4.2.0") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.6.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.1") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.6.1") } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } tasks { - patchPluginXml { - sinceBuild.set("211") - untilBuild.set("231.*") - } + patchPluginXml { + sinceBuild.set("213") + untilBuild.set("231.*") + } - signPlugin { - certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) - privateKey.set(System.getenv("PRIVATE_KEY")) - password.set(System.getenv("PRIVATE_KEY_PASSWORD")) - } + signPlugin { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } - publishPlugin { - token.set(System.getenv("PUBLISH_TOKEN")) - } + publishPlugin { + token.set(System.getenv("PUBLISH_TOKEN")) + } +} + +tasks { + test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + } + } } diff --git a/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java b/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java index 93ab4e74..d8525707 100644 --- a/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java +++ b/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java @@ -14,7 +14,7 @@ public class PluginStartupActivity implements StartupActivity { public void runActivity(@NotNull Project project) { ActionsUtil.refreshActions(ConfigurationState.getInstance().tableData); var settings = SettingsState.getInstance(); - if (settings.apiKey != null && settings.useOpenAIAccountName) { + if (!settings.apiKey.isEmpty() && settings.useOpenAIAccountName) { ClientProvider.getDashboardClient() .getSubscriptionAsync(subscription -> settings.displayName = subscription.getAccountName()); diff --git a/src/main/java/ee/carlrobert/codegpt/client/ClientProvider.java b/src/main/java/ee/carlrobert/codegpt/client/ClientProvider.java index 3a17497a..d76eb793 100644 --- a/src/main/java/ee/carlrobert/codegpt/client/ClientProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/client/ClientProvider.java @@ -53,12 +53,18 @@ public class ClientProvider { var proxyHost = advancedSettings.proxyHost; var proxyPort = advancedSettings.proxyPort; if (!proxyHost.isEmpty() && proxyPort != 0) { - builder.setProxy(new Proxy(advancedSettings.proxyType, new InetSocketAddress(proxyHost, proxyPort))); + builder.setProxy( + new Proxy(advancedSettings.proxyType, new InetSocketAddress(proxyHost, proxyPort))); if (advancedSettings.isProxyAuthSelected) { - builder.setProxyAuthenticator(new ProxyAuthenticator(advancedSettings.proxyUsername, advancedSettings.proxyPassword)); + builder.setProxyAuthenticator( + new ProxyAuthenticator(advancedSettings.proxyUsername, advancedSettings.proxyPassword)); } } + if (!advancedSettings.host.isEmpty()) { + builder.setHost(advancedSettings.host); + } + return builder; } } diff --git a/src/main/java/ee/carlrobert/codegpt/client/EventListener.java b/src/main/java/ee/carlrobert/codegpt/client/EventListener.java index c12e2fd4..e0b7c398 100644 --- a/src/main/java/ee/carlrobert/codegpt/client/EventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/client/EventListener.java @@ -40,14 +40,13 @@ class EventListener implements CompletionEventListener { } private void saveConversation(String response) { - var conversationsState = ConversationsState.getInstance(); var conversationMessages = conversation.getMessages(); - if (isRetry && !conversationMessages.isEmpty()) { conversationMessages.remove(conversationMessages.size() - 1); } message.setResponse(response); - conversationsState.saveConversation(conversation); + conversation.addMessage(message); + ConversationsState.getInstance().saveConversation(conversation); } } diff --git a/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java b/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java index d1437e7c..47d07fa4 100644 --- a/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java +++ b/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java @@ -49,7 +49,6 @@ public class RequestHandler implements ActionListener { } }; swingWorker.execute(); - } public void cancel() { @@ -63,10 +62,11 @@ public class RequestHandler implements ActionListener { if (settings.isChatCompletionOptionSelected) { return ClientProvider.getChatCompletionClient(settings).stream( - requestProvider.buildChatCompletionRequest(settings.chatCompletionBaseModel), - eventListener); + requestProvider.buildChatCompletionRequest(settings.chatCompletionBaseModel), + eventListener); } return ClientProvider.getTextCompletionClient(settings).stream( - requestProvider.buildTextCompletionRequest(settings.textCompletionBaseModel), - eventListener); } + requestProvider.buildTextCompletionRequest(settings.textCompletionBaseModel), + eventListener); + } } diff --git a/src/main/java/ee/carlrobert/codegpt/state/conversations/ConversationsState.java b/src/main/java/ee/carlrobert/codegpt/state/conversations/ConversationsState.java index 6af13048..3bb4515a 100644 --- a/src/main/java/ee/carlrobert/codegpt/state/conversations/ConversationsState.java +++ b/src/main/java/ee/carlrobert/codegpt/state/conversations/ConversationsState.java @@ -122,23 +122,24 @@ public class ConversationsState implements PersistentStateComponent getNextConversation() { + public Optional getPreviousConversation() { return tryGetNextOrPreviousConversation(true); } - public Optional getPreviousConversation() { + public Optional getNextConversation() { return tryGetNextOrPreviousConversation(false); } - private Optional tryGetNextOrPreviousConversation(boolean isNext) { + private Optional tryGetNextOrPreviousConversation(boolean isPrevious) { if (currentConversation != null) { var sortedConversations = getSortedConversations(); for (int i = 0; i < sortedConversations.size(); i++) { var conversation = sortedConversations.get(i); if (conversation != null && conversation.getId().equals(currentConversation.getId())) { - var nextIndex = isNext ? i + 1 : i - 1; - if (isNext ? nextIndex < sortedConversations.size() : nextIndex != -1) { - return Optional.of(sortedConversations.get(nextIndex)); + // higher index indicates older conversation + var previousIndex = isPrevious ? i + 1 : i - 1; + if (isPrevious ? previousIndex < sortedConversations.size() : previousIndex != -1) { + return Optional.of(sortedConversations.get(previousIndex)); } } } @@ -147,9 +148,9 @@ public class ConversationsState implements PersistentStateComponent { + public String host = ""; public String proxyHost = ""; public int proxyPort; public Proxy.Type proxyType = Proxy.Type.SOCKS; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/html/ChatToolWindowTabHtmlPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/html/ChatToolWindowTabHtmlPanel.java index feda9a22..dbb8496f 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/html/ChatToolWindowTabHtmlPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/html/ChatToolWindowTabHtmlPanel.java @@ -123,7 +123,6 @@ public class ChatToolWindowTabHtmlPanel implements ToolWindowTabPanel { public void handleComplete() { stop(); - conversation.addMessage(message); } public void handleTokensExceeded() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveDownAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveDownAction.java index aa5c833c..e6e7b510 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveDownAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveDownAction.java @@ -20,6 +20,6 @@ public class MoveDownAction extends MoveAction { @Override protected Optional getConversation() { - return ConversationsState.getInstance().getNextConversation(); + return ConversationsState.getInstance().getPreviousConversation(); } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveUpAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveUpAction.java index e8c71810..5ef65937 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveUpAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/actions/MoveUpAction.java @@ -20,6 +20,6 @@ public class MoveUpAction extends MoveAction { @Override protected Optional getConversation() { - return ConversationsState.getInstance().getPreviousConversation(); + return ConversationsState.getInstance().getNextConversation(); } } diff --git a/src/main/java/ee/carlrobert/codegpt/util/ThemeUtils.java b/src/main/java/ee/carlrobert/codegpt/util/ThemeUtils.java index a902610f..5432b8e7 100644 --- a/src/main/java/ee/carlrobert/codegpt/util/ThemeUtils.java +++ b/src/main/java/ee/carlrobert/codegpt/util/ThemeUtils.java @@ -17,7 +17,8 @@ public class ThemeUtils { } public static String getFontColorRGB() { - return getRGB(EditorColorsManager.getInstance().getSchemeForCurrentUITheme().getDefaultForeground()); + return getRGB( + EditorColorsManager.getInstance().getSchemeForCurrentUITheme().getDefaultForeground()); } public static String getSeparatorColorRGB() { diff --git a/src/test/java/ee/carlrobert/codegpt/client/CompletionRequestProviderTest.java b/src/test/java/ee/carlrobert/codegpt/client/CompletionRequestProviderTest.java new file mode 100644 index 00000000..7919e3ca --- /dev/null +++ b/src/test/java/ee/carlrobert/codegpt/client/CompletionRequestProviderTest.java @@ -0,0 +1,100 @@ +package ee.carlrobert.codegpt.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import ee.carlrobert.codegpt.state.conversations.ConversationsState; +import ee.carlrobert.codegpt.state.conversations.message.Message; +import ee.carlrobert.openai.client.ClientCode; +import ee.carlrobert.openai.client.completion.chat.ChatCompletionModel; +import ee.carlrobert.openai.client.completion.text.TextCompletionModel; + +public class CompletionRequestProviderTest extends BasePlatformTestCase { + + public void testTextCompletionRequest() { + var conversation = ConversationsState.getInstance() + .createConversation(ClientCode.TEXT_COMPLETION); + conversation.addMessage(new Message("TEST_PROMPT", "TEST_RESPONSE")); + conversation.addMessage(new Message("TEST_PROMPT_2", "TEST_RESPONSE_2")); + + var request = new CompletionRequestProvider("TEST_TEXT_COMPLETION_PROMPT", conversation) + .buildTextCompletionRequest(TextCompletionModel.DAVINCI.getCode()); + + assertThat(request.getPrompt()) + .isEqualTo("You are ChatGPT, a large language model trained by OpenAI.\n" + + "Answer in a markdown language, code blocks should contain language whenever possible.\n" + + "Human: TEST_PROMPT\n" + + "AI: TEST_RESPONSE\n" + + "Human: TEST_PROMPT_2\n" + + "AI: TEST_RESPONSE_2\n" + + "Human: TEST_TEXT_COMPLETION_PROMPT\n" + + "AI: \n"); + } + + public void testChatCompletionRequest() { + var conversation = ConversationsState.getInstance().startConversation(); + var firstMessage = createMessage(500); + var secondMessage = createMessage(250); + conversation.addMessage(firstMessage); + conversation.addMessage(secondMessage); + + var request = new CompletionRequestProvider("TEST_CHAT_COMPLETION_PROMPT", conversation) + .buildChatCompletionRequest(ChatCompletionModel.GPT_3_5.getCode()); + + assertThat(request.getMessages()) + .extracting("role", "content") + .containsExactly( + tuple("system", + "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Include code language in markdown snippets whenever possible."), + tuple("user", "TEST_PROMPT"), + tuple("assistant", firstMessage.getResponse()), + tuple("user", "TEST_PROMPT"), + tuple("assistant", secondMessage.getResponse()), + tuple("user", "TEST_CHAT_COMPLETION_PROMPT")); + } + + public void testReducedChatCompletionRequest() { + var conversation = ConversationsState.getInstance().startConversation(); + conversation.addMessage(createMessage(50)); + conversation.addMessage(createMessage(100)); + conversation.addMessage(createMessage(150)); + var firstRemainingMessage = createMessage(1000); + var secondRemainingMessage = createMessage(2000); + conversation.addMessage(firstRemainingMessage); + conversation.addMessage(secondRemainingMessage); + conversation.discardTokenLimits(); + + var request = new CompletionRequestProvider("TEST_CHAT_COMPLETION_PROMPT", conversation) + .buildChatCompletionRequest(ChatCompletionModel.GPT_3_5.getCode()); + + assertThat(request.getMessages()) + .extracting("role", "content") + .containsExactly( + tuple("system", + "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Include code language in markdown snippets whenever possible."), + tuple("user", "TEST_PROMPT"), + tuple("assistant", firstRemainingMessage.getResponse()), + tuple("user", "TEST_PROMPT"), + tuple("assistant", secondRemainingMessage.getResponse()), + tuple("user", "TEST_CHAT_COMPLETION_PROMPT")); + } + + public void testTotalUsageExceededException() { + var conversation = ConversationsState.getInstance().startConversation(); + conversation.addMessage(createMessage(1500)); + conversation.addMessage(createMessage(1500)); + conversation.addMessage(createMessage(1500)); + + assertThrows(TotalUsageExceededException.class, + () -> new CompletionRequestProvider("TEST_MESSAGE", conversation) + .buildChatCompletionRequest(ChatCompletionModel.GPT_3_5.getCode())); + } + + private Message createMessage(int tokenSize) { + var message = new Message("TEST_PROMPT"); + // 'zz' = 1 token, prompt = 6 tokens, 7 tokens per message (GPT-3), + message.setResponse("zz".repeat((tokenSize) - 6 - 7)); + return message; + } +} diff --git a/src/test/java/ee/carlrobert/codegpt/client/RequestHandlerTest.java b/src/test/java/ee/carlrobert/codegpt/client/RequestHandlerTest.java new file mode 100644 index 00000000..c4dd5141 --- /dev/null +++ b/src/test/java/ee/carlrobert/codegpt/client/RequestHandlerTest.java @@ -0,0 +1,108 @@ +package ee.carlrobert.codegpt.client; + +import static ee.carlrobert.openai.util.JSONUtil.jsonArray; +import static ee.carlrobert.openai.util.JSONUtil.jsonMap; +import static ee.carlrobert.openai.util.JSONUtil.jsonMapResponse; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import ee.carlrobert.codegpt.state.conversations.ConversationsState; +import ee.carlrobert.codegpt.state.conversations.message.Message; +import ee.carlrobert.codegpt.state.settings.SettingsState; +import ee.carlrobert.codegpt.state.settings.advanced.AdvancedSettingsState; +import ee.carlrobert.openai.client.completion.text.TextCompletionModel; +import ee.carlrobert.openai.http.LocalCallbackServer; +import ee.carlrobert.openai.http.exchange.StreamHttpExchange; +import ee.carlrobert.openai.http.expectation.StreamExpectation; +import java.util.List; +import java.util.Map; + +public class RequestHandlerTest extends BasePlatformTestCase { + + private LocalCallbackServer server; + + @Override + protected void setUp() throws Exception { + super.setUp(); + SettingsState.getInstance().apiKey = "TEST_API_KEY"; + AdvancedSettingsState.getInstance().host = "http://localhost:8000"; + server = new LocalCallbackServer(); + } + + @Override + protected void tearDown() throws Exception { + server.stop(); + super.tearDown(); + } + + public void testChatCompletionCall() { + var conversation = ConversationsState.getInstance().startConversation(); + expectStreamRequest("/v1/chat/completions", request -> { + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeaders().get(AUTHORIZATION).get(0)).isEqualTo("Bearer TEST_API_KEY"); + assertThat(request.getBody()) + .extracting( + "model", + "messages") + .containsExactly( + "gpt-3.5-turbo", + List.of( + Map.of("role", "system", "content", + "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Include code language in markdown snippets whenever possible."), + Map.of("role", "user", "content", "TEST_PROMPT"))); + + return List.of( + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "lo")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "!"))))); + }); + + new RequestHandler(conversation).call(new Message("TEST_PROMPT"), false); + + await().atMost(5, SECONDS).until(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".contentEquals(messages.get(0).getResponse()); + }); + } + + public void testTextCompletionCall() { + var conversation = ConversationsState.getInstance().startConversation(); + var settings = SettingsState.getInstance(); + settings.isTextCompletionOptionSelected = true; + settings.isChatCompletionOptionSelected = false; + settings.textCompletionBaseModel = TextCompletionModel.CURIE.getCode(); + expectStreamRequest("/v1/completions", request -> { + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeaders().get("Authorization").get(0)).isEqualTo("Bearer TEST_API_KEY"); + assertThat(request.getBody()) + .extracting( + "model", + "prompt") + .containsExactly( + "text-curie-001", + "The following is a conversation with an AI assistant. " + + "The assistant is helpful, creative, clever, and very friendly.\n\n" + + "Human: TEST_PROMPT\n" + + "AI: \n"); + return List.of( + jsonMapResponse("choices", jsonArray(jsonMap("text", "He"))), + jsonMapResponse("choices", jsonArray(jsonMap("text", "llo"))), + jsonMapResponse("choices", jsonArray(jsonMap("text", "!")))); + }); + + new RequestHandler(conversation).call(new Message("TEST_PROMPT"), false); + + await().atMost(5, SECONDS).until(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".contentEquals(messages.get(0).getResponse()); + }); + } + + private void expectStreamRequest(String path, StreamHttpExchange exchange) { + server.addExpectation(new StreamExpectation(path, exchange)); + } +} diff --git a/src/test/java/ee/carlrobert/codegpt/state/conversations/ConversationsStateTest.java b/src/test/java/ee/carlrobert/codegpt/state/conversations/ConversationsStateTest.java new file mode 100644 index 00000000..766ccfd2 --- /dev/null +++ b/src/test/java/ee/carlrobert/codegpt/state/conversations/ConversationsStateTest.java @@ -0,0 +1,92 @@ +package ee.carlrobert.codegpt.state.conversations; + + +import static org.assertj.core.api.Assertions.assertThat; + +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import ee.carlrobert.codegpt.state.conversations.message.Message; +import ee.carlrobert.codegpt.state.settings.SettingsState; +import ee.carlrobert.openai.client.ClientCode; +import ee.carlrobert.openai.client.completion.chat.ChatCompletionModel; + +public class ConversationsStateTest extends BasePlatformTestCase { + + public void testStartNewDefaultConversation() { + var settings = SettingsState.getInstance(); + settings.isChatCompletionOptionSelected = true; + settings.isTextCompletionOptionSelected = false; + settings.chatCompletionBaseModel = ChatCompletionModel.GPT_3_5.getCode(); + + var conversation = ConversationsState.getInstance().startConversation(); + + assertThat(conversation).isEqualTo(ConversationsState.getCurrentConversation()); + assertThat(conversation) + .extracting("clientCode", "model") + .containsExactly(ClientCode.CHAT_COMPLETION, "gpt-3.5-turbo"); + } + + public void testSaveConversation() { + var instance = ConversationsState.getInstance(); + var conversation = instance.createConversation(ClientCode.CHAT_COMPLETION); + instance.addConversation(conversation); + var message = new Message("TEST_PROMPT"); + message.setResponse("TEST_RESPONSE"); + conversation.addMessage(message); + + instance.saveConversation(conversation); + + var currentConversation = ConversationsState.getCurrentConversation(); + assertThat(currentConversation).isNotNull(); + assertThat(currentConversation.getMessages()) + .flatExtracting("prompt", "response") + .containsExactly("TEST_PROMPT", "TEST_RESPONSE"); + } + + public void testGetPreviousConversation() { + var instance = ConversationsState.getInstance(); + var firstConversation = instance.startConversation(); + instance.startConversation(); + + var previousConversation = instance.getPreviousConversation(); + + assertThat(previousConversation.isPresent()).isTrue(); + assertThat(previousConversation.get()).isEqualTo(firstConversation); + } + + public void testGetNextConversation() { + var instance = ConversationsState.getInstance(); + var firstConversation = instance.startConversation(); + var secondConversation = instance.startConversation(); + instance.setCurrentConversation(firstConversation); + + var nextConversation = instance.getNextConversation(); + + assertThat(nextConversation.isPresent()).isTrue(); + assertThat(nextConversation.get()).isEqualTo(secondConversation); + } + + public void testDeleteSelectedConversation() { + var instance = ConversationsState.getInstance(); + var firstConversation = instance.startConversation(); + instance.startConversation(); + + instance.deleteSelectedConversation(); + + assertThat(ConversationsState.getCurrentConversation()).isEqualTo(firstConversation); + assertThat(instance.getSortedConversations().size()).isEqualTo(1); + assertThat(instance.getSortedConversations()) + .extracting("id") + .containsExactly(firstConversation.getId()); + } + + public void testClearAllConversations() { + var instance = ConversationsState.getInstance(); + instance.startConversation(); + instance.startConversation(); + + instance.clearAll(); + + assertThat(ConversationsState.getCurrentConversation()).isNull(); + assertThat(instance.getSortedConversations().size()).isEqualTo(0); + } +}