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:
gsxdsm
2026-06-02 23:47:32 -07:00
parent 6d3b1c9810
commit 0971eced30
9 changed files with 86 additions and 5 deletions

View File

@@ -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]);

View File

@@ -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" });
});

View File

@@ -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. */