**Stacked on #3025** (its commit is the parent here) — that PR fixes two producers of a dock/plugin-rendered `TaskCard`, and this adds the coverage for the second one. ## The gap #3025 correctly fixes **both** producers, which is the Surface Enumeration discipline working. Its test covers only `MainContent`. Measured by deleting the identical line from `useRightDockController`: ``` MainContent.graph-popout 6 passed RightDock 33 passed TaskCard.host-inventory 1 passed ``` All green with the dock's wiring gone. I checked every suite that touches that hook; none observes the prop. Its own test comment says: > REVERT CHECK: drop `taskColumnFlags` from **either** `renderTaskCard` and this reads "none". For this producer that is not true, and it is the producer that draws cards into the **right dock**, where an operator actually sees them. So the pair could quietly become a single again with every test still green. ## The test | state | result | |---|---| | #3025 as merged | 2/2 pass | | delete the dock's `taskColumnFlags={…}` line | **1 failed / 1 passed** | Driven through the real `renderTaskCard`, captured off the `renderProps` the controller hands `RightDock` — it is not on the returned controller object. `RightDock` itself is stubbed so the assertion cannot fail for unrelated dock plumbing. The paired negative asserts an unresolved task receives `undefined` rather than a fabricated object. That direction matters: inventing flags would make a card claim traits its board never declared, which is worse than the legacy fallback it replaces — the same *report, don't guess* reasoning as #2999's `!target` refusal. ## One harness note My first mock replaced `../RightDock` wholesale and the hook died on `No "readStoredRightDockOpen" export is defined on the mock` — the controller imports its persistence helpers from that module. Spreading `importOriginal()` and overriding only the two components fixes it. Recorded in the file because the next person stubbing this module will hit the same thing. **Verified:** 2/2, `tsc -p tsconfig.app.json` 0 errors in the new file, lint clean, FNXC gate exit 0. If #3025 lands first this rebases to a single test commit; if the two are taken together the stack applies as-is. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-02:20:
|
||||
THE DOCK'S `renderTaskCard` IS THE SECOND PRODUCER OF THIS AFFORDANCE, AND IT HAD NO TEST.
|
||||
|
||||
#3025 fixed both producers of a dock/plugin-rendered `TaskCard` — `MainContent.renderTaskCard` and
|
||||
this hook's — so that the card receives `taskColumnFlags` instead of resolving nothing and falling
|
||||
back to legacy lane ids for every role helper inside it (archive/revert affordances, progress, the
|
||||
elapsed-time indicator, the planning badge).
|
||||
|
||||
Its test covered `MainContent` only. MEASURED: deleting
|
||||
`taskColumnFlags={input.columnFlagsByTaskId?.get(task.id)}` from `useRightDockController` left the
|
||||
whole suite green — `MainContent.graph-popout` 6/6, `RightDock` 33/33, `TaskCard.host-inventory` 1/1.
|
||||
That PR's own revert-check note says dropping the prop "from either `renderTaskCard`" would show up;
|
||||
for this producer it did not.
|
||||
|
||||
So the pair could silently become a single again, on the producer that draws cards into the right
|
||||
dock where an operator actually sees them. One affordance with two producers needs two assertions —
|
||||
the Surface Enumeration rule applies to the coverage, not only to the fix.
|
||||
|
||||
Driven through the hook's real `renderTaskCard`, captured off the `renderProps` the controller hands
|
||||
`RightDock`: that function IS the contract this pins. `RightDock` itself is stubbed so the assertion
|
||||
cannot fail for unrelated dock plumbing.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { useRightDockController } from "../useRightDockController";
|
||||
|
||||
/* Capture the renderProps the controller builds; `renderTaskCard` is not on the returned controller. */
|
||||
const captured: { renderTaskCard?: (task: Task) => React.ReactNode } = {};
|
||||
vi.mock("../RightDock", async (importOriginal) => ({
|
||||
/* Keep the module's real exports — the controller imports its persistence helpers from here, and a
|
||||
bare factory silently drops them ("No readStoredRightDockOpen export is defined on the mock"). */
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
RightDock: ({ renderProps }: { renderProps: { renderTaskCard?: (task: Task) => React.ReactNode } }) => {
|
||||
captured.renderTaskCard = renderProps?.renderTaskCard;
|
||||
return <aside data-testid="dock" />;
|
||||
},
|
||||
RightDockExpandModal: () => null,
|
||||
}));
|
||||
|
||||
/* The probe: absent means the card resolved nothing, which is the pre-#3025 behaviour. */
|
||||
const seen: (Record<string, boolean | undefined> | undefined)[] = [];
|
||||
vi.mock("../TaskCard", () => ({
|
||||
TaskCard: ({ taskColumnFlags }: { taskColumnFlags?: Record<string, boolean | undefined> }) => {
|
||||
seen.push(taskColumnFlags);
|
||||
return <article data-testid="dock-card" />;
|
||||
},
|
||||
}));
|
||||
|
||||
const task = {
|
||||
id: "KB-DOCK-1",
|
||||
title: "a docked card",
|
||||
description: "t",
|
||||
column: "shipped",
|
||||
createdAt: "2026-06-01T00:00:00.000Z",
|
||||
updatedAt: "2026-06-01T00:00:00.000Z",
|
||||
steps: [],
|
||||
} as unknown as Task;
|
||||
|
||||
/** `shipped` is this board's complete lane — it simply is not called `done`. */
|
||||
const SHIPPED_IS_COMPLETE = { complete: true } as const;
|
||||
|
||||
const noop = () => {};
|
||||
const asyncNoop = async () => task;
|
||||
|
||||
function controllerInput(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
active: true,
|
||||
projectId: "proj-1",
|
||||
addToast: noop,
|
||||
settingsLoaded: true,
|
||||
researchReadinessVersion: 0,
|
||||
tasks: [task],
|
||||
columnFlagsByTaskId: new Map([[task.id, SHIPPED_IS_COMPLETE]]),
|
||||
workflowSteps: [],
|
||||
subscribePluginEvents: () => () => {},
|
||||
openDetailTask: noop,
|
||||
openTaskPopup: noop,
|
||||
openMobileTasksInPopup: false,
|
||||
openFileInBrowser: noop,
|
||||
onMoveTask: asyncNoop,
|
||||
onDeleteTask: asyncNoop,
|
||||
onMergeTask: asyncNoop,
|
||||
openSettings: noop,
|
||||
...overrides,
|
||||
} as never;
|
||||
}
|
||||
|
||||
/** Renders the hook and draws one card through the real `renderTaskCard`. */
|
||||
function renderDockCard(input = controllerInput()) {
|
||||
captured.renderTaskCard = undefined;
|
||||
function Harness() {
|
||||
const controller = useRightDockController(input);
|
||||
return <>{controller.dock}{captured.renderTaskCard ? captured.renderTaskCard(task) : null}</>;
|
||||
}
|
||||
const result = render(<Harness />);
|
||||
/* First pass mounts the dock and captures the callback; the second draws the card with it. */
|
||||
result.rerender(<Harness />);
|
||||
return result;
|
||||
}
|
||||
|
||||
describe("the dock's renderTaskCard hands the card its resolved column traits", () => {
|
||||
it("passes the task's own flags through to the card", () => {
|
||||
seen.length = 0;
|
||||
renderDockCard();
|
||||
|
||||
/* Without the wiring this is `undefined` and every role helper in the card reads legacy ids. */
|
||||
expect(seen[seen.length - 1]).toEqual(SHIPPED_IS_COMPLETE);
|
||||
});
|
||||
|
||||
/*
|
||||
The paired negative: a card with no resolved entry must receive `undefined`, not a fabricated
|
||||
object. The role helpers document that fallback, and inventing flags here would make an unresolved
|
||||
card claim traits its board never declared — worse than the legacy default it replaces.
|
||||
*/
|
||||
it("passes undefined when the board has resolved no traits for that task", () => {
|
||||
seen.length = 0;
|
||||
renderDockCard(controllerInput({ columnFlagsByTaskId: new Map() }));
|
||||
|
||||
expect(seen[seen.length - 1]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user