FN-7604: collapse overseer/oversight controls into a single dropdown across surfaces

Unifies the previously mobile-only Oversight overflow menu into a single, always-present dropdown that replaces the scattered desktop oversight buttons and the separate mobile affordance.

- Replace discrete desktop oversight action buttons in TaskDetailModal's footer with one universal "Oversight actions" dropdown trigger, reusing the menu across desktop and mobile breakpoints.
- Simplify TaskDetailModal.tsx footer rendering logic, removing now-redundant responsive branching for oversight controls.
- Update TaskDetailModal.css to drop the old mobile-only oversight-overflow styles and support the unified dropdown across breakpoints.
- Update definition-actions, oversight-controls, oversight-mobile, rendering, and responsive-and-dependencies tests to assert the single dropdown behavior and disambiguate the exact "Actions" button query from the new "Oversight actions" aria-label.
- Refresh docs/dashboard-guide.md to describe the unified oversight dropdown UX.

Files changed:
 docs/dashboard-guide.md                            |  14 +-
 packages/dashboard/app/components/TaskDetailModal.css   |  89 +++++------
 packages/dashboard/app/components/TaskDetailModal.tsx   | 177 +++------------------
 packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx    |  70 ++++----
 packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx    | 130 ++++++++++-----
 packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx      |  42 +++--
 packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx   |  27 ++--
 packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx |  57 ++++---
 8 files changed, 278 insertions(+), 328 deletions(-)

Fusion-Task-Id: FN-7604
Fusion-Task-Lineage: afbb7573-d654-48db-a9ac-aecbd8e22e46
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-05 19:39:18 -07:00
parent 8fb7d51ae9
commit f7dfcb3b09
8 changed files with 276 additions and 326 deletions

View File

@@ -196,12 +196,16 @@ Features:
<!-- FNXC:PlannerOversight 2026-07-04-19:10: FN-7539 fix — the badge was rendering on virtually every card because the schema default `autonomous` tier was treated as "meaningfully configured". Narrowed the gate so an inherited (no per-task-override, no non-default workflow tier) `autonomous` level renders no badge; only an explicit per-task override or a resolved workflow/effective tier that is not the plain inherited default surfaces the badge. -->
- Task cards show a read-only **oversight-level badge** (`Observe`, `Steer`, or `Auto-recovery`) in the meta-badges cluster reflecting the effective planner-oversight level, but only when oversight is *meaningfully configured* — an explicit per-task override (including an explicit `autonomous` override), or a resolved workflow/effective tier of `observe`/`steer` (`data-testid="card-oversight-badge"`). A card that merely **inherits** the schema default `autonomous` tier (no per-task override, no non-default workflow tier) renders no badge and no empty `.card-meta-badges` shell. The badge is also absent when the effective level is explicitly "off", **and** while an inherited (no per-task-override) workflow tier is still being resolved (in flight or not yet fetched) — it never shows a guessed default during that window.
<!-- FNXC:PlannerOversight 2026-07-04-HH:MM: FN-7542 removed the FN-7516 active-overseer-state ("Executor") indicator described above as unwanted per-card noise — it fired on nearly every in-progress card. The oversight-level badge documented above is unaffected. -->
<!-- FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 adds interactive task-detail planner-overseer controls (quick level change, manual nudge, stop oversight, explain current action) alongside the FN-7516 read-only card badges above. These controls live ONLY in TaskDetailModal, not TaskCard. -->
- The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) adds four planner-overseer controls: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes.
<!-- FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 adds interactive task-detail planner-overseer controls (quick level change, manual nudge, stop oversight, explain current action) alongside the FN-7516 read-only card badges above. These controls live ONLY in TaskDetailModal, not TaskCard.
FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 collapses the desktop inline cluster (documented below through FN-7545/FN-7546) into the single universal overflow-menu dropdown that FN-7545 originally built for mobile only — the dropdown is now the ONE canonical surface on every viewport, desktop included. -->
- The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) exposes planner-overseer controls behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` icon, `aria-haspopup="menu"`) on every viewport, desktop and mobile alike. Clicking the trigger opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) containing: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes. The trigger itself is withheld entirely when none of the menu's controls would render (no empty-shell overflow button for the unresolved-default case).
<!-- FNXC:PlannerOversight 2026-07-04-20:30: FN-7546 clarifies the cluster above — operators reported the buttons were unlabeled and looked inert, with only a hover title explaining why. Adds a visible group label and an always-visible disabled-reason line, and makes Explain always openable since it never mutates anything. -->
- The FN-7517 controls above carry a visible, non-interactive **`"Overseer controls"` group label** (`data-testid="detail-oversight-controls-label"`) so Nudge/Stop/Explain read as an identifiable cluster rather than unlabeled chips; the label is gated by the same `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff` condition as the buttons, so it never renders an empty shell when oversight is Off/unresolved. When **Nudge** is disabled, an always-visible helper line (`data-testid="detail-overseer-nudge-disabled-reason"`) states the reason in-DOM (mirroring the existing hover `title`) instead of relying on a mouse-hover tooltip alone. **Explain** is read-only and non-mutating, so it is never disabled purely because the overseer is inactive — clicking it always opens/closes the panel, which shows the overseer's live state when watching or an informative "not currently watching this task" message otherwise. Nudge's mutating enablement rule (`canNudgeOverseer`, including the human-control suppression cases) and Stop's confirmation dialog are unchanged.
<!-- FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 collapses the above controls into a mobile overflow menu below the 768px breakpoint. -->
- On mobile (viewport ≤ 768px), the level-select/nudge/stop/explain controls above collapse behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` icon, `aria-haspopup="menu"`) that opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) listing the same controls as full-width, comfortably tappable `role="menuitem"` entries with the SAME testids and enablement/visibility rules as the desktop inline controls. The trigger itself is withheld entirely when none of those controls would render (no empty-shell overflow button for the oversight-off + overseer-inactive default). Desktop (>768px) keeps the inline cluster unchanged. Opening the menu presents exactly one surface: menu-open auto-focus lands on the first actionable button menuitem, never the native level `<select>`, so the OS option picker never auto-opens on top of the custom popover (FN-7562).
- The controls inside the Oversight menu carry a visible, non-interactive **`"Overseer controls"` group label** (`data-testid="detail-oversight-controls-label"`) so Nudge/Stop/Explain read as an identifiable cluster rather than unlabeled entries; the label is gated by the same `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff` condition as the buttons, so it never renders when oversight is Off/unresolved (opening the menu in that state shows only the level select). When **Nudge** is disabled, an always-visible helper line (`data-testid="detail-overseer-nudge-disabled-reason"`) states the reason in-DOM (mirroring the existing hover `title`) instead of relying on a mouse-hover tooltip alone. **Explain** is read-only and non-mutating, so it is never disabled purely because the overseer is inactive — clicking it always opens/closes the panel, which shows the overseer's live state when watching or an informative "not currently watching this task" message otherwise. Nudge's mutating enablement rule (`canNudgeOverseer`, including the human-control suppression cases) and Stop's confirmation dialog are unchanged.
<!-- FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 originally introduced this overflow menu for mobile only, collapsing the level-select/nudge/stop/explain controls behind the trigger below the 768px breakpoint while desktop kept an inline cluster.
FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 removed that desktop-inline branch. The overflow-menu trigger + popover described above is now the SINGLE canonical surface at EVERY viewport — there is no longer a desktop/mobile split. -->
- The Oversight overflow-menu trigger and its popover render identically at every viewport (desktop and mobile): the same testids, the same enablement/visibility rules, and the same `role="menu"`/`role="menuitem"` popover semantics. Menu-open auto-focus lands on the first actionable button menuitem, never the native level `<select>`, so the OS option picker never auto-opens on top of the custom popover (FN-7562).
<!-- FNXC:PlannerOversight 2026-07-04-18:00: FN-7519 adds a read-only Intervention Timeline. FN-7571 (2026-07-04-19:00) relocates it from an inline mount below the FN-7517 controls into the task-detail Activity view dropdown as a fourth "Interventions" segment, alongside Live/Feed/Raw. -->
- The task detail modal's **Activity** tab view dropdown (Live/Feed/Raw) gains a fourth **Interventions** option, shown only when planner oversight is active for the task (same gate as the former inline mount: `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff`). Selecting it renders the **Intervention Timeline** (`data-testid="planner-intervention-timeline"`) inside the Activity panel, listing every recorded planner-overseer intervention for the task, newest-first: watched stage, reason, action taken, outcome (with a `.status-dot` indicator using semantic outcome tokens), an attempt count/limit badge (only when both are present), and source links (agent log / review comment / failed check / merge error / PR state / generic URL). It renders a calm "No planner interventions yet" empty state rather than an empty shell when there are none. When oversight is off or unresolved, the Interventions option is absent from the dropdown entirely (no leftover empty segment), and if it was previously selected the view falls back to Live rather than leaving a blank panel. Entries are read via `GET /tasks/:id/overseer/interventions`, which assembles them from the existing run-audit store under the `overseer:intervention` mutation type (`recordPlannerIntervention`/`getPlannerInterventionTimeline` in `@fusion/core`). This is a pure read surface — FN-7520 wires the actual intervention-producing call-sites.
- Task detail surfaces show the selected/effective workflow identity near the task's workflow controls so individual cards remain understandable when Board is in **All workflows** or another aggregate/mixed context.

View File

@@ -322,26 +322,23 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P
/*
FNXC:PlannerOversight 2026-07-04-17:00:
FN-7517: the oversight-controls cluster (priority + execution-mode + the
new oversight-level/nudge/stop/explain controls) must wrap without
overflowing the mobile modal width rather than forcing horizontal scroll.
Oversight trigger) must wrap without overflowing the mobile modal width
rather than forcing horizontal scroll.
FNXC:PlannerOversight 2026-07-04-19:00:
FN-7545: the level-select/nudge/stop/explain controls now collapse behind a
single `.detail-oversight-menu-trigger` button below this breakpoint (JS
`isOversightMenuMobile` swap in TaskDetailModal.tsx, not a CSS hide), so the
cluster only ever needs to fit priority + execution-mode + the trigger on
one row; `flex-wrap: wrap` remains as a safety fallback so the cluster never
forces horizontal scroll if it still overflows at very narrow widths.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604: the level-select/nudge/stop/explain controls collapse behind the
single `.detail-oversight-menu-trigger` button at EVERY viewport now (no
longer a mobile-only branch), so the cluster only ever needs to fit
priority + execution-mode + the trigger on one row; `flex-wrap: wrap`
remains as a safety fallback so the cluster never forces horizontal scroll
if it still overflows at very narrow widths. The `.detail-oversight-menu`
positioning (`right: 0`) is already declared once on the base rule above
and applies at every width, so no mobile-only override is needed here.
*/
.detail-meta-inline-controls {
flex-wrap: wrap;
}
.detail-oversight-menu {
right: 0;
left: auto;
}
.detail-provenance {
align-items: center;
}
@@ -542,36 +539,20 @@ uppercases its content; this only removes a duplicated, drift-prone override.
/*
FNXC:PlannerOversight 2026-07-04-17:00:
FN-7517 task-detail planner-overseer controls (quick level-change chip,
FN-7517 task-detail planner-overseer controls (quick level-change select,
manual nudge / stop / explain buttons, explain panel). Reuses the same
`.card-oversight-badge--*` status-color modifiers FN-7516's TaskCard badge
already declares (see TaskCard.css) so oversight-level color stays a single
semantic source, and the same `--detail-priority-control-min-height` chip
height token as the priority/execution-mode controls above so the cluster
renders at a uniform height.
semantic source.
FNXC:TaskDetail 2026-07-05-00:00:
FN-7585 — like `.detail-priority-chip`, override `.card-oversight-badge`'s
transparent border with the shared `--btn-border-width`/`--border`/
`--detail-control-border-radius` trio so every oversight level (including
`--off`, which has a neutral tint) renders the same bordered box as Priority
and Execution-mode. The mobile `.detail-oversight-menu-trigger` swap below
gets the identical trio so both oversight variants match.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — the desktop-only `.detail-oversight-chip` wrapper (the inline
quick-level-change chip rendered outside the overflow menu) was removed along
with the desktop inline branch it styled; the level select now renders only
inside `.detail-oversight-menu-item--select` (see below). `.detail-oversight-select`
itself stays — the dropdown menu item reuses this class for the native
`<select>` styling.
*/
.detail-oversight-chip {
gap: var(--space-xs);
min-height: var(--detail-priority-control-min-height);
padding-block: var(--space-xs);
box-sizing: border-box;
border-width: var(--btn-border-width);
border-color: var(--border);
border-radius: var(--detail-control-border-radius);
}
.detail-oversight-chip--saving {
opacity: 0.75;
}
.detail-oversight-select {
border: 0;
background: transparent;
@@ -694,12 +675,16 @@ than a one-off style.
/*
FNXC:PlannerOversight 2026-07-04-19:00:
FN-7545 — mobile overflow menu for the oversight action controls. Reuses the
same `.detail-move-dropdown` / `.detail-move-menu` popover tokens/pattern
FN-7545 — overflow menu for the oversight action controls. Reuses the same
`.detail-move-dropdown` / `.detail-move-menu` popover tokens/pattern
(position/border/radius/shadow) so the two overflow menus in this modal look
and behave consistently. The popover opens DOWNWARD (`top: calc(100% + ...)`)
since the trigger lives in the header cluster rather than the footer, unlike
`.detail-move-menu` which opens upward from the footer.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — this overflow menu is now the single universal surface at every
viewport (desktop and mobile); it is no longer mobile-only.
*/
.detail-oversight-menu-dropdown {
position: relative;
@@ -714,10 +699,13 @@ since the trigger lives in the header cluster rather than the footer, unlike
box-sizing: border-box;
/*
FNXC:TaskDetail 2026-07-05-00:00:
FN-7585 — mobile swap-in for `.detail-oversight-chip` (JS `isOversightMenuMobile`
swap in TaskDetailModal.tsx). Pin the same shared border/radius trio so the
mobile overflow-trigger variant matches the desktop chip and the
Priority/Execution-mode controls, per the FN-7585 surface enumeration.
FN-7585 — pin the same shared border/radius trio as Priority/Execution-mode
so the Oversight trigger matches the rest of the control cluster.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — this trigger is now the single universal surface at every
viewport (the desktop-only `.detail-oversight-chip` it used to swap in for
was removed).
*/
border-width: var(--btn-border-width);
border-color: var(--border);
@@ -809,10 +797,9 @@ button.detail-oversight-menu-item svg {
/*
FNXC:PlannerOversight 2026-07-04-20:30:
FN-7546 — the same group label / disabled-reason helper as the desktop
cluster above, adapted to the mobile overflow-menu's padded list-item layout
so they read as ordinary (non-interactive) menu rows rather than floating
text.
FN-7546 — the same group label / disabled-reason helper base rules above,
adapted to the overflow-menu's padded list-item layout so they read as
ordinary (non-interactive) menu rows rather than floating text.
*/
.detail-oversight-menu .detail-oversight-controls-label,
.detail-oversight-menu .detail-oversight-controls-helper {

View File

@@ -86,9 +86,8 @@ const ACTIVITY_VIEW_MENU_MIN_WIDTH = 160;
const ACTIVITY_VIEW_MENU_MIN_HEIGHT = 120;
const ACTIVITY_VIEW_MENU_MAX_HEIGHT = 320;
const ACTIVITY_VIEW_MENU_OPEN_VIEWPORT_GUARD_MS = 350;
// FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 — mobile breakpoint for collapsing the oversight action cluster into an overflow menu; matches the `@media (max-width: 768px)` breakpoint used across TaskDetailModal.css.
const OVERSIGHT_MENU_MOBILE_BREAKPOINT = 768;
// FNXC:TaskDetailSwipeBack 2026-07-05-12:30: FN-7587 — mobile breakpoint gating the presentation-only predictive-back slide/fade transition on the modal/list/nested task-detail surface; matches OVERSIGHT_MENU_MOBILE_BREAKPOINT/the `@media (max-width: 768px)` convention already used in this file.
// FNXC:TaskDetailSwipeBack 2026-07-05-12:30: FN-7587 — mobile breakpoint gating the presentation-only predictive-back slide/fade transition on the modal/list/nested task-detail surface; matches the `@media (max-width: 768px)` convention already used in this file.
// FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 — the OVERSIGHT_MENU_MOBILE_BREAKPOINT constant (formerly used to branch the oversight controls between an inline cluster and this overflow menu) was removed; the overflow-menu dropdown is now the single universal surface at every viewport, so no breakpoint gates it.
const TASK_DETAIL_MOBILE_TRANSITION_BREAKPOINT = 768;
type ActivityViewMenuPosition = {
@@ -1070,15 +1069,16 @@ export function TaskDetailContent({
const [overseerExplainSnapshot, setOverseerExplainSnapshot] = useState<PlannerOverseerRuntimeSnapshot | null>(null);
/*
FNXC:PlannerOversight 2026-07-04-19:00:
FN-7545 — collapse the oversight action controls into a mobile overflow
menu so the detail control bar fits narrow viewports; desktop keeps the
inline cluster; menu never renders an empty shell when oversight is
off/inactive. `isOversightMenuMobile` mirrors the `DocumentsView` local
`isMobile` resize-listener pattern (defaults false so JSDOM/unit tests keep
exercising the desktop inline branch unless a test explicitly narrows the
viewport).
FN-7545 — collapse the oversight action controls into an overflow menu so
the detail control bar fits narrow viewports; menu never renders an empty
shell when oversight is off/inactive.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — the overflow menu is now the single universal surface at every
viewport (desktop and mobile); the `isOversightMenuMobile` resize-driven
branch selector was removed since there is no longer a second branch to
select between.
*/
const [isOversightMenuMobile, setIsOversightMenuMobile] = useState(false);
const [showOversightMenu, setShowOversightMenu] = useState(false);
const oversightMenuRef = useRef<HTMLDivElement>(null);
const oversightMenuButtonRef = useRef<HTMLButtonElement>(null);
@@ -1503,26 +1503,6 @@ export function TaskDetailContent({
return () => document.removeEventListener("keydown", handleKeyDown);
}, [showMoveMenu, showActionsMenu, showActivityViewMenu, showOversightMenu]);
// FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 — track the mobile breakpoint locally (mirrors DocumentsView's isMobile resize-listener pattern) so the oversight action cluster can collapse into an overflow menu on narrow viewports while desktop keeps the inline layout.
useEffect(() => {
const updateOversightMenuMobile = () => {
setIsOversightMenuMobile(window.innerWidth <= OVERSIGHT_MENU_MOBILE_BREAKPOINT);
};
updateOversightMenuMobile();
window.addEventListener("resize", updateOversightMenuMobile);
return () => {
window.removeEventListener("resize", updateOversightMenuMobile);
};
}, []);
useEffect(() => {
if (!isOversightMenuMobile) {
setShowOversightMenu(false);
}
}, [isOversightMenuMobile]);
// Reset spec edit state when task changes
useEffect(() => {
setIsEditingSpec(false);
@@ -3983,16 +3963,19 @@ export function TaskDetailContent({
FNXC:PlannerOversight 2026-07-04-19:00:
FN-7545 — collapse the oversight action controls into a mobile
overflow menu so the detail control bar fits narrow viewports;
desktop keeps the inline cluster; menu never renders an empty
shell when oversight is off/inactive. Both branches below share
the SAME enablement gates (`hasTaskOversightOverride`,
menu never renders an empty shell when oversight is off/inactive.
Shares the SAME enablement gates (`hasTaskOversightOverride`,
`workflowOversightResolved`, `oversightIsOff`, `showStopOverseer`,
`canNudgeOverseer`, `canExplainOverseer`) and the SAME handlers
— the mobile branch only changes where the controls render, never
their guard logic.
as before.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — the oversight action controls (level select / nudge /
stop / explain) render ONLY behind the "Oversight" overflow-menu
trigger on every surface (desktop and mobile); the former desktop
inline cluster was removed for a consistent, simpler control bar.
*/}
{isOversightMenuMobile ? (
(hasTaskOversightOverride || workflowOversightResolved) && (
{(hasTaskOversightOverride || workflowOversightResolved) && (
<div className="detail-oversight-menu-dropdown" ref={oversightMenuRef}>
<button
type="button"
@@ -4101,119 +4084,7 @@ export function TaskDetailContent({
</div>
)}
</div>
)
) : (
<>
{(hasTaskOversightOverride || workflowOversightResolved) && (
<label
className={`card-oversight-badge card-oversight-badge--${effectiveOversightLevel} detail-oversight-chip ${isSavingOversightLevel ? "detail-oversight-chip--saving" : ""}`}
>
<span>{t("taskDetail.oversight.label", "Oversight:")}</span>
<select
className="detail-oversight-select"
data-testid="detail-oversight-level-select"
value={hasTaskOversightOverride ? (task.plannerOversightLevel as string) : "__inherit__"}
onChange={(event) => {
void handleOversightLevelChange(event.target.value);
}}
disabled={isSavingOversightLevel}
aria-label={t("taskDetail.oversight.ariaLabel", "Planner oversight level")}
>
<option value="__inherit__">
{t("taskDetail.oversight.inherit", "Inherit ({{level}})", { level: OVERSIGHT_LEVEL_LABEL[effectiveOversightLevel] })}
</option>
{PLANNER_OVERSIGHT_LEVELS.map((levelOption) => (
<option key={levelOption} value={levelOption}>
{OVERSIGHT_LEVEL_LABEL[levelOption]}
</option>
))}
</select>
</label>
)}
{/*
FNXC:PlannerOversight 2026-07-04-17:00:
FN-7517 manual nudge / stop oversight / explain current action
controls. Disabled (with an accessible aria-label reason) rather
than hidden for nudge/explain when the overseer is off/inactive
so operators understand WHY the control is inert instead of it
silently vanishing; stop is hidden once oversight is already off
(nothing left to stop) per the PROMPT's enablement rule, avoiding
an always-on empty shell for the common oversight-off default.
FNXC:PlannerOversight 2026-07-04-20:30:
FN-7546 — operators reported these controls were confusing:
unlabeled inline chips, greyed out most of the time, with the
only "why" behind a mouse-hover `title`. Add a visible,
non-interactive `detail-oversight-controls-label` group label
(gated by the SAME condition as the buttons) so the cluster is
identifiable, and a `detail-overseer-nudge-disabled-reason`
helper line that surfaces the disabled reason in-DOM (not just
on hover) whenever Nudge is unavailable. Explain is read-only
and non-mutating, so its disabled gate is removed entirely
(see handleExplainOverseer below) — it always opens its panel,
which already renders an informative "not currently watching"
empty state. Nudge's mutating gate (`canNudgeOverseer`) and
Stop's confirm dialog are unchanged.
*/}
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && (
<span className="detail-oversight-controls-label" data-testid="detail-oversight-controls-label">
{t("taskDetail.oversight.controlsLabel", "Overseer controls")}
</span>
)}
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && (
<button
type="button"
className={`btn btn-sm detail-overseer-nudge ${isNudgingOverseer ? "detail-overseer-nudge--saving" : ""}`}
data-testid="detail-overseer-nudge"
onClick={() => {
void handleNudgeOverseer();
}}
disabled={!canNudgeOverseer || isNudgingOverseer}
title={canNudgeOverseer ? t("taskDetail.oversight.nudgeTitle", "Inject steering guidance into the current stage now") : nudgeDisabledReason}
aria-label={t("taskDetail.oversight.nudgeAriaLabel", "Manual nudge")}
>
{isNudgingOverseer ? <Loader2 className="spin" aria-hidden="true" /> : <Send aria-hidden="true" />}
<span>{t("taskDetail.oversight.nudge", "Nudge")}</span>
</button>
)}
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && !canNudgeOverseer && (
<span className="detail-oversight-controls-helper" data-testid="detail-overseer-nudge-disabled-reason">
{nudgeDisabledReason}
</span>
)}
{(hasTaskOversightOverride || workflowOversightResolved) && showStopOverseer && (
<button
type="button"
className={`btn btn-sm detail-overseer-stop ${isStoppingOverseer ? "detail-overseer-stop--saving" : ""}`}
data-testid="detail-overseer-stop"
onClick={() => {
void handleStopOverseer();
}}
disabled={isStoppingOverseer}
aria-label={t("taskDetail.oversight.stopAriaLabel", "Stop oversight")}
>
{isStoppingOverseer ? <Loader2 className="spin" aria-hidden="true" /> : <Square aria-hidden="true" />}
<span>{t("taskDetail.oversight.stop", "Stop")}</span>
</button>
)}
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && (
<button
type="button"
className="btn btn-sm detail-overseer-explain"
data-testid="detail-overseer-explain"
onClick={() => {
void handleExplainOverseer();
}}
title={canExplainOverseer ? t("taskDetail.oversight.explainTitle", "Explain the overseer's current action") : t("taskDetail.oversight.explainInactiveTitle", "Overseer is not currently watching this task — Explain shows its last known state")}
aria-label={t("taskDetail.oversight.explainAriaLabel", "Explain current action")}
aria-expanded={overseerExplainOpen}
>
<Info aria-hidden="true" />
<span>{t("taskDetail.oversight.explain", "Explain")}</span>
</button>
)}
</>
)}
)}
</div>
{overseerExplainOpen && (
<div className="detail-overseer-explain-panel" data-testid="detail-overseer-explain-panel" role="region" aria-live="polite">
@@ -6013,8 +5884,8 @@ export function TaskDetailModal({ onClose, ...props }: TaskDetailModalProps) {
const overlayDismissProps = useOverlayDismiss(onClose);
/*
FNXC:TaskDetailSwipeBack 2026-07-05-12:30:
FN-7587 — track the mobile breakpoint locally (mirrors the OVERSIGHT_MENU_MOBILE_BREAKPOINT
resize-listener pattern above) so the list/modal/nested task-detail surface gets the same
FN-7587 — track the mobile breakpoint locally (mirrors the same resize-listener pattern
used elsewhere in this file) so the list/modal/nested task-detail surface gets the same
presentation-only predictive-back slide/fade enter transition as the board main-panel
(MainContent.tsx), without threading a new isMobile prop through App.tsx/AppModals.tsx. This
is presentation-only: it never touches onClose/onRequestClose timing or the underlying

View File

@@ -1,3 +1,11 @@
/*
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — the footer "Actions" dropdown button name is matched EXACTLY
(`{ name: "Actions" }`) throughout this file, not via a loose `/actions/i`
regex. The now-universal Oversight overflow trigger's aria-label is
"Oversight actions", which also matches `/actions/i` and made every such
query ambiguous once the trigger stopped being a mobile-only affordance.
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
@@ -743,7 +751,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown to see Duplicate
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
expect(screen.getByRole("menuitem", { name: "Duplicate" })).toBeTruthy();
@@ -764,7 +772,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown - Duplicate should not be there
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
expect(screen.queryByRole("menuitem", { name: "Duplicate" })).toBeNull();
});
@@ -787,7 +795,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" }));
@@ -820,7 +828,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" }));
@@ -852,7 +860,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
const pauseItem = screen.getByRole("menuitem", { name: "Pause" });
fireEvent.pointerUp(pauseItem, { pointerType: "touch", pointerId: 1 });
@@ -884,7 +892,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" }));
@@ -915,7 +923,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" }));
@@ -945,7 +953,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" }));
@@ -976,7 +984,7 @@ describe("TaskDetailModal", () => {
addToast={noop}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
const item = screen.queryByRole("menuitem", { name: "Refine" });
if (shouldShow) expect(item).toBeTruthy();
else expect(item).toBeNull();
@@ -1012,7 +1020,7 @@ describe("TaskDetailModal", () => {
/>,
);
expect(screen.getByRole("button", { name: /actions/i })).toBeTruthy();
expect(screen.getByRole("button", { name: "Actions" })).toBeTruthy();
});
it("renders Unpause button for a paused triage task", () => {
@@ -1029,7 +1037,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
expect(screen.getByRole("menuitem", { name: "Unpause" })).toBeTruthy();
});
@@ -1051,7 +1059,7 @@ describe("TaskDetailModal", () => {
/>,
);
await userEvent.click(screen.getByRole("button", { name: /actions/i }));
await userEvent.click(screen.getByRole("button", { name: "Actions" }));
await userEvent.click(screen.getByRole("menuitem", { name: "Unpause" }));
await waitFor(() => {
@@ -1084,7 +1092,7 @@ describe("TaskDetailModal", () => {
expect(mockFetchAgent).toHaveBeenCalledWith("agent-1", undefined);
});
await userEvent.click(screen.getByRole("button", { name: /actions/i }));
await userEvent.click(screen.getByRole("button", { name: "Actions" }));
await userEvent.click(screen.getByRole("menuitem", { name: "Unpause" }));
await waitFor(() => {
@@ -1115,7 +1123,7 @@ describe("TaskDetailModal", () => {
expect(mockFetchAgent).toHaveBeenCalledWith("agent-1", undefined);
});
await userEvent.click(screen.getByRole("button", { name: /actions/i }));
await userEvent.click(screen.getByRole("button", { name: "Actions" }));
expect(screen.getByRole("menuitem", { name: "Unpause" })).toBeTruthy();
expect(await screen.findByText("Paused by agent")).toBeTruthy();
@@ -1145,7 +1153,7 @@ describe("TaskDetailModal", () => {
expect(mockFetchAgent).toHaveBeenCalledWith("agent-1", undefined);
});
await userEvent.click(screen.getByRole("button", { name: /actions/i }));
await userEvent.click(screen.getByRole("button", { name: "Actions" }));
await userEvent.click(screen.getByRole("menuitem", { name: "Pause" }));
await waitFor(() => {
@@ -1177,7 +1185,7 @@ describe("TaskDetailModal", () => {
/>,
);
await userEvent.click(screen.getByRole("button", { name: /actions/i }));
await userEvent.click(screen.getByRole("button", { name: "Actions" }));
expect(screen.getByRole("menuitem", { name: expectedLabel })).toBeTruthy();
});
@@ -1196,7 +1204,7 @@ describe("TaskDetailModal", () => {
/>,
);
await userEvent.click(screen.getByRole("button", { name: /actions/i }));
await userEvent.click(screen.getByRole("button", { name: "Actions" }));
expect(screen.queryByRole("menuitem", { name: "Pause" })).toBeNull();
expect(screen.queryByRole("menuitem", { name: "Unpause" })).toBeNull();
@@ -1216,7 +1224,7 @@ describe("TaskDetailModal", () => {
/>,
);
expect(screen.queryByRole("button", { name: /actions/i })).toBeNull();
expect(screen.queryByRole("button", { name: "Actions" })).toBeNull();
});
it("clicking Refine opens the refinement modal", () => {
@@ -1234,7 +1242,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1258,7 +1266,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1281,7 +1289,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1309,7 +1317,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1333,7 +1341,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1364,7 +1372,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1392,7 +1400,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1443,7 +1451,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
@@ -1495,7 +1503,7 @@ describe("TaskDetailModal", () => {
await screen.findByTestId("task-detail-workflow-badge");
expect(screen.getByTestId("task-detail-workflow-badge")).toHaveTextContent("Custom refinement lane");
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Refine" }));
fireEvent.change(screen.getByPlaceholderText("Enter your feedback here..."), { target: { value: "Keep the same workflow lane" } });
fireEvent.click(screen.getByText("Create Refinement Task"));
@@ -1531,7 +1539,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
// Click Refine from the dropdown
@@ -1562,7 +1570,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
// Click Refine from the dropdown
@@ -1596,7 +1604,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
// Click Refine from the dropdown
@@ -1630,7 +1638,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
// Click Refine from the dropdown

View File

@@ -3,8 +3,18 @@ FNXC:PlannerOversight 2026-07-04-17:00:
FN-7517 coverage for the task-detail planner-overseer controls: the quick
oversight-level-change select, the manual nudge/stop/explain buttons, and
their enablement/leftover-shell rules (Surface Enumeration).
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — the desktop inline cluster was removed; ALL oversight controls
(including at the historically "desktop" 1024px-ish jsdom default width) now
render only behind the `detail-oversight-menu-trigger` overflow menu, the
same surface the mobile suite below already exercised. This describe block's
tests are retargeted to open the trigger via the shared `openOversightMenu()`
helper before querying level-select/nudge/stop/explain, mirroring the
pre-existing FN-7545/FN-7558 mobile pattern exactly — there is no longer a
separate desktop-only assertion path.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import type { PlannerOverseerRuntimeSnapshot } from "@fusion/core";
import {
@@ -34,6 +44,20 @@ const activeSnapshot: PlannerOverseerRuntimeSnapshot = {
lastAction: "inject_guidance",
};
/*
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — shared open-the-overflow-menu helper reused across every describe
block in this file (the pattern the FN-7521/FN-7545 mobile-only describe
block already used). Since the dropdown is now the universal surface at
every viewport, every test that needs to observe the level select or the
nudge/stop/explain buttons must click the trigger first.
*/
async function openOversightMenu() {
const trigger = await screen.findByTestId("detail-oversight-menu-trigger");
fireEvent.click(trigger);
return trigger;
}
describe("TaskDetailModal oversight controls", () => {
beforeEach(async () => {
vi.clearAllMocks();
@@ -63,6 +87,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const select = await screen.findByTestId("detail-oversight-level-select");
expect((select as HTMLSelectElement).value).toBe("observe");
@@ -90,6 +115,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const select = await screen.findByTestId("detail-oversight-level-select");
fireEvent.change(select, { target: { value: "__inherit__" } });
@@ -114,6 +140,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).not.toBeDisabled();
fireEvent.click(nudgeBtn);
@@ -136,6 +163,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).toBeDisabled();
});
@@ -153,6 +181,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const label = await screen.findByTestId("detail-oversight-controls-label");
expect(label).toHaveTextContent("Overseer controls");
@@ -181,6 +210,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).toBeDisabled();
@@ -203,6 +233,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).toBeDisabled();
@@ -225,6 +256,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).not.toBeDisabled();
expect(screen.queryByTestId("detail-overseer-nudge-disabled-reason")).not.toBeInTheDocument();
@@ -243,6 +275,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).toBeDisabled();
});
@@ -260,6 +293,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).toBeDisabled();
});
@@ -280,6 +314,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const stopBtn = await screen.findByTestId("detail-overseer-stop");
fireEvent.click(stopBtn);
@@ -302,6 +337,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
await screen.findByTestId("detail-oversight-level-select");
expect(screen.queryByTestId("detail-overseer-stop")).not.toBeInTheDocument();
});
@@ -322,6 +358,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const explainBtn = await screen.findByTestId("detail-overseer-explain");
fireEvent.click(explainBtn);
@@ -349,6 +386,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const explainBtn = await screen.findByTestId("detail-overseer-explain");
fireEvent.click(explainBtn);
@@ -372,6 +410,7 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
await openOversightMenu();
const explainBtn = await screen.findByTestId("detail-overseer-explain");
// Read-only Explain must never be disabled purely because the overseer
// isn't actively watching — that inactive state is exactly what the
@@ -397,9 +436,11 @@ describe("TaskDetailModal oversight controls", () => {
/>,
);
// The quick level-change select still renders (it's always editable so an
// operator can opt IN to oversight), but nudge/stop/explain must not
// render an always-on empty shell for the common off+inactive default.
// The quick level-change select still renders inside the opened menu
// (it's always editable so an operator can opt IN to oversight), but
// nudge/stop/explain must not render an always-on empty 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();
@@ -416,8 +457,10 @@ sites pass a slim `Task` (no `prompt` key) that never carries
`plannerOverseerState` — only the full-detail fetch response does. These
tests reproduce that exact path: a slim task prop with NO snapshot, plus a
mocked `fetchTaskDetail` resolving a full TaskDetail WITH an active snapshot,
and assert Nudge enables (helper absent) once the fetched detail lands — at
both the desktop inline site and the mobile overflow-menu site.
and assert Nudge enables (helper absent) once the fetched detail lands —
behind the (now universal, FN-7604) overflow-menu trigger at both a
"desktop" and a narrow-viewport width, exercised via the shared
`openOversightMenu()` helper.
*/
describe("TaskDetailModal oversight controls — snapshot delivered via fetched full detail (FN-7600)", () => {
const originalInnerWidth = window.innerWidth;
@@ -471,6 +514,7 @@ describe("TaskDetailModal oversight controls — snapshot delivered via fetched
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
await waitFor(() => {
expect(nudgeBtn).not.toBeDisabled();
@@ -498,6 +542,7 @@ describe("TaskDetailModal oversight controls — snapshot delivered via fetched
/>,
);
await openOversightMenu();
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).toBeDisabled();
const reason = await screen.findByTestId("detail-overseer-nudge-disabled-reason");
@@ -545,18 +590,22 @@ describe("TaskDetailModal oversight controls — snapshot delivered via fetched
* 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.
* menu behind a `detail-oversight-menu-trigger` button instead of inline —
* the old flat queries no longer find them. This suite drives the real
* FN-7545 overflow-menu affordance: open the trigger, then query the menu
* items.
*
* FNXC:PlannerOversight 2026-07-05-00:00 (FN-7604):
* The overflow menu this suite exercises is now the UNIVERSAL surface at
* every viewport, not a mobile-only branch — the desktop describe block
* above drives the exact same menu via the shared `openOversightMenu()`
* helper. Forcing `window.innerWidth = 375` here no longer selects a
* different code path; it is kept purely as a documented regression guard
* that the popover still renders/behaves correctly at a narrow width (e.g.
* `.detail-oversight-menu { right: 0 }` positioning), not because a second
* branch exists to select between.
*/
describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-7545 overflow menu)", () => {
describe("TaskDetailModal oversight controls — narrow-viewport regression guard (FN-7521, FN-7545/FN-7604 universal overflow menu)", () => {
const originalInnerWidth = window.innerWidth;
beforeEach(async () => {
@@ -568,10 +617,10 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-
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.
// Force a narrow viewport as a regression guard for the popover's mobile
// positioning/rendering; the overflow menu itself is the universal
// surface at every width post-FN-7604, so this no longer selects a
// separate branch.
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
});
@@ -579,13 +628,9 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-
Object.defineProperty(window, "innerWidth", { value: originalInnerWidth, configurable: true });
});
async function openOversightMenu() {
const trigger = await screen.findByTestId("detail-oversight-menu-trigger");
fireEvent.click(trigger);
return trigger;
}
// Reuses the shared `openOversightMenu()` helper defined at file scope.
it("still renders the quick level-change select behind the mobile overflow menu and writes on change", async () => {
it("still renders the quick level-change select behind the 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);
@@ -613,7 +658,7 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-
});
});
it("still renders enabled nudge/stop/explain controls behind the mobile overflow menu when the overseer is actively watching", async () => {
it("still renders enabled nudge/stop/explain controls behind the overflow menu at a narrow viewport when the overseer is actively watching", async () => {
render(
<TaskDetailModal
task={makeTask({ id: "FN-202", column: "in-progress", plannerOversightLevel: "autonomous", plannerOverseerState: activeSnapshot })}
@@ -633,7 +678,7 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-
expect(await screen.findByTestId("detail-overseer-explain")).toBeTruthy();
});
it("still shows the reworded periodic-observation copy (not the old alarming phrase) behind the mobile overflow menu (FN-7582)", async () => {
it("still shows the reworded periodic-observation copy (not the old alarming phrase) behind the overflow menu at a narrow viewport (FN-7582)", async () => {
render(
<TaskDetailModal
task={makeTask({ id: "FN-216", column: "in-progress", plannerOversightLevel: "autonomous" })}
@@ -656,7 +701,7 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-
expect(reason).toHaveTextContent("Nudge becomes available once the overseer is observing this task's current stage");
});
it("still renders no oversight-control leftover shell behind the mobile overflow menu for the off+inactive default case", async () => {
it("still renders no oversight-control leftover shell behind the overflow menu at a narrow viewport for the off+inactive default case", async () => {
render(
<TaskDetailModal
task={makeTask({ id: "FN-203", column: "todo", plannerOversightLevel: "off" })}
@@ -690,8 +735,17 @@ inline mount used. These assertions cover: (a) no inline mount remains,
(b) the dropdown option appears/renders the timeline when oversight is
active, (c) the option is absent and nothing mounts when oversight is off,
and (d) selecting Interventions then losing oversight falls back to Live
with no blank panel. Runs at the default (desktop) breakpoint, matching the
first describe block's setup rather than the FN-7521/FN-7545 mobile one.
with no blank panel.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — these tests previously used `detail-overseer-nudge`/
`detail-oversight-level-select` as a "wait for the oversight cluster to
mount" sync point. Since ALL oversight controls now render only inside the
closed-by-default overflow menu, that sync point no longer mounts without
first opening the trigger. Retargeted to sync on `detail-oversight-menu-trigger`
instead (the now-universal mount point) — the Intervention-Timeline-specific
assertions (open Activity dropdown → click "Interventions" → assert
`planner-intervention-timeline`) are otherwise unchanged.
*/
describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)", () => {
beforeEach(async () => {
@@ -726,7 +780,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)"
/>,
);
await screen.findByTestId("detail-overseer-nudge");
await screen.findByTestId("detail-oversight-menu-trigger");
expect(screen.queryByTestId("planner-intervention-timeline")).not.toBeInTheDocument();
});
@@ -743,7 +797,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)"
/>,
);
await screen.findByTestId("detail-overseer-nudge");
await screen.findByTestId("detail-oversight-menu-trigger");
openActivityViewMenu();
const option = screen.getByRole("menuitem", { name: "Interventions" });
fireEvent.click(option);
@@ -764,7 +818,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)"
/>,
);
await screen.findByTestId("detail-oversight-level-select");
await screen.findByTestId("detail-oversight-menu-trigger");
openActivityViewMenu();
expect(screen.queryByRole("menuitem", { name: "Interventions" })).not.toBeInTheDocument();
expect(screen.queryByTestId("planner-intervention-timeline")).not.toBeInTheDocument();
@@ -783,7 +837,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)"
/>,
);
await screen.findByTestId("detail-overseer-nudge");
await screen.findByTestId("detail-oversight-menu-trigger");
openActivityViewMenu();
fireEvent.click(screen.getByRole("menuitem", { name: "Interventions" }));
expect(await screen.findByTestId("planner-intervention-timeline")).toBeInTheDocument();
@@ -828,7 +882,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)"
/>,
);
await screen.findByTestId("detail-overseer-nudge");
await screen.findByTestId("detail-oversight-menu-trigger");
openActivityViewMenu();
fireEvent.click(screen.getByRole("menuitem", { name: "Feed" }));
@@ -858,7 +912,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)"
/>,
);
await screen.findByTestId("detail-overseer-nudge");
await screen.findByTestId("detail-oversight-menu-trigger");
openActivityViewMenu();
fireEvent.click(screen.getByRole("menuitem", { name: "Interventions" }));

View File

@@ -1,15 +1,21 @@
/*
FNXC:PlannerOversight 2026-07-04-19:00:
FN-7545 coverage for the mobile collapse of the FN-7517 oversight action
controls into a single overflow menu (`detail-oversight-menu-trigger`). The
suite forces the narrow-viewport branch by setting `window.innerWidth` below
the `TaskDetailModal.tsx` `OVERSIGHT_MENU_MOBILE_BREAKPOINT` (768) BEFORE
render, since the component reads `window.innerWidth` on mount via a resize
listener (mirroring `DocumentsView`'s local `isMobile` pattern) rather than a
CSS media query. Every action inside the menu reuses the SAME handlers and
enablement gates as the desktop suite
(`TaskDetailModal.oversight-controls.test.tsx`) — this file only asserts the
collapsed-menu affordance, not new guard logic.
FN-7545 coverage for the collapse of the FN-7517 oversight action controls
into a single overflow menu (`detail-oversight-menu-trigger`). Every action
inside the menu reuses the SAME handlers and enablement gates as the desktop
suite (`TaskDetailModal.oversight-controls.test.tsx`) — this file only
asserts the collapsed-menu affordance, not new guard logic.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — the overflow menu is now the SINGLE UNIVERSAL surface at every
viewport (desktop and mobile); it is no longer a narrow-viewport-only branch
selected by a JS `isOversightMenuMobile` resize listener (that state, the
`OVERSIGHT_MENU_MOBILE_BREAKPOINT` constant, and its effects were removed
from `TaskDetailModal.tsx`). `setViewportWidth`/`MOBILE_WIDTH`/`DESKTOP_WIDTH`
no longer select which branch mounts — both widths mount the exact same
dropdown — they are kept as a documented regression guard that the popover
still renders/positions/behaves correctly across a narrow AND a desktop
viewport, per the Surface Enumeration breakpoint requirement.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
@@ -455,7 +461,7 @@ describe("TaskDetailModal oversight controls — mobile overflow menu", () => {
expect(screen.getAllByRole("menu")).toHaveLength(1);
});
it("desktop inline oversight select is unaffected by the mobile auto-focus fix", async () => {
it("the overflow-menu popover renders identically at a desktop viewport (FN-7604 universal dropdown)", async () => {
setViewportWidth(DESKTOP_WIDTH);
render(
@@ -470,12 +476,18 @@ describe("TaskDetailModal oversight controls — mobile overflow menu", () => {
/>,
);
// Desktop renders the inline native select directly (no overflow trigger,
// no custom popover) — confirm that surface is untouched by this fix.
expect(screen.queryByTestId("detail-oversight-menu-trigger")).not.toBeInTheDocument();
// FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 — there is no longer a
// desktop-only inline select surface; the overflow-menu trigger is the
// single universal mount point at every viewport, including desktop. The
// popover stays closed until clicked, exactly like the mobile width.
const trigger = await screen.findByTestId("detail-oversight-menu-trigger");
expect(screen.queryByTestId("detail-oversight-level-select")).not.toBeInTheDocument();
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
fireEvent.click(trigger);
const select = await screen.findByTestId("detail-oversight-level-select");
expect(select).toBeInTheDocument();
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
expect(screen.getByRole("menu")).toBeInTheDocument();
setViewportWidth(MOBILE_WIDTH);
});

View File

@@ -1,6 +1,13 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
FNXC:PlannerOversight 2026-07-05-00:00:
FN-7604 — the footer "Actions" dropdown button name is matched EXACTLY
(`{ name: "Actions" }`) throughout this file, not via a loose `/actions/i`
regex. The now-universal Oversight overflow trigger's aria-label is
"Oversight actions", which also matches `/actions/i` and made every such
query ambiguous once the trigger stopped being a mobile-only affordance.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
@@ -1538,7 +1545,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown to see Retry
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
expect(screen.getByRole("menuitem", { name: "Retry" })).toBeTruthy();
@@ -1560,7 +1567,7 @@ describe("TaskDetailModal", () => {
);
// No Retry should be visible in the Actions dropdown
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
expect(screen.queryByRole("menuitem", { name: "Retry" })).toBeNull();
});
@@ -1599,7 +1606,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown and check for exactly one Retry
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
const retryButtons = screen.getAllByRole("menuitem", { name: "Retry" });
@@ -1622,7 +1629,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown and check for exactly one Retry
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
const retryButtons = screen.getAllByRole("menuitem", { name: "Retry" });
@@ -1644,7 +1651,7 @@ describe("TaskDetailModal", () => {
/>,
);
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
const retryButtons = screen.getAllByRole("menuitem", { name: "Retry" });
@@ -1670,7 +1677,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown and click Retry
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
await act(async () => {
fireEvent.click(actionsBtn);
});
@@ -1706,7 +1713,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown and click Retry
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
await act(async () => {
fireEvent.click(actionsBtn);
});
@@ -1746,7 +1753,7 @@ describe("TaskDetailModal", () => {
);
// Open Actions dropdown and click Retry
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
await act(async () => {
fireEvent.click(actionsBtn);
});
@@ -1787,7 +1794,7 @@ describe("TaskDetailModal", () => {
expect(screen.getByRole("menuitem", { name: "Back to In Progress" })).toBeTruthy();
expect(screen.queryByRole("menuitem", { name: "Move to Todo" })).toBeNull();
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
expect(screen.queryByRole("menuitem", { name: "Retry" })).toBeNull();
});
@@ -1807,7 +1814,7 @@ describe("TaskDetailModal", () => {
/>,
);
const actionsBtn = screen.getByRole("button", { name: /actions/i });
const actionsBtn = screen.getByRole("button", { name: "Actions" });
await act(async () => {
fireEvent.click(actionsBtn);
});

View File

@@ -232,13 +232,16 @@ describe("TaskDetailModal", () => {
const inlineControlsBlock = getStandaloneCssRuleBlock(css, ".detail-meta-inline-controls");
const priorityChipBlock = getExactCssRuleBlock(css, ".detail-priority-chip");
const executionToggleBlock = getExactCssRuleBlock(css, ".detail-execution-mode-toggle");
const oversightChipBlock = getExactCssRuleBlock(css, ".detail-oversight-chip");
const oversightTriggerBlock = getExactCssRuleBlock(css, ".detail-oversight-menu-trigger");
// The cluster declares one shared border-radius token; all four controls
// must reference it rather than four independent literal radii.
// The cluster declares one shared border-radius token; all three
// controls must reference it rather than independent literal radii.
// FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 removed the desktop-only
// `.detail-oversight-chip` wrapper (the inline branch it styled was
// deleted); the Oversight surface is now represented solely by
// `.detail-oversight-menu-trigger`, which already carried this trio.
expect(inlineControlsBlock).toContain("--detail-control-border-radius: var(--radius-md);");
for (const block of [priorityChipBlock, executionToggleBlock, oversightChipBlock, oversightTriggerBlock]) {
for (const block of [priorityChipBlock, executionToggleBlock, oversightTriggerBlock]) {
expect(block).toContain("border-radius: var(--detail-control-border-radius);");
expect(block).toContain("border-width: var(--btn-border-width);");
expect(block).toContain("border-color: var(--border);");
@@ -247,26 +250,25 @@ describe("TaskDetailModal", () => {
expect(block).toContain("box-sizing: border-box;");
}
// Guard against regressing back to four independent literal radius values
// (e.g. reintroducing a bare `var(--radius-pill)` on only the chips).
// Guard against regressing back to independent literal radius values
// (e.g. reintroducing a bare `var(--radius-pill)` on the priority chip).
expect(priorityChipBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/);
expect(oversightChipBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/);
expect(oversightTriggerBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/);
});
it("renders the Priority dropdown chip like the Oversight dropdown chip, on every surface (FN-7597)", () => {
const css = readDashboardStylesSource();
const priorityChipBlock = getExactCssRuleBlock(css, ".detail-priority-chip");
const oversightChipBlock = getExactCssRuleBlock(css, ".detail-oversight-chip");
const oversightTriggerBlock = getExactCssRuleBlock(css, ".detail-oversight-menu-trigger");
const prioritySelectBlock = getExactCssRuleBlock(css, ".detail-priority-select");
const oversightSelectBlock = getExactCssRuleBlock(css, ".detail-oversight-select");
const prioritySelectOptionBlock = getExactCssRuleBlock(css, ".detail-priority-select option");
const oversightSelectOptionBlock = getExactCssRuleBlock(css, ".detail-oversight-select option");
// Same box size AND same border source for the desktop Priority chip vs.
// BOTH oversight surfaces (desktop chip and the mobile overflow trigger).
for (const block of [priorityChipBlock, oversightChipBlock, oversightTriggerBlock]) {
// Same box size AND same border source for the Priority chip vs. the
// (now-universal, FN-7604) Oversight overflow trigger.
for (const block of [priorityChipBlock, oversightTriggerBlock]) {
expect(block).toContain("min-height: var(--detail-priority-control-min-height);");
expect(block).toContain("border-width: var(--btn-border-width);");
expect(block).toContain("border-color: var(--border);");
@@ -613,8 +615,13 @@ describe("TaskDetailModal", () => {
/>,
);
// Actions are now in a dropdown - open it first
const actionsBtn = screen.getByRole("button", { name: /actions/i });
// Actions are now in a dropdown - open it first.
// FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 — the footer "Actions"
// dropdown button name must be matched EXACTLY (not `/actions/i`) because
// the now-universal Oversight overflow trigger's aria-label is "Oversight
// actions", which also matches a loose /actions/i regex and made this
// query ambiguous once the trigger stopped being mobile-only.
const actionsBtn = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionsBtn);
// Now the dropdown items should be visible
@@ -653,7 +660,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -680,7 +687,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -707,7 +714,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -732,7 +739,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -771,7 +778,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -833,7 +840,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -870,7 +877,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -907,7 +914,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -950,7 +957,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -981,7 +988,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -1018,7 +1025,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
@@ -1052,7 +1059,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: /actions/i }));
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {