feat(FN-4604): complete Step 1 — add milestone acceptance derivation helper
Fusion-Task-Id: FN-4604 Fusion-Task-Lineage: 71752cd0-eadb-4434-a2a3-d9fff562ea63
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { MissionStore } from "../mission-store.js";
|
||||
import { MissionStore, deriveMilestoneAcceptanceCriteriaFromFeatures } from "../mission-store.js";
|
||||
import { Database } from "../db.js";
|
||||
import type { MissionFeature } from "../mission-types.js";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -916,6 +917,77 @@ describe("MissionStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("milestone acceptance criteria derivation", () => {
|
||||
const makeFeature = (overrides: Partial<MissionFeature>): MissionFeature => ({
|
||||
id: "F-1",
|
||||
sliceId: "SL-1",
|
||||
title: "Feature",
|
||||
status: "defined",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it("derives milestone acceptance from feature acceptance criteria", () => {
|
||||
const derived = deriveMilestoneAcceptanceCriteriaFromFeatures([
|
||||
makeFeature({ title: "Login", acceptanceCriteria: " Auth succeeds " }),
|
||||
]);
|
||||
|
||||
expect(derived).toBe("- Login: Auth succeeds");
|
||||
});
|
||||
|
||||
it("falls back to feature description when acceptance criteria is blank", () => {
|
||||
const derived = deriveMilestoneAcceptanceCriteriaFromFeatures([
|
||||
makeFeature({ title: "Login", acceptanceCriteria: " ", description: " Works across browsers " }),
|
||||
]);
|
||||
|
||||
expect(derived).toBe("- Login: Works across browsers");
|
||||
});
|
||||
|
||||
it("skips features without acceptance text and returns undefined when none contribute", () => {
|
||||
const derived = deriveMilestoneAcceptanceCriteriaFromFeatures([
|
||||
makeFeature({ title: "Login", acceptanceCriteria: "", description: " " }),
|
||||
]);
|
||||
|
||||
expect(derived).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not overwrite explicit milestone acceptance criteria", () => {
|
||||
const mission = store.createMission({ title: "Mission" });
|
||||
const milestone = store.addMilestone(mission.id, {
|
||||
title: "Milestone",
|
||||
acceptanceCriteria: "Explicit criteria",
|
||||
});
|
||||
const slice = store.addSlice(milestone.id, { title: "Slice" });
|
||||
store.addFeature(slice.id, {
|
||||
title: "Feature",
|
||||
acceptanceCriteria: "Feature criteria",
|
||||
});
|
||||
|
||||
const updated = store.applyDerivedMilestoneAcceptanceCriteria(milestone.id);
|
||||
expect(updated.acceptanceCriteria).toBe("Explicit criteria");
|
||||
});
|
||||
|
||||
it("preserves explicit milestone criteria when re-applied after feature changes", () => {
|
||||
const mission = store.createMission({ title: "Mission" });
|
||||
const milestone = store.addMilestone(mission.id, { title: "Milestone" });
|
||||
const slice = store.addSlice(milestone.id, { title: "Slice" });
|
||||
const feature = store.addFeature(slice.id, {
|
||||
title: "Feature",
|
||||
acceptanceCriteria: "Initial acceptance",
|
||||
});
|
||||
|
||||
const firstDerived = store.applyDerivedMilestoneAcceptanceCriteria(milestone.id);
|
||||
expect(firstDerived.acceptanceCriteria).toBe("- Feature: Initial acceptance");
|
||||
|
||||
store.updateMilestone(milestone.id, { acceptanceCriteria: "Manual lock" });
|
||||
store.updateFeature(feature.id, { acceptanceCriteria: "Changed acceptance" });
|
||||
|
||||
const preserved = store.applyDerivedMilestoneAcceptanceCriteria(milestone.id);
|
||||
expect(preserved.acceptanceCriteria).toBe("Manual lock");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Slice CRUD Tests ──────────────────────────────────────────────────
|
||||
|
||||
describe("Slice CRUD", () => {
|
||||
|
||||
@@ -62,6 +62,32 @@ import {
|
||||
*/
|
||||
const DEFAULT_IMPLEMENTATION_RETRY_BUDGET = 3;
|
||||
|
||||
export function deriveMilestoneAcceptanceCriteriaFromFeatures(features: MissionFeature[]): string | undefined {
|
||||
const lines = features
|
||||
.map((feature) => {
|
||||
const acceptance = feature.acceptanceCriteria?.trim();
|
||||
const description = feature.description?.trim();
|
||||
const text = acceptance && acceptance.length > 0
|
||||
? acceptance
|
||||
: description && description.length > 0
|
||||
? description
|
||||
: undefined;
|
||||
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `- ${feature.title}: ${text}`;
|
||||
})
|
||||
.filter((line): line is string => Boolean(line));
|
||||
|
||||
if (lines.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ── Mission Summary Type ─────────────────────────────────────────────
|
||||
|
||||
/** Status summary for a mission, computed from its hierarchy. */
|
||||
@@ -1341,6 +1367,26 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
return this.updateMilestone(id, { interviewState: state });
|
||||
}
|
||||
|
||||
applyDerivedMilestoneAcceptanceCriteria(milestoneId: string): Milestone {
|
||||
const milestone = this.getMilestone(milestoneId);
|
||||
if (!milestone) {
|
||||
throw new Error(`Milestone ${milestoneId} not found`);
|
||||
}
|
||||
|
||||
if (milestone.acceptanceCriteria?.trim()) {
|
||||
return milestone;
|
||||
}
|
||||
|
||||
const features = this.listSlices(milestoneId).flatMap((slice) => this.listFeatures(slice.id));
|
||||
const derivedAcceptanceCriteria = deriveMilestoneAcceptanceCriteriaFromFeatures(features);
|
||||
|
||||
if (!derivedAcceptanceCriteria) {
|
||||
return milestone;
|
||||
}
|
||||
|
||||
return this.updateMilestone(milestoneId, { acceptanceCriteria: derivedAcceptanceCriteria });
|
||||
}
|
||||
|
||||
// ── Slice Operations ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1664,6 +1710,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
// update the parent milestone and mission statuses. Calling recomputeSliceStatus
|
||||
// here ensures the full chain is updated atomically when a feature is added.
|
||||
this.recomputeSliceStatus(sliceId);
|
||||
this.applyDerivedMilestoneAcceptanceCriteria(slice.milestoneId);
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user