fix(FN-690): fix migration schema, dashboard type guards, and test stability
- Fix SQL parameter mismatch in db-migrate task INSERT (add baseCommitSha, modifiedFiles, sliceId columns) - Fix dashboard routes type predicate to guard against null entries from git diff parsing - Stabilize central-core activity cleanup test with fake timers - Align executor test assertions with updated steering comment log messages - Align store test comment log assertion with new action/outcome format
This commit is contained in:
@@ -712,29 +712,36 @@ describe("CentralCore", () => {
|
||||
});
|
||||
|
||||
it("should cleanup old activity entries", async () => {
|
||||
const projectPath = join(tempDir, "cleanup-activity");
|
||||
mkdirSync(projectPath);
|
||||
projectPaths.push(projectPath);
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2026-01-15T12:00:00.000Z");
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const project = await central.registerProject({
|
||||
name: "Cleanup Activity",
|
||||
path: projectPath,
|
||||
});
|
||||
try {
|
||||
const projectPath = join(tempDir, "cleanup-activity");
|
||||
mkdirSync(projectPath);
|
||||
projectPaths.push(projectPath);
|
||||
|
||||
// Log a recent activity
|
||||
await central.logActivity({
|
||||
type: "task:created",
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: "Recent",
|
||||
});
|
||||
const project = await central.registerProject({
|
||||
name: "Cleanup Activity",
|
||||
path: projectPath,
|
||||
});
|
||||
|
||||
const deleted = await central.cleanupOldActivity(0); // Delete all older than 0 days
|
||||
expect(deleted).toBe(0); // The recent one shouldn't be deleted
|
||||
await central.logActivity({
|
||||
type: "task:created",
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
timestamp: now.toISOString(),
|
||||
details: "Recent",
|
||||
});
|
||||
|
||||
const countAfter = await central.getActivityCount();
|
||||
expect(countAfter).toBe(1);
|
||||
const deleted = await central.cleanupOldActivity(-1);
|
||||
expect(deleted).toBe(0);
|
||||
|
||||
const countAfter = await central.getActivityCount();
|
||||
expect(countAfter).toBe(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -162,15 +162,15 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO tasks (
|
||||
id, title, description, "column", status, size, reviewLevel, currentStep,
|
||||
worktree, blockedBy, paused, baseBranch, modelPresetId, modelProvider,
|
||||
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
|
||||
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
|
||||
dependencies, steps, log, attachments,
|
||||
worktree, blockedBy, paused, baseBranch, baseCommitSha, modelPresetId,
|
||||
modelProvider, modelId, validatorModelProvider, validatorModelId,
|
||||
mergeRetries, error, summary, thinkingLevel, createdAt, updatedAt,
|
||||
columnMovedAt, dependencies, steps, log, attachments, steeringComments,
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps
|
||||
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -184,16 +184,20 @@ 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
|
||||
}))];
|
||||
const steeringComments = Array.isArray((task as any).steeringComments)
|
||||
? (task as any).steeringComments
|
||||
: [];
|
||||
const comments = Array.isArray(task.comments) ? task.comments : [];
|
||||
const mergedComments = [
|
||||
...steeringComments.map((comment: any) => ({
|
||||
id: comment.id,
|
||||
text: comment.text,
|
||||
author: comment.author,
|
||||
createdAt: comment.createdAt,
|
||||
updatedAt: comment.updatedAt ?? comment.createdAt,
|
||||
})),
|
||||
...comments,
|
||||
];
|
||||
|
||||
insertStmt.run(
|
||||
task.id,
|
||||
@@ -208,6 +212,7 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
task.blockedBy ?? null,
|
||||
task.paused ? 1 : 0,
|
||||
task.baseBranch ?? null,
|
||||
task.baseCommitSha ?? null,
|
||||
task.modelPresetId ?? null,
|
||||
task.modelProvider ?? null,
|
||||
task.modelId ?? null,
|
||||
@@ -224,16 +229,16 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
toJson(task.steps || []),
|
||||
toJson(task.log || []),
|
||||
toJson(task.attachments || []),
|
||||
toJson(task.attachments || []),
|
||||
"[]", // steeringComments column - no longer used, write empty array
|
||||
// Merge legacy steeringComments into unified comments field during migration
|
||||
toJson([...((task as any).steeringComments || []), ...(task.comments || [])]),
|
||||
"[]",
|
||||
toJson(mergedComments),
|
||||
toJson(task.workflowStepResults || []),
|
||||
toJsonNullable(task.prInfo),
|
||||
toJsonNullable(task.issueInfo),
|
||||
toJsonNullable(task.mergeDetails),
|
||||
task.breakIntoSubtasks ? 1 : 0,
|
||||
toJson(task.enabledWorkflowSteps || []),
|
||||
toJson(task.modifiedFiles || []),
|
||||
task.sliceId ?? null,
|
||||
);
|
||||
migrated++;
|
||||
} catch (err) {
|
||||
|
||||
@@ -1587,7 +1587,7 @@ Task with acceptance criteria
|
||||
const task = await createTestTask();
|
||||
const updated = await store.addComment(task.id, "Comment with log");
|
||||
|
||||
expect(updated.log.some((l) => l.action === "Comment added by user")).toBe(true);
|
||||
expect(updated.log.some((l) => l.action === "Comment added" && l.outcome === "by user")).toBe(true);
|
||||
});
|
||||
|
||||
it("updates updatedAt timestamp", async () => {
|
||||
|
||||
@@ -2005,27 +2005,31 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
if (statusCode === "R") {
|
||||
const oldPath = parts[1];
|
||||
const path = parts[2];
|
||||
return {
|
||||
path,
|
||||
oldPath,
|
||||
status: "renamed" as const,
|
||||
diff: "",
|
||||
};
|
||||
return path
|
||||
? {
|
||||
path,
|
||||
oldPath,
|
||||
status: "renamed" as const,
|
||||
diff: "",
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
const path = parts[1];
|
||||
return {
|
||||
path,
|
||||
status:
|
||||
statusCode === "A"
|
||||
? ("added" as const)
|
||||
: statusCode === "D"
|
||||
? ("deleted" as const)
|
||||
: ("modified" as const),
|
||||
diff: "",
|
||||
};
|
||||
return path
|
||||
? {
|
||||
path,
|
||||
status:
|
||||
statusCode === "A"
|
||||
? ("added" as const)
|
||||
: statusCode === "D"
|
||||
? ("deleted" as const)
|
||||
: ("modified" as const),
|
||||
diff: "",
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter((entry): entry is TaskFileDiff => Boolean(entry.path));
|
||||
.filter((entry): entry is TaskFileDiff => entry !== null);
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -1725,7 +1725,7 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).toContain("## Steering Comments");
|
||||
|
||||
// Verify explanatory header text
|
||||
expect(result).toContain("The following comments were added during execution");
|
||||
expect(result).toContain("The following comments were added by the user 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
|
||||
@@ -4240,13 +4240,13 @@ describe("Real-time steering injection", () => {
|
||||
|
||||
// Verify steer was called with the formatted message
|
||||
expect(steerFn).toHaveBeenCalledOnce();
|
||||
expect(steerFn.mock.calls[0][0]).toContain("📣 **New steering feedback**");
|
||||
expect(steerFn.mock.calls[0][0]).toContain("📣 **New feedback**");
|
||||
expect(steerFn.mock.calls[0][0]).toContain("Please use a different approach");
|
||||
|
||||
// Verify log entry was created
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
expect.stringContaining("Steering comment received mid-execution"),
|
||||
expect.stringContaining("Comment received mid-execution"),
|
||||
"by user"
|
||||
);
|
||||
|
||||
@@ -4464,6 +4464,13 @@ describe("Real-time steering injection", () => {
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||
comments: [{
|
||||
id: "existing-comment",
|
||||
text: "Original",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: "user",
|
||||
}],
|
||||
steeringComments: [{
|
||||
id: "existing-comment",
|
||||
text: "Original",
|
||||
|
||||
Reference in New Issue
Block a user