feat(FN-4578): complete Step 2 — persist milestone acceptance criteria

Fusion-Task-Id: FN-4578
Fusion-Task-Lineage: 1be0fa08-4d83-4988-a218-326225dfe4b5
This commit is contained in:
Fusion
2026-05-15 02:34:55 -07:00
committed by gsxdsm
parent 9415e28fbd
commit 3c205f331b
2 changed files with 74 additions and 6 deletions

View File

@@ -790,6 +790,69 @@ describe("MissionStore", () => {
expect(updated.status).toBe("active");
});
it("persists milestone acceptance criteria on create", () => {
const mission = store.createMission({ title: "Parent" });
const milestone = store.addMilestone(mission.id, {
title: "Original",
acceptanceCriteria: "Ship all phase outputs",
});
const fetched = store.getMilestone(milestone.id);
expect(fetched?.acceptanceCriteria).toBe("Ship all phase outputs");
});
it("updates milestone acceptance criteria and persists across reopen", () => {
const fileDb = new Database(fusionDir);
fileDb.init();
const fileStore = new MissionStore(fusionDir, fileDb);
const mission = fileStore.createMission({ title: "Parent" });
const milestone = fileStore.addMilestone(mission.id, { title: "Original" });
fileStore.updateMilestone(milestone.id, { acceptanceCriteria: "All validators pass" });
fileDb.close();
const reopenedDb = new Database(fusionDir);
reopenedDb.init();
const reopenedStore = new MissionStore(fusionDir, reopenedDb);
const reopened = reopenedStore.getMilestone(milestone.id);
expect(reopened?.acceptanceCriteria).toBe("All validators pass");
reopenedDb.close();
});
it("partial milestone acceptance criteria update preserves other fields", () => {
const mission = store.createMission({ title: "Parent" });
const milestone = store.addMilestone(mission.id, {
title: "Original",
description: "Phase 1",
verification: "Run smoke tests",
});
const updated = store.updateMilestone(milestone.id, {
acceptanceCriteria: "Phase complete when smoke tests pass",
});
expect(updated.title).toBe("Original");
expect(updated.description).toBe("Phase 1");
expect(updated.verification).toBe("Run smoke tests");
expect(updated.acceptanceCriteria).toBe("Phase complete when smoke tests pass");
});
it("clears milestone acceptance criteria when updated with undefined", () => {
const mission = store.createMission({ title: "Parent" });
const milestone = store.addMilestone(mission.id, {
title: "Original",
acceptanceCriteria: "Initial criteria",
});
const updated = store.updateMilestone(milestone.id, { acceptanceCriteria: undefined });
const fetched = store.getMilestone(milestone.id);
expect(updated.acceptanceCriteria).toBeUndefined();
expect(fetched?.acceptanceCriteria).toBeUndefined();
});
it("deletes a milestone", () => {
const mission = store.createMission({ title: "Parent" });
const milestone = store.addMilestone(mission.id, { title: "To Delete" });

View File

@@ -335,6 +335,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
dependencies: fromJson<string[]>(row.dependencies) || [],
planningNotes: row.planningNotes || undefined,
verification: row.verification || undefined,
acceptanceCriteria: row.acceptanceCriteria || undefined,
validationState: (row.validationState as MilestoneValidationState) || "not_started",
createdAt: row.createdAt,
updatedAt: row.updatedAt,
@@ -1158,14 +1159,15 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
dependencies: input.dependencies || [],
planningNotes: input.planningNotes,
verification: input.verification,
acceptanceCriteria: input.acceptanceCriteria,
validationState: "not_started",
createdAt: now,
updatedAt: now,
};
this.db.prepare(`
INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, validationState, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, acceptanceCriteria, validationState, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
milestone.id,
milestone.missionId,
@@ -1177,6 +1179,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
toJson(milestone.dependencies),
milestone.planningNotes ?? null,
milestone.verification ?? null,
milestone.acceptanceCriteria ?? null,
milestone.validationState as string,
milestone.createdAt,
milestone.updatedAt,
@@ -1245,6 +1248,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
dependencies = ?,
planningNotes = ?,
verification = ?,
acceptanceCriteria = ?,
validationState = ?,
updatedAt = ?
WHERE id = ?
@@ -1257,6 +1261,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
toJson(updated.dependencies),
updated.planningNotes ?? null,
updated.verification ?? null,
updated.acceptanceCriteria ?? null,
updated.validationState || "not_started",
updated.updatedAt,
updated.id,
@@ -3340,14 +3345,14 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
}
for (const milestone of snapshot.payload.milestones) {
this.db.prepare(`INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, validationState, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
this.db.prepare(`INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, acceptanceCriteria, validationState, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title=excluded.title, description=excluded.description, status=excluded.status, orderIndex=excluded.orderIndex,
interviewState=excluded.interviewState, dependencies=excluded.dependencies, planningNotes=excluded.planningNotes, verification=excluded.verification,
validationState=excluded.validationState, updatedAt=excluded.updatedAt`).run(
acceptanceCriteria=excluded.acceptanceCriteria, validationState=excluded.validationState, updatedAt=excluded.updatedAt`).run(
milestone.id, milestone.missionId, milestone.title, milestone.description ?? null, milestone.status, milestone.orderIndex,
milestone.interviewState, toJsonNullable(milestone.dependencies), milestone.planningNotes ?? null, milestone.verification ?? null,
milestone.validationState ?? null, milestone.createdAt, milestone.updatedAt,
milestone.acceptanceCriteria ?? null, milestone.validationState ?? null, milestone.createdAt, milestone.updatedAt,
);
applied++;
}