diff --git a/ui/src/components/assistant/PromoteTransition.test.tsx b/ui/src/components/assistant/PromoteTransition.test.tsx
new file mode 100644
index 00000000..df6e5cd1
--- /dev/null
+++ b/ui/src/components/assistant/PromoteTransition.test.tsx
@@ -0,0 +1,154 @@
+// @vitest-environment jsdom
+
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { PromoteTransition } from "./PromoteTransition";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+describe("", () => {
+ let container: HTMLDivElement;
+ let root: ReturnType | null = null;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ root = null;
+ });
+
+ afterEach(() => {
+ if (root) {
+ act(() => {
+ root!.unmount();
+ });
+ root = null;
+ }
+ if (container.parentNode) container.remove();
+ });
+
+ function render(node: React.ReactNode) {
+ root = createRoot(container);
+ act(() => {
+ root!.render(node);
+ });
+ }
+
+ it("renders nothing in idle state", () => {
+ render(
+ panel}
+ >
+ ribbon
+ ,
+ );
+ expect(container.querySelector('[data-testid="promote-transition"]')).toBeNull();
+ });
+
+ it("renders nothing in done state", () => {
+ render(
+ panel}
+ >
+ ribbon
+ ,
+ );
+ expect(container.querySelector('[data-testid="promote-transition"]')).toBeNull();
+ });
+
+ it("renders nothing in error state", () => {
+ render(
+ panel}
+ >
+ ribbon
+ ,
+ );
+ expect(container.querySelector('[data-testid="promote-transition"]')).toBeNull();
+ });
+
+ it("renders the overlay, ribbon, panel and label in prompting state", () => {
+ render(
+ panel}
+ >
+ ribbon
+ ,
+ );
+ const overlay = container.querySelector('[data-testid="promote-transition"]') as HTMLElement;
+ expect(overlay).not.toBeNull();
+ expect(overlay.getAttribute("data-state")).toBe("prompting");
+ expect(overlay.getAttribute("aria-live")).toBe("polite");
+ expect(overlay.getAttribute("role")).toBe("region");
+
+ const ribbon = container.querySelector('[data-testid="promote-chat-ribbon"]') as HTMLElement;
+ const panelContainer = container.querySelector('[data-testid="promote-panel-container"]') as HTMLElement;
+ const label = container.querySelector('[data-testid="promote-source-label"]') as HTMLElement;
+
+ expect(ribbon).not.toBeNull();
+ expect(panelContainer).not.toBeNull();
+ expect(label).not.toBeNull();
+
+ // State data attribute is threaded to the animated children so CSS can
+ // select them.
+ expect(ribbon.getAttribute("data-pstate")).toBe("prompting");
+ expect(panelContainer.getAttribute("data-pstate")).toBe("prompting");
+ expect(label.getAttribute("data-pstate")).toBe("prompting");
+
+ // Children render inside the right slots.
+ expect(ribbon.querySelector('[data-testid="ribbon"]')).not.toBeNull();
+ expect(panelContainer.querySelector('[data-testid="panel"]')).not.toBeNull();
+
+ // Source conversation label text.
+ expect(label.textContent?.trim().toLowerCase()).toBe("source conversation");
+ });
+
+ it("updates data-state to creating while the API call is in flight", () => {
+ render(
+ panel}
+ >
+ ribbon
+ ,
+ );
+ const overlay = container.querySelector('[data-testid="promote-transition"]') as HTMLElement;
+ expect(overlay.getAttribute("data-state")).toBe("creating");
+ const ribbon = container.querySelector('[data-testid="promote-chat-ribbon"]') as HTMLElement;
+ expect(ribbon.getAttribute("data-pstate")).toBe("creating");
+ });
+
+ it("inlines a
+
+ {/* SOURCE CONVERSATION label */}
+
+ Source Conversation
+
+
+ {/* Compressed chat ribbon */}
+
+ {children}
+
+
+ {/* Rising brainstormer panel */}
+
+ {panelChildren}
+
+
+ );
+}