diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index bb80724d..47a22e50 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -28,7 +28,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0", org.apache.commons.logging;bundle-version="1.2.0", slf4j.api;bundle-version="2.0.13", org.apache.commons.lang3;bundle-version="3.14.0" -Bundle-Classpath: target/classes/, +Bundle-Classpath: ., target/dependency/annotations-2.28.26.jar, target/dependency/apache-client-2.28.26.jar, target/dependency/auth-2.28.26.jar, diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java index a58f52c0..78bccc34 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java @@ -3,35 +3,112 @@ package software.aws.toolkits.eclipse.amazonq.broker; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.schedulers.Schedulers; -import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subjects.BehaviorSubject; import io.reactivex.rxjava3.subjects.Subject; import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; +/** + * A thread-safe event broker that implements the publish-subscribe pattern + * using RxJava. + * + * This broker manages event distribution using BehaviorSubjects, which cache + * the most recent event for each event type. It provides type-safe event + * publishing and subscription, with automatic resource management for + * subscriptions. Events are published and consumed on dedicated threads so + * operations are non-blocking. + */ public final class EventBroker { - private final Subject eventBus = PublishSubject.create().toSerialized(); + /** Maps event types to their corresponding subjects for event distribution. */ + private final Map, Subject> subjectsForType; + + /** Tracks all subscriptions for proper cleanup. */ + private final CompositeDisposable disposableSubscriptions; + + public EventBroker() { + subjectsForType = new ConcurrentHashMap<>(); + disposableSubscriptions = new CompositeDisposable(); + } - public void post(final T event) { + /** + * Posts an event of the specified type to all subscribers and caches it for + * late-subscribers. + * + * @param The type of the event + * @param eventType The class object representing the event type + * @param event The event to publish + */ + public void post(final Class eventType, final T event) { if (event == null) { return; } - eventBus.onNext(event); + getOrCreateSubject(eventType).onNext(event); + } + + /** + * Gets or creates a Subject for the specified event type. Creates a new + * serialized BehaviorSubject if none exists. + * + * @param The type of events the subject will handle + * @param eventType The class object representing the event type + * @return A Subject that handles events of the specified type + */ + private Subject getOrCreateSubject(final Class eventType) { + return subjectsForType.computeIfAbsent(eventType, k -> { + Subject subject = BehaviorSubject.create().toSerialized(); + subject.subscribeOn(Schedulers.computation()); + return subject; + }); } + /** + * Subscribes an observer to events of a specific type. The observer will + * receive events on a computation thread by default. The subscription is + * automatically tracked for disposal management. + * + * @param the type of events to observe + * @param eventType the Class object representing the event type + * @param observer the observer that will handle emitted events + * @return a Disposable that can be used to unsubscribe from the events + */ public Disposable subscribe(final Class eventType, final EventObserver observer) { - Consumer consumer = new Consumer<>() { - @Override - public void accept(final T event) { - observer.onEvent(event); - } - }; - - return eventBus.ofType(eventType) - .observeOn(Schedulers.computation()) - .subscribe(consumer); - } + Disposable subscription = ofObservable(eventType) + .observeOn(Schedulers.computation()) // subscribe on dedicated thread + .subscribe(observer::onEvent); + disposableSubscriptions.add(subscription); // track subscription for dispose call + return subscription; + } + + /** + * Returns an Observable for the specified event type. This Observable can be + * used to create custom subscription chains with additional operators. + * + * @param the type of events the Observable will emit + * @param eventType the Class object representing the event type + * @return an Observable that emits events of the specified type + */ + public Observable ofObservable(final Class eventType) { + return getOrCreateSubject(eventType).ofType(eventType); + } + + /** + * Disposes of all subscriptions managed by this broker by clearing the disposable subscriptions collection. + * This method should be called when the broker is no longer needed to prevent memory leaks. + * After disposal, any existing subscriptions will be terminated and new events will not be delivered + * to their observers. + * + * Note: This only disposes of the subscriptions, not the underlying Observables. + * The EventBroker can be reused after disposal by creating new subscriptions. + */ + public void dispose() { + disposableSubscriptions.clear(); + } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java index 9f34ee44..0703c072 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java @@ -121,7 +121,7 @@ private void updateState(final AuthStateType authStatusType, final LoginType log */ AuthState newAuthState = getAuthState(); if (previousAuthState == null || newAuthState.authStateType() != previousAuthState.authStateType()) { - Activator.getEventBroker().post(newAuthState); + Activator.getEventBroker().post(AuthState.class, newAuthState); } previousAuthState = newAuthState; } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/AmazonQViewType.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/AmazonQViewType.java new file mode 100644 index 00000000..3f710a5a --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/AmazonQViewType.java @@ -0,0 +1,9 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.router; + +public enum AmazonQViewType { + TOOLKIT_LOGIN_VIEW, CHAT_VIEW, DEPENDENCY_MISSING_VIEW, RE_AUTHENTICATE_VIEW, CHAT_ASSET_MISSING_VIEW, + LSP_STARTUP_FAILED_VIEW +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/PluginState.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/PluginState.java new file mode 100644 index 00000000..7e7b6f18 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/PluginState.java @@ -0,0 +1,10 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.router; + +import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; +import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspState; + +public record PluginState(AuthState authState, LspState lspState) { +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouter.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouter.java new file mode 100644 index 00000000..dbd5113d --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouter.java @@ -0,0 +1,156 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.router; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; +import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; +import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspState; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; + +/** + * Routes to appropriate views based on the combined auth and lsp states (plugin + * state). This router observes plugin state changes and updates the active view + * accordingly, broadcasting view changes through the event broker. + */ +public final class ViewRouter implements EventObserver { + + private AmazonQViewType activeView; + + /** + * Constructs a ViewRouter with the specified builder configuration. Initializes + * state observation and sets up view routing logic. Primarily useful for + * testing and injecting observables. When none are passed, the router get the + * observables directly from the event broker and combines them to create the + * PluginState stream. + * + * @param builder The builder containing auth and lsp state observables + */ + private ViewRouter(final Builder builder) { + if (builder.authStateObservable == null) { + builder.authStateObservable = Activator.getEventBroker().ofObservable(AuthState.class); + } + + if (builder.lspStateObservable == null) { + builder.lspStateObservable = Activator.getEventBroker().ofObservable(LspState.class); + } + + /* + * Combine auth and lsp streams and publish combined state updates on changes to + * either stream consisting of the latest events from both streams (this will + * happen only after one event has been published to both streams): + */ + Observable.combineLatest(builder.authStateObservable, builder.lspStateObservable, PluginState::new) + .observeOn(Schedulers.computation()).subscribe(this::onEvent); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Handles plugin state changes by refreshing the active view. + * + * @param pluginState The current combined state auth and lsp state of the plugin + */ + @Override + public void onEvent(final PluginState pluginState) { + refreshActiveView(pluginState); + } + + /** + * Determines and sets the appropriate view based on the order of resolution. + * View selection follows a priority order: + * 1. Dependency Missing: can browsers be created. + * 2. LSP Startup Failed: has the language server initialization failed (not pending/active). + * 3. Chat UI Asset Missing: have chat assets been fetched and available? + * 4. Authentication Logged out: if user logged out, needs to login again. + * 5. Authentication Expired: if auth has expired, needs to be refreshed. + * 5. Chat View: happy path. + * + * @param pluginState The current combined auth and lsp state of the plugin + */ + private void refreshActiveView(final PluginState pluginState) { + AmazonQViewType newActiveView; + + if (isDependencyMissing()) { // TODO: dependency missing check logic needs to be implemented + newActiveView = AmazonQViewType.DEPENDENCY_MISSING_VIEW; + } else if (pluginState.lspState() == LspState.FAILED) { + newActiveView = AmazonQViewType.LSP_STARTUP_FAILED_VIEW; + } else if (isChatUIAssetMissing()) { // TODO: chat missing logic needs to be implemented + newActiveView = AmazonQViewType.CHAT_ASSET_MISSING_VIEW; + } else if (pluginState.authState().isLoggedOut()) { + newActiveView = AmazonQViewType.TOOLKIT_LOGIN_VIEW; + } else if (pluginState.authState().isExpired()) { + newActiveView = AmazonQViewType.RE_AUTHENTICATE_VIEW; + } else { + newActiveView = AmazonQViewType.CHAT_VIEW; + } + + updateActiveView(newActiveView); + } + + /** + * Updates the active view if it has changed and notifies observers of the + * change. + * + * @param newActiveViewId The new view to be activated + */ + private void updateActiveView(final AmazonQViewType newActiveViewId) { + if (activeView != newActiveViewId) { + activeView = newActiveViewId; + notifyActiveViewChange(); + } + } + + /** + * Broadcasts the active view change through the event broker. + */ + private void notifyActiveViewChange() { + Activator.getEventBroker().post(AmazonQViewType.class, activeView); + } + + /** + * Checks if browsers available are compatible or is dependency missing. + * TODO: Implement actual dependency checking logic + * + * @return true if dependencies are missing, false otherwise + */ + private boolean isDependencyMissing() { + return false; + } + + /** + * Checks if required chat UI assets are missing. + * TODO: Implement actual asset checking logic + * + * @return true if chat UI assets are missing, false otherwise + */ + private boolean isChatUIAssetMissing() { + return false; + } + + public static final class Builder { + + private Observable authStateObservable; + private Observable lspStateObservable; + + public Builder withAuthStateObservable(final Observable authStateObservable) { + this.authStateObservable = authStateObservable; + return this; + } + + public Builder withLspStateObservable(final Observable lspStateObservable) { + this.lspStateObservable = lspStateObservable; + return this; + } + + public ViewRouter build() { + return new ViewRouter(this); + } + + } + +} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/broker/EventBrokerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/broker/EventBrokerTest.java index 2fe0995a..ccd0f074 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/broker/EventBrokerTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/broker/EventBrokerTest.java @@ -3,15 +3,19 @@ package software.aws.toolkits.eclipse.amazonq.broker; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import io.reactivex.rxjava3.disposables.Disposable; import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; @@ -21,6 +25,9 @@ public final class EventBrokerTest { private record TestEvent(String message, int id) { } + private record OtherTestEvent() { + } + private EventBroker eventBroker; @BeforeEach @@ -30,11 +37,11 @@ void setupBeforeEach() { @Test void testEventDelivery() { - TestEvent testEvent = new TestEvent("test message 1", 1); + TestEvent testEvent = new TestEvent("test message 1=", 1); EventObserver mockObserver = mock(EventObserver.class); Disposable subscription = eventBroker.subscribe(TestEvent.class, mockObserver); - eventBroker.post(testEvent); + eventBroker.post(TestEvent.class, testEvent); verify(mockObserver, timeout(100)).onEvent(testEvent); @@ -42,13 +49,12 @@ void testEventDelivery() { } @Test - void testNullEventsIgnored() { - EventObserver mockObserver = mock(EventObserver.class); + void testNullDoesNotThrowException() { + EventObserver mockObserver = mock(EventObserver.class); - Disposable subscription = eventBroker.subscribe(String.class, mockObserver); - eventBroker.post(null); + Disposable subscription = eventBroker.subscribe(TestEvent.class, mockObserver); - verify(mockObserver, never()).onEvent(any(String.class)); + assertDoesNotThrow(() -> eventBroker.post(TestEvent.class, null)); subscription.dispose(); } @@ -60,16 +66,16 @@ void verifyEventOrderingMaintained() { TestEvent thirdEvent = new TestEvent("a message", 3); EventObserver mockObserver = mock(EventObserver.class); + InOrder inOrder = inOrder(mockObserver); Disposable subscription = eventBroker.subscribe(TestEvent.class, mockObserver); - eventBroker.post(firstEvent); - eventBroker.post(secondEvent); - eventBroker.post(thirdEvent); - - verify(mockObserver, timeout(100)).onEvent(firstEvent); - verify(mockObserver, timeout(100)).onEvent(secondEvent); - verify(mockObserver, timeout(100)).onEvent(thirdEvent); + eventBroker.post(TestEvent.class, firstEvent); + eventBroker.post(TestEvent.class, secondEvent); + eventBroker.post(TestEvent.class, thirdEvent); + inOrder.verify(mockObserver, timeout(100)).onEvent(firstEvent); + inOrder.verify(mockObserver, timeout(100)).onEvent(secondEvent); + inOrder.verify(mockObserver, timeout(100)).onEvent(thirdEvent); verifyNoMoreInteractions(mockObserver); subscription.dispose(); @@ -77,21 +83,9 @@ void verifyEventOrderingMaintained() { @Test void testDifferentEventTypesIsolation() { - class OtherTestEvent { - private final int value; - - OtherTestEvent(final int value) { - this.value = value; - } - - public int getValue() { - return value; - } - } - TestEvent testEvent = new TestEvent("test message", 1); TestEvent secondEvent = new TestEvent("test message", 2); - OtherTestEvent otherEvent = new OtherTestEvent(42); + OtherTestEvent otherEvent = new OtherTestEvent(); EventObserver testEventObserver = mock(EventObserver.class); EventObserver otherEventObserver = mock(EventObserver.class); @@ -99,9 +93,9 @@ public int getValue() { Disposable testEventSubscription = eventBroker.subscribe(TestEvent.class, testEventObserver); Disposable otherEventSubscription = eventBroker.subscribe(OtherTestEvent.class, otherEventObserver); - eventBroker.post(testEvent); - eventBroker.post(otherEvent); - eventBroker.post(secondEvent); + eventBroker.post(TestEvent.class, testEvent); + eventBroker.post(OtherTestEvent.class, otherEvent); + eventBroker.post(TestEvent.class, secondEvent); verify(testEventObserver, timeout(100).times(2)).onEvent(any()); verify(otherEventObserver, timeout(100).times(1)).onEvent(any()); @@ -113,4 +107,93 @@ public int getValue() { otherEventSubscription.dispose(); } + @Test + void testLatestValueEmittedOnSubscription() throws InterruptedException { + OtherTestEvent otherEvent = new OtherTestEvent(); + TestEvent firstTestEvent = new TestEvent("test message", 1); + TestEvent secondTestEvent = new TestEvent("test message 2", 2); + + EventObserver firstEventObserver = mock(EventObserver.class); + EventObserver secondEventObserver = mock(EventObserver.class); + EventObserver otherEventObserver = mock(EventObserver.class); + + eventBroker.post(TestEvent.class, firstTestEvent); + eventBroker.post(OtherTestEvent.class, otherEvent); + + Disposable firstTestEventSubscription = eventBroker.subscribe(TestEvent.class, firstEventObserver); + Disposable otherEventSubscription = eventBroker.subscribe(OtherTestEvent.class, otherEventObserver); + + verify(firstEventObserver, timeout(100).times(1)).onEvent(firstTestEvent); + + eventBroker.post(TestEvent.class, secondTestEvent); + eventBroker.post(OtherTestEvent.class, otherEvent); + + Thread.sleep(100); + + Disposable secondTestEventSubscription = eventBroker.subscribe(TestEvent.class, secondEventObserver); + + verify(firstEventObserver, timeout(100).times(1)).onEvent(secondTestEvent); + verify(secondEventObserver, timeout(100).times(1)).onEvent(secondTestEvent); + verify(otherEventObserver, timeout(100).times(2)).onEvent(otherEvent); + + firstTestEventSubscription.dispose(); + secondTestEventSubscription.dispose(); + otherEventSubscription.dispose(); + } + + @Test + void testVerifyNoEventsEmitUnlessEventTypeMatches() { + OtherTestEvent otherEvent = new OtherTestEvent(); + + EventObserver eventObserver = mock(EventObserver.class); + EventObserver otherEventObserver = mock(EventObserver.class); + + eventBroker.post(OtherTestEvent.class, otherEvent); + + Disposable eventSubscription = eventBroker.subscribe(TestEvent.class, eventObserver); + Disposable otherEventSubscription = eventBroker.subscribe(OtherTestEvent.class, otherEventObserver); + + verifyNoInteractions(eventObserver); + verify(otherEventObserver, timeout(100).times(1)).onEvent(otherEvent); + + eventSubscription.dispose(); + otherEventSubscription.dispose(); + } + + @Test + void testDisposeClearsAllSubscriptions() { + EventObserver eventObserver = mock(EventObserver.class); + EventObserver otherEventObserver = mock(EventObserver.class); + + Disposable eventSubscription = eventBroker.subscribe(TestEvent.class, eventObserver); + Disposable otherEventSubscription = eventBroker.subscribe(OtherTestEvent.class, otherEventObserver); + + eventBroker.dispose(); + + assertTrue(eventSubscription.isDisposed()); + assertTrue(otherEventSubscription.isDisposed()); + } + + @Test + void testSubscriptionDisposalAndReconnectionEmitsLatestEvent() { + EventObserver eventObserver = mock(EventObserver.class); + TestEvent testEvent = new TestEvent("test message", 1); + + eventBroker.post(TestEvent.class, testEvent); + + Disposable firstEventSubscription = eventBroker.subscribe(TestEvent.class, eventObserver); + verify(eventObserver, timeout(100).times(1)).onEvent(testEvent); + firstEventSubscription.dispose(); + + TestEvent anotherEvent = new TestEvent("test message", 2); + eventBroker.post(TestEvent.class, anotherEvent); + + TestEvent thirdEvent = new TestEvent("test message", 3); + eventBroker.post(TestEvent.class, thirdEvent); + + Disposable secondEventSubscription = eventBroker.subscribe(TestEvent.class, eventObserver); + verify(eventObserver, timeout(100).times(1)).onEvent(thirdEvent); + secondEventSubscription.dispose(); + } + } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouterTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouterTest.java new file mode 100644 index 00000000..448a9196 --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouterTest.java @@ -0,0 +1,89 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.router; + +import static org.mockito.Mockito.verify; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import software.aws.toolkits.eclipse.amazonq.broker.EventBroker; +import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension; +import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; +import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthStateType; +import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspState; + +public final class ViewRouterTest { + + private PublishSubject publishSubject; + + private Observable authStateObservable; + private Observable lspStateObservable; + + private ViewRouter viewRouter; + private EventBroker eventBrokerMock; + + @RegisterExtension + private static ActivatorStaticMockExtension activatorStaticMockExtension = new ActivatorStaticMockExtension(); + + @BeforeEach + void setupBeforeEach() { + // ensure event handlers run on same thread + RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline()); + + publishSubject = PublishSubject.create(); + + authStateObservable = publishSubject.ofType(AuthState.class); + lspStateObservable = publishSubject.ofType(LspState.class); + + eventBrokerMock = activatorStaticMockExtension.getMock(EventBroker.class); + + viewRouter = ViewRouter.builder().withAuthStateObservable(authStateObservable) + .withLspStateObservable(lspStateObservable).build(); + } + + @AfterEach + public void resetAfterEach() { + RxJavaPlugins.reset(); + } + + @ParameterizedTest + @MethodSource("provideStateSource") + void testActiveViewResolutionBasedOnPluginState(final LspState lspState, final AuthState authState, + final AmazonQViewType expectedActiveViewId) { + publishSubject.onNext(authState); + publishSubject.onNext(lspState); + + verify(eventBrokerMock).post(AmazonQViewType.class, expectedActiveViewId); + } + + private static Stream provideStateSource() { + return Stream.of(Arguments.of(LspState.FAILED, getAuthStateObject(AuthStateType.LOGGED_IN), + AmazonQViewType.LSP_STARTUP_FAILED_VIEW), + Arguments.of(LspState.FAILED, getAuthStateObject(AuthStateType.LOGGED_OUT), + AmazonQViewType.LSP_STARTUP_FAILED_VIEW), + Arguments.of(LspState.FAILED, getAuthStateObject(AuthStateType.EXPIRED), + AmazonQViewType.LSP_STARTUP_FAILED_VIEW), + Arguments.of(LspState.PENDING, getAuthStateObject(AuthStateType.LOGGED_OUT), AmazonQViewType.TOOLKIT_LOGIN_VIEW), + Arguments.of(LspState.ACTIVE, getAuthStateObject(AuthStateType.LOGGED_OUT), AmazonQViewType.TOOLKIT_LOGIN_VIEW), + Arguments.of(LspState.PENDING, getAuthStateObject(AuthStateType.EXPIRED), AmazonQViewType.RE_AUTHENTICATE_VIEW), + Arguments.of(LspState.ACTIVE, getAuthStateObject(AuthStateType.EXPIRED), AmazonQViewType.RE_AUTHENTICATE_VIEW), + Arguments.of(LspState.ACTIVE, getAuthStateObject(AuthStateType.LOGGED_IN), AmazonQViewType.CHAT_VIEW)); + } + + private static AuthState getAuthStateObject(final AuthStateType authStateType) { + return new AuthState(authStateType, null, null, null, null); + } + +}