Skip to content

Commit 2d99d98

Browse files
Merge pull request #117 from EmergentKnowledgeGroup/codex/glass-office-p5-policy
feat(glass): plain-language Policy room - the ninth and final Basement room
2 parents a49de31 + b11fa43 commit 2d99d98

24 files changed

Lines changed: 4252 additions & 66 deletions

apps/mission-control/e2e/mockGateway.mjs

Lines changed: 371 additions & 2 deletions
Large diffs are not rendered by default.

apps/mission-control/e2e/p5-policy-slice.spec.ts

Lines changed: 916 additions & 0 deletions
Large diffs are not rendered by default.

apps/mission-control/src/App.tsx

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { SimpleIntegrationWizard } from "./features/connectors/SimpleIntegration
3232
import type { SimpleIntegrationId } from "./features/connectors/simpleIntegrations";
3333
import { useConnectorsController } from "./features/connectors/useConnectorsController";
3434
import { useExecassOfficeController } from "./features/execassOffice/useExecassOfficeController";
35+
import { useExecassPolicyController } from "./features/execassPolicy/useExecassPolicyController";
3536
import type { SetupSurfaceProps } from "./features/setup/SetupControls";
3637
import { useGlassWindowController } from "./features/glassWindow/useGlassWindowController";
3738
import { findRoom, roomForTab } from "./glass/floors";
@@ -281,9 +282,13 @@ export default function App() {
281282
[addToast],
282283
);
283284

284-
const [boards, setBoards] = useState<BoardSummary[]>([]);
285-
const [agents, setAgents] = useState<Agent[]>([]);
286-
const [tokenConfiguredChecked, setTokenConfiguredChecked] = useState(false);
285+
const [boards, setBoards] = useState<BoardSummary[]>([]);
286+
const [agents, setAgents] = useState<Agent[]>([]);
287+
const [tokenConfiguredChecked, setTokenConfiguredChecked] = useState(false);
288+
const [authIdentityGeneration, setAuthIdentityGeneration] = useState(0);
289+
const markAuthIdentityChanged = useCallback(() => {
290+
setAuthIdentityGeneration((generation) => generation + 1);
291+
}, []);
287292
const [guidedTourOpen, setGuidedTourOpen] = useState(false);
288293
const [guidedTourStep, setGuidedTourStep] = useState(0);
289294
const [safeModeReason, setSafeModeReason] = useState<string | null>(null);
@@ -440,10 +445,11 @@ export default function App() {
440445
],
441446
[connectorsHubEnabled, memoryHubEnabled, runbookHubEnabled]
442447
);
443-
// Setup shares the Connectors render route, but it is the authority used to
444-
// turn Connectors back on. Keep that stable room independently available
445-
// when the optional Connectors product surface is disabled.
446-
const alwaysAvailableElevatorRooms = useMemo(() => ["setup"], []);
448+
// Setup and Policy share the Connectors render route, but Setup is the
449+
// authority used to turn Connectors back on and Policy is the owner's
450+
// autonomy surface. Keep both stable rooms independently available when
451+
// the optional Connectors product surface is disabled.
452+
const alwaysAvailableElevatorRooms = useMemo(() => ["setup", "policy"], []);
447453
const elevatorFloors = useResolvedElevator(
448454
availableTabs,
449455
alwaysAvailableElevatorRooms,
@@ -684,6 +690,16 @@ export default function App() {
684690
active: activeTab === "assistant",
685691
setNotice,
686692
});
693+
// The one App-owned policy controller. Its invalidation signal comes from
694+
// the Office controller's durable stream - never a second websocket.
695+
const policyController = useExecassPolicyController({
696+
settings,
697+
tokenConfigured,
698+
active: activeTab === "connectors" && resolvedActiveRoomId === "policy",
699+
authIdentityGeneration,
700+
policyInvalidationGeneration: officeController.policyInvalidationGeneration,
701+
setNotice,
702+
});
687703
const glassWindowController = useGlassWindowController({
688704
settings,
689705
tokenConfigured,
@@ -711,8 +727,9 @@ export default function App() {
711727
setSettings,
712728
setGatewayDraft,
713729
setTokenDraft,
714-
setTokenConfigured,
715-
setTokenConfiguredChecked,
730+
setTokenConfigured,
731+
setTokenConfiguredChecked,
732+
onAuthIdentityChanged: markAuthIdentityChanged,
716733
setHealthState,
717734
setWsState,
718735
setNotice,
@@ -903,7 +920,8 @@ export default function App() {
903920
if (
904921
activeTab === "connectors" &&
905922
!connectorsHubEnabled &&
906-
activeRoomId !== "setup"
923+
activeRoomId !== "setup" &&
924+
activeRoomId !== "policy"
907925
) {
908926
selectRoom("setup", elevatorFloors);
909927
setNotice({
@@ -1343,6 +1361,7 @@ export default function App() {
13431361
setNotice={setNotice}
13441362
usageChartsEnabled={usageChartsEnabled}
13451363
setupSurface={setupSurface}
1364+
policyController={policyController}
13461365
onOpenSimpleIntegrationWizard={openSimpleIntegrationWizard}
13471366
quickGuidesCollapsed={quickGuideState.collapsed}
13481367
quickGuideOpenTab={quickGuideState.openTab}

apps/mission-control/src/app/AppContent.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { type CockpitWidgetLayoutV2 } from "../features/cockpit/cockpitLayout";
2525
import { CockpitWidgetRenderer } from "../features/cockpit/CockpitWidgetRenderer";
2626
import { ConnectorsPage } from "../features/connectors/ConnectorsPage";
2727
import type { SetupSurfaceProps } from "../features/setup/SetupControls";
28+
import type { ExecassPolicyController } from "../features/execassPolicy/useExecassPolicyController";
2829
import type { SimpleIntegrationId } from "../features/connectors/simpleIntegrations";
2930
import { useConnectorsController } from "../features/connectors/useConnectorsController";
3031
import { EventsPage } from "../features/events/EventsPage";
@@ -98,6 +99,8 @@ interface AppContentProps {
9899
usageChartsEnabled: boolean;
99100
/** The one shared Setup authority; the Basement Setup room renders it. */
100101
setupSurface: SetupSurfaceProps;
102+
/** The one App-owned policy controller; the Basement Policy room renders it. */
103+
policyController: ExecassPolicyController;
101104
onOpenSimpleIntegrationWizard: (integrationId?: SimpleIntegrationId) => void;
102105
quickGuidesCollapsed: boolean;
103106
quickGuideOpenTab: HelpTab | null;
@@ -1045,6 +1048,7 @@ export function AppContent(props: AppContentProps) {
10451048
onOpenSimpleIntegrationWizard={props.onOpenSimpleIntegrationWizard}
10461049
activeRoomId={props.activeRoomId}
10471050
setupSurface={props.setupSurface}
1051+
policyController={props.policyController}
10481052
/>
10491053
</TabBoundaryPane>
10501054

apps/mission-control/src/app/useRuntimeConnectionController.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const refreshBoard = vi.fn<(boardId: string, runtimeSettings?: RuntimeConnection
9696
const loadMissionControlReadModels = vi.fn<(runtimeSettings?: RuntimeConnectionSettings) => Promise<void>>();
9797
const loadRunbookReadModels = vi.fn<(runtimeSettings?: RuntimeConnectionSettings) => Promise<void>>();
9898
const loadAgentMailReadModels = vi.fn<(runtimeSettings?: RuntimeConnectionSettings) => Promise<void>>();
99+
const onAuthIdentityChanged = vi.fn();
99100

100101
interface HarnessProps {
101102
initialGatewayUrl: string;
@@ -126,6 +127,7 @@ function Harness(props: HarnessProps) {
126127
setTokenDraft,
127128
setTokenConfigured,
128129
setTokenConfiguredChecked,
130+
onAuthIdentityChanged,
129131
setHealthState,
130132
setWsState,
131133
setNotice: pushNotice,
@@ -375,6 +377,7 @@ describe("useRuntimeConnectionController save truth", () => {
375377
});
376378
expect(snapshot.settings.gateway_url).toBe("http://10.0.0.9:18789/");
377379
expect(setGatewayTokenMock).toHaveBeenCalledWith("secret-token");
380+
expect(onAuthIdentityChanged).toHaveBeenCalledTimes(1);
378381
});
379382

380383
it("adopts an authoritative save into the one shared gateway draft", async () => {
@@ -397,6 +400,7 @@ describe("useRuntimeConnectionController save truth", () => {
397400
await controller.saveConnectionFromInputs(GATEWAY_URL, "");
398401
});
399402
expect(setGatewayTokenMock).not.toHaveBeenCalled();
403+
expect(onAuthIdentityChanged).not.toHaveBeenCalled();
400404
expect(healthMock).toHaveBeenCalledTimes(1);
401405
expect(notices).toEqual([
402406
{ tone: "info", message: "Connection settings saved." },
@@ -410,6 +414,7 @@ describe("useRuntimeConnectionController save truth", () => {
410414
await controller.saveConnectionFromInputs(GATEWAY_URL, "");
411415
});
412416
expect(healthMock).not.toHaveBeenCalled();
417+
expect(onAuthIdentityChanged).not.toHaveBeenCalled();
413418
expect(notices).toEqual([]);
414419
});
415420

@@ -420,6 +425,7 @@ describe("useRuntimeConnectionController save truth", () => {
420425
await controller.saveConnection();
421426
});
422427
expect(snapshot.tokenDraft).toBe("typed-secret");
428+
expect(onAuthIdentityChanged).not.toHaveBeenCalled();
423429
expect(notices).toHaveLength(1);
424430
expect(notices[0].tone).toBe("critical");
425431
expect(notices[0].message).toContain("Connection save failed");
@@ -470,6 +476,7 @@ describe("useRuntimeConnectionController save truth", () => {
470476
expect(notices[0].message).toContain("Agent roster unavailable");
471477
expect(notices[0].message).not.toContain("Connection settings saved");
472478
expect(snapshot.tokenDraft).toBe("typed-secret");
479+
expect(onAuthIdentityChanged).toHaveBeenCalledTimes(1);
473480
});
474481
});
475482

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

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

apps/mission-control/src/app/useRuntimeConnectionController.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ interface UseRuntimeConnectionControllerOptions {
2626
setTokenDraft: Dispatch<SetStateAction<string>>;
2727
setTokenConfigured: Dispatch<SetStateAction<boolean>>;
2828
setTokenConfiguredChecked: Dispatch<SetStateAction<boolean>>;
29+
/** Invalidates consumers whose truth is scoped to the secure token identity. */
30+
onAuthIdentityChanged: () => void;
2931
setHealthState: Dispatch<SetStateAction<string>>;
3032
setWsState: Dispatch<SetStateAction<WsLifecycleState>>;
3133
setNotice: NotifyFn;
@@ -61,6 +63,7 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
6163
setTokenDraft,
6264
setTokenConfigured,
6365
setTokenConfiguredChecked,
66+
onAuthIdentityChanged,
6467
setHealthState,
6568
setWsState,
6669
setNotice,
@@ -302,6 +305,7 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
302305
if (nextToken) {
303306
await setGatewayToken(nextToken);
304307
tokenTruthGenerationRef.current += 1;
308+
onAuthIdentityChanged();
305309
}
306310

307311
const hasToken = await isGatewayTokenConfigured();
@@ -327,6 +331,7 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
327331
},
328332
[
329333
loadBaseline,
334+
onAuthIdentityChanged,
330335
setGatewayDraft,
331336
setNotice,
332337
setSettings,
@@ -363,11 +368,13 @@ export function useRuntimeConnectionController(options: UseRuntimeConnectionCont
363368
connectionActionLockRef.current = false;
364369
}
365370
tokenTruthGenerationRef.current += 1;
371+
onAuthIdentityChanged();
366372
setTokenConfigured(false);
367373
setTokenConfiguredChecked(true);
368374
setWsState("idle");
369375
setNotice({ tone: "info", message: "Gateway token cleared." });
370376
}, [
377+
onAuthIdentityChanged,
371378
setNotice,
372379
setTokenConfigured,
373380
setTokenConfiguredChecked,

apps/mission-control/src/features/connectors/ConnectorsPage.test.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
66

77
import { ConnectorsPage } from "./ConnectorsPage";
88
import type { useConnectorsController } from "./useConnectorsController";
9+
import { fixturePolicyResponse } from "../../glass/execass/fixtures";
910
import { DEFAULT_OPSUX_RUNTIME_CONFIG } from "../../lib/opsUxConfig";
11+
import type { ExecassPolicyController } from "../execassPolicy/useExecassPolicyController";
1012
import type { SetupSurfaceProps } from "../setup/SetupControls";
1113

1214
vi.mock("../../lib/api", () => ({
@@ -124,10 +126,33 @@ function stubSetupSurface(): SetupSurfaceProps {
124126
};
125127
}
126128

129+
function stubPolicyController(
130+
overrides: Partial<ExecassPolicyController> = {},
131+
): ExecassPolicyController {
132+
return {
133+
phase: "loaded",
134+
policy: fixturePolicyResponse(),
135+
error: null,
136+
draft: null,
137+
conflict: false,
138+
updateBusy: false,
139+
refresh: vi.fn(async () => {}),
140+
beginDraft: vi.fn(),
141+
setDraftProfile: vi.fn(),
142+
setDraftRule: vi.fn(),
143+
setDraftChangeSummary: vi.fn(),
144+
discardDraft: vi.fn(),
145+
reconcileDraft: vi.fn(() => ({ ok: true as const, message: "rebased" })),
146+
applyDraft: vi.fn(async () => ({ ok: true as const, message: "ok" })),
147+
...overrides,
148+
};
149+
}
150+
127151
async function render(
128152
controller: ConnectorsController,
129153
activeRoomId: string | null,
130154
setupSurface: SetupSurfaceProps = stubSetupSurface(),
155+
policyController: ExecassPolicyController = stubPolicyController(),
131156
) {
132157
await act(async () => {
133158
if (!root) {
@@ -139,6 +164,7 @@ async function render(
139164
onOpenSimpleIntegrationWizard={() => {}}
140165
activeRoomId={activeRoomId}
141166
setupSurface={setupSurface}
167+
policyController={policyController}
142168
/>,
143169
);
144170
});
@@ -204,6 +230,75 @@ describe("ConnectorsPage Basement room seam", () => {
204230
},
205231
);
206232

233+
it("lands the Policy room on its distinct owner-language surface", async () => {
234+
await render(stubController(), "policy");
235+
expect(
236+
container.querySelector('[data-testid="policy-room-page"]'),
237+
).toBeTruthy();
238+
expect(container.textContent).toContain("The deal");
239+
expect(container.textContent).toContain("Autonomy profile");
240+
// The connector tab strip belongs to the Connectors room only.
241+
expect(container.querySelector(".mc-connectors-tab-bar")).toBeNull();
242+
expect(container.textContent).not.toContain("Quick Setup");
243+
});
244+
245+
it.each([
246+
["disabled", { enabled: false, availability: "disabled" }],
247+
["unsupported", { enabled: true, availability: "unsupported" }],
248+
["error", { enabled: true, availability: "error" }],
249+
["cold loading", { enabled: true, availability: "loading" }],
250+
] as const)(
251+
"keeps Policy available through a %s Connectors controller",
252+
async (_label, override) => {
253+
const controller = stubController({
254+
...override,
255+
installedConnectors: [],
256+
});
257+
await render(controller, "policy");
258+
259+
expect(
260+
container.querySelector('[data-testid="policy-room-page"]'),
261+
).not.toBeNull();
262+
expect(container.textContent).toContain("Autonomy profile");
263+
expect(container.textContent).not.toContain("Connectors are disabled");
264+
expect(container.textContent).not.toContain("Connectors surface unavailable");
265+
expect(container.textContent).not.toContain("Connectors failed to load");
266+
expect(container.textContent).not.toContain("Loading Connectors");
267+
},
268+
);
269+
270+
it("keeps the user's connector tab across a Policy room visit and relands exactly", async () => {
271+
const controller = stubController();
272+
await render(controller, "connectors");
273+
await act(async () => tabButton("Catalog")!.click());
274+
expect(tabButton("Catalog")?.className).toContain("active");
275+
276+
await render(controller, "policy");
277+
expect(
278+
container.querySelector('[data-testid="policy-room-page"]'),
279+
).toBeTruthy();
280+
281+
await render(controller, "connectors");
282+
expect(tabButton("Registry")?.className).toContain("active");
283+
});
284+
285+
it("offers the Policy pin only under the Policy room identity", async () => {
286+
const controller = stubController();
287+
await render(controller, "policy");
288+
const policyPin = Array.from(container.querySelectorAll("button")).find(
289+
(button) => button.getAttribute("aria-label") === "Pin Policy to Office",
290+
);
291+
expect(policyPin).toBeTruthy();
292+
293+
await render(controller, "connectors");
294+
const stalePolicyPin = Array.from(
295+
container.querySelectorAll("button"),
296+
).find(
297+
(button) => button.getAttribute("aria-label") === "Pin Policy to Office",
298+
);
299+
expect(stalePolicyPin).toBeUndefined();
300+
});
301+
207302
it("keeps connector quick setup reachable inside the Connectors room", async () => {
208303
await render(stubController(), "connectors");
209304
await act(async () => tabButton("Setup")!.click());

0 commit comments

Comments
 (0)