FN-5764: expose mission assertion backfill through API and CLI
Expose mission assertion backfill controls and run paths across engine, API, and CLI surfaces. - add mission assertion backfill gating classification and reliability coverage for assertion-linked validation recovery - add dashboard mission routes plus legacy API coverage and e2e checks for running assertion backfill - expose new extension tool support and update Fusion skill docs/capability references - add a changeset for @runfusion/fusion and refresh mission documentation Files changed: .changeset/fn-5764-mission-backfill-assertions.md | 9 ++ docs/missions-completion-contract.md | 7 +- docs/missions.md | 7 +- packages/cli/skill/fusion/SKILL.md | 2 +- .../cli/skill/fusion/references/extension-tools.md | 9 ++ .../skill/fusion/references/fusion-capabilities.md | 1 + packages/cli/src/__tests__/extension.test.ts | 41 +++++++ packages/cli/src/extension.ts | 43 +++++++ .../dashboard/app/__tests__/api-missions.test.ts | 44 +++++++ packages/dashboard/app/api/legacy.ts | 34 ++++++ .../dashboard/src/__tests__/mission-e2e.test.ts | 127 +++++++++++++++++++++ packages/dashboard/src/mission-routes.ts | 29 +++++ .../mission-validation-trigger-gap.test.ts | 66 +++++++++++ .../workflow-step-readonly-allowlist.test.ts | 1 + packages/engine/src/gating-classifications.ts | 1 + 15 files changed, 418 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-5764 Fusion-Task-Lineage: b16123c2-7cfe-4b17-a899-4e30044a7a49
This commit is contained in:
@@ -237,4 +237,70 @@ describe("FN-5715 reliability: mission validation trigger gap", () => {
|
||||
expect(noAssertionEvents).toHaveLength(1);
|
||||
loop.stop();
|
||||
});
|
||||
|
||||
it("routes through validator after assertion backfill instead of no-assertion auto-pass", async () => {
|
||||
const feature = makeFeature({ status: "done", acceptanceCriteria: "must pass", loopState: "implementing" });
|
||||
const currentFeature = { ...feature };
|
||||
const linkedAssertions: Array<{ id: string }> = [];
|
||||
|
||||
const missionStore = {
|
||||
listMissions: vi.fn(() => [{ id: "M-001", status: "active" }]),
|
||||
getMissionWithHierarchy: vi.fn(() => ({
|
||||
id: "M-001",
|
||||
status: "active",
|
||||
milestones: [{ status: "active", slices: [{ status: "active", features: [feature] }] }],
|
||||
})),
|
||||
getFeatureByTaskId: vi.fn(() => currentFeature),
|
||||
getFeature: vi.fn(() => currentFeature),
|
||||
updateFeatureStatus: vi.fn((_featureId: string, status: "done") => ({ ...currentFeature, status })),
|
||||
updateFeature: vi.fn((_featureId: string, patch: Partial<MissionFeature>) => {
|
||||
Object.assign(currentFeature, patch);
|
||||
return { ...currentFeature };
|
||||
}),
|
||||
listAssertionsForFeature: vi.fn(() => linkedAssertions),
|
||||
startValidatorRun: vi.fn(() => ({ id: "VR-001", featureId: "F-001" })),
|
||||
completeValidatorRun: vi.fn(),
|
||||
getSlice: vi.fn(() => ({ id: "SL-001", milestoneId: "MS-001", status: "active" })),
|
||||
getMilestone: vi.fn(() => ({ id: "MS-001", missionId: "M-001" })),
|
||||
logMissionEvent: vi.fn(),
|
||||
transitionLoopState: vi.fn(),
|
||||
setFeatureCurrentTaskRunId: vi.fn(),
|
||||
getFailuresForRun: vi.fn(() => []),
|
||||
};
|
||||
const taskStore = {
|
||||
getTask: vi.fn(async () => ({ id: "FN-001", column: "done", status: "done" })),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
};
|
||||
|
||||
const loop = new MissionExecutionLoop({ missionStore: missionStore as any, taskStore: taskStore as any, rootDir: process.cwd() });
|
||||
vi.spyOn(loop as any, "runValidation").mockResolvedValue({ status: "pass", summary: "ok" });
|
||||
loop.start();
|
||||
|
||||
await loop.recoverActiveMissions();
|
||||
|
||||
const noAssertionEventsBefore = missionStore.logMissionEvent.mock.calls.filter(
|
||||
([, type, , payload]) => type === "warning" && payload?.code === "validation_auto_passed_no_assertions",
|
||||
);
|
||||
expect(noAssertionEventsBefore).toHaveLength(1);
|
||||
expect(missionStore.startValidatorRun).not.toHaveBeenCalled();
|
||||
|
||||
linkedAssertions.push({ id: "CA-001" });
|
||||
currentFeature.loopState = "implementing";
|
||||
currentFeature.lastValidatorStatus = undefined;
|
||||
|
||||
await loop.processTaskOutcome("FN-001");
|
||||
|
||||
expect(missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion");
|
||||
const noAssertionEventsAfter = missionStore.logMissionEvent.mock.calls.filter(
|
||||
([, type, , payload]) => type === "warning" && payload?.code === "validation_auto_passed_no_assertions",
|
||||
);
|
||||
expect(noAssertionEventsAfter).toHaveLength(1);
|
||||
expect(missionStore.updateFeature).toHaveBeenCalledWith(
|
||||
"F-001",
|
||||
expect.objectContaining({ loopState: "passed", lastValidatorStatus: "passed" }),
|
||||
);
|
||||
|
||||
loop.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("workflow-step readonly allowlist policy", () => {
|
||||
"fn_mission_create",
|
||||
"fn_mission_delete",
|
||||
"fn_mission_update",
|
||||
"fn_mission_backfill_assertions",
|
||||
"fn_milestone_add",
|
||||
"fn_slice_add",
|
||||
"fn_feature_add",
|
||||
|
||||
@@ -30,6 +30,7 @@ const PERMANENT_TASK_AGENT_ONLY_TOOLS = [
|
||||
"fn_mission_create",
|
||||
"fn_mission_delete",
|
||||
"fn_mission_update",
|
||||
"fn_mission_backfill_assertions",
|
||||
"fn_milestone_add",
|
||||
"fn_slice_add",
|
||||
"fn_feature_add",
|
||||
|
||||
Reference in New Issue
Block a user