diff --git a/apps/web/src/components/mission-control/__tests__/GlobalAgentRoster.test.tsx b/apps/web/src/components/mission-control/__tests__/GlobalAgentRoster.test.tsx
new file mode 100644
index 0000000..76f0fe8
--- /dev/null
+++ b/apps/web/src/components/mission-control/__tests__/GlobalAgentRoster.test.tsx
@@ -0,0 +1,133 @@
+import type { ReactElement } from "react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { GlobalAgentRoster } from "../GlobalAgentRoster";
+
+const { mockApiGet, mockApiPost } = vi.hoisted(() => ({
+ mockApiGet: vi.fn(),
+ mockApiPost: vi.fn(),
+}));
+
+vi.mock("@/lib/api/client", () => ({
+ apiGet: mockApiGet,
+ apiPost: mockApiPost,
+}));
+
+function renderWithQueryClient(ui: ReactElement): void {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ });
+
+ render({ui});
+}
+
+describe("GlobalAgentRoster (__tests__)", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", vi.fn());
+ mockApiGet.mockReset();
+ mockApiPost.mockReset();
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it("renders empty state when no sessions", async () => {
+ mockApiGet.mockResolvedValueOnce([]);
+
+ renderWithQueryClient();
+
+ expect(await screen.findByText("No active agents")).toBeInTheDocument();
+ });
+
+ it("renders session rows grouped by provider", async () => {
+ mockApiGet.mockResolvedValueOnce([
+ {
+ id: "sess-int-123456",
+ providerId: "internal",
+ providerType: "internal",
+ status: "active",
+ createdAt: "2026-03-07T19:00:00.000Z",
+ updatedAt: "2026-03-07T19:00:00.000Z",
+ },
+ {
+ id: "sess-rem-654321",
+ providerId: "remote-a",
+ providerType: "remote",
+ status: "paused",
+ createdAt: "2026-03-07T19:00:00.000Z",
+ updatedAt: "2026-03-07T19:00:00.000Z",
+ },
+ ]);
+
+ renderWithQueryClient();
+
+ expect(await screen.findByText("internal")).toBeInTheDocument();
+ expect(screen.getByText("remote-a (remote)")).toBeInTheDocument();
+ expect(screen.getByText("sess-int")).toBeInTheDocument();
+ expect(screen.getByText("sess-rem")).toBeInTheDocument();
+ });
+
+ it("kill button per row calls the API", async () => {
+ const user = userEvent.setup();
+
+ mockApiGet.mockResolvedValueOnce([
+ {
+ id: "killme123456",
+ providerId: "internal",
+ providerType: "internal",
+ status: "active",
+ createdAt: "2026-03-07T19:00:00.000Z",
+ updatedAt: "2026-03-07T19:00:00.000Z",
+ },
+ ]);
+
+ mockApiPost.mockResolvedValue({ message: "ok" });
+
+ renderWithQueryClient();
+
+ const killButton = await screen.findByRole("button", { name: "Kill session killme12" });
+ await user.click(killButton);
+
+ await waitFor(() => {
+ expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/killme123456/kill", {
+ force: false,
+ });
+ });
+ });
+
+ it("onSelectSession callback fires on row click", async () => {
+ const user = userEvent.setup();
+ const onSelectSession = vi.fn();
+
+ mockApiGet.mockResolvedValueOnce([
+ {
+ id: "selectme123456",
+ providerId: "internal",
+ providerType: "internal",
+ status: "active",
+ createdAt: "2026-03-07T19:00:00.000Z",
+ updatedAt: "2026-03-07T19:00:00.000Z",
+ },
+ ]);
+
+ renderWithQueryClient();
+
+ const sessionLabel = await screen.findByText("selectme");
+ const row = sessionLabel.closest('[role="button"]');
+
+ if (!row) {
+ throw new Error("Expected session row for selectme123456");
+ }
+
+ await user.click(row);
+
+ expect(onSelectSession).toHaveBeenCalledWith("selectme123456");
+ });
+});
diff --git a/apps/web/src/components/mission-control/__tests__/KillAllDialog.test.tsx b/apps/web/src/components/mission-control/__tests__/KillAllDialog.test.tsx
new file mode 100644
index 0000000..fafb03e
--- /dev/null
+++ b/apps/web/src/components/mission-control/__tests__/KillAllDialog.test.tsx
@@ -0,0 +1,96 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { AgentSession } from "@mosaic/shared";
+import { KillAllDialog } from "../KillAllDialog";
+import * as apiClient from "@/lib/api/client";
+
+vi.mock("@/lib/api/client", () => ({
+ apiPost: vi.fn(),
+}));
+
+const mockApiPost = vi.mocked(apiClient.apiPost);
+const baseDate = new Date("2026-03-07T14:00:00.000Z");
+
+const sessions: AgentSession[] = [
+ {
+ id: "session-internal-1",
+ providerId: "provider-internal-1",
+ providerType: "internal",
+ status: "active",
+ createdAt: baseDate,
+ updatedAt: baseDate,
+ },
+ {
+ id: "session-internal-2",
+ providerId: "provider-internal-2",
+ providerType: "internal",
+ status: "paused",
+ createdAt: baseDate,
+ updatedAt: baseDate,
+ },
+ {
+ id: "session-external-1",
+ providerId: "provider-openclaw-1",
+ providerType: "openclaw",
+ status: "active",
+ createdAt: baseDate,
+ updatedAt: baseDate,
+ },
+];
+
+describe("KillAllDialog (__tests__)", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", vi.fn());
+ mockApiPost.mockResolvedValue({ message: "killed" } as never);
+ });
+
+ it('Confirm button disabled until "KILL ALL" typed exactly', async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ await user.click(screen.getByRole("button", { name: "Kill All" }));
+
+ const input = screen.getByLabelText("Type KILL ALL to confirm");
+ const confirmButton = screen.getByRole("button", { name: "Kill All Agents" });
+
+ expect(confirmButton).toBeDisabled();
+
+ await user.type(input, "kill all");
+ expect(confirmButton).toBeDisabled();
+
+ await user.clear(input);
+ await user.type(input, "KILL ALL");
+ expect(confirmButton).toBeEnabled();
+ });
+
+ it("fires kill API for each session on confirm", async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ await user.click(screen.getByRole("button", { name: "Kill All" }));
+ await user.click(screen.getByLabelText("All providers (3)"));
+ await user.type(screen.getByLabelText("Type KILL ALL to confirm"), "KILL ALL");
+ await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
+
+ await waitFor(() => {
+ expect(mockApiPost).toHaveBeenCalledTimes(3);
+ });
+
+ expect(mockApiPost).toHaveBeenCalledWith(
+ "/api/mission-control/sessions/session-internal-1/kill",
+ { force: true }
+ );
+ expect(mockApiPost).toHaveBeenCalledWith(
+ "/api/mission-control/sessions/session-internal-2/kill",
+ { force: true }
+ );
+ expect(mockApiPost).toHaveBeenCalledWith(
+ "/api/mission-control/sessions/session-external-1/kill",
+ { force: true }
+ );
+ });
+});
diff --git a/apps/web/src/components/mission-control/__tests__/OrchestratorPanel.test.tsx b/apps/web/src/components/mission-control/__tests__/OrchestratorPanel.test.tsx
new file mode 100644
index 0000000..4b185ac
--- /dev/null
+++ b/apps/web/src/components/mission-control/__tests__/OrchestratorPanel.test.tsx
@@ -0,0 +1,93 @@
+import { render, screen } from "@testing-library/react";
+import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { OrchestratorPanel } from "../OrchestratorPanel";
+import * as missionControlHooks from "@/hooks/useMissionControl";
+
+vi.mock("@/hooks/useMissionControl", () => ({
+ useSessionStream: vi.fn(),
+ useSessions: vi.fn(),
+}));
+
+vi.mock("@/components/mission-control/PanelControls", () => ({
+ PanelControls: (): React.JSX.Element =>
,
+}));
+
+vi.mock("@/components/mission-control/BargeInInput", () => ({
+ BargeInInput: ({ sessionId }: { sessionId: string }): React.JSX.Element => (
+ barge-in:{sessionId}
+ ),
+}));
+
+vi.mock("date-fns", () => ({
+ formatDistanceToNow: (): string => "moments ago",
+}));
+
+const mockUseSessionStream = vi.mocked(missionControlHooks.useSessionStream);
+const mockUseSessions = vi.mocked(missionControlHooks.useSessions);
+
+beforeAll(() => {
+ Object.defineProperty(window.HTMLElement.prototype, "scrollIntoView", {
+ configurable: true,
+ value: vi.fn(),
+ });
+});
+
+describe("OrchestratorPanel (__tests__)", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ mockUseSessionStream.mockReturnValue({
+ messages: [],
+ status: "connected",
+ error: null,
+ });
+
+ mockUseSessions.mockReturnValue({
+ sessions: [],
+ loading: false,
+ error: null,
+ });
+ });
+
+ it("renders empty state when no sessionId", () => {
+ render();
+
+ expect(screen.getByText("Select an agent to view its stream")).toBeInTheDocument();
+ });
+
+ it("renders connection indicator", () => {
+ const { container } = render();
+
+ expect(screen.getByText("Connected")).toBeInTheDocument();
+ expect(container.querySelector(".bg-emerald-500")).toBeInTheDocument();
+ });
+
+ it("renders message list when messages are present", () => {
+ mockUseSessionStream.mockReturnValue({
+ messages: [
+ {
+ id: "msg-1",
+ sessionId: "session-1",
+ role: "assistant",
+ content: "Mission update one",
+ timestamp: "2026-03-07T21:00:00.000Z",
+ },
+ {
+ id: "msg-2",
+ sessionId: "session-1",
+ role: "tool",
+ content: "Mission update two",
+ timestamp: "2026-03-07T21:00:01.000Z",
+ },
+ ],
+ status: "connected",
+ error: null,
+ });
+
+ render();
+
+ expect(screen.getByText("Mission update one")).toBeInTheDocument();
+ expect(screen.getByText("Mission update two")).toBeInTheDocument();
+ expect(screen.queryByText("Waiting for messages...")).not.toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/mission-control/__tests__/PanelControls.test.tsx b/apps/web/src/components/mission-control/__tests__/PanelControls.test.tsx
new file mode 100644
index 0000000..5ac15db
--- /dev/null
+++ b/apps/web/src/components/mission-control/__tests__/PanelControls.test.tsx
@@ -0,0 +1,70 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { PanelControls } from "../PanelControls";
+import * as apiClient from "@/lib/api/client";
+
+vi.mock("@/lib/api/client", () => ({
+ apiPost: vi.fn(),
+}));
+
+const mockApiPost = vi.mocked(apiClient.apiPost);
+
+function renderPanelControls(status: string): void {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ render(
+
+
+
+ );
+}
+
+describe("PanelControls (__tests__)", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", vi.fn());
+ mockApiPost.mockResolvedValue({ message: "ok" } as never);
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it("Pause button disabled when status=paused", () => {
+ renderPanelControls("paused");
+
+ expect(screen.getByRole("button", { name: "Pause session" })).toBeDisabled();
+ });
+
+ it("Resume button disabled when status=active", () => {
+ renderPanelControls("active");
+
+ expect(screen.getByRole("button", { name: "Resume session" })).toBeDisabled();
+ });
+
+ it("Kill buttons disabled when status=killed", () => {
+ renderPanelControls("killed");
+
+ expect(screen.getByRole("button", { name: "Gracefully kill session" })).toBeDisabled();
+ expect(screen.getByRole("button", { name: "Force kill session" })).toBeDisabled();
+ });
+
+ it("clicking pause calls the API", async () => {
+ const user = userEvent.setup();
+
+ renderPanelControls("active");
+
+ await user.click(screen.getByRole("button", { name: "Pause session" }));
+
+ await waitFor(() => {
+ expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-1/pause");
+ });
+ });
+});
diff --git a/apps/web/src/components/mission-control/__tests__/mission-control-phase2.gate.test.tsx b/apps/web/src/components/mission-control/__tests__/mission-control-phase2.gate.test.tsx
new file mode 100644
index 0000000..0a83d06
--- /dev/null
+++ b/apps/web/src/components/mission-control/__tests__/mission-control-phase2.gate.test.tsx
@@ -0,0 +1,34 @@
+import type { ReactNode } from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { MissionControlLayout } from "../MissionControlLayout";
+
+vi.mock("@/components/mission-control/AuditLogDrawer", () => ({
+ AuditLogDrawer: ({ trigger }: { trigger: ReactNode }): React.JSX.Element => (
+ {trigger}
+ ),
+}));
+
+vi.mock("@/components/mission-control/GlobalAgentRoster", () => ({
+ GlobalAgentRoster: (): React.JSX.Element => ,
+}));
+
+vi.mock("@/components/mission-control/MissionControlPanel", () => ({
+ MissionControlPanel: (): React.JSX.Element => ,
+ MIN_PANEL_COUNT: 1,
+ MAX_PANEL_COUNT: 6,
+}));
+
+describe("Mission Control Phase 2 Gate", () => {
+ it("Phase 2 gate: MissionControlLayout renders with all components present", () => {
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation((..._args) => undefined);
+
+ render();
+
+ expect(screen.getByTestId("global-agent-roster")).toBeInTheDocument();
+ expect(screen.getByTestId("mission-control-panel")).toBeInTheDocument();
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
+
+ consoleErrorSpy.mockRestore();
+ });
+});