fix(FEAT-009-FIX-001): resolve 3 blocking issues from integration user testing
1. Fix TypeError in run history: fetchValidationRuns now correctly
destructures .runs from paginated API response {runs, total, offset}
2. Fix assertions panel rendering: inline fetch calls in loadMissionDetail
and toggleMilestoneExpanded to avoid forward-reference of callback hooks.
Added Array.isArray guard to prevent .map on undefined state values.
3. Add missing UI components:
- Clickable lineage indicator for fix features (navigates to source)
- Retry budget 'Attempt X of Y' display on feature cards
- 'No fix features generated' empty state
- Milestone validation state badge in header (already existed, now
loads correctly)
- Validation rollup badge with coverage bar in milestone header
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3652,13 +3652,22 @@ export function fetchValidationLoopState(featureId: string, projectId?: string):
|
|||||||
return api<MissionFeatureLoopSnapshot>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/validation-loop`, projectId));
|
return api<MissionFeatureLoopSnapshot>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/validation-loop`, projectId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Paginated response wrapper for validation runs */
|
||||||
|
export interface ValidationRunsResponse {
|
||||||
|
runs: MissionValidatorRun[];
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** List validation runs for a feature */
|
/** List validation runs for a feature */
|
||||||
export function fetchValidationRuns(featureId: string, options?: { limit?: number; offset?: number }, projectId?: string): Promise<MissionValidatorRun[]> {
|
export function fetchValidationRuns(featureId: string, options?: { limit?: number; offset?: number }, projectId?: string): Promise<MissionValidatorRun[]> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||||
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
||||||
const suffix = params.size > 0 ? `?${params.toString()}` : "";
|
const suffix = params.size > 0 ? `?${params.toString()}` : "";
|
||||||
return api<MissionValidatorRun[]>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/validation-runs${suffix}`, projectId));
|
return api<ValidationRunsResponse>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/validation-runs${suffix}`, projectId))
|
||||||
|
.then((response) => response.runs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get a single validator run */
|
/** Get a single validator run */
|
||||||
|
|||||||
@@ -576,7 +576,23 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
setSelectedMission(data);
|
setSelectedMission(data);
|
||||||
// Auto-expand first milestone and slice
|
// Auto-expand first milestone and slice
|
||||||
if (data.milestones.length > 0) {
|
if (data.milestones.length > 0) {
|
||||||
setExpandedMilestones(new Set([data.milestones[0].id]));
|
const firstMilestoneId = data.milestones[0].id;
|
||||||
|
setExpandedMilestones(new Set([firstMilestoneId]));
|
||||||
|
// Load assertions and validation rollup for the first milestone (inline to avoid forward ref)
|
||||||
|
fetchAssertions(firstMilestoneId, projectId).then((assertions) => {
|
||||||
|
setAssertionsByMilestone((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(firstMilestoneId, assertions);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}).catch(() => { /* silently fail */ });
|
||||||
|
fetchMilestoneValidation(firstMilestoneId, projectId).then((rollup) => {
|
||||||
|
setValidationRollupByMilestone((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(firstMilestoneId, rollup);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}).catch(() => { /* silently fail */ });
|
||||||
if (data.milestones[0].slices.length > 0) {
|
if (data.milestones[0].slices.length > 0) {
|
||||||
setExpandedSlices(new Set([data.milestones[0].slices[0].id]));
|
setExpandedSlices(new Set([data.milestones[0].slices[0].id]));
|
||||||
}
|
}
|
||||||
@@ -1054,14 +1070,26 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
if (isExpanding) {
|
if (isExpanding) {
|
||||||
next.add(milestoneId);
|
next.add(milestoneId);
|
||||||
// Load assertions and validation rollup when expanding milestone
|
// Load assertions and validation rollup when expanding milestone
|
||||||
void loadAssertionsForMilestone(milestoneId);
|
fetchAssertions(milestoneId, projectId).then((assertions) => {
|
||||||
void loadValidationRollup(milestoneId);
|
setAssertionsByMilestone((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(milestoneId, assertions);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}).catch(() => { /* silently fail */ });
|
||||||
|
fetchMilestoneValidation(milestoneId, projectId).then((rollup) => {
|
||||||
|
setValidationRollupByMilestone((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(milestoneId, rollup);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}).catch(() => { /* silently fail */ });
|
||||||
} else {
|
} else {
|
||||||
next.delete(milestoneId);
|
next.delete(milestoneId);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, [projectId]);
|
||||||
|
|
||||||
// Slice handlers
|
// Slice handlers
|
||||||
const handleCreateSlice = useCallback((milestoneId: string) => {
|
const handleCreateSlice = useCallback((milestoneId: string) => {
|
||||||
@@ -2244,14 +2272,40 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
{feature.loopState === "blocked" && "🚫"}
|
{feature.loopState === "blocked" && "🚫"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* Lineage indicator for fix features */}
|
{/* Lineage indicator for fix features - click to navigate to source feature */}
|
||||||
{feature.generatedFromFeatureId && (
|
{feature.generatedFromFeatureId && (
|
||||||
<span
|
<button
|
||||||
className="mission-feature__lineage"
|
className="mission-feature__lineage"
|
||||||
title={`Generated from fix for assertion failure`}
|
onClick={() => {
|
||||||
|
// Navigate to source feature: find its milestone/slice and expand to it
|
||||||
|
// Find the source feature in the mission hierarchy
|
||||||
|
for (const m of selectedMission?.milestones ?? []) {
|
||||||
|
for (const s of m.slices) {
|
||||||
|
const sourceFeature = s.features.find((f) => f.id === feature.generatedFromFeatureId);
|
||||||
|
if (sourceFeature) {
|
||||||
|
setExpandedMilestones((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(m.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setExpandedSlices((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(s.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setExpandedFeatureId(sourceFeature.id);
|
||||||
|
// Load loop state for source feature
|
||||||
|
void loadFeatureLoopState(sourceFeature.id);
|
||||||
|
void loadValidationRuns(sourceFeature.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={`Generated from feature: ${feature.generatedFromFeatureId}`}
|
||||||
>
|
>
|
||||||
🔗 Fix
|
🔗 Fix
|
||||||
</span>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* Retry budget display */}
|
{/* Retry budget display */}
|
||||||
{feature.loopState && feature.loopState !== "idle" && featureLoopStates.get(feature.id) && (
|
{feature.loopState && feature.loopState !== "idle" && featureLoopStates.get(feature.id) && (
|
||||||
@@ -2423,7 +2477,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
<div className="mission-feature__run-history-header">
|
<div className="mission-feature__run-history-header">
|
||||||
<span className="mission-feature__run-history-title">Validation Runs</span>
|
<span className="mission-feature__run-history-title">Validation Runs</span>
|
||||||
</div>
|
</div>
|
||||||
{validationRunsByFeature.get(feature.id)?.map((run) => (
|
{(validationRunsByFeature.get(feature.id) ?? []).map((run) => (
|
||||||
<div key={run.id} className="mission-run">
|
<div key={run.id} className="mission-run">
|
||||||
<div
|
<div
|
||||||
className="mission-run__header"
|
className="mission-run__header"
|
||||||
@@ -2536,6 +2590,14 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Empty state when no features exist and not creating */}
|
||||||
|
{!isCreatingFeature && (!slice.features || slice.features.length === 0) && (
|
||||||
|
<div className="mission-manager__empty mission-features__empty">
|
||||||
|
<Box size={16} />
|
||||||
|
<span>No fix features generated.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2638,7 +2700,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
|
|
||||||
{/* Assertions list */}
|
{/* Assertions list */}
|
||||||
<div className="mission-assertions__list">
|
<div className="mission-assertions__list">
|
||||||
{assertionsByMilestone.get(milestone.id)?.map((assertion) => (
|
{(Array.isArray(assertionsByMilestone.get(milestone.id)) ? assertionsByMilestone.get(milestone.id)! : [] as MissionContractAssertion[]).map((assertion) => (
|
||||||
<div key={assertion.id} className="mission-assertion">
|
<div key={assertion.id} className="mission-assertion">
|
||||||
<div className="mission-assertion__header">
|
<div className="mission-assertion__header">
|
||||||
{editingAssertionId === assertion.id ? (
|
{editingAssertionId === assertion.id ? (
|
||||||
@@ -2680,8 +2742,8 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
<span
|
<span
|
||||||
className="mission-status-badge mission-status-badge--sm"
|
className="mission-status-badge mission-status-badge--sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: assertionStatusColors[assertion.status].bg,
|
backgroundColor: (assertionStatusColors[assertion.status] ?? assertionStatusColors.pending).bg,
|
||||||
color: assertionStatusColors[assertion.status].text,
|
color: (assertionStatusColors[assertion.status] ?? assertionStatusColors.pending).text,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{assertion.status}
|
{assertion.status}
|
||||||
|
|||||||
Reference in New Issue
Block a user