Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 371 additions & 2 deletions apps/mission-control/e2e/mockGateway.mjs

Large diffs are not rendered by default.

916 changes: 916 additions & 0 deletions apps/mission-control/e2e/p5-policy-slice.spec.ts

Large diffs are not rendered by default.

39 changes: 29 additions & 10 deletions apps/mission-control/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { SimpleIntegrationWizard } from "./features/connectors/SimpleIntegration
import type { SimpleIntegrationId } from "./features/connectors/simpleIntegrations";
import { useConnectorsController } from "./features/connectors/useConnectorsController";
import { useExecassOfficeController } from "./features/execassOffice/useExecassOfficeController";
import { useExecassPolicyController } from "./features/execassPolicy/useExecassPolicyController";
import type { SetupSurfaceProps } from "./features/setup/SetupControls";
import { useGlassWindowController } from "./features/glassWindow/useGlassWindowController";
import { findRoom, roomForTab } from "./glass/floors";
Expand Down Expand Up @@ -281,9 +282,13 @@ export default function App() {
[addToast],
);

const [boards, setBoards] = useState<BoardSummary[]>([]);
const [agents, setAgents] = useState<Agent[]>([]);
const [tokenConfiguredChecked, setTokenConfiguredChecked] = useState(false);
const [boards, setBoards] = useState<BoardSummary[]>([]);
const [agents, setAgents] = useState<Agent[]>([]);
const [tokenConfiguredChecked, setTokenConfiguredChecked] = useState(false);
const [authIdentityGeneration, setAuthIdentityGeneration] = useState(0);
const markAuthIdentityChanged = useCallback(() => {
setAuthIdentityGeneration((generation) => generation + 1);
}, []);
const [guidedTourOpen, setGuidedTourOpen] = useState(false);
const [guidedTourStep, setGuidedTourStep] = useState(0);
const [safeModeReason, setSafeModeReason] = useState<string | null>(null);
Expand Down Expand Up @@ -440,10 +445,11 @@ export default function App() {
],
[connectorsHubEnabled, memoryHubEnabled, runbookHubEnabled]
);
// Setup shares the Connectors render route, but it is the authority used to
// turn Connectors back on. Keep that stable room independently available
// when the optional Connectors product surface is disabled.
const alwaysAvailableElevatorRooms = useMemo(() => ["setup"], []);
// Setup and Policy share the Connectors render route, but Setup is the
// authority used to turn Connectors back on and Policy is the owner's
// autonomy surface. Keep both stable rooms independently available when
// the optional Connectors product surface is disabled.
const alwaysAvailableElevatorRooms = useMemo(() => ["setup", "policy"], []);
const elevatorFloors = useResolvedElevator(
availableTabs,
alwaysAvailableElevatorRooms,
Expand Down Expand Up @@ -684,6 +690,16 @@ export default function App() {
active: activeTab === "assistant",
setNotice,
});
// The one App-owned policy controller. Its invalidation signal comes from
// the Office controller's durable stream - never a second websocket.
const policyController = useExecassPolicyController({
settings,
tokenConfigured,
active: activeTab === "connectors" && resolvedActiveRoomId === "policy",
authIdentityGeneration,
policyInvalidationGeneration: officeController.policyInvalidationGeneration,
setNotice,
});
const glassWindowController = useGlassWindowController({
settings,
tokenConfigured,
Expand Down Expand Up @@ -711,8 +727,9 @@ export default function App() {
setSettings,
setGatewayDraft,
setTokenDraft,
setTokenConfigured,
setTokenConfiguredChecked,
setTokenConfigured,
setTokenConfiguredChecked,
onAuthIdentityChanged: markAuthIdentityChanged,
setHealthState,
setWsState,
setNotice,
Expand Down Expand Up @@ -903,7 +920,8 @@ export default function App() {
if (
activeTab === "connectors" &&
!connectorsHubEnabled &&
activeRoomId !== "setup"
activeRoomId !== "setup" &&
activeRoomId !== "policy"
) {
selectRoom("setup", elevatorFloors);
setNotice({
Expand Down Expand Up @@ -1343,6 +1361,7 @@ export default function App() {
setNotice={setNotice}
usageChartsEnabled={usageChartsEnabled}
setupSurface={setupSurface}
policyController={policyController}
onOpenSimpleIntegrationWizard={openSimpleIntegrationWizard}
quickGuidesCollapsed={quickGuideState.collapsed}
quickGuideOpenTab={quickGuideState.openTab}
Expand Down
4 changes: 4 additions & 0 deletions apps/mission-control/src/app/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { type CockpitWidgetLayoutV2 } from "../features/cockpit/cockpitLayout";
import { CockpitWidgetRenderer } from "../features/cockpit/CockpitWidgetRenderer";
import { ConnectorsPage } from "../features/connectors/ConnectorsPage";
import type { SetupSurfaceProps } from "../features/setup/SetupControls";
import type { ExecassPolicyController } from "../features/execassPolicy/useExecassPolicyController";
import type { SimpleIntegrationId } from "../features/connectors/simpleIntegrations";
import { useConnectorsController } from "../features/connectors/useConnectorsController";
import { EventsPage } from "../features/events/EventsPage";
Expand Down Expand Up @@ -98,6 +99,8 @@ interface AppContentProps {
usageChartsEnabled: boolean;
/** The one shared Setup authority; the Basement Setup room renders it. */
setupSurface: SetupSurfaceProps;
/** The one App-owned policy controller; the Basement Policy room renders it. */
policyController: ExecassPolicyController;
onOpenSimpleIntegrationWizard: (integrationId?: SimpleIntegrationId) => void;
quickGuidesCollapsed: boolean;
quickGuideOpenTab: HelpTab | null;
Expand Down Expand Up @@ -1045,6 +1048,7 @@ export function AppContent(props: AppContentProps) {
onOpenSimpleIntegrationWizard={props.onOpenSimpleIntegrationWizard}
activeRoomId={props.activeRoomId}
setupSurface={props.setupSurface}
policyController={props.policyController}
/>
</TabBoundaryPane>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const refreshBoard = vi.fn<(boardId: string, runtimeSettings?: RuntimeConnection
const loadMissionControlReadModels = vi.fn<(runtimeSettings?: RuntimeConnectionSettings) => Promise<void>>();
const loadRunbookReadModels = vi.fn<(runtimeSettings?: RuntimeConnectionSettings) => Promise<void>>();
const loadAgentMailReadModels = vi.fn<(runtimeSettings?: RuntimeConnectionSettings) => Promise<void>>();
const onAuthIdentityChanged = vi.fn();

interface HarnessProps {
initialGatewayUrl: string;
Expand Down Expand Up @@ -126,6 +127,7 @@ function Harness(props: HarnessProps) {
setTokenDraft,
setTokenConfigured,
setTokenConfiguredChecked,
onAuthIdentityChanged,
setHealthState,
setWsState,
setNotice: pushNotice,
Expand Down Expand Up @@ -375,6 +377,7 @@ describe("useRuntimeConnectionController save truth", () => {
});
expect(snapshot.settings.gateway_url).toBe("http://10.0.0.9:18789/");
expect(setGatewayTokenMock).toHaveBeenCalledWith("secret-token");
expect(onAuthIdentityChanged).toHaveBeenCalledTimes(1);
});

it("adopts an authoritative save into the one shared gateway draft", async () => {
Expand All @@ -397,6 +400,7 @@ describe("useRuntimeConnectionController save truth", () => {
await controller.saveConnectionFromInputs(GATEWAY_URL, "");
});
expect(setGatewayTokenMock).not.toHaveBeenCalled();
expect(onAuthIdentityChanged).not.toHaveBeenCalled();
expect(healthMock).toHaveBeenCalledTimes(1);
expect(notices).toEqual([
{ tone: "info", message: "Connection settings saved." },
Expand All @@ -410,6 +414,7 @@ describe("useRuntimeConnectionController save truth", () => {
await controller.saveConnectionFromInputs(GATEWAY_URL, "");
});
expect(healthMock).not.toHaveBeenCalled();
expect(onAuthIdentityChanged).not.toHaveBeenCalled();
expect(notices).toEqual([]);
});

Expand All @@ -420,6 +425,7 @@ describe("useRuntimeConnectionController save truth", () => {
await controller.saveConnection();
});
expect(snapshot.tokenDraft).toBe("typed-secret");
expect(onAuthIdentityChanged).not.toHaveBeenCalled();
expect(notices).toHaveLength(1);
expect(notices[0].tone).toBe("critical");
expect(notices[0].message).toContain("Connection save failed");
Expand Down Expand Up @@ -470,6 +476,7 @@ describe("useRuntimeConnectionController save truth", () => {
expect(notices[0].message).toContain("Agent roster unavailable");
expect(notices[0].message).not.toContain("Connection settings saved");
expect(snapshot.tokenDraft).toBe("typed-secret");
expect(onAuthIdentityChanged).toHaveBeenCalledTimes(1);
});
});

Expand Down Expand Up @@ -525,6 +532,7 @@ describe("useRuntimeConnectionController clear-token truth", () => {
expect(snapshot.tokenConfigured).toBe(false);
expect(snapshot.wsState).toBe("idle");
expect(notices).toEqual([{ tone: "info", message: "Gateway token cleared." }]);
expect(onAuthIdentityChanged).toHaveBeenCalledTimes(1);
});

it("keeps the configured-token truth and reports failure when the secure clear fails", async () => {
Expand All @@ -542,6 +550,7 @@ describe("useRuntimeConnectionController clear-token truth", () => {
expect(notices[0].tone).toBe("critical");
expect(notices[0].message).toContain("Forget token failed");
expect(notices[0].message).not.toContain("Gateway token cleared");
expect(onAuthIdentityChanged).not.toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ interface UseRuntimeConnectionControllerOptions {
setTokenDraft: Dispatch<SetStateAction<string>>;
setTokenConfigured: Dispatch<SetStateAction<boolean>>;
setTokenConfiguredChecked: Dispatch<SetStateAction<boolean>>;
/** Invalidates consumers whose truth is scoped to the secure token identity. */
onAuthIdentityChanged: () => void;
setHealthState: Dispatch<SetStateAction<string>>;
setWsState: Dispatch<SetStateAction<WsLifecycleState>>;
setNotice: NotifyFn;
Expand Down Expand Up @@ -61,6 +63,7 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
setTokenDraft,
setTokenConfigured,
setTokenConfiguredChecked,
onAuthIdentityChanged,
setHealthState,
setWsState,
setNotice,
Expand Down Expand Up @@ -302,6 +305,7 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
if (nextToken) {
await setGatewayToken(nextToken);
tokenTruthGenerationRef.current += 1;
onAuthIdentityChanged();
}

const hasToken = await isGatewayTokenConfigured();
Expand All @@ -327,6 +331,7 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
},
[
loadBaseline,
onAuthIdentityChanged,
setGatewayDraft,
setNotice,
setSettings,
Expand Down Expand Up @@ -363,11 +368,13 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
connectionActionLockRef.current = false;
}
tokenTruthGenerationRef.current += 1;
onAuthIdentityChanged();
setTokenConfigured(false);
setTokenConfiguredChecked(true);
setWsState("idle");
setNotice({ tone: "info", message: "Gateway token cleared." });
}, [
onAuthIdentityChanged,
setNotice,
setTokenConfigured,
setTokenConfiguredChecked,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { ConnectorsPage } from "./ConnectorsPage";
import type { useConnectorsController } from "./useConnectorsController";
import { fixturePolicyResponse } from "../../glass/execass/fixtures";
import { DEFAULT_OPSUX_RUNTIME_CONFIG } from "../../lib/opsUxConfig";
import type { ExecassPolicyController } from "../execassPolicy/useExecassPolicyController";
import type { SetupSurfaceProps } from "../setup/SetupControls";

vi.mock("../../lib/api", () => ({
Expand Down Expand Up @@ -124,10 +126,33 @@ function stubSetupSurface(): SetupSurfaceProps {
};
}

function stubPolicyController(
overrides: Partial<ExecassPolicyController> = {},
): ExecassPolicyController {
return {
phase: "loaded",
policy: fixturePolicyResponse(),
error: null,
draft: null,
conflict: false,
updateBusy: false,
refresh: vi.fn(async () => {}),
beginDraft: vi.fn(),
setDraftProfile: vi.fn(),
setDraftRule: vi.fn(),
setDraftChangeSummary: vi.fn(),
discardDraft: vi.fn(),
reconcileDraft: vi.fn(() => ({ ok: true as const, message: "rebased" })),
applyDraft: vi.fn(async () => ({ ok: true as const, message: "ok" })),
...overrides,
};
}

async function render(
controller: ConnectorsController,
activeRoomId: string | null,
setupSurface: SetupSurfaceProps = stubSetupSurface(),
policyController: ExecassPolicyController = stubPolicyController(),
) {
await act(async () => {
if (!root) {
Expand All @@ -139,6 +164,7 @@ async function render(
onOpenSimpleIntegrationWizard={() => {}}
activeRoomId={activeRoomId}
setupSurface={setupSurface}
policyController={policyController}
/>,
);
});
Expand Down Expand Up @@ -204,6 +230,75 @@ describe("ConnectorsPage Basement room seam", () => {
},
);

it("lands the Policy room on its distinct owner-language surface", async () => {
await render(stubController(), "policy");
expect(
container.querySelector('[data-testid="policy-room-page"]'),
).toBeTruthy();
expect(container.textContent).toContain("The deal");
expect(container.textContent).toContain("Autonomy profile");
// The connector tab strip belongs to the Connectors room only.
expect(container.querySelector(".mc-connectors-tab-bar")).toBeNull();
expect(container.textContent).not.toContain("Quick Setup");
});

it.each([
["disabled", { enabled: false, availability: "disabled" }],
["unsupported", { enabled: true, availability: "unsupported" }],
["error", { enabled: true, availability: "error" }],
["cold loading", { enabled: true, availability: "loading" }],
] as const)(
"keeps Policy available through a %s Connectors controller",
async (_label, override) => {
const controller = stubController({
...override,
installedConnectors: [],
});
await render(controller, "policy");

expect(
container.querySelector('[data-testid="policy-room-page"]'),
).not.toBeNull();
expect(container.textContent).toContain("Autonomy profile");
expect(container.textContent).not.toContain("Connectors are disabled");
expect(container.textContent).not.toContain("Connectors surface unavailable");
expect(container.textContent).not.toContain("Connectors failed to load");
expect(container.textContent).not.toContain("Loading Connectors");
},
);

it("keeps the user's connector tab across a Policy room visit and relands exactly", async () => {
const controller = stubController();
await render(controller, "connectors");
await act(async () => tabButton("Catalog")!.click());
expect(tabButton("Catalog")?.className).toContain("active");

await render(controller, "policy");
expect(
container.querySelector('[data-testid="policy-room-page"]'),
).toBeTruthy();

await render(controller, "connectors");
expect(tabButton("Registry")?.className).toContain("active");
});

it("offers the Policy pin only under the Policy room identity", async () => {
const controller = stubController();
await render(controller, "policy");
const policyPin = Array.from(container.querySelectorAll("button")).find(
(button) => button.getAttribute("aria-label") === "Pin Policy to Office",
);
expect(policyPin).toBeTruthy();

await render(controller, "connectors");
const stalePolicyPin = Array.from(
container.querySelectorAll("button"),
).find(
(button) => button.getAttribute("aria-label") === "Pin Policy to Office",
);
expect(stalePolicyPin).toBeUndefined();
});

it("keeps connector quick setup reachable inside the Connectors room", async () => {
await render(stubController(), "connectors");
await act(async () => tabButton("Setup")!.click());
Expand Down
Loading
Loading