FN-8251: hide overseer eye when oversight is off
Resolve inherited workflow oversight before rendering task-card overseer indicators. - Resolve selected-workflow oversight using the planning workflow identity - Fail closed for unknown, pending, malformed, and failed inherited settings - Cover workflow changes and mobile suppression, and document the behavior - Add a patch changeset for the corrected card indicator Files changed: .changeset/fn-8251-card-eye-effective-oversight.md | 7 + docs/dashboard-guide.md | 4 +- packages/dashboard/app/components/TaskCard.tsx | 116 ++++++++------- .../__tests__/TaskCard.oversight.test.tsx | 161 ++++++++++++++++++++- .../app/components/__tests__/TaskCard.test.tsx | 6 +- 5 files changed, 234 insertions(+), 60 deletions(-) Fusion-Task-Id: FN-8251 Fusion-Task-Lineage: 72dfa67e-e6d5-4557-91b3-23aff6dcf9e7 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8251-card-eye-effective-oversight.md
Normal file
7
.changeset/fn-8251-card-eye-effective-oversight.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Hide the task-card overseer eye when the selected workflow has oversight turned off.
|
||||
category: fix
|
||||
dev: TaskCard resolves planner oversight from the per-workflow board identity and fails closed when inherited oversight cannot be resolved.
|
||||
@@ -234,10 +234,10 @@ Features:
|
||||
- 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-11-00:00: FN-7592 reintroduced a compact active-overseer state indicator as an Eye glyph instead of a wide text badge, using the engine-provided transient plannerOverseerState rather than locally guessing from task fields. -->
|
||||
<!-- FNXC:PlannerOversight 2026-07-18-00:00: FN-8239 requires the card Eye badge and card-header-badges wrapper to use the same freshly-resolved effective oversight level as Task Detail, preventing stale non-off runtime snapshots from contradicting the detail's EyeOff state or leaving an empty wrapper. -->
|
||||
<!-- FNXC:PlannerOversight 2026-07-17-15:50: FN-8251 requires selected-workflow cards to resolve inherited oversight from their trusted board workflow ID when aggregate workflowBadge metadata is absent. Identity-less, pending, failed, and malformed inherited resolution fails closed: the Eye and otherwise-empty card-header-badges wrapper appear only after active effective oversight is positively resolved. -->
|
||||
<!-- FNXC:TaskRevert 2026-07-16-00:00: FN-8066 adds durable source-task revert provenance to the shared board/List TaskCard footer. -->
|
||||
- Completed and archived task cards show a compact **Reverted** footer chip after a clean or already-reverted git outcome has persisted the source task's revert marker; conflicts, AI undo tasks, and revert PRs awaiting merge do not show it.
|
||||
- Task cards show a compact **planner-overseer eye badge** (`data-testid="planner-overseer-state-badge"`) only when the engine reports a non-idle, non-off transient `plannerOverseerState` **and** the task's freshly-resolved effective oversight level is not `off`. When effective oversight is off, the eye badge and otherwise-empty `.card-header-badges` wrapper are both absent, matching the Task Detail `EyeOff` state. The eye badge is an active-overseer state marker, not a human-read/view indicator: `watching` means passive monitoring, `steering`/`recovering` mean active guidance or recovery is underway, and `awaiting-confirmation` means a human decision is required before the overseer can continue. Hover exposes the composed tooltip with the overseer's reason, watched stage/signal, and pending-confirmation note when present.
|
||||
- Task cards show a compact **planner-overseer eye badge** (`data-testid="planner-overseer-state-badge"`) only when the engine reports a non-idle, non-off transient `plannerOverseerState` **and** the task's effective oversight is positively resolved as active. Aggregate cards resolve inherited oversight through their task workflow badge; selected-workflow board cards resolve it through their trusted board workflow ID. Identity-less cards and pending, failed, or malformed inherited workflow-setting loads fail closed. When effective oversight is off or cannot be positively resolved, the eye badge and otherwise-empty `.card-header-badges` wrapper are both absent. This matches—but does not alter—the separate Task Detail `EyeOff` menu trigger. The eye badge is an active-overseer state marker, not a human-read/view indicator: `watching` means passive monitoring, `steering`/`recovering` mean active guidance or recovery is underway, and `awaiting-confirmation` means a human decision is required before the overseer can continue. Hover exposes the composed tooltip with the overseer's reason, watched stage/signal, and pending-confirmation note when present.
|
||||
<!-- 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.
|
||||
|
||||
@@ -147,7 +147,14 @@ async function getAgentName(agentId: string, projectId?: string): Promise<string
|
||||
* possible follow-up (see FN-7516 delivery notes) but is no longer required
|
||||
* for correctness.
|
||||
*/
|
||||
const workflowOversightEffectiveCache = new Map<string, PlannerOversightLevel | undefined>();
|
||||
type WorkflowOversightResolution = {
|
||||
level: PlannerOversightLevel | undefined;
|
||||
resolved: boolean;
|
||||
/** FNXC:PlannerOversight 2026-07-17-15:50: Cache identity prevents an old workflow's active tier leaking during a prop switch. */
|
||||
workflowCacheKey?: string;
|
||||
};
|
||||
|
||||
const workflowOversightEffectiveCache = new Map<string, WorkflowOversightResolution>();
|
||||
const workflowOversightInflight = new Map<string, Promise<void>>();
|
||||
|
||||
/** @internal Test helper to reset the workflow-effective-oversight cache between tests */
|
||||
@@ -160,29 +167,35 @@ function getWorkflowOversightCacheKey(workflowId: string, projectId?: string): s
|
||||
return `${projectId ?? "default"}::${workflowId}`;
|
||||
}
|
||||
|
||||
function normalizeWorkflowId(value: string | null | undefined): string | undefined {
|
||||
const normalized = value?.trim();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function isPlannerOversightLevelValue(value: unknown): value is PlannerOversightLevel {
|
||||
return typeof value === "string" && (PLANNER_OVERSIGHT_LEVELS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Fetch (with in-flight de-dup) and cache the workflow's effective
|
||||
* `plannerOversightLevel` setting value for a given `(workflowId, projectId)`.
|
||||
* Never throws — an error caches `undefined` so the resolver falls through to
|
||||
* the schema default rather than retrying every render. */
|
||||
async function loadWorkflowOversightEffectiveLevel(workflowId: string, projectId: string | undefined): Promise<PlannerOversightLevel | undefined> {
|
||||
* Never throws; failed or malformed responses stay explicitly unresolved so
|
||||
* callers cannot mistake an unknown inherited tier for the schema default. */
|
||||
async function loadWorkflowOversightEffectiveLevel(workflowId: string, projectId: string | undefined): Promise<WorkflowOversightResolution> {
|
||||
const key = getWorkflowOversightCacheKey(workflowId, projectId);
|
||||
if (workflowOversightEffectiveCache.has(key)) {
|
||||
return workflowOversightEffectiveCache.get(key);
|
||||
}
|
||||
const cached = workflowOversightEffectiveCache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
let inflight = workflowOversightInflight.get(key);
|
||||
if (!inflight) {
|
||||
inflight = fetchWorkflowSettingValues(workflowId, projectId)
|
||||
.then((payload) => {
|
||||
const raw = payload.effective?.plannerOversightLevel;
|
||||
workflowOversightEffectiveCache.set(key, isPlannerOversightLevelValue(raw) ? raw : undefined);
|
||||
workflowOversightEffectiveCache.set(key, isPlannerOversightLevelValue(raw)
|
||||
? { level: raw, resolved: true }
|
||||
: { level: undefined, resolved: false });
|
||||
})
|
||||
.catch(() => {
|
||||
workflowOversightEffectiveCache.set(key, undefined);
|
||||
workflowOversightEffectiveCache.set(key, { level: undefined, resolved: false });
|
||||
})
|
||||
.finally(() => {
|
||||
workflowOversightInflight.delete(key);
|
||||
@@ -190,7 +203,7 @@ async function loadWorkflowOversightEffectiveLevel(workflowId: string, projectId
|
||||
workflowOversightInflight.set(key, inflight);
|
||||
}
|
||||
await inflight;
|
||||
return workflowOversightEffectiveCache.get(key);
|
||||
return workflowOversightEffectiveCache.get(key) ?? { level: undefined, resolved: false };
|
||||
}
|
||||
|
||||
function normalizeTaskPriorityValue(priority: Task["priority"]): TaskPriority {
|
||||
@@ -1022,65 +1035,56 @@ function TaskCardComponent({
|
||||
}, [agentsMap, task.assignedAgentId, projectId]);
|
||||
|
||||
/*
|
||||
* FNXC:PlannerOversight 2026-07-04-16:00:
|
||||
* Fetch (and cache, see loadWorkflowOversightEffectiveLevel above) the
|
||||
* workflow's effective plannerOversightLevel setting so the card can
|
||||
* resolve the TRUE effective oversight tier for tasks with no per-task
|
||||
* override, rather than always falling through to the schema default
|
||||
* (FN-7516 code-review fix). `Task` has no `workflowId` field (workflow
|
||||
* selection lives in the separate `task_workflow_selection` table, not on
|
||||
* the task payload — verified against packages/core/src/types.ts), so this
|
||||
* reads the workflow id from the already-existing `workflowBadge` prop
|
||||
* (populated by Column/WorktreeGroup board callers). Only fires when a
|
||||
* workflowBadge.workflowId is present; synchronous cache hits skip the
|
||||
* state churn entirely. Surfaces that don't pass `workflowBadge` (dock,
|
||||
* MainContent) fall back to the resolver's schema default immediately
|
||||
* (treated as "resolved" — there is no pending fetch to gate on for those
|
||||
* surfaces).
|
||||
*
|
||||
* Second code-review fix (this pass): while the workflow-tier fetch is
|
||||
* in flight, `workflowOversightEffectiveLevel` was `undefined`, which the
|
||||
* resolver treats identically to "no workflow setting exists" and falls
|
||||
* back to `DEFAULT_PLANNER_OVERSIGHT_LEVEL` ("autonomous"). That rendered
|
||||
* a wrong default badge for tasks inheriting a workflow explicitly
|
||||
* configured to Off/Observe/Steer, for the whole window before the fetch
|
||||
* resolved (and forever on fetch failure, since failures also cache
|
||||
* `undefined`). Track resolution explicitly via `workflowOversightResolved`
|
||||
* and gate the badge (`showOversightBadge` below) so nothing renders from
|
||||
* the unresolved workflow tier — only a task-level override (known
|
||||
* synchronously from the task payload) can show a badge before the
|
||||
* workflow tier is known.
|
||||
* FNXC:PlannerOversight 2026-07-17-15:50:
|
||||
* FN-8251 requires per-workflow cards to resolve inherited oversight with
|
||||
* their trusted selected `planningWorkflowId` when aggregate-only
|
||||
* `workflowBadge` metadata is absent. Prefer a task-specific aggregate ID;
|
||||
* normalize blank IDs; and fail closed for identity-less, pending, failed,
|
||||
* or malformed inherited resolution. The Eye is allowed only when effective
|
||||
* oversight is positively known active, while a valid task override remains
|
||||
* authoritative without a workflow fetch.
|
||||
*/
|
||||
const workflowIdForOversight = workflowBadge?.workflowId;
|
||||
const [workflowOversightState, setWorkflowOversightState] = useState<{ level: PlannerOversightLevel | undefined; resolved: boolean }>(() => {
|
||||
if (!workflowIdForOversight) return { level: undefined, resolved: true };
|
||||
const key = getWorkflowOversightCacheKey(workflowIdForOversight, projectId);
|
||||
return workflowOversightEffectiveCache.has(key)
|
||||
? { level: workflowOversightEffectiveCache.get(key), resolved: true }
|
||||
: { level: undefined, resolved: false };
|
||||
const workflowIdForOversight = normalizeWorkflowId(workflowBadge?.workflowId)
|
||||
?? normalizeWorkflowId(planningWorkflowId);
|
||||
const workflowOversightCacheKey = workflowIdForOversight
|
||||
? getWorkflowOversightCacheKey(workflowIdForOversight, projectId)
|
||||
: undefined;
|
||||
const [workflowOversightState, setWorkflowOversightState] = useState<WorkflowOversightResolution>(() => {
|
||||
if (!workflowOversightCacheKey) return { level: undefined, resolved: false };
|
||||
return {
|
||||
...(workflowOversightEffectiveCache.get(workflowOversightCacheKey) ?? { level: undefined, resolved: false }),
|
||||
workflowCacheKey: workflowOversightCacheKey,
|
||||
};
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!workflowIdForOversight) {
|
||||
setWorkflowOversightState({ level: undefined, resolved: true });
|
||||
if (!workflowIdForOversight || !workflowOversightCacheKey) {
|
||||
setWorkflowOversightState({ level: undefined, resolved: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowId = workflowIdForOversight;
|
||||
const key = getWorkflowOversightCacheKey(workflowId, projectId);
|
||||
if (workflowOversightEffectiveCache.has(key)) {
|
||||
setWorkflowOversightState({ level: workflowOversightEffectiveCache.get(key), resolved: true });
|
||||
const key = workflowOversightCacheKey;
|
||||
const cached = workflowOversightEffectiveCache.get(key);
|
||||
if (cached) {
|
||||
setWorkflowOversightState({ ...cached, workflowCacheKey: key });
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowOversightState({ level: undefined, resolved: false });
|
||||
setWorkflowOversightState({ level: undefined, resolved: false, workflowCacheKey: key });
|
||||
let cancelled = false;
|
||||
void loadWorkflowOversightEffectiveLevel(workflowId, projectId).then((level) => {
|
||||
if (!cancelled) setWorkflowOversightState({ level, resolved: true });
|
||||
void loadWorkflowOversightEffectiveLevel(workflowId, projectId).then((resolution) => {
|
||||
if (!cancelled) setWorkflowOversightState({ ...resolution, workflowCacheKey: key });
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [workflowIdForOversight, projectId]);
|
||||
const workflowOversightEffectiveLevel = workflowOversightState.level;
|
||||
const workflowOversightResolved = workflowOversightState.resolved;
|
||||
}, [workflowIdForOversight, workflowOversightCacheKey, projectId]);
|
||||
// FNXC:PlannerOversight 2026-07-17-15:50: Switching a memoized card between
|
||||
// workflows must fail closed in the render before its effect resets state;
|
||||
// an active tier resolved for the prior workflow cannot authorize this Eye.
|
||||
const currentWorkflowOversightState = workflowOversightState.workflowCacheKey === workflowOversightCacheKey
|
||||
? workflowOversightState
|
||||
: { level: undefined, resolved: false };
|
||||
const workflowOversightEffectiveLevel = currentWorkflowOversightState.level;
|
||||
const workflowOversightResolved = currentWorkflowOversightState.resolved;
|
||||
|
||||
// Auto-focus and auto-resize description textarea when entering edit mode
|
||||
useEffect(() => {
|
||||
|
||||
@@ -98,7 +98,14 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function renderCard(overrides: Partial<Task> = {}, cardProps: { workflowBadge?: { workflowId: string; workflowName: string; workflowIcon?: string } } = {}) {
|
||||
function renderCard(
|
||||
overrides: Partial<Task> = {},
|
||||
cardProps: {
|
||||
workflowBadge?: { workflowId: string; workflowName: string; workflowIcon?: string };
|
||||
planningWorkflowId?: string | null;
|
||||
projectId?: string;
|
||||
} = {},
|
||||
) {
|
||||
return render(<TaskCard task={makeTask(overrides)} onOpenDetail={noop} addToast={noop} {...cardProps} />);
|
||||
}
|
||||
|
||||
@@ -380,6 +387,147 @@ describe("TaskCard workflow-effective oversight level (FN-7516 code-review fix)"
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* FNXC:PlannerOversight 2026-07-17-15:50:
|
||||
* FN-8251 regression coverage keeps card-level inherited workflow resolution
|
||||
* fail-closed. Selected-workflow boards provide `planningWorkflowId` without
|
||||
* aggregate `workflowBadge` metadata, so no stale runtime Eye may appear
|
||||
* unless the selected workflow's effective oversight is positively active.
|
||||
*/
|
||||
describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
|
||||
const staleSnapshot = (column: "in-progress" | "in-review") => ({
|
||||
column,
|
||||
status: undefined,
|
||||
plannerOverseerState: {
|
||||
state: "watching" as const,
|
||||
oversightLevel: "autonomous" as const,
|
||||
watchedStage: column === "in-review" ? "reviewer" as const : "executor" as const,
|
||||
signal: "progressing" as const,
|
||||
attemptCount: 0,
|
||||
attemptLimit: 3,
|
||||
pendingConfirmation: false,
|
||||
observedAt: 1700000000000,
|
||||
},
|
||||
});
|
||||
|
||||
it.each(["in-progress", "in-review"] as const)("uses planningWorkflowId and hides the stale eye before and after selected-workflow off resolves in %s", async (column) => {
|
||||
vi.mocked(fetchWorkflowSettingValues).mockResolvedValueOnce({
|
||||
stored: { plannerOversightLevel: "off" },
|
||||
effective: { plannerOversightLevel: "off" },
|
||||
orphaned: [],
|
||||
});
|
||||
|
||||
renderCard(staleSnapshot(column), { planningWorkflowId: " selected-workflow-off ", projectId: "project-8251" });
|
||||
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
expect(screen.queryByTestId("card-header-badges")).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("selected-workflow-off", "project-8251");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
expect(screen.queryByTestId("card-header-badges")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["rejected", () => vi.mocked(fetchWorkflowSettingValues).mockRejectedValueOnce(new Error("unavailable"))],
|
||||
["missing", () => vi.mocked(fetchWorkflowSettingValues).mockResolvedValueOnce({ stored: {}, effective: {}, orphaned: [] })],
|
||||
["invalid", () => vi.mocked(fetchWorkflowSettingValues).mockResolvedValueOnce({ stored: {}, effective: { plannerOversightLevel: "unknown" }, orphaned: [] })],
|
||||
])("fails closed after a %s inherited workflow resolution", async (_state, arrange) => {
|
||||
arrange();
|
||||
renderCard(staleSnapshot("in-progress"), { planningWorkflowId: `workflow-${_state}` });
|
||||
|
||||
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalled());
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
expect(screen.queryByTestId("card-header-badges")).toBeNull();
|
||||
expect(screen.queryByLabelText(/overseer/i)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed without workflow identity but keeps a valid active task override authoritative", () => {
|
||||
const { rerender } = renderCard(staleSnapshot("in-progress"));
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
expect(screen.queryByTestId("card-header-badges")).toBeNull();
|
||||
|
||||
rerender(
|
||||
<TaskCard
|
||||
task={makeTask({ ...staleSnapshot("in-progress"), plannerOversightLevel: "steer" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("planner-overseer-state-badge")).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["aggregate", { workflowBadge: { workflowId: "aggregate-active", workflowName: "Aggregate active" } }],
|
||||
["selected workflow", { planningWorkflowId: "selected-active" }],
|
||||
])("renders the eye only after positively resolved active oversight for %s cards", async (_surface, props) => {
|
||||
vi.mocked(fetchWorkflowSettingValues).mockResolvedValueOnce({
|
||||
stored: { plannerOversightLevel: "steer" },
|
||||
effective: { plannerOversightLevel: "steer" },
|
||||
orphaned: [],
|
||||
});
|
||||
renderCard(staleSnapshot("in-progress"), props);
|
||||
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
expect(await screen.findByTestId("planner-overseer-state-badge")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("re-resolves when the selected workflow identity changes", async () => {
|
||||
vi.mocked(fetchWorkflowSettingValues)
|
||||
.mockResolvedValueOnce({ stored: { plannerOversightLevel: "off" }, effective: { plannerOversightLevel: "off" }, orphaned: [] })
|
||||
.mockResolvedValueOnce({ stored: { plannerOversightLevel: "observe" }, effective: { plannerOversightLevel: "observe" }, orphaned: [] });
|
||||
const task = makeTask(staleSnapshot("in-progress"));
|
||||
const { rerender } = render(
|
||||
<TaskCard task={task} onOpenDetail={noop} addToast={noop} planningWorkflowId="first-workflow" />,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("first-workflow", undefined));
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
rerender(<TaskCard task={task} onOpenDetail={noop} addToast={noop} planningWorkflowId="second-workflow" />);
|
||||
|
||||
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("second-workflow", undefined));
|
||||
expect(await screen.findByTestId("planner-overseer-state-badge")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides the eye synchronously when an active selected workflow changes to an unresolved off workflow", async () => {
|
||||
vi.mocked(fetchWorkflowSettingValues)
|
||||
.mockResolvedValueOnce({ stored: { plannerOversightLevel: "steer" }, effective: { plannerOversightLevel: "steer" }, orphaned: [] })
|
||||
.mockResolvedValueOnce({ stored: { plannerOversightLevel: "off" }, effective: { plannerOversightLevel: "off" }, orphaned: [] });
|
||||
const task = makeTask(staleSnapshot("in-progress"));
|
||||
const { rerender } = render(
|
||||
<TaskCard task={task} onOpenDetail={noop} addToast={noop} planningWorkflowId="active-workflow" />,
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId("planner-overseer-state-badge")).toBeTruthy();
|
||||
rerender(<TaskCard task={task} onOpenDetail={noop} addToast={noop} planningWorkflowId="off-workflow" />);
|
||||
|
||||
// FNXC:PlannerOversight 2026-07-17-15:50: A useEffect reset is too late:
|
||||
// this render must not reuse the prior workflow's active resolution while
|
||||
// the selected off workflow is loading.
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("off-workflow", undefined));
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps selected-workflow suppression at the 375px mobile viewport", async () => {
|
||||
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
|
||||
vi.mocked(fetchWorkflowSettingValues).mockResolvedValueOnce({
|
||||
stored: { plannerOversightLevel: "off" },
|
||||
effective: { plannerOversightLevel: "off" },
|
||||
orphaned: [],
|
||||
});
|
||||
renderCard(staleSnapshot("in-review"), { planningWorkflowId: "mobile-off" });
|
||||
|
||||
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("mobile-off", undefined));
|
||||
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
|
||||
expect(screen.queryByTestId("card-header-badges")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* FNXC:PlannerOversight 2026-07-04-HH:MM:
|
||||
* FN-7542 dropped the `pausedReason`/`reviewState`/`workflowTransitionNotification`
|
||||
@@ -412,6 +560,17 @@ describe("TaskCard memo comparator — oversight level (FN-7516)", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when planningWorkflowId changes, so selected-workflow cards re-resolve their effective tier", () => {
|
||||
const task = makeTask({});
|
||||
|
||||
expect(
|
||||
__test_areTaskCardPropsEqual(
|
||||
{ task, planningWorkflowId: "wf-a", onOpenDetail: noop, addToast: noop } as any,
|
||||
{ task, planningWorkflowId: "wf-b", onOpenDetail: noop, addToast: noop } as any,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when nothing relevant changes, including plannerOversightLevel", () => {
|
||||
const base = makeTask({ plannerOversightLevel: "steer" });
|
||||
const same = makeTask({ plannerOversightLevel: "steer" });
|
||||
|
||||
@@ -292,8 +292,9 @@ describe("TaskCard", () => {
|
||||
});
|
||||
|
||||
it("repaints the memoized card when plannerOverseerState changes, and renders nothing when absent", () => {
|
||||
const idleTask = makeTask({ plannerOverseerState: undefined });
|
||||
const idleTask = makeTask({ plannerOversightLevel: "autonomous", plannerOverseerState: undefined });
|
||||
const watchingTask = makeTask({
|
||||
plannerOversightLevel: "autonomous",
|
||||
plannerOverseerState: {
|
||||
state: "watching",
|
||||
oversightLevel: "autonomous",
|
||||
@@ -370,6 +371,7 @@ describe("TaskCard", () => {
|
||||
it("explains an in-review awaiting-confirmation badge with a readable label and a reason-bearing tooltip", () => {
|
||||
const task = makeTask({
|
||||
column: "in-review",
|
||||
plannerOversightLevel: "autonomous",
|
||||
plannerOverseerState: {
|
||||
state: "awaiting-confirmation",
|
||||
oversightLevel: "autonomous",
|
||||
@@ -403,6 +405,7 @@ describe("TaskCard", () => {
|
||||
it("renders readable labels for in-progress watching and recovering overseer states", () => {
|
||||
const watchingTask = makeTask({
|
||||
column: "in-progress",
|
||||
plannerOversightLevel: "autonomous",
|
||||
plannerOverseerState: {
|
||||
state: "watching",
|
||||
oversightLevel: "autonomous",
|
||||
@@ -426,6 +429,7 @@ describe("TaskCard", () => {
|
||||
|
||||
const recoveringTask = makeTask({
|
||||
column: "in-progress",
|
||||
plannerOversightLevel: "autonomous",
|
||||
plannerOverseerState: {
|
||||
state: "recovering",
|
||||
oversightLevel: "autonomous",
|
||||
|
||||
Reference in New Issue
Block a user