feat(FN-1787): merge fusion/fn-1787
This commit is contained in:
@@ -1044,6 +1044,8 @@ describe("MissionStore", () => {
|
||||
|
||||
expect(linked.taskId).toBe("FN-001");
|
||||
expect(linked.status).toBe("triaged");
|
||||
expect(linked.loopState).toBe("implementing");
|
||||
expect(linked.implementationAttemptCount).toBe(1);
|
||||
expect(taskRow.missionId).toBe(mission.id);
|
||||
expect(taskRow.sliceId).toBe(slice.id);
|
||||
});
|
||||
@@ -1542,6 +1544,8 @@ describe("MissionStore", () => {
|
||||
// Feature should be triaged with a taskId
|
||||
expect(triaged.status).toBe("triaged");
|
||||
expect(triaged.taskId).toBeTruthy();
|
||||
expect(triaged.loopState).toBe("implementing");
|
||||
expect(triaged.implementationAttemptCount).toBe(1);
|
||||
|
||||
// Task should exist with correct properties
|
||||
const task = await ts.getTask(triaged.taskId!);
|
||||
|
||||
@@ -1667,10 +1667,17 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
|
||||
const linkage = this.resolveTaskLinkage(feature.sliceId);
|
||||
|
||||
// When first linking (loopState is idle or falsy), transition to implementing
|
||||
const shouldTransitionLoop = !feature.loopState || feature.loopState === "idle";
|
||||
const loopStateUpdates: Partial<MissionFeature> = shouldTransitionLoop
|
||||
? { loopState: "implementing", implementationAttemptCount: 1 }
|
||||
: {};
|
||||
|
||||
const updated = this.db.transaction(() => {
|
||||
const featureUpdate = this.updateFeature(featureId, {
|
||||
taskId,
|
||||
status: "triaged",
|
||||
...loopStateUpdates,
|
||||
});
|
||||
|
||||
// Also update the task's mission/slice linkage for bidirectional linking.
|
||||
|
||||
@@ -220,6 +220,14 @@ function createMockMissionStore() {
|
||||
|
||||
return fixFeature;
|
||||
}),
|
||||
triageFeature: vi.fn(async (featureId: string) => {
|
||||
const feature = features.get(featureId);
|
||||
if (!feature) throw new Error(`Feature ${featureId} not found`);
|
||||
// Simulate triage by updating the feature
|
||||
const updated = { ...feature, status: "triaged" as const, updatedAt: new Date().toISOString() };
|
||||
features.set(featureId, updated);
|
||||
return updated;
|
||||
}),
|
||||
|
||||
// Event emitter
|
||||
on: vi.fn(),
|
||||
@@ -821,6 +829,11 @@ describe("MissionExecutionLoop", () => {
|
||||
expect.arrayContaining(["CA-1"]),
|
||||
);
|
||||
|
||||
// triageFeature called for the fix feature
|
||||
expect(missionStore.triageFeature).toHaveBeenCalledWith(
|
||||
expect.stringContaining("FIX-"),
|
||||
);
|
||||
|
||||
// validation:failed event emitted
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
"validation:failed",
|
||||
@@ -832,6 +845,68 @@ describe("MissionExecutionLoop", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should emit validation:failed even if triageFeature throws", async () => {
|
||||
const assertions: MissionContractAssertion[] = [
|
||||
{
|
||||
id: "CA-1",
|
||||
milestoneId: "MS-001",
|
||||
title: "Test assertion",
|
||||
assertion: "Should work",
|
||||
status: "pending",
|
||||
orderIndex: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const feature = createMockFeature({
|
||||
loopState: "implementing",
|
||||
taskId: "FN-001",
|
||||
id: "F-001",
|
||||
implementationAttemptCount: 1,
|
||||
});
|
||||
missionStore._setFeature(feature);
|
||||
missionStore.getFeatureByTaskId = vi.fn().mockReturnValue(feature);
|
||||
missionStore.listAssertionsForFeature = vi.fn().mockReturnValue(assertions);
|
||||
|
||||
// Mock AI to return fail response
|
||||
const failResponse = JSON.stringify({
|
||||
status: "fail",
|
||||
assertions: [{ assertionId: "CA-1", passed: false, message: "Failed", expected: "ok", actual: "not ok" }],
|
||||
summary: "Assertion failed",
|
||||
});
|
||||
mockSessionHolder.session.state.messages = [
|
||||
{ role: "user", content: "Validate this" },
|
||||
{ role: "assistant", content: failResponse },
|
||||
];
|
||||
|
||||
// Make triageFeature throw an error
|
||||
missionStore.triageFeature = vi.fn().mockRejectedValue(new Error("Triage failed"));
|
||||
|
||||
taskStore._setTask({ id: "FN-001", title: "Test", description: "Implementation", log: [] });
|
||||
|
||||
loop = new MissionExecutionLoop({
|
||||
taskStore: taskStore as any,
|
||||
missionStore: missionStore as any,
|
||||
rootDir: "/tmp",
|
||||
});
|
||||
const emitSpy = vi.spyOn(loop, "emit");
|
||||
loop.start();
|
||||
|
||||
await loop.processTaskOutcome("FN-001");
|
||||
|
||||
// triageFeature was called but threw
|
||||
expect(missionStore.triageFeature).toHaveBeenCalledWith(expect.stringContaining("FIX-"));
|
||||
|
||||
// validation:failed event should still be emitted
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
"validation:failed",
|
||||
expect.objectContaining({
|
||||
featureId: "F-001",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── handleValidationBlocked ───────────────────────────────────────────────
|
||||
|
||||
@@ -755,6 +755,16 @@ ${taskContext ? `\n\nImplementation context:\n${taskContext}` : ""}`;
|
||||
);
|
||||
loopLog.log(`Created fix feature ${fixFeature.id} for ${featureId}`);
|
||||
|
||||
// Auto-triage the fix feature so the retry loop can continue
|
||||
try {
|
||||
await this.missionStore.triageFeature(fixFeature.id);
|
||||
loopLog.log(`Auto-triaged fix feature ${fixFeature.id}`);
|
||||
} catch (triageErr) {
|
||||
const triageMessage = triageErr instanceof Error ? triageErr.message : String(triageErr);
|
||||
loopLog.error(`Error triaging fix feature ${fixFeature.id}:`, triageMessage);
|
||||
// Continue even if triage fails - the fix feature was created and can be triaged manually
|
||||
}
|
||||
|
||||
this.emit("validation:failed", {
|
||||
featureId,
|
||||
runId,
|
||||
|
||||
Reference in New Issue
Block a user