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:
committed by
gsxdsm
parent
f183186743
commit
cfadcf3835
111
packages/core/src/__tests__/store-review-comments.test.ts
Normal file
111
packages/core/src/__tests__/store-review-comments.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6807,7 +6807,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
id: string,
|
id: string,
|
||||||
text: string,
|
text: string,
|
||||||
author: string = "user",
|
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,
|
runContext?: RunMutationContext,
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
// Phase 1: Add comment under lock
|
// Phase 1: Add comment under lock
|
||||||
@@ -6820,22 +6825,36 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
task.log = [];
|
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
|
// Generate unique ID: timestamp + random suffix for collision resistance
|
||||||
const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
const comment: import("./types.js").TaskComment = {
|
const comment: import("./types.js").TaskComment = {
|
||||||
id: commentId,
|
id: commentId,
|
||||||
text,
|
text,
|
||||||
author,
|
author,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: now,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: now,
|
||||||
|
source: options?.source,
|
||||||
|
externalId: options?.externalId,
|
||||||
|
reviewState: options?.reviewState,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!task.comments) {
|
|
||||||
task.comments = [];
|
|
||||||
}
|
|
||||||
task.comments.push(comment);
|
task.comments.push(comment);
|
||||||
task.updatedAt = new Date().toISOString();
|
task.updatedAt = now;
|
||||||
const logEntry: TaskLogEntry = {
|
const logEntry: TaskLogEntry = {
|
||||||
timestamp: task.updatedAt,
|
timestamp: task.updatedAt,
|
||||||
action: `Comment added by ${author}`,
|
action: `Comment added by ${author}`,
|
||||||
@@ -6854,7 +6873,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
domain: "database",
|
domain: "database",
|
||||||
mutationType: "task:comment",
|
mutationType: "task:comment",
|
||||||
target: task.id,
|
target: task.id,
|
||||||
metadata: { author, commentId },
|
metadata: { author, commentId, source: options?.source ?? null, externalId: options?.externalId ?? null },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.atomicWriteTaskJson(dir, task);
|
await this.atomicWriteTaskJson(dir, task);
|
||||||
|
|||||||
@@ -750,6 +750,7 @@ export interface PrInfo {
|
|||||||
checkRollup?: "success" | "failure" | "pending" | "none";
|
checkRollup?: "success" | "failure" | "pending" | "none";
|
||||||
lastCommentAt?: string;
|
lastCommentAt?: string;
|
||||||
lastCheckedAt?: string;
|
lastCheckedAt?: string;
|
||||||
|
lastReviewDecision?: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IssueState = "open" | "closed";
|
export type IssueState = "open" | "closed";
|
||||||
@@ -916,6 +917,9 @@ export interface TaskComment {
|
|||||||
author: string;
|
author: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
|
source?: "user" | "agent" | "github-review" | "github-review-comment";
|
||||||
|
externalId?: string;
|
||||||
|
reviewState?: "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskCommentInput {
|
export interface TaskCommentInput {
|
||||||
|
|||||||
Reference in New Issue
Block a user