feat(FN-4613): complete Step 3 — guard acceptance visibility regressions
Fusion-Task-Id: FN-4613 Fusion-Task-Lineage: 07ecbe9c-cfee-42d8-b9d2-8b99b2cce9e6
This commit is contained in:
@@ -824,30 +824,56 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
return;
|
||||
}
|
||||
setSelectedMission(data);
|
||||
// Auto-expand first milestone and slice
|
||||
if (data.milestones.length > 0) {
|
||||
const firstMilestoneId = data.milestones[0].id;
|
||||
setSelectedMilestoneId(firstMilestoneId);
|
||||
const milestoneIds = new Set(data.milestones.map((milestone) => milestone.id));
|
||||
const selectedMilestoneStillExists = selectedMilestoneIdRef.current
|
||||
&& milestoneIds.has(selectedMilestoneIdRef.current);
|
||||
const nextSelectedMilestoneId = selectedMilestoneStillExists
|
||||
? selectedMilestoneIdRef.current
|
||||
: firstMilestoneId;
|
||||
|
||||
setSelectedMilestoneId(nextSelectedMilestoneId);
|
||||
setValidationRoundsExpanded(true);
|
||||
setExpandedMilestones(new Set([firstMilestoneId]));
|
||||
// Load assertions and validation rollup for the first milestone (inline to avoid forward ref)
|
||||
fetchAssertions(firstMilestoneId, projectId).then((assertions) => {
|
||||
|
||||
// FN-4613: preserve user-expanded milestones/slices across refetch instead of resetting to first-only.
|
||||
setExpandedMilestones((prev) => {
|
||||
const next = new Set(Array.from(prev).filter((milestoneId) => milestoneIds.has(milestoneId)));
|
||||
next.add(nextSelectedMilestoneId);
|
||||
return next;
|
||||
});
|
||||
|
||||
const availableSliceIds = new Set(
|
||||
data.milestones.flatMap((milestone) => milestone.slices.map((slice) => slice.id)),
|
||||
);
|
||||
const selectedMilestone = data.milestones.find((milestone) => milestone.id === nextSelectedMilestoneId);
|
||||
const selectedMilestoneFirstSliceId = selectedMilestone?.slices[0]?.id ?? null;
|
||||
setExpandedSlices((prev) => {
|
||||
const next = new Set(Array.from(prev).filter((sliceId) => availableSliceIds.has(sliceId)));
|
||||
if (selectedMilestoneFirstSliceId) {
|
||||
const hasExpandedSliceForSelectedMilestone = selectedMilestone?.slices.some((slice) => next.has(slice.id)) ?? false;
|
||||
if (!hasExpandedSliceForSelectedMilestone) {
|
||||
next.add(selectedMilestoneFirstSliceId);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
// Load assertions and validation rollup for the selected milestone.
|
||||
fetchAssertions(nextSelectedMilestoneId, projectId).then((assertions) => {
|
||||
setAssertionsByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(firstMilestoneId, assertions);
|
||||
next.set(nextSelectedMilestoneId, assertions);
|
||||
return next;
|
||||
});
|
||||
}).catch(() => { /* silently fail */ });
|
||||
fetchMilestoneValidation(firstMilestoneId, projectId).then((rollup) => {
|
||||
fetchMilestoneValidation(nextSelectedMilestoneId, projectId).then((rollup) => {
|
||||
setValidationRollupByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(firstMilestoneId, rollup);
|
||||
next.set(nextSelectedMilestoneId, rollup);
|
||||
return next;
|
||||
});
|
||||
}).catch(() => { /* silently fail */ });
|
||||
if (data.milestones[0].slices.length > 0) {
|
||||
setExpandedSlices(new Set([data.milestones[0].slices[0].id]));
|
||||
}
|
||||
} else {
|
||||
setSelectedMilestoneId(null);
|
||||
setValidationTelemetry(null);
|
||||
@@ -898,6 +924,18 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
|
||||
useEffect(() => {
|
||||
setValidationRoundsExpanded(true);
|
||||
if (!selectedMilestoneId) {
|
||||
return;
|
||||
}
|
||||
// FN-4613: selecting a milestone from any navigation path keeps its acceptance criteria visible.
|
||||
setExpandedMilestones((prev) => {
|
||||
if (prev.has(selectedMilestoneId)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Set(prev);
|
||||
next.add(selectedMilestoneId);
|
||||
return next;
|
||||
});
|
||||
}, [selectedMilestoneId]);
|
||||
|
||||
const refreshValidationTelemetry = useCallback((milestoneId: string) => {
|
||||
|
||||
@@ -4234,6 +4234,127 @@ describe("MissionManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("FN-4613: persisted acceptance criteria visibility", () => {
|
||||
const createFn4613MissionDetail = () => {
|
||||
const missionDetail = JSON.parse(JSON.stringify(mockMissionDetail)) as typeof mockMissionDetail;
|
||||
missionDetail.milestones = [
|
||||
{
|
||||
...missionDetail.milestones[0],
|
||||
id: "MS-001",
|
||||
title: "Milestone One",
|
||||
acceptanceCriteria: "",
|
||||
slices: [
|
||||
{
|
||||
...missionDetail.milestones[0].slices[0],
|
||||
id: "SL-001",
|
||||
title: "Slice One",
|
||||
features: [
|
||||
{
|
||||
...missionDetail.milestones[0].slices[0].features[0],
|
||||
id: "F-001",
|
||||
title: "Feature One",
|
||||
acceptanceCriteria: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...missionDetail.milestones[0],
|
||||
id: "MS-002",
|
||||
title: "Milestone Two",
|
||||
acceptanceCriteria: "Milestone two acceptance criteria",
|
||||
slices: [
|
||||
{
|
||||
...missionDetail.milestones[0].slices[0],
|
||||
id: "SL-002",
|
||||
title: "Slice Two",
|
||||
milestoneId: "MS-002",
|
||||
features: [
|
||||
{
|
||||
...missionDetail.milestones[0].slices[0].features[0],
|
||||
id: "F-002",
|
||||
title: "Feature Two",
|
||||
sliceId: "SL-002",
|
||||
acceptanceCriteria: "Feature two acceptance criteria",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return missionDetail;
|
||||
};
|
||||
|
||||
it("keeps non-first milestone acceptance discoverable on initial load", async () => {
|
||||
globalThis.fetch = createDetailFetchMockForMissionDetail(createFn4613MissionDetail());
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
fireEvent.click(await screen.findByText("Build Auth System"));
|
||||
await waitForDetailLoaded("Milestone One");
|
||||
|
||||
fireEvent.click(screen.getByText("Milestone Two"));
|
||||
expect(screen.getByText("Milestone two acceptance criteria", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("preserves expanded non-first milestone after mission detail refetch", async () => {
|
||||
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
|
||||
const missionDetail = createFn4613MissionDetail();
|
||||
const fetchMock = createDetailFetchMockForMissionDetail(missionDetail);
|
||||
globalThis.fetch = fetchMock;
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
fireEvent.click(await screen.findByText("Build Auth System"));
|
||||
await waitForDetailLoaded("Milestone One");
|
||||
|
||||
fireEvent.click(screen.getByText("Milestone Two"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Milestone two acceptance criteria", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
for (const source of MockEventSource.instances) {
|
||||
source.emit("mission:updated", { id: "M-001", title: "Build Auth System", status: "active" });
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Milestone two acceptance criteria", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes("/api/missions/M-001")).length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("surfaces feature acceptance for selected milestone without requiring slice expansion", async () => {
|
||||
const missionDetail = createFn4613MissionDetail();
|
||||
missionDetail.milestones[1].acceptanceCriteria = "";
|
||||
globalThis.fetch = createDetailFetchMockForMissionDetail(missionDetail);
|
||||
|
||||
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByText("Build Auth System"));
|
||||
await waitForDetailLoaded("Milestone One");
|
||||
|
||||
fireEvent.click(screen.getByText("Milestone Two"));
|
||||
const rollup = await screen.findByTestId("milestone-feature-acceptance-rollup");
|
||||
expect(within(rollup).getByText("Feature Two")).toBeInTheDocument();
|
||||
expect(within(rollup).getByText("Feature two acceptance criteria", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps milestone acceptance precedence over feature rollup", async () => {
|
||||
const missionDetail = createFn4613MissionDetail();
|
||||
globalThis.fetch = createDetailFetchMockForMissionDetail(missionDetail);
|
||||
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByText("Build Auth System"));
|
||||
await waitForDetailLoaded("Milestone One");
|
||||
|
||||
fireEvent.click(screen.getByText("Milestone Two"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Milestone two acceptance criteria", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId("milestone-feature-acceptance-rollup")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("milestone assertions empty-state", () => {
|
||||
const emptyAssertionsCopy = "No assertions defined. Add one to define completion criteria.";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user