FN-5920: prefer mission detail event count for activity tab
Use mission detail event counts as the authoritative pre-load source for mission activity. - add mission hierarchy eventCount support in core and dashboard mission types - return mission eventCount from mission detail APIs and document the field - prefer the detail eventCount over stale list summaries before activity events load - add coverage for store, API, and MissionManager pre-load count behavior Files changed: docs/missions.md | 2 +- packages/core/src/__tests__/mission-store.test.ts | 18 +++++++++ packages/core/src/mission-store.ts | 6 +++ packages/core/src/mission-types.ts | 2 + packages/dashboard/app/api/legacy.ts | 2 + packages/dashboard/app/components/MissionManager.tsx | 12 +++++- packages/dashboard/app/components/__tests__/MissionManager.test.tsx | 46 +++++++++++++++++++++- packages/dashboard/app/components/mission-types.ts | 1 + packages/dashboard/src/__tests__/mission-e2e.test.ts | 2 + 9 files changed, 86 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-5920 Fusion-Task-Lineage: a0269499-d34a-495d-89fc-0534bb62a94e
This commit is contained in:
@@ -6980,6 +6980,8 @@ export interface SliceWithFeatures extends Slice {
|
||||
|
||||
/** Full mission hierarchy */
|
||||
export interface MissionWithHierarchy extends Mission {
|
||||
/** Unfiltered total of all mission lifecycle events, matching MissionSummary.eventCount and getMissionEvents total with no eventType filter */
|
||||
eventCount?: number;
|
||||
milestones: MilestoneWithSlices[];
|
||||
}
|
||||
|
||||
|
||||
@@ -922,8 +922,16 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
if (!selectedMission?.id) {
|
||||
return eventsTotal;
|
||||
}
|
||||
return missions.find((mission) => mission.id === selectedMission.id)?.summary?.eventCount ?? eventsTotal;
|
||||
}, [eventsTotal, missions, selectedMission?.id]);
|
||||
|
||||
const baseCount = selectedMission.eventCount
|
||||
?? missions.find((mission) => mission.id === selectedMission.id)?.summary?.eventCount;
|
||||
|
||||
if (baseCount == null) {
|
||||
return eventsTotal;
|
||||
}
|
||||
|
||||
return Math.max(baseCount, eventsTotal);
|
||||
}, [eventsTotal, missions, selectedMission?.eventCount, selectedMission?.id]);
|
||||
|
||||
const displayedMissionEvents = useMemo(() => [...missionEvents].reverse(), [missionEvents]);
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ const mockMissionDetail = {
|
||||
title: "Build Auth System",
|
||||
description: "Complete authentication flow",
|
||||
status: "planning",
|
||||
eventCount: 4,
|
||||
linkedGoals: [] as Array<{ id: string; title: string; status: "active" | "archived"; createdAt: string; updatedAt: string; description?: string }>,
|
||||
milestones: [
|
||||
{
|
||||
@@ -651,6 +652,8 @@ function createDetailFetchMockForMissionDetail(
|
||||
missionDetail: typeof mockMissionDetail,
|
||||
telemetryOverride: unknown = mockMilestoneValidationTelemetry,
|
||||
assertionsResponse: unknown[] = [],
|
||||
missionsResponse = mockMissions,
|
||||
eventsResponse = mockMissionEvents,
|
||||
) {
|
||||
return vi.fn().mockImplementation((url: string) => {
|
||||
if (url.includes("/missions/health")) {
|
||||
@@ -658,7 +661,7 @@ function createDetailFetchMockForMissionDetail(
|
||||
}
|
||||
|
||||
if (url.includes("/events")) {
|
||||
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, mockMissionEvents)));
|
||||
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, eventsResponse)));
|
||||
}
|
||||
|
||||
if (url.includes("/health")) {
|
||||
@@ -686,7 +689,7 @@ function createDetailFetchMockForMissionDetail(
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(mockApiResponse(mockMissions));
|
||||
return Promise.resolve(mockApiResponse(missionsResponse));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1228,6 +1231,44 @@ describe("MissionManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers mission detail event count when the list summary is stale before activity events load", async () => {
|
||||
const staleSummaryMissions = mockMissions.map((mission) => mission.id === "M-001"
|
||||
? {
|
||||
...mission,
|
||||
summary: {
|
||||
...mission.summary,
|
||||
eventCount: 0,
|
||||
},
|
||||
}
|
||||
: mission);
|
||||
const missionDetailWithAuthoritativeCount = {
|
||||
...mockMissionDetail,
|
||||
eventCount: 7,
|
||||
};
|
||||
|
||||
globalThis.fetch = createDetailFetchMockForMissionDetail(
|
||||
missionDetailWithAuthoritativeCount,
|
||||
mockMilestoneValidationTelemetry,
|
||||
[],
|
||||
staleSummaryMissions,
|
||||
[],
|
||||
);
|
||||
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
|
||||
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Build Auth System")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByText("Build Auth System"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mission-tab-activity")).toHaveTextContent("Activity (7)");
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("mission-activity-events")).toBeNull();
|
||||
});
|
||||
|
||||
it("auto-scrolls to the latest mission activity on initial load", async () => {
|
||||
globalThis.fetch = createDetailFetchMock(mockMissionEvents);
|
||||
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
|
||||
@@ -1309,6 +1350,7 @@ describe("MissionManager", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Real-time warning event")).toBeDefined();
|
||||
expect(screen.getByTestId("mission-tab-activity")).toHaveTextContent("Activity (5)");
|
||||
expect(scrollIntoViewSpy).toHaveBeenLastCalledWith({ block: "end", behavior: "auto" });
|
||||
});
|
||||
|
||||
|
||||
@@ -270,6 +270,7 @@ export type MissionWithSummary = Mission & { summary?: MissionSummary };
|
||||
export interface MissionWithHierarchy extends Mission {
|
||||
linkedGoals?: Goal[];
|
||||
milestones: Milestone[];
|
||||
eventCount?: number;
|
||||
}
|
||||
|
||||
/** Mission event categories emitted by mission observability APIs. */
|
||||
|
||||
Reference in New Issue
Block a user