test(KB-601): add test for pause/unpause with missing directory

This commit is contained in:
gsxdsm
2026-03-31 14:43:55 -07:00
parent a6acf8a5f4
commit ace0732a1d
21 changed files with 1090 additions and 24 deletions

View File

@@ -141,9 +141,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
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),
issueInfo: fromJson<import("./types.js").IssueInfo>(row.issueInfo),
mergeDetails: fromJson<import("./types.js").MergeDetails>(row.mergeDetails),
breakIntoSubtasks: row.breakIntoSubtasks ? true : undefined,
enabledWorkflowSteps: (() => { const e = fromJson<string[]>(row.enabledWorkflowSteps); return e && e.length > 0 ? e : undefined; })(),
};
@@ -160,11 +162,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
dependencies, steps, log, attachments, steeringComments,
workflowStepResults, prInfo, issueInfo, breakIntoSubtasks,
enabledWorkflowSteps
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
breakIntoSubtasks, enabledWorkflowSteps
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`).run(
task.id,
@@ -196,9 +198,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
toJson(task.log || []),
toJson(task.attachments || []),
toJson(task.steeringComments || []),
toJson(task.comments || []),
toJson(task.workflowStepResults || []),
toJsonNullable(task.prInfo),
toJsonNullable(task.issueInfo),
toJsonNullable(task.mergeDetails),
task.breakIntoSubtasks ? 1 : 0,
toJson(task.enabledWorkflowSteps || []),
);
@@ -1162,6 +1166,58 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
});
}
private collectMergeDetails(id: string, branch: string, task: Task, commitMessage: string): import("./types.js").MergeDetails {
const mergedAt = new Date().toISOString();
let commitSha: string | undefined;
let filesChanged: number | undefined;
let insertions: number | undefined;
let deletions: number | undefined;
try {
commitSha = execSync("git rev-parse HEAD", {
cwd: this.rootDir,
stdio: "pipe",
encoding: "utf-8",
}).trim() || undefined;
} catch {
commitSha = undefined;
}
try {
const statsOutput = execSync("git show --shortstat --format= HEAD", {
cwd: this.rootDir,
stdio: "pipe",
encoding: "utf-8",
}).trim();
const normalized = statsOutput.replace(/\n/g, " ");
const filesMatch = normalized.match(/(\d+) files? changed/);
const insertionsMatch = normalized.match(/(\d+) insertions?\(\+\)/);
const deletionsMatch = normalized.match(/(\d+) deletions?\(-\)/);
filesChanged = filesMatch ? Number.parseInt(filesMatch[1], 10) : 0;
insertions = insertionsMatch ? Number.parseInt(insertionsMatch[1], 10) : 0;
deletions = deletionsMatch ? Number.parseInt(deletionsMatch[1], 10) : 0;
} catch {
filesChanged = undefined;
insertions = undefined;
deletions = undefined;
}
return {
commitSha,
filesChanged,
insertions,
deletions,
mergeCommitMessage: commitMessage,
mergedAt,
mergeConfirmed: true,
prNumber: task.prInfo?.number,
resolutionStrategy: task.mergeDetails?.resolutionStrategy,
resolutionMethod: task.mergeDetails?.resolutionMethod,
attemptsMade: task.mergeDetails?.attemptsMade,
autoResolvedCount: task.mergeDetails?.autoResolvedCount,
};
}
/**
* Merge an in-review task's branch into the current branch,
* clean up the worktree, and move the task to done.
@@ -1196,6 +1252,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
} catch {
// No branch — might have been manually merged. Just move to done.
result.error = `Branch '${branch}' not found — moving to done without merge`;
task.mergeDetails = {
mergedAt: new Date().toISOString(),
mergeConfirmed: false,
prNumber: task.prInfo?.number,
};
await this.moveToDone(task, dir);
result.task = { ...task, column: "done" };
this.emit("task:merged", result);
@@ -1203,16 +1264,20 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
// 2. Merge the branch
const mergeCommitMessage = `feat(${id}): merge ${branch}`;
try {
execSync(`git merge --squash "${branch}"`, {
cwd: this.rootDir,
stdio: "pipe",
});
execSync(`git commit --no-edit -m "feat(${id}): merge ${branch}"`, {
execSync(`git commit --no-edit -m "${mergeCommitMessage}"`, {
cwd: this.rootDir,
stdio: "pipe",
});
result.merged = true;
const mergeDetails = this.collectMergeDetails(id, branch, task, mergeCommitMessage);
task.mergeDetails = mergeDetails;
Object.assign(result, mergeDetails);
} catch (err: any) {
// Squash conflict — reset and report
try {
@@ -1806,6 +1871,95 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
this.emit("agent:log", entry);
}
async addTaskComment(id: string, text: string, author: string): Promise<Task> {
return this.withTaskLock(id, async () => {
const dir = this.taskDir(id);
const task = await this.readTaskJson(dir);
if (!task.log) {
task.log = [];
}
const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const comment: import("./types.js").TaskComment = {
id: commentId,
text,
author,
createdAt: new Date().toISOString(),
};
if (!task.comments) {
task.comments = [];
}
task.comments.push(comment);
task.updatedAt = new Date().toISOString();
task.log.push({
timestamp: task.updatedAt,
action: `Comment added by ${author}`,
});
await this.atomicWriteTaskJson(dir, task);
if (this.watcher) this.taskCache.set(id, { ...task });
this.emit("task:updated", task);
return task;
});
}
async updateTaskComment(id: string, commentId: string, text: string): Promise<Task> {
return this.withTaskLock(id, async () => {
const dir = this.taskDir(id);
const task = await this.readTaskJson(dir);
const comments = task.comments || [];
const comment = comments.find((entry) => entry.id === commentId);
if (!comment) {
throw new Error(`Comment ${commentId} not found on task ${id}`);
}
comment.text = text;
comment.updatedAt = new Date().toISOString();
task.comments = comments;
task.updatedAt = comment.updatedAt;
task.log.push({
timestamp: task.updatedAt,
action: "Comment updated",
});
await this.atomicWriteTaskJson(dir, task);
if (this.watcher) this.taskCache.set(id, { ...task });
this.emit("task:updated", task);
return task;
});
}
async deleteTaskComment(id: string, commentId: string): Promise<Task> {
return this.withTaskLock(id, async () => {
const dir = this.taskDir(id);
const task = await this.readTaskJson(dir);
const currentComments = task.comments || [];
const nextComments = currentComments.filter((entry) => entry.id !== commentId);
if (nextComments.length === currentComments.length) {
throw new Error(`Comment ${commentId} not found on task ${id}`);
}
task.comments = nextComments.length > 0 ? nextComments : undefined;
task.updatedAt = new Date().toISOString();
task.log.push({
timestamp: task.updatedAt,
action: "Comment deleted",
});
await this.atomicWriteTaskJson(dir, task);
if (this.watcher) this.taskCache.set(id, { ...task });
this.emit("task:updated", task);
return task;
});
}
/**
* Add a steering comment to a task.
* Steering comments are user-provided feedback injected into the AI execution context.