feat(FN-4785): complete Step 1 — add batched task column lookup

Fusion-Task-Id: FN-4785
Fusion-Task-Lineage: be241ee7-9df0-433c-a00b-997bcd558862
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 12:43:51 -07:00
committed by gsxdsm
parent d89a4569f1
commit 5ca7f45fe1
2 changed files with 115 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
describe("TaskStore.getTaskColumns", () => {
const harness = createTaskStoreTestHarness();
beforeEach(async () => {
await harness.beforeEach();
});
afterEach(async () => {
await harness.afterEach();
});
it("returns empty map for empty input", async () => {
const store = harness.store();
const prepareSpy = vi.spyOn((store as any).db, "prepare");
const result = await store.getTaskColumns([]);
expect(result.size).toBe(0);
expect(prepareSpy).not.toHaveBeenCalled();
});
it("returns columns for active tasks", async () => {
const store = harness.store();
const one = await harness.createTestTask();
const two = await harness.createTestTask();
await store.moveTask(two.id, "in-progress");
const result = await store.getTaskColumns([one.id, two.id]);
expect(result.get(one.id)).toBe("todo");
expect(result.get(two.id)).toBe("in-progress");
});
it("maps archived tasks to archived column", async () => {
const store = harness.store();
const task = await harness.createTestTask();
await store.moveTask(task.id, "done");
await store.archiveTask(task.id);
const result = await store.getTaskColumns([task.id]);
expect(result.get(task.id)).toBe("archived");
});
it("handles mixed active, archived, and unknown ids", async () => {
const store = harness.store();
const active = await harness.createTestTask();
const archived = await harness.createTestTask();
await store.moveTask(archived.id, "done");
await store.archiveTask(archived.id);
const result = await store.getTaskColumns([active.id, archived.id, "FN-DOES-NOT-EXIST"]);
expect(result.get(active.id)).toBe("todo");
expect(result.get(archived.id)).toBe("archived");
expect(result.has("FN-DOES-NOT-EXIST")).toBe(false);
});
it("queries live tasks once for large batches", async () => {
const store = harness.store();
const tasks = await Promise.all(Array.from({ length: 120 }, () => harness.createTestTask()));
const ids = tasks.map((task) => task.id);
const prepareSpy = vi.spyOn((store as any).db, "prepare");
const result = await store.getTaskColumns(ids);
const liveColumnQueryCalls = prepareSpy.mock.calls.filter(([sql]) =>
typeof sql === "string" && sql.includes('SELECT id, "column" FROM tasks WHERE id IN ('),
);
expect(result.size).toBe(ids.length);
expect(liveColumnQueryCalls).toHaveLength(1);
});
});

View File

@@ -3300,6 +3300,44 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
});
}
async getTaskColumns(ids: string[]): Promise<Map<string, Column>> {
if (ids.length === 0) {
return new Map();
}
const uniqueIds = [...new Set(ids)];
const placeholders = uniqueIds.map(() => "?").join(",");
const rows = this.db
.prepare(`SELECT id, "column" FROM tasks WHERE id IN (${placeholders})`)
.all(...uniqueIds) as Array<{ id: string; column: Column }>;
const activeById = new Map<string, Column>();
for (const row of rows) {
activeById.set(row.id, row.column);
}
const missingIds: string[] = [];
for (const id of uniqueIds) {
if (!activeById.has(id)) {
missingIds.push(id);
}
}
const archivedSet = missingIds.length > 0 ? this.archiveDb.filterArchived(missingIds) : new Set<string>();
const result = new Map<string, Column>();
for (const id of uniqueIds) {
const activeColumn = activeById.get(id);
if (activeColumn !== undefined) {
result.set(id, activeColumn);
} else if (archivedSet.has(id)) {
result.set(id, "archived");
}
}
return result;
}
async listTasks(options?: {
limit?: number;
offset?: number;