diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx
index 97319feb15..42292dccde 100644
--- a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx
@@ -359,19 +359,24 @@ describe("TaskDetailModal oversight controls", () => {
});
/*
- * FNXC:PlannerOversight 2026-07-04-19:45:
- * FN-7521 Surface Enumeration requirement: cover the oversight quick-controls
- * cluster (`.detail-meta-inline-controls`) at both desktop AND mobile
- * (@media max-width: 768px) breakpoints. TaskDetailModal.tsx has no JS-side
- * isMobile/matchMedia branch for this cluster — TaskDetailModal.css only
- * wraps `.detail-meta-inline-controls` via a `@media (max-width: 768px)` CSS
- * rule (flex-wrap, no conditional DOM). This suite proves the same controls
- * are present (no leftover/missing shell) at a narrow `window.innerWidth`,
- * guarding against a future mobile-only regression (FN-6115→FN-6123
- * precedent in AGENTS.md: an affordance removal left an empty control shell
- * on mobile only, because only the desktop surface was checked).
+ * FNXC:PlannerOversight 2026-07-04-20:30 (FN-7558):
+ * FN-7521's original mobile suite asserted the oversight quick-controls
+ * cluster rendered its FLAT inline testids at a narrow `window.innerWidth`,
+ * matching the pre-FN-7545 DOM (CSS-only `@media (max-width: 768px)` wrap,
+ * no conditional mount). FN-7545 then collapsed that cluster's action
+ * controls (level select / nudge / stop / explain) into a mobile overflow
+ * menu: at `window.innerWidth <= OVERSIGHT_MENU_MOBILE_BREAKPOINT` (768) the
+ * mount-time `updateOversightMenuMobile()` effect flips `isOversightMenuMobile`
+ * to true, so those controls now render INSIDE a closed `detail-oversight-menu`
+ * behind a `detail-oversight-menu-trigger` button instead of inline — the old
+ * flat queries no longer find them. This suite is corrected to drive the real
+ * FN-7545 mobile affordance: open the trigger, then query the menu items. It
+ * still asserts the same invariants FN-7521 required — select-writes-on-change,
+ * enabled nudge/stop/explain when the overseer is active, and no leftover
+ * empty-menu shell for the off+inactive default — just through the shipped
+ * mobile surface. The desktop branch (first `describe` above) is unchanged.
*/
-describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, @media max-width: 768px)", () => {
+describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-7545 overflow menu)", () => {
const originalInnerWidth = window.innerWidth;
beforeEach(async () => {
@@ -383,6 +388,10 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, @me
vi.mocked(api.nudgeOverseer).mockResolvedValue({ applied: false, reason: "oversight-off" });
vi.mocked(api.stopOverseer).mockResolvedValue({ applied: true, reason: "stopped" });
vi.mocked(api.explainOverseer).mockResolvedValue({ snapshot: null });
+ // Setting innerWidth before render is sufficient: TaskDetailModal's mount
+ // effect calls `updateOversightMenuMobile()` once on mount, reading
+ // `window.innerWidth` synchronously, which flips `isOversightMenuMobile`
+ // before the first paint the tests observe.
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
});
@@ -390,7 +399,13 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, @me
Object.defineProperty(window, "innerWidth", { value: originalInnerWidth, configurable: true });
});
- it("still renders the quick level-change select at a 375px mobile viewport and writes on change", async () => {
+ async function openOversightMenu() {
+ const trigger = await screen.findByTestId("detail-oversight-menu-trigger");
+ fireEvent.click(trigger);
+ return trigger;
+ }
+
+ it("still renders the quick level-change select behind the mobile overflow menu and writes on change", async () => {
const api = await import("../../api");
const mockUpdate = vi.fn().mockResolvedValue(makeTask({ id: "FN-201", plannerOversightLevel: "steer" }));
vi.mocked(api.updateTask).mockImplementation(mockUpdate as any);
@@ -407,8 +422,10 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, @me
/>,
);
+ await openOversightMenu();
+
const select = await screen.findByTestId("detail-oversight-level-select");
- expect(select).toBeTruthy();
+ expect((select as HTMLSelectElement).value).toBe("observe");
fireEvent.change(select, { target: { value: "steer" } });
await waitFor(() => {
@@ -416,7 +433,7 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, @me
});
});
- it("still renders enabled nudge/stop/explain controls at a 375px mobile viewport when the overseer is actively watching", async () => {
+ it("still renders enabled nudge/stop/explain controls behind the mobile overflow menu when the overseer is actively watching", async () => {
render(
,
);
+ await openOversightMenu();
+
expect(await screen.findByTestId("detail-overseer-nudge")).not.toBeDisabled();
expect(await screen.findByTestId("detail-overseer-stop")).toBeTruthy();
expect(await screen.findByTestId("detail-overseer-explain")).toBeTruthy();
});
- it("still renders no oversight-control leftover shell at a 375px mobile viewport for the off+inactive default case", async () => {
+ it("still renders no oversight-control leftover shell behind the mobile overflow menu for the off+inactive default case", async () => {
render(
,
);
+ // The overflow-menu trigger itself always renders (an operator must still
+ // be able to opt IN to oversight), but the menu must not carry an empty
+ // nudge/stop/explain shell for the common off+inactive default.
+ await openOversightMenu();
+
await screen.findByTestId("detail-oversight-level-select");
expect(screen.queryByTestId("detail-overseer-nudge")).not.toBeInTheDocument();
expect(screen.queryByTestId("detail-overseer-stop")).not.toBeInTheDocument();