feat(FN-4761): complete Step 1 — core review comment ingestion

Fusion-Task-Id: FN-4761
Fusion-Task-Lineage: b6b1ed73-c396-4dd2-b97f-366272d05c7f
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 10:38:40 -07:00
committed by gsxdsm
parent f183186743
commit cfadcf3835
3 changed files with 142 additions and 8 deletions

View File

@@ -0,0 +1,111 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { TaskStore } from "../store.js";
describe("TaskStore review comment ingestion", () => {
let rootDir: string;
let globalDir: string;
let store: TaskStore;
beforeEach(async () => {
rootDir = await mkdtemp(join(tmpdir(), "store-review-comments-"));
globalDir = join(rootDir, ".fusion-global-settings");
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
});
afterEach(async () => {
await store.close();
await rm(rootDir, { recursive: true, force: true });
});
it("inserts github review comment metadata on first write", async () => {
const task = await store.createTask({ description: "review ingest", column: "in-review" });
await store.addComment(task.id, "Needs fixes", "github:alice", {
skipRefinement: true,
source: "github-review",
externalId: "review-101",
reviewState: "CHANGES_REQUESTED",
});
const updated = await store.getTask(task.id);
expect(updated.comments).toHaveLength(1);
expect(updated.comments?.[0]).toMatchObject({
source: "github-review",
externalId: "review-101",
reviewState: "CHANGES_REQUESTED",
author: "github:alice",
});
});
it("deduplicates repeated writes by source + externalId", async () => {
const task = await store.createTask({ description: "dedupe", column: "in-review" });
await store.addComment(task.id, "Please address", "github:bob", {
skipRefinement: true,
source: "github-review",
externalId: "review-102",
reviewState: "CHANGES_REQUESTED",
});
await store.addComment(task.id, "Please address updated", "github:bob", {
skipRefinement: true,
source: "github-review",
externalId: "review-102",
reviewState: "CHANGES_REQUESTED",
});
const updated = await store.getTask(task.id);
expect(updated.comments).toHaveLength(1);
expect(updated.comments?.[0]?.text).toBe("Please address");
});
it("keeps interleaved review and review-comment threads distinct", async () => {
const task = await store.createTask({ description: "interleave", column: "in-review" });
await store.addComment(task.id, "Review summary", "github:alice", {
skipRefinement: true,
source: "github-review",
externalId: "review-201",
reviewState: "COMMENTED",
});
await store.addComment(task.id, "Inline comment 1", "github:alice", {
skipRefinement: true,
source: "github-review-comment",
externalId: "comment-301",
reviewState: "COMMENTED",
});
await store.addComment(task.id, "Inline comment 2", "github:alice", {
skipRefinement: true,
source: "github-review-comment",
externalId: "comment-302",
reviewState: "COMMENTED",
});
const updated = await store.getTask(task.id);
expect(updated.comments).toHaveLength(3);
expect(updated.comments?.map((comment) => `${comment.source}:${comment.externalId}`)).toEqual([
"github-review:review-201",
"github-review-comment:comment-301",
"github-review-comment:comment-302",
]);
});
it("respects skipRefinement for done task github comments", async () => {
const task = await store.createTask({ description: "done", column: "done" });
await store.addComment(task.id, "changes requested", "github:reviewer", {
skipRefinement: true,
source: "github-review",
externalId: "review-500",
reviewState: "CHANGES_REQUESTED",
});
const tasks = await store.listTasks();
expect(tasks).toHaveLength(1);
expect(tasks[0]?.id).toBe(task.id);
});
});

View File

@@ -6807,7 +6807,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
id: string,
text: string,
author: string = "user",
options?: { skipRefinement?: boolean },
options?: {
skipRefinement?: boolean;
source?: "user" | "agent" | "github-review" | "github-review-comment";
externalId?: string;
reviewState?: "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED";
},
runContext?: RunMutationContext,
): Promise<Task> {
// Phase 1: Add comment under lock
@@ -6820,22 +6825,36 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.log = [];
}
if (!task.comments) {
task.comments = [];
}
const externalSource = options?.source;
const externalId = options?.externalId;
if (externalSource && externalId) {
const existing = task.comments.find((entry) => entry.source === externalSource && entry.externalId === externalId);
if (existing) {
return task;
}
}
// Generate unique ID: timestamp + random suffix for collision resistance
const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const now = new Date().toISOString();
const comment: import("./types.js").TaskComment = {
id: commentId,
text,
author,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdAt: now,
updatedAt: now,
source: options?.source,
externalId: options?.externalId,
reviewState: options?.reviewState,
};
if (!task.comments) {
task.comments = [];
}
task.comments.push(comment);
task.updatedAt = new Date().toISOString();
task.updatedAt = now;
const logEntry: TaskLogEntry = {
timestamp: task.updatedAt,
action: `Comment added by ${author}`,
@@ -6854,7 +6873,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
domain: "database",
mutationType: "task:comment",
target: task.id,
metadata: { author, commentId },
metadata: { author, commentId, source: options?.source ?? null, externalId: options?.externalId ?? null },
});
} else {
await this.atomicWriteTaskJson(dir, task);

View File

@@ -750,6 +750,7 @@ export interface PrInfo {
checkRollup?: "success" | "failure" | "pending" | "none";
lastCommentAt?: string;
lastCheckedAt?: string;
lastReviewDecision?: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
}
export type IssueState = "open" | "closed";
@@ -916,6 +917,9 @@ export interface TaskComment {
author: string;
createdAt: string;
updatedAt?: string;
source?: "user" | "agent" | "github-review" | "github-review-comment";
externalId?: string;
reviewState?: "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED";
}
export interface TaskCommentInput {