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:
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user