feat(FN-5060): complete Step 5 — wire engine and mission dedup guard
This commit is contained in:
committed by
gsxdsm
parent
6365fceee4
commit
a54496f45d
@@ -1890,11 +1890,31 @@ describe("MissionStore", () => {
|
||||
expect(task!.description).toBe("Custom description for the task");
|
||||
});
|
||||
|
||||
it("emits feature:linked event", async () => {
|
||||
it("links duplicate feature triage calls to the same canonical task", async () => {
|
||||
const { TaskStore } = await import("../store.js");
|
||||
const ts = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"), { inMemoryDb: true });
|
||||
const msWithTs = ts.getMissionStore();
|
||||
|
||||
const mission = msWithTs.createMission({ title: "Mission" });
|
||||
const milestone = msWithTs.addMilestone(mission.id, { title: "Milestone" });
|
||||
const slice = msWithTs.addSlice(milestone.id, { title: "Slice" });
|
||||
const featureA = msWithTs.addFeature(slice.id, { title: "Feature A" });
|
||||
const featureB = msWithTs.addFeature(slice.id, { title: "Feature B" });
|
||||
|
||||
const first = await msWithTs.triageFeature(featureA.id, "Same Task", "Same deterministic description");
|
||||
const second = await msWithTs.triageFeature(featureB.id, "Same Task", "Same deterministic description");
|
||||
|
||||
expect(first.taskId).toBeTruthy();
|
||||
expect(second.taskId).toBe(first.taskId);
|
||||
|
||||
const tasks = await ts.listTasks({ slim: true });
|
||||
expect(tasks).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("emits feature:linked event", async () => { const { TaskStore } = await import("../store.js");
|
||||
const ts = new TaskStore(tmpDir, join(tmpDir, ".fusion-global-settings"), { inMemoryDb: true });
|
||||
const msWithTs = ts.getMissionStore();
|
||||
|
||||
const linkedHandler = vi.fn();
|
||||
msWithTs.on("feature:linked", linkedHandler);
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
validateSnapshotEnvelope,
|
||||
type MissionHierarchySnapshot,
|
||||
} from "./shared-mesh-state.js";
|
||||
import { reconcileDeterministicDuplicate, runDeterministicDuplicateGuard } from "./duplicate-guard.js";
|
||||
// ── Constants ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -3127,26 +3128,52 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
const milestone = slice ? this.getMilestone(slice.milestoneId) : undefined;
|
||||
const missionId = milestone?.missionId;
|
||||
|
||||
// Create the task
|
||||
const task = await this.taskStore.createTask({
|
||||
const lockScope = missionId ? `mission:${missionId}` : `mission-store:${this.db.dbPath}`;
|
||||
const guard = await runDeterministicDuplicateGuard(this.taskStore, {
|
||||
title: taskTitle || feature.title,
|
||||
description,
|
||||
branch: branchOptions?.branch,
|
||||
baseBranch: branchOptions?.baseBranch,
|
||||
...(missionId
|
||||
? {
|
||||
branchContext: {
|
||||
groupId: `mission:${missionId}`,
|
||||
source: "mission" as const,
|
||||
assignmentMode: branchOptions?.assignmentMode ?? "shared",
|
||||
inheritedBaseBranch: branchOptions?.baseBranch,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}, { lockScope });
|
||||
|
||||
// Link the feature to the new task (this also updates feature status to "triaged")
|
||||
const updated = this.linkFeatureToTask(featureId, task.id);
|
||||
let linkedTaskId: string;
|
||||
try {
|
||||
if (guard.action === "duplicate" && guard.existing) {
|
||||
linkedTaskId = guard.existing.id;
|
||||
} else {
|
||||
const createdTask = await this.taskStore.createTask({
|
||||
title: taskTitle || feature.title,
|
||||
description,
|
||||
branch: branchOptions?.branch,
|
||||
baseBranch: branchOptions?.baseBranch,
|
||||
...(missionId
|
||||
? {
|
||||
branchContext: {
|
||||
groupId: `mission:${missionId}`,
|
||||
source: "mission" as const,
|
||||
assignmentMode: branchOptions?.assignmentMode ?? "shared",
|
||||
inheritedBaseBranch: branchOptions?.baseBranch,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (guard.fingerprint) {
|
||||
await this.taskStore.updateTask(createdTask.id, {
|
||||
sourceMetadataPatch: { contentFingerprint: guard.fingerprint },
|
||||
});
|
||||
}
|
||||
|
||||
const reconcile = await reconcileDeterministicDuplicate(this.taskStore, {
|
||||
createdTask,
|
||||
fingerprint: guard.fingerprint,
|
||||
});
|
||||
linkedTaskId = reconcile.canonical.id;
|
||||
}
|
||||
} finally {
|
||||
guard.releaseLock();
|
||||
}
|
||||
|
||||
// Link the feature to the task (this also updates feature status to "triaged")
|
||||
const updated = this.linkFeatureToTask(featureId, linkedTaskId);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user