feat(FN-1569): add mission validation telemetry APIs and UI

- Emit fix-feature mission events and forward validator telemetry through mission execution
- Add core and dashboard API types plus a validation telemetry route in mission-routes
- Update SSE mission event handling so validation telemetry updates stream reliably to clients
- Add telemetry surfaces in MissionManager with corresponding mission-specific UI styling
- Expand MissionManager and mission e2e test coverage for validation telemetry flows
This commit is contained in:
Fusion
2026-04-16 23:33:05 -07:00
committed by gsxdsm
parent c0e68f21d8
commit 0d94bc39df
11 changed files with 1242 additions and 78 deletions

View File

@@ -22,6 +22,8 @@ import type {
MissionHealth,
MissionContractAssertion,
ContractAssertionCreateInput,
MissionValidatorRun,
MissionAssertionFailureRecord,
} from "@fusion/core";
import type { AiSessionRow } from "./ai-session-store.js";
import {
@@ -45,6 +47,8 @@ function createMockMissionStore() {
const missionEvents: Map<string, MissionEvent[]> = new Map();
const assertions: Map<string, MissionContractAssertion> = new Map();
const assertionLinks: Array<{ featureId: string; assertionId: string }> = [];
const validatorRuns: Map<string, MissionValidatorRun> = new Map();
const runFailures: Map<string, MissionAssertionFailureRecord[]> = new Map();
let missionCounter = 1;
let milestoneCounter = 1;
@@ -421,6 +425,39 @@ function createMockMissionStore() {
assertionLinks.push({ featureId, assertionId });
}),
listAssertionsForFeature: vi.fn((featureId: string) =>
assertionLinks
.filter((link) => link.featureId === featureId)
.map((link) => assertions.get(link.assertionId))
.filter((assertion): assertion is MissionContractAssertion => Boolean(assertion))
),
listFeaturesForAssertion: vi.fn((assertionId: string) =>
assertionLinks
.filter((link) => link.assertionId === assertionId)
.map((link) => features.get(link.featureId))
.filter((feature): feature is MissionFeature => Boolean(feature))
),
getValidatorRunsByFeature: vi.fn((featureId: string) =>
Array.from(validatorRuns.values())
.filter((run) => run.featureId === featureId)
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
),
getFailuresForRun: vi.fn((runId: string) => runFailures.get(runId) ?? []),
getMilestoneValidationRollup: vi.fn((milestoneId: string) => ({
milestoneId,
totalAssertions: 0,
passedAssertions: 0,
failedAssertions: 0,
blockedAssertions: 0,
pendingAssertions: 0,
unlinkedAssertions: 0,
state: "not_started",
})),
reorderMilestones: vi.fn((missionId: string, orderedIds: string[]) => {
orderedIds.forEach((id, index) => {
const milestone = milestones.get(id);
@@ -4316,6 +4353,172 @@ describe("Mission API", () => {
expect(res.status).toBe(404);
});
});
describe("GET /milestones/:milestoneId/validation-telemetry", () => {
it("returns empty grouped telemetry for milestones without assertions or runs", async () => {
const { app, missionStore } = buildApp();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
const mission = ms.createMission({ title: "Telemetry Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Milestone A" });
const res = await get(app, `/api/missions/milestones/${milestone.id}/validation-telemetry`);
expect(res.status).toBe(200);
expect(res.body.validationContract.assertions).toEqual([]);
expect(res.body.validationContract.featureFulfillment).toEqual({});
expect(res.body.validationTelemetry.validationRounds).toEqual([]);
expect(res.body.validationTelemetry.lastValidatorStatus).toBeNull();
expect(res.body.validationTelemetry.totalRuns).toBe(0);
expect(res.body.fixFeatures).toEqual([]);
expect(res.body.rollup.state).toBe("not_started");
});
it("returns contract assertions and feature fulfillment links", async () => {
const { app, missionStore } = buildApp();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
const mission = ms.createMission({ title: "Contract Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Milestone B" });
const slice = ms.addSlice(milestone.id, { title: "Slice B" });
const featureOne = ms.addFeature(slice.id, { title: "Feature One" });
const featureTwo = ms.addFeature(slice.id, { title: "Feature Two" });
const assertionOne = ms.addContractAssertion(milestone.id, {
title: "Assertion One",
assertion: "Feature one must pass",
});
const assertionTwo = ms.addContractAssertion(milestone.id, {
title: "Assertion Two",
assertion: "Feature two must pass",
});
ms.linkFeatureToAssertion(featureOne.id, assertionOne.id);
ms.linkFeatureToAssertion(featureTwo.id, assertionTwo.id);
const res = await get(app, `/api/missions/milestones/${milestone.id}/validation-telemetry`);
expect(res.status).toBe(200);
expect(res.body.validationContract.assertions).toHaveLength(2);
expect(res.body.validationContract.assertions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: assertionOne.id,
title: assertionOne.title,
assertion: assertionOne.assertion,
status: assertionOne.status,
}),
expect.objectContaining({
id: assertionTwo.id,
title: assertionTwo.title,
assertion: assertionTwo.assertion,
status: assertionTwo.status,
}),
])
);
expect(res.body.validationContract.featureFulfillment[featureOne.id]).toEqual({
assertionIds: [assertionOne.id],
featureTitle: featureOne.title,
featureStatus: featureOne.status,
});
expect(res.body.validationContract.featureFulfillment[featureTwo.id]).toEqual({
assertionIds: [assertionTwo.id],
featureTitle: featureTwo.title,
featureStatus: featureTwo.status,
});
});
it("returns validator rounds and generated fix-feature lineage", async () => {
const { app, missionStore } = buildApp();
const ms = missionStore as ReturnType<typeof createMockMissionStore>;
const mission = ms.createMission({ title: "Validation Mission" });
const milestone = ms.addMilestone(mission.id, { title: "Milestone C" });
const slice = ms.addSlice(milestone.id, { title: "Slice C" });
const sourceFeature = ms.addFeature(slice.id, { title: "Source Feature" });
const fixFeature = ms.addFeature(slice.id, { title: "Fix Feature" });
const assertion = ms.addContractAssertion(milestone.id, {
title: "Fails assertion",
assertion: "Must not regress",
});
ms.updateFeature(fixFeature.id, {
generatedFromFeatureId: sourceFeature.id,
generatedFromRunId: "VR-FAILED-001",
});
(missionStore.getValidatorRunsByFeature as ReturnType<typeof vi.fn>).mockImplementation((featureId: string) => {
if (featureId !== sourceFeature.id) {
return [];
}
return [
{
id: "VR-FAILED-001",
featureId: sourceFeature.id,
milestoneId: milestone.id,
sliceId: slice.id,
status: "failed",
implementationAttempt: 2,
validatorAttempt: 2,
startedAt: "2026-04-16T12:00:00.000Z",
completedAt: "2026-04-16T12:02:00.000Z",
createdAt: "2026-04-16T12:00:00.000Z",
updatedAt: "2026-04-16T12:02:00.000Z",
},
] as MissionValidatorRun[];
});
(missionStore.getFailuresForRun as ReturnType<typeof vi.fn>).mockImplementation((runId: string) => {
if (runId !== "VR-FAILED-001") {
return [];
}
return [
{
id: "VAF-001",
runId: "VR-FAILED-001",
featureId: sourceFeature.id,
assertionId: assertion.id,
message: "Assertion failed",
createdAt: "2026-04-16T12:01:00.000Z",
},
] as MissionAssertionFailureRecord[];
});
const res = await get(app, `/api/missions/milestones/${milestone.id}/validation-telemetry`);
expect(res.status).toBe(200);
expect(res.body.validationTelemetry.validationRounds).toEqual(
expect.arrayContaining([
expect.objectContaining({
roundId: "VR-FAILED-001",
validatorStatus: "failed",
failedAssertionIds: [assertion.id],
generatedFixFeatureIds: [fixFeature.id],
}),
])
);
expect(res.body.fixFeatures).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: fixFeature.id,
sourceFeatureId: sourceFeature.id,
runId: "VR-FAILED-001",
failedAssertionIds: [assertion.id],
}),
])
);
});
it("returns 404 when milestone does not exist", async () => {
const { app } = buildApp();
const res = await get(app, "/api/missions/milestones/MS-MISSING-TST/validation-telemetry");
expect(res.status).toBe(404);
expect(res.body.error).toBe("Milestone not found");
});
});
});
/**