FN-5695: sync feature assertions for late-added mission features
Close the assertion-graph gap by making mission feature assertions store-managed and automatically synchronized. - add `sourceFeatureId` to contract assertions (types, schema migration, persistence, snapshot restore) - auto-create/update/delete managed assertions when features are added, edited, or removed in `MissionStore` - remove duplicate/manual assertion creation from mission routes and rely on centralized store behavior - expand core, dashboard, engine, and plugin tests plus docs to cover validator behavior for later-added features Files changed: docs/missions.md | 11 ++- docs/storage.md | 2 +- packages/core/src/__tests__/db-migrate.test.ts | 12 ++-- packages/core/src/__tests__/db.test.ts | 34 ++++----- packages/core/src/__tests__/goals-schema.test.ts | 2 +- packages/core/src/__tests__/insight-store.test.ts | 10 +-- packages/core/src/__tests__/mission-store.test.ts | 82 +++++++++++++++++++--- packages/core/src/__tests__/run-audit.test.ts | 2 +- packages/core/src/__tests__/secrets-schema.test.ts | 6 +- .../core/src/__tests__/store-merge-queue.test.ts | 2 +- packages/core/src/__tests__/task-documents.test.ts | 2 +- packages/core/src/db.ts | 11 ++- packages/core/src/mission-store.ts | 69 ++++++++++++++++-- packages/core/src/mission-types.ts | 4 ++ .../dashboard/src/__tests__/mission-e2e.test.ts | 70 ++++++++++++------ packages/dashboard/src/mission-routes.ts | 16 +---- .../src/__tests__/mission-execution-loop.test.ts | 46 +++++++++++- .../src/store/__tests__/roadmap-store.test.ts | 4 +- 18 files changed, 288 insertions(+), 97 deletions(-) Fusion-Task-Id: FN-5695 Fusion-Task-Lineage: 337f030c-e883-41aa-b982-13ebe45bd5ee
This commit is contained in:
@@ -306,6 +306,31 @@ function createMockMissionStore() {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
features.set(feature.id, feature);
|
||||
|
||||
const slice = slices.get(sliceId);
|
||||
if (slice) {
|
||||
const text = input.acceptanceCriteria?.trim()
|
||||
|| input.description?.trim()
|
||||
|| `Verify implementation of: ${input.title}`;
|
||||
const existingAssertions = Array.from(assertions.values()).filter((a) => a.milestoneId === slice.milestoneId);
|
||||
const orderIndex = existingAssertions.length > 0
|
||||
? Math.max(...existingAssertions.map((a) => a.orderIndex)) + 1
|
||||
: 0;
|
||||
const assertion: MissionContractAssertion = {
|
||||
id: generateAssertionId(),
|
||||
milestoneId: slice.milestoneId,
|
||||
sourceFeatureId: feature.id,
|
||||
title: input.title,
|
||||
assertion: text,
|
||||
status: "pending",
|
||||
orderIndex,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
assertions.set(assertion.id, assertion);
|
||||
assertionLinks.push({ featureId: feature.id, assertionId: assertion.id });
|
||||
}
|
||||
|
||||
return feature;
|
||||
}),
|
||||
|
||||
@@ -416,6 +441,7 @@ function createMockMissionStore() {
|
||||
const assertion: MissionContractAssertion = {
|
||||
id: generateAssertionId(),
|
||||
milestoneId,
|
||||
sourceFeatureId: input.sourceFeatureId,
|
||||
title: input.title,
|
||||
assertion: input.assertion,
|
||||
status: input.status ?? "pending",
|
||||
@@ -2560,8 +2586,9 @@ describe("Mission API", () => {
|
||||
// Verify features are created
|
||||
expect(slice.features).toHaveLength(2);
|
||||
|
||||
// Verify assertions were created at milestone + slice + feature levels
|
||||
expect(missionStore.addContractAssertion).toHaveBeenCalledTimes(4);
|
||||
// Route now creates only milestone + slice assertions directly;
|
||||
// feature assertions are store-managed inside addFeature.
|
||||
expect(missionStore.addContractAssertion).toHaveBeenCalledTimes(2);
|
||||
|
||||
const milestoneCall = (missionStore.addContractAssertion as ReturnType<typeof vi.fn>).mock.calls.find(
|
||||
call => call[1].title === "Milestone: First Milestone"
|
||||
@@ -2573,23 +2600,22 @@ describe("Mission API", () => {
|
||||
);
|
||||
expect(sliceCall).toBeDefined();
|
||||
|
||||
// Verify Feature One assertion uses acceptanceCriteria
|
||||
const featureOneCall = (missionStore.addContractAssertion as ReturnType<typeof vi.fn>).mock.calls.find(
|
||||
call => call[1].title === "Feature One"
|
||||
);
|
||||
expect(featureOneCall).toBeDefined();
|
||||
expect(featureOneCall![1].assertion).toBe("Feature one criteria");
|
||||
expect(featureOneCall![1].title).toBe("Feature One");
|
||||
const featureOne = slice.features.find((f: MissionFeature) => f.title === "Feature One");
|
||||
const featureTwo = slice.features.find((f: MissionFeature) => f.title === "Feature Two");
|
||||
expect(featureOne).toBeDefined();
|
||||
expect(featureTwo).toBeDefined();
|
||||
|
||||
// Verify Feature Two assertion uses description (no acceptanceCriteria, has description)
|
||||
const featureTwoCall = (missionStore.addContractAssertion as ReturnType<typeof vi.fn>).mock.calls.find(
|
||||
call => call[1].title === "Feature Two"
|
||||
);
|
||||
expect(featureTwoCall).toBeDefined();
|
||||
expect(featureTwoCall![1].assertion).toBe("Feature two description");
|
||||
const featureOneAssertions = missionStore.listAssertionsForFeature(featureOne!.id);
|
||||
const featureTwoAssertions = missionStore.listAssertionsForFeature(featureTwo!.id);
|
||||
expect(featureOneAssertions).toHaveLength(1);
|
||||
expect(featureTwoAssertions).toHaveLength(1);
|
||||
expect(featureOneAssertions[0].assertion).toBe("Feature one criteria");
|
||||
expect(featureTwoAssertions[0].assertion).toBe("Feature two description");
|
||||
expect(featureOneAssertions[0].sourceFeatureId).toBe(featureOne!.id);
|
||||
expect(featureTwoAssertions[0].sourceFeatureId).toBe(featureTwo!.id);
|
||||
|
||||
// Verify assertions are linked to features
|
||||
expect(missionStore.linkFeatureToAssertion).toHaveBeenCalledTimes(2);
|
||||
// No route-level feature-linking call; linking is internal to addFeature
|
||||
expect(missionStore.linkFeatureToAssertion).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("uses fallback assertion text when feature has no acceptanceCriteria or description", async () => {
|
||||
@@ -2649,12 +2675,10 @@ describe("Mission API", () => {
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
|
||||
// Verify the assertion uses the fallback text
|
||||
const fallbackCall = (missionStore.addContractAssertion as ReturnType<typeof vi.fn>).mock.calls.find(
|
||||
call => call[1].title === "Minimal Feature"
|
||||
);
|
||||
expect(fallbackCall).toBeDefined();
|
||||
expect(fallbackCall![1].assertion).toBe("Verify implementation of: Minimal Feature");
|
||||
const feature = res.body.milestones[0].slices[0].features[0] as MissionFeature;
|
||||
const linkedAssertions = missionStore.listAssertionsForFeature(feature.id);
|
||||
expect(linkedAssertions).toHaveLength(1);
|
||||
expect(linkedAssertions[0].assertion).toBe("Verify implementation of: Minimal Feature");
|
||||
});
|
||||
|
||||
it("derives milestone acceptance criteria from feature acceptance criteria when omitted", async () => {
|
||||
|
||||
Reference in New Issue
Block a user