feat(KB-622): unify steeringComments and comments into single field
- Add database migration to merge steeringComments into comments field - Update SQLite schema and queries to use unified comments column - Refactor TaskStore to handle single comments field instead of dual fields - Update dashboard components (SteeringTab, TaskCard) for unified comments - Update engine executor and PR comment handler for new field structure - Remove deprecated steeringComments from types and interfaces
This commit is contained in:
@@ -541,7 +541,8 @@ describe("migrateFromLegacy", () => {
|
||||
expect(JSON.parse(row.steps)).toHaveLength(2);
|
||||
expect(JSON.parse(row.log)).toHaveLength(1);
|
||||
expect(JSON.parse(row.attachments)).toHaveLength(1);
|
||||
expect(JSON.parse(row.steeringComments)).toHaveLength(1);
|
||||
// steeringComments should be merged into comments
|
||||
expect(JSON.parse(row.comments)).toHaveLength(1); // 1 steering comment migrated
|
||||
expect(JSON.parse(row.workflowStepResults)).toHaveLength(1);
|
||||
expect(JSON.parse(row.prInfo).number).toBe(1);
|
||||
expect(JSON.parse(row.issueInfo).number).toBe(10);
|
||||
|
||||
@@ -165,12 +165,12 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
worktree, blockedBy, paused, baseBranch, modelPresetId, modelProvider,
|
||||
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
|
||||
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
|
||||
dependencies, steps, log, attachments, steeringComments,
|
||||
dependencies, steps, log, attachments,
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -184,6 +184,17 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
const raw = await readFile(taskJsonPath, "utf-8");
|
||||
const task: Task = JSON.parse(raw);
|
||||
|
||||
// Merge steeringComments into comments (unified comments field)
|
||||
const existingComments = task.comments || [];
|
||||
const steeringComments = (task as any).steeringComments || [];
|
||||
const mergedComments = [...existingComments, ...steeringComments.map((sc: any) => ({
|
||||
id: sc.id,
|
||||
text: sc.text,
|
||||
author: sc.author,
|
||||
createdAt: sc.createdAt,
|
||||
updatedAt: sc.createdAt, // Steering comments didn't have updatedAt
|
||||
}))];
|
||||
|
||||
insertStmt.run(
|
||||
task.id,
|
||||
task.title ?? null,
|
||||
@@ -213,8 +224,7 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
toJson(task.steps || []),
|
||||
toJson(task.log || []),
|
||||
toJson(task.attachments || []),
|
||||
toJson(task.steeringComments || []),
|
||||
toJson(task.comments || []),
|
||||
toJson(mergedComments),
|
||||
toJson(task.workflowStepResults || []),
|
||||
toJsonNullable(task.prInfo),
|
||||
toJsonNullable(task.issueInfo),
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -109,7 +109,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -684,7 +684,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 4 (includes v1→v2, v2→v3, and v3→v4 migrations)
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -709,11 +709,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -808,7 +808,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 4
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -864,7 +864,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -58,7 +58,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 4;
|
||||
const SCHEMA_VERSION = 5;
|
||||
|
||||
const SCHEMA_SQL = `
|
||||
-- Tasks table with JSON columns for nested data
|
||||
@@ -333,8 +333,15 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 5) {
|
||||
this.applyMigration(5, () => {
|
||||
// Migrate steeringComments to comments (unified comments field)
|
||||
this.migrateSteeringCommentsToComments();
|
||||
});
|
||||
}
|
||||
|
||||
// Future migrations go here:
|
||||
// if (version < 3) { this.applyMigration(3, () => { ... }); }
|
||||
// if (version < 6) { this.applyMigration(6, () => { ... }); }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,6 +375,60 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate steeringComments data to the unified comments field.
|
||||
* This is a one-way migration from schema version 4 to 5.
|
||||
*/
|
||||
private migrateSteeringCommentsToComments(): void {
|
||||
// Only run if steeringComments column exists
|
||||
if (!this.hasColumn("tasks", "steeringComments")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all tasks that have steering comments
|
||||
const tasksWithSteering = this.db
|
||||
.prepare("SELECT id, steeringComments, comments FROM tasks WHERE steeringComments != '[]'")
|
||||
.all() as Array<{ id: string; steeringComments: string; comments: string }>;
|
||||
|
||||
for (const task of tasksWithSteering) {
|
||||
try {
|
||||
const steeringComments = JSON.parse(task.steeringComments) as Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
author: "user" | "agent";
|
||||
}>;
|
||||
const existingComments = JSON.parse(task.comments || "[]") as Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
author: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}>;
|
||||
|
||||
// Convert steering comments to the unified format
|
||||
const migratedComments = steeringComments.map((sc) => ({
|
||||
id: sc.id,
|
||||
text: sc.text,
|
||||
author: sc.author,
|
||||
createdAt: sc.createdAt,
|
||||
updatedAt: sc.createdAt, // Steering comments didn't have updatedAt
|
||||
}));
|
||||
|
||||
// Merge: existing comments first, then migrated steering comments
|
||||
const mergedComments = [...existingComments, ...migratedComments];
|
||||
|
||||
// Update the task with merged comments
|
||||
this.db
|
||||
.prepare("UPDATE tasks SET comments = ? WHERE id = ?")
|
||||
.run(JSON.stringify(mergedComments), task.id);
|
||||
} catch {
|
||||
// Skip tasks with invalid JSON in steeringComments
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, SteeringComment, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent } from "./types.js";
|
||||
export { AgentStore } from "./agent-store.js";
|
||||
export type { AgentStoreEvents } from "./agent-store.js";
|
||||
export { TaskStore } from "./store.js";
|
||||
|
||||
@@ -856,18 +856,18 @@ describe("TaskStore", () => {
|
||||
expect(fetched.steps[0].status).toBe("in-progress");
|
||||
});
|
||||
|
||||
it("addSteeringComment recreates missing task directory before persisting metadata", async () => {
|
||||
it("addComment recreates missing task directory before persisting metadata", async () => {
|
||||
const task = await createTestTask();
|
||||
const dir = await deleteTaskDir(task.id);
|
||||
|
||||
const updated = await store.addSteeringComment(task.id, "Please recover from missing directory");
|
||||
const updated = await store.addComment(task.id, "Please recover from missing directory");
|
||||
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(existsSync(dir)).toBe(true);
|
||||
expect(existsSync(join(dir, "task.json"))).toBe(true);
|
||||
|
||||
const fetched = await store.getTask(task.id);
|
||||
expect(fetched.steeringComments).toHaveLength(1);
|
||||
expect(fetched.comments).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("appendAgentLog recreates missing task directory before writing agent.log", async () => {
|
||||
@@ -1276,68 +1276,68 @@ describe("TaskStore", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("persists task comments independently from steering comments", async () => {
|
||||
it("unifies task comments and steering comments into single comments field", async () => {
|
||||
const task = await createTestTask();
|
||||
await store.addTaskComment(task.id, "General note", "alice");
|
||||
await store.addSteeringComment(task.id, "Execution note");
|
||||
await store.addComment(task.id, "Execution note");
|
||||
|
||||
const reopened = await store.getTask(task.id);
|
||||
expect(reopened.comments).toHaveLength(1);
|
||||
// Both comments should now be in the unified comments field
|
||||
expect(reopened.comments).toHaveLength(2);
|
||||
expect(reopened.comments![0].text).toBe("General note");
|
||||
expect(reopened.steeringComments).toHaveLength(1);
|
||||
expect(reopened.steeringComments![0].text).toBe("Execution note");
|
||||
expect(reopened.comments![1].text).toBe("Execution note");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addSteeringComment", () => {
|
||||
describe("addComment", () => {
|
||||
it("adds a steering comment to a task", async () => {
|
||||
const task = await createTestTask();
|
||||
const updated = await store.addSteeringComment(task.id, "Please handle the edge case");
|
||||
const updated = await store.addComment(task.id, "Please handle the edge case");
|
||||
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
expect(updated.steeringComments![0].text).toBe("Please handle the edge case");
|
||||
expect(updated.steeringComments![0].author).toBe("user");
|
||||
expect(updated.steeringComments![0].id).toBeDefined();
|
||||
expect(updated.steeringComments![0].createdAt).toBeDefined();
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(updated.comments![0].text).toBe("Please handle the edge case");
|
||||
expect(updated.comments![0].author).toBe("user");
|
||||
expect(updated.comments![0].id).toBeDefined();
|
||||
expect(updated.comments![0].createdAt).toBeDefined();
|
||||
});
|
||||
|
||||
it("accepts agent as author", async () => {
|
||||
const task = await createTestTask();
|
||||
const updated = await store.addSteeringComment(task.id, "Note from agent", "agent");
|
||||
const updated = await store.addComment(task.id, "Note from agent", "agent");
|
||||
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
expect(updated.steeringComments![0].author).toBe("agent");
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(updated.comments![0].author).toBe("agent");
|
||||
});
|
||||
|
||||
it("initializes steeringComments array if undefined", async () => {
|
||||
it("initializes comments array if undefined", async () => {
|
||||
const task = await createTestTask();
|
||||
expect(task.steeringComments).toBeUndefined();
|
||||
expect(task.comments).toBeUndefined();
|
||||
|
||||
const updated = await store.addSteeringComment(task.id, "First comment");
|
||||
expect(updated.steeringComments).toBeDefined();
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
const updated = await store.addComment(task.id, "First comment");
|
||||
expect(updated.comments).toBeDefined();
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("appends multiple comments in order", async () => {
|
||||
const task = await createTestTask();
|
||||
await store.addSteeringComment(task.id, "First comment");
|
||||
await store.addSteeringComment(task.id, "Second comment");
|
||||
await store.addSteeringComment(task.id, "Third comment");
|
||||
await store.addComment(task.id, "First comment");
|
||||
await store.addComment(task.id, "Second comment");
|
||||
await store.addComment(task.id, "Third comment");
|
||||
|
||||
const fetched = await store.getTask(task.id);
|
||||
expect(fetched.steeringComments).toHaveLength(3);
|
||||
expect(fetched.steeringComments![0].text).toBe("First comment");
|
||||
expect(fetched.steeringComments![1].text).toBe("Second comment");
|
||||
expect(fetched.steeringComments![2].text).toBe("Third comment");
|
||||
expect(fetched.comments).toHaveLength(3);
|
||||
expect(fetched.comments![0].text).toBe("First comment");
|
||||
expect(fetched.comments![1].text).toBe("Second comment");
|
||||
expect(fetched.comments![2].text).toBe("Third comment");
|
||||
});
|
||||
|
||||
it("generates unique IDs for each comment", async () => {
|
||||
const task = await createTestTask();
|
||||
const updated1 = await store.addSteeringComment(task.id, "Comment 1");
|
||||
const updated2 = await store.addSteeringComment(task.id, "Comment 2");
|
||||
const updated1 = await store.addComment(task.id, "Comment 1");
|
||||
const updated2 = await store.addComment(task.id, "Comment 2");
|
||||
|
||||
const id1 = updated1.steeringComments![0].id;
|
||||
const id2 = updated2.steeringComments![1].id;
|
||||
const id1 = updated1.comments![0].id;
|
||||
const id2 = updated2.comments![1].id;
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
|
||||
@@ -1346,28 +1346,28 @@ describe("TaskStore", () => {
|
||||
const events: any[] = [];
|
||||
store.on("task:updated", (t) => events.push(t));
|
||||
|
||||
await store.addSteeringComment(task.id, "Test comment");
|
||||
await store.addComment(task.id, "Test comment");
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].steeringComments).toHaveLength(1);
|
||||
expect(events[0].steeringComments![0].text).toBe("Test comment");
|
||||
expect(events[0].comments).toHaveLength(1);
|
||||
expect(events[0].comments![0].text).toBe("Test comment");
|
||||
});
|
||||
|
||||
it("persists to disk and round-trips correctly", async () => {
|
||||
const task = await createTestTask();
|
||||
await store.addSteeringComment(task.id, "Persisted comment");
|
||||
await store.addComment(task.id, "Persisted comment");
|
||||
|
||||
const fetched = await store.getTask(task.id);
|
||||
expect(fetched.steeringComments).toHaveLength(1);
|
||||
expect(fetched.steeringComments![0].text).toBe("Persisted comment");
|
||||
expect(fetched.steeringComments![0].author).toBe("user");
|
||||
expect(fetched.comments).toHaveLength(1);
|
||||
expect(fetched.comments![0].text).toBe("Persisted comment");
|
||||
expect(fetched.comments![0].author).toBe("user");
|
||||
});
|
||||
|
||||
it("adds log entry for the action", async () => {
|
||||
const task = await createTestTask();
|
||||
const updated = await store.addSteeringComment(task.id, "Comment with log");
|
||||
const updated = await store.addComment(task.id, "Comment with log");
|
||||
|
||||
expect(updated.log.some((l) => l.action === "Steering comment added")).toBe(true);
|
||||
expect(updated.log.some((l) => l.action === "Comment added")).toBe(true);
|
||||
expect(updated.log.some((l) => l.outcome === "by user")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1376,7 +1376,7 @@ describe("TaskStore", () => {
|
||||
const before = task.updatedAt;
|
||||
await new Promise((r) => setTimeout(r, 10)); // Ensure time passes
|
||||
|
||||
const updated = await store.addSteeringComment(task.id, "Timestamp test");
|
||||
const updated = await store.addComment(task.id, "Timestamp test");
|
||||
expect(updated.updatedAt).not.toBe(before);
|
||||
});
|
||||
|
||||
@@ -1389,7 +1389,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Need to fix edge case");
|
||||
await store.addComment(task.id, "Need to fix edge case");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length + 1);
|
||||
@@ -1406,7 +1406,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Some feedback");
|
||||
await store.addComment(task.id, "Some feedback");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
@@ -1419,7 +1419,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Some feedback");
|
||||
await store.addComment(task.id, "Some feedback");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
@@ -1433,7 +1433,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Some feedback");
|
||||
await store.addComment(task.id, "Some feedback");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
@@ -1446,10 +1446,10 @@ describe("TaskStore", () => {
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
const updated = await store.addSteeringComment(task.id, "Need to fix edge case");
|
||||
const updated = await store.addComment(task.id, "Need to fix edge case");
|
||||
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
expect(updated.steeringComments![0].text).toBe("Need to fix edge case");
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(updated.comments![0].text).toBe("Need to fix edge case");
|
||||
});
|
||||
|
||||
it("refinement task has correct dependency on original done task", async () => {
|
||||
@@ -1459,7 +1459,7 @@ describe("TaskStore", () => {
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
await store.addSteeringComment(task.id, "Need to fix edge case");
|
||||
await store.addComment(task.id, "Need to fix edge case");
|
||||
|
||||
const allTasks = await store.listTasks();
|
||||
const refinement = allTasks.find((t) => t.id !== task.id && t.dependencies?.includes(task.id));
|
||||
@@ -1477,7 +1477,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Agent feedback", "agent");
|
||||
await store.addComment(task.id, "Agent feedback", "agent");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
@@ -1491,10 +1491,10 @@ describe("TaskStore", () => {
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
// Should not throw - refineTask will reject empty feedback but we catch it
|
||||
const updated = await store.addSteeringComment(task.id, " ");
|
||||
const updated = await store.addComment(task.id, " ");
|
||||
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
expect(updated.steeringComments![0].text).toBe(" ");
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(updated.comments![0].text).toBe(" ");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1504,7 +1504,7 @@ describe("TaskStore", () => {
|
||||
const reopened = await store.getTask(task.id);
|
||||
|
||||
expect(reopened.comments).toBeUndefined();
|
||||
expect(reopened.steeringComments).toBeUndefined();
|
||||
expect(reopened.comments).toBeUndefined();
|
||||
});
|
||||
|
||||
it("supports the task comment and merge details shapes", async () => {
|
||||
@@ -2240,11 +2240,11 @@ describe("TaskStore", () => {
|
||||
|
||||
it("does NOT copy steering comments", async () => {
|
||||
const task = await store.createTask({ description: "Test task" });
|
||||
await store.addSteeringComment(task.id, "Test comment");
|
||||
await store.addComment(task.id, "Test comment");
|
||||
|
||||
const duplicated = await store.duplicateTask(task.id);
|
||||
|
||||
expect(duplicated.steeringComments).toBeUndefined();
|
||||
expect(duplicated.comments).toBeUndefined();
|
||||
});
|
||||
|
||||
it("emits task:created event", async () => {
|
||||
|
||||
@@ -150,7 +150,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
steps: fromJson<import("./types.js").TaskStep[]>(row.steps) || [],
|
||||
log: fromJson<import("./types.js").TaskLogEntry[]>(row.log) || [],
|
||||
attachments: (() => { const a = fromJson<TaskAttachment[]>(row.attachments); return a && a.length > 0 ? a : undefined; })(),
|
||||
steeringComments: (() => { const s = fromJson<import("./types.js").SteeringComment[]>(row.steeringComments); return s && s.length > 0 ? s : undefined; })(),
|
||||
comments: (() => { const c = fromJson<import("./types.js").TaskComment[]>(row.comments); return c && c.length > 0 ? c : undefined; })(),
|
||||
workflowStepResults: (() => { const w = fromJson<import("./types.js").WorkflowStepResult[]>(row.workflowStepResults); return w && w.length > 0 ? w : undefined; })(),
|
||||
prInfo: fromJson<import("./types.js").PrInfo>(row.prInfo),
|
||||
@@ -172,12 +171,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
worktree, blockedBy, paused, baseBranch, baseCommitSha, modelPresetId, modelProvider,
|
||||
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
|
||||
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
|
||||
dependencies, steps, log, attachments, steeringComments,
|
||||
dependencies, steps, log, attachments,
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`).run(
|
||||
task.id,
|
||||
@@ -209,7 +208,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
toJson(task.steps || []),
|
||||
toJson(task.log || []),
|
||||
toJson(task.attachments || []),
|
||||
toJson(task.steeringComments || []),
|
||||
toJson(task.comments || []),
|
||||
toJson(task.workflowStepResults || []),
|
||||
toJsonNullable(task.prInfo),
|
||||
@@ -691,7 +689,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
// Explicitly NOT copied: worktree, status, blockedBy, paused, baseBranch,
|
||||
// attachments, steeringComments, prInfo, agent logs, size, reviewLevel
|
||||
// attachments, comments, prInfo, agent logs, size, reviewLevel
|
||||
};
|
||||
|
||||
const newDir = this.taskDir(newId);
|
||||
@@ -2025,17 +2023,17 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a steering comment to a task.
|
||||
* Steering comments are user-provided feedback injected into the AI execution context.
|
||||
* When a steering comment is added to a task in the "done" column by a user,
|
||||
* Add a comment to a task.
|
||||
* Comments are injected into the AI execution context.
|
||||
* When a comment is added to a task in the "done" column by a user,
|
||||
* automatically creates a refinement task with the comment text as feedback.
|
||||
*/
|
||||
async addSteeringComment(
|
||||
async addComment(
|
||||
id: string,
|
||||
text: string,
|
||||
author: "user" | "agent" = "user",
|
||||
): Promise<Task> {
|
||||
// Phase 1: Add steering comment under lock
|
||||
// Phase 1: Add comment under lock
|
||||
const task = await this.withTaskLock(id, async () => {
|
||||
const dir = this.taskDir(id);
|
||||
const task = await this.readTaskJson(dir);
|
||||
@@ -2048,21 +2046,22 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
// Generate unique ID: timestamp + random suffix for collision resistance
|
||||
const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const comment: import("./types.js").SteeringComment = {
|
||||
const comment: import("./types.js").TaskComment = {
|
||||
id: commentId,
|
||||
text,
|
||||
createdAt: new Date().toISOString(),
|
||||
author,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (!task.steeringComments) {
|
||||
task.steeringComments = [];
|
||||
if (!task.comments) {
|
||||
task.comments = [];
|
||||
}
|
||||
task.steeringComments.push(comment);
|
||||
task.comments.push(comment);
|
||||
task.updatedAt = new Date().toISOString();
|
||||
task.log.push({
|
||||
timestamp: task.updatedAt,
|
||||
action: "Steering comment added",
|
||||
action: "Comment added",
|
||||
outcome: `by ${author}`,
|
||||
});
|
||||
|
||||
@@ -2080,7 +2079,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
await this.refineTask(id, text);
|
||||
} catch {
|
||||
// Silently ignore - refinement is best-effort and shouldn't fail
|
||||
// the steering comment addition. refineTask already validates
|
||||
// the comment addition. refineTask already validates
|
||||
// feedback text, so empty/whitespace comments won't create refinements.
|
||||
}
|
||||
}
|
||||
@@ -2374,7 +2373,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
validatorModelId: entry.validatorModelId,
|
||||
breakIntoSubtasks: entry.breakIntoSubtasks,
|
||||
modifiedFiles: entry.modifiedFiles,
|
||||
// Intentionally NOT restoring: worktree, status, blockedBy, paused, baseBranch, baseCommitSha, error, steeringComments
|
||||
// Intentionally NOT restoring: worktree, status, blockedBy, paused, baseBranch, baseCommitSha, error, comments
|
||||
};
|
||||
|
||||
// Write task.json
|
||||
|
||||
@@ -327,13 +327,6 @@ export interface TaskAttachment {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SteeringComment {
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
author: "user" | "agent";
|
||||
}
|
||||
|
||||
export interface TaskComment {
|
||||
id: string;
|
||||
text: string;
|
||||
@@ -392,7 +385,6 @@ export interface Task {
|
||||
* Set by the executor when creating the worktree. */
|
||||
baseCommitSha?: string;
|
||||
attachments?: TaskAttachment[];
|
||||
steeringComments?: SteeringComment[];
|
||||
comments?: TaskComment[];
|
||||
/** PR information for tasks linked to GitHub pull requests */
|
||||
prInfo?: PrInfo;
|
||||
|
||||
@@ -307,7 +307,7 @@ export function deleteTaskComment(id: string, commentId: string): Promise<Task>
|
||||
});
|
||||
}
|
||||
|
||||
export function addSteeringComment(id: string, text: string): Promise<Task> {
|
||||
export function addComment(id: string, text: string): Promise<Task> {
|
||||
return api<Task>(`/tasks/${id}/steer`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import type { TaskDetail } from "@fusion/core";
|
||||
import { addSteeringComment } from "../api";
|
||||
import { addComment } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
function formatTimestamp(iso: string): string {
|
||||
@@ -24,7 +24,7 @@ interface SteeringTabProps {
|
||||
}
|
||||
|
||||
export function SteeringTab({ task, addToast }: SteeringTabProps) {
|
||||
const [comments, setComments] = useState(task.steeringComments || []);
|
||||
const [comments, setComments] = useState(task.comments || []);
|
||||
const [newComment, setNewComment] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@@ -35,10 +35,10 @@ export function SteeringTab({ task, addToast }: SteeringTabProps) {
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const updated = await addSteeringComment(task.id, newComment.trim());
|
||||
setComments(updated.steeringComments || []);
|
||||
const updated = await addComment(task.id, newComment.trim());
|
||||
setComments(updated.comments || []);
|
||||
setNewComment("");
|
||||
addToast("Steering comment added", "success");
|
||||
addToast("Comment added", "success");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} finally {
|
||||
@@ -60,7 +60,7 @@ export function SteeringTab({ task, addToast }: SteeringTabProps) {
|
||||
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<h4>Steering Comments</h4>
|
||||
<h4>Comments</h4>
|
||||
<p style={{ fontSize: "13px", opacity: 0.7, marginBottom: "12px" }}>
|
||||
Add comments to guide the AI during task execution. These are injected into the execution context.
|
||||
</p>
|
||||
@@ -113,7 +113,7 @@ export function SteeringTab({ task, addToast }: SteeringTabProps) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ opacity: 0.5, marginBottom: "16px" }}>(no steering comments yet)</div>
|
||||
<div style={{ opacity: 0.5, marginBottom: "16px" }}>(no comments yet)</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
@@ -121,7 +121,7 @@ export function SteeringTab({ task, addToast }: SteeringTabProps) {
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Add a steering comment... (Ctrl+Enter to submit)"
|
||||
placeholder="Add a comment... (Ctrl+Enter to submit)"
|
||||
maxLength={MAX_LENGTH}
|
||||
rows={4}
|
||||
style={{
|
||||
@@ -160,7 +160,7 @@ export function SteeringTab({ task, addToast }: SteeringTabProps) {
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Adding…" : "Add Steering Comment"}
|
||||
{isSubmitting ? "Adding…" : "Add Comment"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
||||
previousTask.reviewLevel === nextTask.reviewLevel &&
|
||||
previousTask.mergeRetries === nextTask.mergeRetries &&
|
||||
JSON.stringify(previousTask.attachments ?? []) === JSON.stringify(nextTask.attachments ?? []) &&
|
||||
JSON.stringify(previousTask.steeringComments ?? []) === JSON.stringify(nextTask.steeringComments ?? []) &&
|
||||
JSON.stringify(previousTask.comments ?? []) === JSON.stringify(nextTask.comments ?? []) &&
|
||||
areTaskDependenciesEqual(previousTask.dependencies, nextTask.dependencies) &&
|
||||
areTaskStepsEqual(previousTask.steps, nextTask.steps) &&
|
||||
areTaskBadgeInfosEqual(previousTask.prInfo, nextTask.prInfo) &&
|
||||
|
||||
@@ -71,7 +71,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
getGlobalSettingsStore: vi.fn().mockReturnValue(createMockGlobalSettingsStore()),
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
getAgentLogs: vi.fn().mockResolvedValue([]),
|
||||
addSteeringComment: vi.fn(),
|
||||
addComment: vi.fn(),
|
||||
addTaskComment: vi.fn(),
|
||||
updateTaskComment: vi.fn(),
|
||||
deleteTaskComment: vi.fn(),
|
||||
@@ -1850,7 +1850,7 @@ describe("Pause/Unpause endpoints", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
(store.addSteeringComment as ReturnType<typeof vi.fn>).mockResolvedValue(mockComment);
|
||||
(store.addComment as ReturnType<typeof vi.fn>).mockResolvedValue(mockComment);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
@@ -1862,7 +1862,7 @@ describe("Pause/Unpause endpoints", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual(mockComment);
|
||||
expect(store.addSteeringComment).toHaveBeenCalledWith(
|
||||
expect(store.addComment).toHaveBeenCalledWith(
|
||||
"KB-001",
|
||||
"Please handle the edge case",
|
||||
"user"
|
||||
@@ -1909,7 +1909,7 @@ describe("Pause/Unpause endpoints", () => {
|
||||
it("returns 404 when task not found", async () => {
|
||||
const error = new Error("Task not found") as Error & { code?: string };
|
||||
error.code = "ENOENT";
|
||||
(store.addSteeringComment as ReturnType<typeof vi.fn>).mockRejectedValue(error);
|
||||
(store.addComment as ReturnType<typeof vi.fn>).mockRejectedValue(error);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
@@ -1923,7 +1923,7 @@ describe("Pause/Unpause endpoints", () => {
|
||||
});
|
||||
|
||||
it("returns 500 on unexpected errors", async () => {
|
||||
(store.addSteeringComment as ReturnType<typeof vi.fn>).mockRejectedValue(
|
||||
(store.addComment as ReturnType<typeof vi.fn>).mockRejectedValue(
|
||||
new Error("Database error")
|
||||
);
|
||||
|
||||
|
||||
@@ -2114,7 +2114,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
res.status(400).json({ error: "text must be between 1 and 2000 characters" });
|
||||
return;
|
||||
}
|
||||
const task = await store.addSteeringComment(req.params.id, text, "user");
|
||||
const task = await store.addComment(req.params.id, text, "user");
|
||||
res.json(task);
|
||||
} catch (err: any) {
|
||||
const status = err.code === "ENOENT" ? 404 : 500;
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"jsx": "react-jsx",
|
||||
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/__tests__/**/*"]
|
||||
}
|
||||
|
||||
@@ -1631,7 +1631,7 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("## Comments");
|
||||
expect(result).toContain("**user**");
|
||||
expect(result).toContain("> Please handle the edge case");
|
||||
expect(result).toContain("The following comments were added by the user");
|
||||
expect(result).toContain("The following comments were added during execution");
|
||||
});
|
||||
|
||||
it("formats multiple comments correctly", () => {
|
||||
@@ -1675,14 +1675,14 @@ describe("buildExecutionPrompt", () => {
|
||||
});
|
||||
|
||||
it("includes only the 10 most recent comments", () => {
|
||||
const steeringComments = Array.from({ length: 15 }, (_, i) => ({
|
||||
const comments = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: `${i}`,
|
||||
text: `Comment ${i}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
author: "user" as const,
|
||||
}));
|
||||
|
||||
const task = createMockTaskDetail({ steeringComments });
|
||||
const task = createMockTaskDetail({ comments });
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
// Should include comments 5-14 (the 10 most recent), not 0-4
|
||||
@@ -1725,7 +1725,7 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("## Comments");
|
||||
|
||||
// Verify explanatory header text
|
||||
expect(result).toContain("The following comments were added by the user during execution");
|
||||
expect(result).toContain("The following comments were added during execution");
|
||||
expect(result).toContain("Consider adjusting your approach or replanning remaining steps based on this feedback");
|
||||
|
||||
// Verify all three comments appear with correct author badges
|
||||
|
||||
@@ -237,21 +237,21 @@ export class TaskExecutor {
|
||||
// Mark as seen BEFORE attempting injection to prevent retry loops on failure
|
||||
seenSteeringIds.add(comment.id);
|
||||
|
||||
// Format and inject the steering comment
|
||||
const steeringMessage = formatSteeringCommentForInjection(comment);
|
||||
// Format and inject the comment
|
||||
const commentMessage = formatCommentForInjection(comment);
|
||||
try {
|
||||
executorLog.log(`Injecting steering comment into ${task.id}: ${summary}`);
|
||||
await session.steer(steeringMessage);
|
||||
executorLog.log(`Successfully injected steering comment into ${task.id}`);
|
||||
executorLog.log(`Injecting comment into ${task.id}: ${summary}`);
|
||||
await session.steer(commentMessage);
|
||||
executorLog.log(`Successfully injected comment into ${task.id}`);
|
||||
|
||||
// Log to the task that steering was received
|
||||
// Log to the task that comment was received
|
||||
await this.store.logEntry(
|
||||
task.id,
|
||||
`Steering comment received mid-execution: ${summary}`,
|
||||
`Comment received mid-execution: ${summary}`,
|
||||
`by ${comment.author}`
|
||||
);
|
||||
} catch (err) {
|
||||
executorLog.error(`Failed to inject steering comment for ${task.id}:`, err);
|
||||
executorLog.error(`Failed to inject comment for ${task.id}:`, err);
|
||||
// Comment is already marked as seen - we won't retry to avoid spamming
|
||||
// the agent with failed injections. The error is logged for debugging.
|
||||
}
|
||||
@@ -551,10 +551,10 @@ export class TaskExecutor {
|
||||
sessionRef.current = session;
|
||||
|
||||
// Register session so the pause listener can terminate it
|
||||
// Initialize with empty set of seen steering comments
|
||||
// Initialize with empty set of seen comments
|
||||
const seenSteeringIds = new Set<string>();
|
||||
if (detail.steeringComments) {
|
||||
for (const comment of detail.steeringComments) {
|
||||
if (detail.comments) {
|
||||
for (const comment of detail.comments) {
|
||||
seenSteeringIds.add(comment.id);
|
||||
}
|
||||
}
|
||||
@@ -1874,10 +1874,10 @@ When all steps are complete: call \`task_done()\``;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a steering comment for injection into a running agent session.
|
||||
* Format a comment for injection into a running agent session.
|
||||
* Used for real-time steering during task execution.
|
||||
*/
|
||||
function formatSteeringCommentForInjection(comment: import("@fusion/core").SteeringComment): string {
|
||||
function formatCommentForInjection(comment: import("@fusion/core").TaskComment): string {
|
||||
const timestamp = formatTimestamp(comment.createdAt);
|
||||
return `📣 **New steering feedback** — ${timestamp} (${comment.author}):\n\n${comment.text}\n\nPlease adjust your approach based on this feedback.`;
|
||||
return `📣 **New feedback** — ${timestamp} (${comment.author}):\n\n${comment.text}\n\nPlease adjust your approach based on this feedback.`;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PrCommentHandler } from "./pr-comment-handler.js";
|
||||
import type { TaskStore, Task } from "@fusion/core";
|
||||
|
||||
const mockStore = {
|
||||
addSteeringComment: vi.fn<(id: string, text: string, author?: "user" | "agent") => Promise<Task>>(),
|
||||
addComment: vi.fn<(id: string, text: string, author?: "user" | "agent") => Promise<Task>>(),
|
||||
createTask: vi.fn<(input: Parameters<TaskStore["createTask"]>[0]) => Promise<Task>>().mockResolvedValue({ id: "FN-123" } as Task),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockStore.addSteeringComment).not.toHaveBeenCalled();
|
||||
expect(mockStore.addComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockStore.addSteeringComment).toHaveBeenCalled();
|
||||
expect(mockStore.addComment).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockStore.addSteeringComment).toHaveBeenCalled();
|
||||
expect(mockStore.addComment).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates steering comment for inline code suggestions", async () => {
|
||||
@@ -109,7 +109,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockStore.addSteeringComment).toHaveBeenCalled();
|
||||
expect(mockStore.addComment).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +126,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const call = (mockStore.addSteeringComment as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
const call = (mockStore.addComment as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
const text = call[1] as string;
|
||||
|
||||
expect(text).toContain("PR Review Feedback");
|
||||
@@ -151,7 +151,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const call = (mockStore.addSteeringComment as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
const call = (mockStore.addComment as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
const text = call[1] as string;
|
||||
|
||||
expect(text.length).toBeLessThan(longBody.length);
|
||||
@@ -170,7 +170,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockStore.addSteeringComment).toHaveBeenCalledWith(
|
||||
expect(mockStore.addComment).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
expect.any(String),
|
||||
"agent"
|
||||
@@ -189,7 +189,7 @@ describe("PrCommentHandler", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const text = (mockStore.addSteeringComment as ReturnType<typeof vi.fn>).mock.calls[0][1] as string;
|
||||
const text = (mockStore.addComment as ReturnType<typeof vi.fn>).mock.calls[0][1] as string;
|
||||
expect(text).toContain("This PR is already merged");
|
||||
expect(text).toContain("follow-up work");
|
||||
});
|
||||
|
||||
@@ -85,10 +85,10 @@ export class PrCommentHandler {
|
||||
const text = this.buildSteeringText(prInfo, comment, hasCodeSuggestions);
|
||||
|
||||
try {
|
||||
await this.store.addSteeringComment(taskId, text, "agent");
|
||||
prMonitorLog.log(`Added steering comment for PR review #${comment.id}`);
|
||||
await this.store.addComment(taskId, text, "agent");
|
||||
prMonitorLog.log(`Added comment for PR review #${comment.id}`);
|
||||
} catch (err) {
|
||||
prMonitorLog.error(`Failed to add steering comment for ${taskId}:`, err);
|
||||
prMonitorLog.error(`Failed to add comment for ${taskId}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user