feat(FN-1787): merge fusion/fn-1787

This commit is contained in:
gsxdsm
2026-04-15 00:46:08 -07:00
parent 336c9c4fdf
commit 889fd169e1
4 changed files with 96 additions and 0 deletions

View File

@@ -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!);

View File

@@ -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.

View File

@@ -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 ───────────────────────────────────────────────

View File

@@ -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,