feat(FN-4652): complete Step 2 — narrow milestone rollup suppression

Fusion-Task-Id: FN-4652
Fusion-Task-Lineage: a660f18e-2f34-4e8a-9bca-3d1a55a2b448
This commit is contained in:
Fusion
2026-05-15 11:12:09 -07:00
committed by gsxdsm
parent aeada25a6c
commit 28fca5665b
2 changed files with 82 additions and 27 deletions

View File

@@ -3464,30 +3464,33 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
))}
{(!assertionsByMilestone.get(milestone.id) || assertionsByMilestone.get(milestone.id)?.length === 0)
&& !isCreatingAssertion
&& !milestone.acceptanceCriteria?.trim() && (
featuresWithAcceptanceCriteria.length > 0 ? (
// Product contract (2026-05-15 #product): completion criteria canonically
// live on MissionFeature.acceptanceCriteria; MissionContractAssertion rows
// are an additive milestone-level structure. Missing assertions do not
// mean missing criteria when child features already carry acceptance text.
// If FN-4578/4579/4580 (or successors) change the model, update this.
<div className="mission-assertions__list" data-testid="milestone-feature-acceptance-rollup">
<span className="mission-assertions__title">Completion criteria (from features)</span>
{featuresWithAcceptanceCriteria.map((feature) => (
<div key={feature.id} className="mission-assertion">
<span className="mission-assertion__title">{feature.title}</span>
<p className="mission-assertion__text">
<strong>Acceptance:</strong> {feature.acceptanceCriteria}
</p>
&& (
featuresWithAcceptanceCriteria.length > 0 ? (
// Product contract (2026-05-15 #product): completion criteria canonically
// live on MissionFeature.acceptanceCriteria; MissionContractAssertion rows
// are an additive milestone-level structure. Missing assertions do not
// mean missing criteria when child features already carry acceptance text,
// even when milestone.acceptanceCriteria is also populated (FN-4613/FN-4652).
// If FN-4578/4579/4580 (or successors) change the model, update this.
<div className="mission-assertions__list" data-testid="milestone-feature-acceptance-rollup">
<span className="mission-assertions__title">Completion criteria (from features)</span>
{featuresWithAcceptanceCriteria.map((feature) => (
<div key={feature.id} className="mission-assertion">
<span className="mission-assertion__title">{feature.title}</span>
<p className="mission-assertion__text">
<strong>Acceptance:</strong> {feature.acceptanceCriteria}
</p>
</div>
))}
</div>
) : (
!milestone.acceptanceCriteria?.trim() ? (
<div className="mission-manager__empty mission-assertions__empty">
<span>No assertions defined. Add one to define completion criteria.</span>
</div>
))}
</div>
) : (
<div className="mission-manager__empty mission-assertions__empty">
<span>No assertions defined. Add one to define completion criteria.</span>
</div>
)
)}
) : null
)
)}
</div>
</div>
</div>

View File

@@ -4339,7 +4339,7 @@ describe("MissionManager", () => {
expect(within(rollup).getByText("Feature two acceptance criteria", { exact: false })).toBeInTheDocument();
});
it("keeps milestone acceptance precedence over feature rollup", async () => {
it("shows feature rollup alongside milestone acceptance criteria", async () => {
const missionDetail = createFn4613MissionDetail();
globalThis.fetch = createDetailFetchMockForMissionDetail(missionDetail);
@@ -4351,7 +4351,57 @@ describe("MissionManager", () => {
await waitFor(() => {
expect(screen.getByText("Milestone two acceptance criteria", { exact: false })).toBeInTheDocument();
});
expect(screen.queryByTestId("milestone-feature-acceptance-rollup")).not.toBeInTheDocument();
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();
expect(rollup).toHaveClass("mission-assertions__list");
});
});
describe("FN-4652: feature acceptance coexists with milestone acceptance", () => {
it("renders milestone acceptance text and feature rollup together for milestone M1-like shape", async () => {
const missionDetail = JSON.parse(JSON.stringify(mockMissionDetail)) as typeof mockMissionDetail;
missionDetail.milestones[0].acceptanceCriteria = "Milestone-level acceptance summary for M1";
missionDetail.milestones[0].slices = [
{
...missionDetail.milestones[0].slices[0],
id: "SL-M1-A",
title: "Slice A",
features: [
{
...missionDetail.milestones[0].slices[0].features[0],
id: "F-M1-A",
title: "Orchestration flow",
acceptanceCriteria: "DAG branches execute in dependency order",
},
],
},
{
...missionDetail.milestones[0].slices[0],
id: "SL-M1-B",
title: "Slice B",
features: [
{
...missionDetail.milestones[0].slices[0].features[0],
id: "F-M1-B",
title: "Recovery behavior",
acceptanceCriteria: "Failed nodes retry with bounded backoff",
},
],
},
];
globalThis.fetch = createDetailFetchMockForMissionDetail(missionDetail);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
fireEvent.click(await screen.findByText("Build Auth System"));
await waitForDetailLoaded();
expect(screen.getByText("Milestone-level acceptance summary for M1", { exact: false })).toBeInTheDocument();
const rollup = await screen.findByTestId("milestone-feature-acceptance-rollup");
expect(rollup).toHaveClass("mission-assertions__list");
expect(within(rollup).getByText("DAG branches execute in dependency order", { exact: false })).toBeInTheDocument();
expect(within(rollup).getByText("Failed nodes retry with bounded backoff", { exact: false })).toBeInTheDocument();
});
});
@@ -4377,7 +4427,7 @@ describe("MissionManager", () => {
expect(screen.getAllByText(emptyAssertionsCopy)).toHaveLength(1);
});
it("hides feature acceptance rollup when milestone acceptance criteria already exists", async () => {
it("shows feature acceptance rollup when milestone acceptance criteria already exists", async () => {
const missionDetail = JSON.parse(JSON.stringify(mockMissionDetail)) as typeof mockMissionDetail;
missionDetail.milestones[0].acceptanceCriteria = "- Session handling: Session refresh succeeds without logout";
missionDetail.milestones[0].slices[0].features = [
@@ -4396,7 +4446,9 @@ describe("MissionManager", () => {
await waitForDetailLoaded();
expect(screen.getAllByText(/Acceptance:/).length).toBeGreaterThan(0);
expect(screen.queryByTestId("milestone-feature-acceptance-rollup")).not.toBeInTheDocument();
const rollup = await screen.findByTestId("milestone-feature-acceptance-rollup");
expect(within(rollup).getByText("Session handling")).toBeInTheDocument();
expect(within(rollup).getByText("Session refresh succeeds without logout", { exact: false })).toBeInTheDocument();
});
it("shows feature acceptance rollup instead of false empty-state when assertions are absent", async () => {