FN-5645: fix mission autopilot progression to wait for assertion runs

Fusion-Task-Id: FN-5645

Fusion-Task-Lineage: ed9d2045-2bfe-4070-a7e3-caca010e6cc6
This commit is contained in:
gsxdsm
2026-05-28 23:58:47 -07:00
parent 230efa1cdc
commit b8c8642cae
7 changed files with 229 additions and 6 deletions

View File

@@ -13,7 +13,7 @@ function makeHarness({
withAssertions?: boolean;
initialFeatureStatus?: string;
initialTaskColumn?: string;
runValidationImpl?: () => Promise<{ status: "pass"; assertions: []; summary: string }>;
runValidationImpl?: () => Promise<{ status: "pass" | "error"; assertions: []; summary: string }>;
} = {}) {
const mission = {
id: "M-001",
@@ -231,4 +231,26 @@ describe("mission autopilot end-to-end wiring", () => {
expect(h.missionStore.startValidatorRun).toHaveBeenCalledTimes(1);
h.scheduler.stop();
});
it("emits mission error event when validator returns error", async () => {
const h = makeHarness({
withAssertions: true,
runValidationImpl: async () => ({ status: "error", assertions: [], summary: "runtime unavailable" }),
});
await h.emitTaskMoved("done");
expect(h.missionStore.completeValidatorRun).toHaveBeenCalledWith("VR-001", "error", "runtime unavailable");
expect(h.missionStore.logMissionEvent).toHaveBeenCalledWith(
"M-001",
"error",
expect.stringContaining("Validation error"),
expect.objectContaining({
code: "validation_error",
featureId: "F-001",
error: "runtime unavailable",
}),
);
h.scheduler.stop();
});
});

View File

@@ -155,6 +155,7 @@ function createMockMissionStore() {
milestones: [createMockMilestone({ missionId: id })],
};
}),
logMissionEvent: vi.fn(),
// Feature methods
getFeature: vi.fn((id: string) => features.get(id)),
@@ -449,6 +450,17 @@ describe("MissionExecutionLoop", () => {
await loop.processTaskOutcome("FN-001");
expect(missionStore.startValidatorRun).not.toHaveBeenCalled();
expect(missionStore.logMissionEvent).toHaveBeenCalledWith(
expect.any(String),
"warning",
expect.stringContaining("Validation skipped"),
expect.objectContaining({
code: "validation_skipped_loop_state",
featureId: "F-001",
taskId: "FN-001",
loopState: "idle",
}),
);
});
it("should auto-pass if feature has no linked assertions", async () => {
@@ -543,6 +555,16 @@ describe("MissionExecutionLoop", () => {
await loop.processTaskOutcome("FN-001");
expect(missionStore.startValidatorRun).not.toHaveBeenCalled();
expect(missionStore.logMissionEvent).toHaveBeenCalledWith(
expect.any(String),
"warning",
expect.stringContaining("duplicate trigger"),
expect.objectContaining({
code: "validation_deduplicated",
featureId: "F-001",
taskId: "FN-001",
}),
);
});
it("calls startValidatorRun without a board task ID", async () => {
@@ -1169,6 +1191,16 @@ describe("MissionExecutionLoop", () => {
error: "Invalid status in validation response",
}),
);
expect(missionStore.logMissionEvent).toHaveBeenCalledWith(
expect.any(String),
"error",
expect.stringContaining("Validation error"),
expect.objectContaining({
code: "validation_error",
featureId: "F-001",
error: "Invalid status in validation response",
}),
);
expectNoValidationBoardTaskMutation(taskStore);
});
});
@@ -1339,6 +1371,7 @@ describe("MissionExecutionLoop", () => {
rootDir: "/tmp",
});
const processTaskOutcomeSpy = vi.spyOn(loop, "processTaskOutcome");
taskStore._setTask({ id: "FN-VALIDATING", column: "done" });
loop.start();
await loop.recoverActiveMissions();
@@ -1385,6 +1418,7 @@ describe("MissionExecutionLoop", () => {
rootDir: "/tmp",
});
const processTaskOutcomeSpy = vi.spyOn(loop, "processTaskOutcome");
taskStore._setTask({ id: "FN-NEEDS-FIX", column: "done" });
loop.start();
await loop.recoverActiveMissions();
@@ -1438,6 +1472,53 @@ describe("MissionExecutionLoop", () => {
expect(missionStore.transitionLoopState).toHaveBeenCalledWith("F-VALIDATING", "implementing");
});
it("should not call processTaskOutcome when validating feature task is still in-progress", async () => {
const feature = createMockFeature({
id: "F-VALIDATING-IN-PROGRESS",
sliceId: "SL-001",
loopState: "validating",
taskId: "FN-IN-PROGRESS",
});
missionStore._setFeature(feature);
missionStore.getMissionWithHierarchy = vi.fn().mockReturnValue({
id: "M-TEST1",
title: "Test Mission",
status: "active",
interviewState: "not_started",
autoAdvance: true,
autopilotEnabled: true,
autopilotState: "inactive",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
milestones: [
{
...createMockMilestone(),
slices: [
{
...createMockSlice(),
features: [feature],
},
],
},
],
});
loop = new MissionExecutionLoop({
taskStore: taskStore as any,
missionStore: missionStore as any,
rootDir: "/tmp",
});
const processTaskOutcomeSpy = vi.spyOn(loop, "processTaskOutcome");
taskStore._setTask({ id: "FN-IN-PROGRESS", column: "in-progress" });
loop.start();
await loop.recoverActiveMissions();
expect(processTaskOutcomeSpy).not.toHaveBeenCalled();
expect(missionStore.transitionLoopState).toHaveBeenCalledWith("F-VALIDATING-IN-PROGRESS", "implementing");
});
it("should not call processTaskOutcome for needs_fix features without taskId", async () => {
const feature = createMockFeature({
id: "F-NO-TASK",