feat(FN-4053): unify task ID allocation with store-owned distributed alloca
Unifies task ID allocation under a single store-owned distributed allocator, removing redundant ID-generation logic from dashboard route handlers and simplifying the overall flow; Step 1 merges the allocation authority into the store, Step 3 removes the now-unnecessary mixed-path routing, and tests Fusion-Task-Id: FN-4053
This commit is contained in:
@@ -562,28 +562,20 @@ describe("TaskStore", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("logs allocateId disk sync failures while preserving task creation", async () => {
|
||||
it("creates tasks through distributed allocation without config.json sync dependency", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const storeAny = store as any;
|
||||
const originalConfigPath = storeAny.configPath;
|
||||
storeAny.configPath = join(rootDir, ".fusion", "missing-sync", "config.json");
|
||||
|
||||
try {
|
||||
const task = await store.createTask({ description: "allocate despite sync failure" });
|
||||
const task = await store.createTask({ description: "allocate without config sync" });
|
||||
expect(task.id).toBe("FN-001");
|
||||
|
||||
const warningCall = warnSpy.mock.calls.find(
|
||||
(call) => typeof call[0] === "string" && call[0].includes("[task-store] Backward-compat config.json sync failed after ID allocation"),
|
||||
(call) => typeof call[0] === "string" && call[0].includes("after ID allocation"),
|
||||
);
|
||||
expect(warningCall).toBeDefined();
|
||||
|
||||
const [, context] = warningCall as [string, Record<string, unknown>];
|
||||
expect(context).toMatchObject({
|
||||
phase: "allocateId:disk-sync",
|
||||
configPath: join(rootDir, ".fusion", "missing-sync", "config.json"),
|
||||
taskId: task.id,
|
||||
});
|
||||
expect(typeof context.error).toBe("string");
|
||||
expect(warningCall).toBeUndefined();
|
||||
} finally {
|
||||
storeAny.configPath = originalConfigPath;
|
||||
warnSpy.mockRestore();
|
||||
|
||||
@@ -707,6 +707,53 @@ describe("TaskStore", () => {
|
||||
|
||||
|
||||
describe("distributed task-id allocator seam", () => {
|
||||
it("commits allocator reservations for createTask, duplicateTask, and refineTask", async () => {
|
||||
const created = await store.createTask({ description: "created with allocator" });
|
||||
const duplicate = await store.duplicateTask(created.id);
|
||||
|
||||
await store.moveTask(created.id, "todo");
|
||||
await store.moveTask(created.id, "in-progress");
|
||||
await store.moveTask(created.id, "in-review");
|
||||
await store.moveTask(created.id, "done");
|
||||
const refined = await store.refineTask(created.id, "refine this");
|
||||
|
||||
const reservationRows = store
|
||||
.getDatabase()
|
||||
.prepare("SELECT taskId, status FROM distributed_task_id_reservations WHERE taskId IN (?, ?, ?) ORDER BY taskId")
|
||||
.all(created.id, duplicate.id, refined.id) as Array<{ taskId: string; status: string }>;
|
||||
|
||||
expect(reservationRows).toEqual([
|
||||
{ taskId: created.id, status: "committed" },
|
||||
{ taskId: duplicate.id, status: "committed" },
|
||||
{ taskId: refined.id, status: "committed" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps IDs collision-free across mixed store and direct reservation creates", async () => {
|
||||
// Regression for FN-4053: before unifying local allocation, store.createTask()
|
||||
// used config.nextId while direct distributed reservations advanced
|
||||
// distributed_task_id_state. Interleaving both paths could reuse IDs.
|
||||
const first = await store.createTask({ description: "first via store" });
|
||||
|
||||
const allocator = store.getDistributedTaskIdAllocator();
|
||||
const reservation = await allocator.reserveDistributedTaskId({ prefix: "FN", nodeId: "node-a" });
|
||||
const second = await store.createTaskWithReservedId(
|
||||
{ description: "second via direct reservation" },
|
||||
{ taskId: reservation.taskId },
|
||||
);
|
||||
await allocator.commitDistributedTaskIdReservation({
|
||||
reservationId: reservation.reservationId,
|
||||
nodeId: "node-a",
|
||||
});
|
||||
|
||||
const third = await store.createTask({ description: "third via store" });
|
||||
|
||||
expect(first.id).toBe("FN-001");
|
||||
expect(second.id).toBe("FN-002");
|
||||
expect(third.id).toBe("FN-003");
|
||||
expect(third.id).not.toBe(second.id);
|
||||
});
|
||||
|
||||
it("returns a stable allocator instance", () => {
|
||||
const first = store.getDistributedTaskIdAllocator();
|
||||
const second = store.getDistributedTaskIdAllocator();
|
||||
|
||||
@@ -134,11 +134,10 @@ describe("TaskStore", () => {
|
||||
const sortedIds = [...ids].sort();
|
||||
expect(sortedIds).toEqual(["FN-001", "FN-002", "FN-003", "FN-004", "FN-005"]);
|
||||
|
||||
// config.json should be valid JSON with nextId = 6
|
||||
// config.json should still be valid JSON after concurrent task creation
|
||||
const configPath = join(rootDir, ".fusion", "config.json");
|
||||
const raw = await readFile(configPath, "utf-8");
|
||||
const config = JSON.parse(raw);
|
||||
expect(config.nextId).toBe(6);
|
||||
expect(() => JSON.parse(raw)).not.toThrow();
|
||||
|
||||
// No .tmp files left behind
|
||||
const haiDir = join(rootDir, ".fusion");
|
||||
|
||||
@@ -2054,7 +2054,7 @@ describe("TaskStore", () => {
|
||||
expect(task.id).toBe("PROJ-001");
|
||||
});
|
||||
|
||||
it("prefix change mid-stream continues sequence", async () => {
|
||||
it("prefix change mid-stream starts a fresh per-prefix sequence", async () => {
|
||||
const t1 = await harness.store().createTask({ description: "First" });
|
||||
const t2 = await harness.store().createTask({ description: "Second" });
|
||||
expect(t1.id).toBe("FN-001");
|
||||
@@ -2062,7 +2062,7 @@ describe("TaskStore", () => {
|
||||
|
||||
await harness.store().updateSettings({ taskPrefix: "PROJ" });
|
||||
const t3 = await harness.store().createTask({ description: "Third" });
|
||||
expect(t3.id).toBe("PROJ-003");
|
||||
expect(t3.id).toBe("PROJ-001");
|
||||
});
|
||||
|
||||
it("listTasks returns tasks regardless of prefix", async () => {
|
||||
@@ -2072,7 +2072,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const tasks = await harness.store().listTasks();
|
||||
expect(tasks).toHaveLength(2);
|
||||
expect(tasks.map((t) => t.id).sort()).toEqual(["FN-001", "PROJ-002"]);
|
||||
expect(tasks.map((t) => t.id).sort()).toEqual(["FN-001", "PROJ-001"]);
|
||||
});
|
||||
|
||||
it("supports pagination with limit and offset", async () => {
|
||||
|
||||
Reference in New Issue
Block a user