Recovery: re-land onto main
This commit is contained in:
@@ -524,6 +524,27 @@ describe("MissionExecutionLoop", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("skips duplicate trigger when feature already has active validation", async () => {
|
||||
const feature = createMockFeature({ loopState: "implementing", taskId: "FN-001", sliceId: "SL-001" });
|
||||
missionStore._setFeature(feature);
|
||||
missionStore.getFeatureByTaskId = vi.fn().mockReturnValue(feature);
|
||||
missionStore.listAssertionsForFeature = vi.fn().mockReturnValue([
|
||||
{ id: "CA-1", milestoneId: "MS-001", title: "Test assertion", assertion: "Should work", status: "pending" as const, orderIndex: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
||||
]);
|
||||
|
||||
loop = new MissionExecutionLoop({
|
||||
taskStore: taskStore as any,
|
||||
missionStore: missionStore as any,
|
||||
rootDir: "/tmp",
|
||||
});
|
||||
loop.start();
|
||||
(loop as any).activeValidations.add("F-001");
|
||||
|
||||
await loop.processTaskOutcome("FN-001");
|
||||
|
||||
expect(missionStore.startValidatorRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls startValidatorRun without a board task ID", async () => {
|
||||
const feature = createMockFeature({ loopState: "implementing", taskId: "FN-001", sliceId: "SL-001" });
|
||||
missionStore._setFeature(feature);
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { TaskStore, Task, TaskDetail } from "@fusion/core";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { schedulerLog } from "../logger.js";
|
||||
import { MissionExecutionLoop } from "../mission-execution-loop.js";
|
||||
|
||||
const staleReporterReportMock = vi.fn();
|
||||
const backlogPressureReporterReportMock = vi.fn();
|
||||
@@ -3697,6 +3698,116 @@ describe("Scheduler", () => {
|
||||
expect(mockAutopilot.handleTaskCompletion).toHaveBeenCalledWith("FN-001");
|
||||
});
|
||||
|
||||
it("starts validator run through missionExecutionLoop when a linked task moves to done", async () => {
|
||||
const feature = {
|
||||
id: "F-001",
|
||||
sliceId: "SL-001",
|
||||
status: "in-progress",
|
||||
loopState: "implementing",
|
||||
taskId: "FN-001",
|
||||
};
|
||||
const missionStore = {
|
||||
getFeatureByTaskId: vi.fn().mockReturnValue(feature),
|
||||
updateFeatureStatus: vi.fn(),
|
||||
getSlice: vi.fn().mockReturnValue({ id: "SL-001", milestoneId: "MS-001", status: "active" }),
|
||||
getMilestone: vi.fn().mockReturnValue({ id: "MS-001", missionId: "M-001" }),
|
||||
listAssertionsForFeature: vi.fn().mockReturnValue([
|
||||
{
|
||||
id: "CA-1",
|
||||
milestoneId: "MS-001",
|
||||
title: "Must pass",
|
||||
assertion: "Should pass",
|
||||
status: "pending",
|
||||
orderIndex: 0,
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
]),
|
||||
startValidatorRun: vi.fn().mockReturnValue({
|
||||
id: "VR-001",
|
||||
featureId: "F-001",
|
||||
milestoneId: "MS-001",
|
||||
sliceId: "SL-001",
|
||||
status: "running",
|
||||
triggerType: "task_completion",
|
||||
implementationAttempt: 1,
|
||||
validatorAttempt: 1,
|
||||
startedAt: "2026-01-01T00:00:00Z",
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
}),
|
||||
completeValidatorRun: vi.fn(),
|
||||
recordValidatorFailures: vi.fn(),
|
||||
createGeneratedFixFeature: vi.fn(),
|
||||
triageFeature: vi.fn(),
|
||||
};
|
||||
const taskStore = createMockStore({
|
||||
getTask: vi.fn().mockResolvedValue(createMockTask({
|
||||
id: "FN-001",
|
||||
title: "Mission task",
|
||||
description: "done",
|
||||
column: "done",
|
||||
sliceId: "SL-001",
|
||||
log: [],
|
||||
})),
|
||||
});
|
||||
const missionExecutionLoop = new MissionExecutionLoop({
|
||||
taskStore: taskStore as any,
|
||||
missionStore: missionStore as any,
|
||||
rootDir: "/tmp",
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(taskStore, {
|
||||
missionStore: missionStore as any,
|
||||
missionExecutionLoop,
|
||||
});
|
||||
const startSpy = vi.spyOn(missionExecutionLoop, "start");
|
||||
|
||||
await (scheduler as any).handleMissionTaskMove("FN-001", "done");
|
||||
await Promise.resolve();
|
||||
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
expect(missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion");
|
||||
expect(startSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
missionStore.startValidatorRun.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps processTaskOutcome guarded when loop is not started", async () => {
|
||||
const feature = {
|
||||
id: "F-001",
|
||||
sliceId: "SL-001",
|
||||
status: "in-progress",
|
||||
loopState: "implementing",
|
||||
taskId: "FN-001",
|
||||
};
|
||||
const missionStore = {
|
||||
getFeatureByTaskId: vi.fn().mockReturnValue(feature),
|
||||
listAssertionsForFeature: vi.fn().mockReturnValue([
|
||||
{
|
||||
id: "CA-1",
|
||||
milestoneId: "MS-001",
|
||||
title: "Must pass",
|
||||
assertion: "Should pass",
|
||||
status: "pending",
|
||||
orderIndex: 0,
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
]),
|
||||
startValidatorRun: vi.fn(),
|
||||
};
|
||||
const loop = new MissionExecutionLoop({
|
||||
taskStore: createMockStore() as any,
|
||||
missionStore: missionStore as any,
|
||||
rootDir: "/tmp",
|
||||
});
|
||||
|
||||
await loop.processTaskOutcome("FN-001");
|
||||
|
||||
expect(missionStore.startValidatorRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not mark a feature done when the completed task is blocked", async () => {
|
||||
const store = createMockStore({
|
||||
getTask: vi.fn(async (taskId: string) => {
|
||||
|
||||
@@ -240,6 +240,11 @@ export class MissionExecutionLoop extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeValidations.has(feature.id)) {
|
||||
loopLog.log(`Feature ${feature.id} already has an active validation; skipping duplicate trigger`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get linked assertions for this feature
|
||||
const assertions = this.missionStore.listAssertionsForFeature(feature.id);
|
||||
if (assertions.length === 0) {
|
||||
|
||||
@@ -1757,6 +1757,13 @@ export class Scheduler {
|
||||
// This is called regardless of whether the slice is complete - the loop
|
||||
// handles the validation cycle independently
|
||||
if (this.options.missionExecutionLoop) {
|
||||
if (!this.options.missionExecutionLoop.isRunning()) {
|
||||
schedulerLog.warn(
|
||||
`MissionExecutionLoop was not running during task completion for ${taskId}; starting loop before processing outcome`,
|
||||
);
|
||||
this.options.missionExecutionLoop.start();
|
||||
}
|
||||
|
||||
void this.options.missionExecutionLoop.processTaskOutcome(taskId).catch((err) => {
|
||||
schedulerLog.error(`Error in missionExecutionLoop.processTaskOutcome for ${taskId}:`, err);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user