feat(FN-4908): complete Step 1 — add SQL reliability aggregations

Fusion-Task-Id: FN-4908
Fusion-Task-Lineage: 97d96ea4-8618-43cc-8448-0132cd57974e
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 22:58:47 -07:00
committed by gsxdsm
parent b1d1221ba6
commit 3037bbf9d6
2 changed files with 266 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createTaskStoreTestHarness } from "./store-test-helpers.js";
import type { TaskStore } from "../store.js";
describe("TaskStore reliability aggregations", () => {
const harness = createTaskStoreTestHarness();
let store: TaskStore;
beforeEach(async () => {
await harness.beforeEach();
store = harness.store();
});
afterEach(async () => {
await harness.afterEach();
});
const insertActivity = (entry: {
id: string;
timestamp: string;
type: string;
taskId?: string;
metadata?: Record<string, unknown>;
}) => {
(store as any).db
.prepare(
`INSERT INTO activityLog (id, timestamp, type, taskId, taskTitle, details, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
)
.run(
entry.id,
entry.timestamp,
entry.type,
entry.taskId ?? null,
null,
"test",
entry.metadata ? JSON.stringify(entry.metadata) : null,
);
};
it("returns empty results when no rows match", async () => {
const counts = await store.getTaskMovedCountsByDay({
since: "2026-05-10T00:00:00.000Z",
until: "2026-05-20T00:00:00.000Z",
toColumn: "in-review",
});
const durationEvents = await store.getInReviewDurationEvents({
since: "2026-05-10T00:00:00.000Z",
until: "2026-05-20T00:00:00.000Z",
});
const mergedTaskIds = await store.getTaskMergedTaskIds({
since: "2026-05-10T00:00:00.000Z",
until: "2026-05-20T00:00:00.000Z",
});
expect(counts).toEqual({});
expect(durationEvents).toEqual([]);
expect(mergedTaskIds).toEqual(new Set());
});
it("aggregates task:moved rows by day with from/to filters", async () => {
insertActivity({
id: "a1",
timestamp: "2026-05-16T10:00:00.000Z",
type: "task:moved",
taskId: "FN-1",
metadata: { from: "todo", to: "in-review" },
});
insertActivity({
id: "a2",
timestamp: "2026-05-16T12:00:00.000Z",
type: "task:moved",
taskId: "FN-2",
metadata: { from: "todo", to: "in-review" },
});
insertActivity({
id: "a3",
timestamp: "2026-05-17T09:00:00.000Z",
type: "task:moved",
taskId: "FN-3",
metadata: { from: "in-review", to: "in-progress" },
});
const entered = await store.getTaskMovedCountsByDay({
since: "2026-05-15T00:00:00.000Z",
until: "2026-05-18T00:00:00.000Z",
toColumn: "in-review",
});
const bounced = await store.getTaskMovedCountsByDay({
since: "2026-05-15T00:00:00.000Z",
until: "2026-05-18T00:00:00.000Z",
fromColumn: "in-review",
toColumn: "in-progress",
});
expect(entered).toEqual({ "2026-05-16": 2 });
expect(bounced).toEqual({ "2026-05-17": 1 });
});
it("uses strict since and inclusive until boundaries", async () => {
insertActivity({
id: "b1",
timestamp: "2026-05-16T00:00:00.000Z",
type: "task:moved",
taskId: "FN-1",
metadata: { from: "todo", to: "in-review" },
});
insertActivity({
id: "b2",
timestamp: "2026-05-16T00:00:00.001Z",
type: "task:moved",
taskId: "FN-2",
metadata: { from: "todo", to: "in-review" },
});
const counts = await store.getTaskMovedCountsByDay({
since: "2026-05-16T00:00:00.000Z",
until: "2026-05-16T00:00:00.001Z",
toColumn: "in-review",
});
expect(counts).toEqual({ "2026-05-16": 1 });
});
it("returns focused in-review duration event set ordered ascending", async () => {
insertActivity({
id: "d1",
timestamp: "2026-05-16T10:00:00.000Z",
type: "task:moved",
taskId: "FN-1",
metadata: { from: "todo", to: "in-review" },
});
insertActivity({
id: "d2",
timestamp: "2026-05-16T11:00:00.000Z",
type: "task:moved",
taskId: "FN-1",
metadata: { from: "in-review", to: "done" },
});
insertActivity({
id: "d3",
timestamp: "2026-05-16T12:00:00.000Z",
type: "task:moved",
taskId: "FN-1",
metadata: { from: "in-review", to: "in-progress" },
});
const events = await store.getInReviewDurationEvents({
since: "2026-05-16T09:00:00.000Z",
until: "2026-05-16T13:00:00.000Z",
});
expect(events.map((event) => event.id)).toEqual(["d1", "d2"]);
});
it("returns distinct merged task ids in window", async () => {
insertActivity({ id: "m1", timestamp: "2026-05-16T10:00:00.000Z", type: "task:merged", taskId: "FN-1" });
insertActivity({ id: "m2", timestamp: "2026-05-16T11:00:00.000Z", type: "task:merged", taskId: "FN-1" });
insertActivity({ id: "m3", timestamp: "2026-05-16T12:00:00.000Z", type: "task:merged", taskId: "FN-2" });
const mergedTaskIds = await store.getTaskMergedTaskIds({
since: "2026-05-16T09:00:00.000Z",
until: "2026-05-16T12:00:00.000Z",
});
expect(mergedTaskIds).toEqual(new Set(["FN-1", "FN-2"]));
});
it("aggregates correctly with 60k+ rows", async () => {
const insert = (store as any).db.prepare(
`INSERT INTO activityLog (id, timestamp, type, taskId, taskTitle, details, metadata)
VALUES (?, ?, 'task:moved', ?, NULL, 'bulk', ?)`,
);
for (let i = 0; i < 60_100; i += 1) {
const day = i < 100 ? "2026-05-15" : "2026-05-16";
insert.run(`bulk-${i}`, `${day}T12:00:00.000Z`, `FN-${i}`, JSON.stringify({ from: "todo", to: "in-review" }));
}
const counts = await store.getTaskMovedCountsByDay({
since: "2026-05-14T00:00:00.000Z",
until: "2026-05-17T00:00:00.000Z",
toColumn: "in-review",
});
expect(counts).toEqual({
"2026-05-15": 100,
"2026-05-16": 60_000,
});
});
});

View File

@@ -8546,6 +8546,80 @@ ${notificationsSection}`;
}));
}
async getTaskMovedCountsByDay(options: {
since: string;
until: string;
fromColumn?: string;
toColumn?: string;
}): Promise<Record<string, number>> {
let sql =
"SELECT substr(timestamp, 1, 10) AS day, COUNT(*) AS count FROM activityLog WHERE type = 'task:moved' AND timestamp > ? AND timestamp <= ?";
const params: (string | number)[] = [options.since, options.until];
if (options.fromColumn) {
sql += " AND json_extract(metadata, '$.from') = ?";
params.push(options.fromColumn);
}
if (options.toColumn) {
sql += " AND json_extract(metadata, '$.to') = ?";
params.push(options.toColumn);
}
sql += " GROUP BY substr(timestamp, 1, 10)";
const rows = this.db.prepare(sql).all(...params) as Array<{ day: string; count: number }>;
const countsByDay: Record<string, number> = {};
for (const row of rows) {
countsByDay[row.day] = row.count;
}
return countsByDay;
}
async getInReviewDurationEvents(options: { since: string; until: string }): Promise<ActivityLogEntry[]> {
const rows = this.db
.prepare(
`SELECT * FROM activityLog
WHERE type = 'task:moved'
AND timestamp > ?
AND timestamp <= ?
AND (
json_extract(metadata, '$.to') = 'in-review'
OR (
json_extract(metadata, '$.from') = 'in-review'
AND json_extract(metadata, '$.to') = 'done'
)
)
ORDER BY timestamp ASC
LIMIT ?`,
)
.all(options.since, options.until, 200_000) as unknown as ActivityLogRow[];
return rows.map((row) => ({
id: row.id,
timestamp: row.timestamp,
type: row.type as ActivityEventType,
taskId: row.taskId || undefined,
taskTitle: row.taskTitle || undefined,
details: row.details,
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
}));
}
async getTaskMergedTaskIds(options: { since: string; until: string }): Promise<Set<string>> {
const rows = this.db
.prepare(
`SELECT DISTINCT taskId FROM activityLog
WHERE type = 'task:merged'
AND timestamp > ?
AND timestamp <= ?
AND taskId IS NOT NULL`,
)
.all(options.since, options.until) as Array<{ taskId: string }>;
return new Set(rows.map((row) => row.taskId));
}
/**
* Clear all activity log entries.
* Use with caution - this permanently deletes activity history.