From 0bdc9bf4fbb6598de4426adc0da84f244eb8b198 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 11:42:15 -0700 Subject: [PATCH] fix(dashboard): archived tasks stayed in the research picker on a renamed board (#3215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## The defect The enrich-mode task picker filtered with `task.column !== "archived"`. On a board whose archive lane is renamed, that matched nothing — so filed-away tasks stayed in the picker and an operator could attach research findings to work they had deliberately archived. ## Census before / after | | before | after | |---|---|---| | COLUMN guards (backlog) | 10 | **9** | | `ResearchTaskActionModal.tsx` | 1 | **0 — converted** | Baseline re-recorded in the same commit; `--strict` green. ## This site was declined twice, and I wrote the second wrong estimate #3213 left it counted, correctly, on the note that was here — which was mine. Both prior cost estimates were wrong, so this corrects my own work: 1. **"Needs a data-fetch change"** — reasoned about `columnFlagsByTaskId`, a per-**task** map built from board-resident rows. Right that such a map can't help (archived rows are exactly what a board map omits), but this guard asks a per-**column** question, so it never needed one. 2. **"Needs prop threading, MainContent → ResearchView → here"** — right that the answer is column-keyed, wrong about where it lives. `ListView` builds `columnFlagsById` *inline*, which made it look like the owner. The data is `useBoardWorkflows`, a hook already called from `App`, `Board`, and `HeaderWorkflowSwitcherSlot`. **Measured cost: one file.** The modal already takes `projectId`, and `ResearchView` renders it only when a finding is open (`open` hardcoded beside `if (!finding) return null`) — so the hook cannot fetch for a closed modal, which was the one real objection to calling it here. Union across workflows keyed by column id, first declaration wins — the same convention `ListView` uses, so the two cannot disagree about a shared id. `isArchivedColumnRole` fail-softs to the legacy id when a column has no flags, so an unresolved workflow behaves exactly as the literal did. ## Tests — the invariant, not the repro Per the surface-enumeration rule, four cases: renamed archive lane, legacy id, unresolved workflow (fail-soft), and a second workflow's archive lane through the cross-workflow union. A repro-only test would pass on the legacy board and prove nothing about the case the guard exists for. **Anti-vacuity control:** | | renamed lane | union | legacy id | fail-soft | |---|---|---|---|---| | pre-fix literal | **FAIL** | **FAIL** | pass | pass | | converted | pass | pass | pass | pass | The legacy and fail-soft cases hold in both directions **on purpose** — they pin that this conversion did not change the pre-resolution answer. Flagging that so 4/4 isn't read as four independent proofs. ## Measured | check | result | |---|---| | `census --strict` / `check-fnxc-future-dates` | exit 0 / exit 0 | | `eslint` | clean | | `tsc -p tsconfig.app.json` (the config that actually covers `app/`) | exit 0 | | new tests | 4/4 | | `pnpm test:gate` | exit 0 (744 tests) | ## Note on process My first attempt at the control silently did nothing — the revert script threw a `SyntaxError`, so the "pre-fix" run was the fixed code and reported 4/4. Caught it because the error printed. The table above is from the re-run. --- ...earch-modal-archive-lane-resolved.test.tsx | 109 ++++++++++++++++++ .../components/ResearchTaskActionModal.tsx | 81 +++++++------ .../lib/lifecycle-column-census-baseline.json | 4 +- 3 files changed, 153 insertions(+), 41 deletions(-) create mode 100644 packages/dashboard/app/__tests__/research-modal-archive-lane-resolved.test.tsx diff --git a/packages/dashboard/app/__tests__/research-modal-archive-lane-resolved.test.tsx b/packages/dashboard/app/__tests__/research-modal-archive-lane-resolved.test.tsx new file mode 100644 index 0000000000..a33aa02050 --- /dev/null +++ b/packages/dashboard/app/__tests__/research-modal-archive-lane-resolved.test.tsx @@ -0,0 +1,109 @@ +/* +FNXC:WorkflowResolvedColumns 2026-07-31-11:55 (u12 — the last convertible census guard in this file): +The enrich-mode task picker filtered archived rows with `task.column !== "archived"`. On a board whose +archive lane is renamed, that matched nothing, so filed-away tasks stayed in the picker and an operator +could attach research findings to work they had deliberately archived. + +Asserted as the INVARIANT rather than the single repro, per the surface-enumeration rule: a renamed +archive lane, the legacy id, an unresolved workflow (fail-soft), and a second workflow's archive lane +in the cross-workflow union. A repro-only test here would pass on the legacy board and prove nothing +about the case the guard exists for. +*/ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import type { Task } from "@fusion/core"; + +import { ResearchTaskActionModal } from "../components/ResearchTaskActionModal"; +import { fetchTasks } from "../api"; +import { useBoardWorkflows } from "../hooks/useBoardWorkflows"; + +vi.mock("../api", () => ({ fetchTasks: vi.fn() })); +vi.mock("../hooks/useBoardWorkflows", () => ({ useBoardWorkflows: vi.fn() })); +vi.mock("../hooks/useMobileScrollLock", () => ({ useMobileScrollLock: vi.fn() })); +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (_key: string, fallback?: string) => fallback ?? _key }), +})); + +const task = (id: string, column: string): Task => ({ + id, title: `task ${id}`, description: "", column, + dependencies: [], steps: [], currentStep: 0, log: [], + createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", +} as unknown as Task); + +function workflows(columns: { id: string; flags: Record }[][]) { + return { + boardWorkflows: { + defaultWorkflowId: "wf0", + workflows: columns.map((cols, i) => ({ id: `wf${i}`, name: `wf${i}`, columns: cols })), + }, + }; +} + +/* +The kept tasks render into a , whose