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:
gsxdsm
2026-04-01 09:19:50 -07:00
parent 5c367d2a97
commit 5e949ff4aa
6 changed files with 88 additions and 60 deletions

View File

@@ -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();
}
});
});

View File

@@ -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) {

View File

@@ -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 () => {

View File

@@ -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 {

View File

@@ -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",