328 lines
9.9 KiB
TypeScript
328 lines
9.9 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type { StepStatus } from "./types.js";
|
|
import { getTaskCompletionBlocker, getTaskMergeBlocker, isTaskReadyForMerge } from "./task-merge.js";
|
|
|
|
const baseTask = {
|
|
column: "in-review" as const,
|
|
paused: false,
|
|
status: undefined as string | undefined,
|
|
error: undefined as string | undefined,
|
|
steps: [] as Array<{ name: string; status: StepStatus }>,
|
|
workflowStepResults: undefined as any,
|
|
};
|
|
|
|
const baseCompletionTask = {
|
|
dependencies: [] as string[],
|
|
blockedBy: undefined as string | undefined,
|
|
};
|
|
|
|
describe("getTaskMergeBlocker", () => {
|
|
it("returns undefined for a clean task in review", () => {
|
|
expect(getTaskMergeBlocker(baseTask)).toBeUndefined();
|
|
});
|
|
|
|
it("returns reason when task is not in review", () => {
|
|
expect(getTaskMergeBlocker({ ...baseTask, column: "todo" }))
|
|
.toContain("must be in 'in-review'");
|
|
});
|
|
|
|
it("returns reason when task is paused", () => {
|
|
expect(getTaskMergeBlocker({ ...baseTask, paused: true }))
|
|
.toBe("task is paused");
|
|
});
|
|
|
|
it("returns reason when task has failed status", () => {
|
|
expect(getTaskMergeBlocker({ ...baseTask, status: "failed" }))
|
|
.toContain("failed");
|
|
});
|
|
|
|
it("returns reason when task has awaiting-user-review status", () => {
|
|
expect(getTaskMergeBlocker({ ...baseTask, status: "awaiting-user-review" }))
|
|
.toContain("awaiting-user-review");
|
|
});
|
|
|
|
it("returns reason when task has awaiting-inspection status", () => {
|
|
expect(getTaskMergeBlocker({ ...baseTask, status: "awaiting-inspection" }))
|
|
.toContain("awaiting-inspection");
|
|
});
|
|
|
|
it("returns reason when task has incomplete steps", () => {
|
|
expect(getTaskMergeBlocker({
|
|
...baseTask,
|
|
steps: [{ name: "Step 1", status: "in-progress" }],
|
|
})).toBe("task has incomplete steps");
|
|
});
|
|
|
|
// ── Workflow Step Phase Awareness ──────────────────────────────────────
|
|
|
|
it("blocks merge when pre-merge workflow step has failed", () => {
|
|
const result = getTaskMergeBlocker({
|
|
...baseTask,
|
|
workflowStepResults: [{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Pre-merge Check",
|
|
phase: "pre-merge",
|
|
status: "failed",
|
|
output: "Check failed",
|
|
}],
|
|
});
|
|
expect(result).toContain("pre-merge workflow steps");
|
|
});
|
|
|
|
it("blocks merge when legacy workflow step (no phase) has failed", () => {
|
|
const result = getTaskMergeBlocker({
|
|
...baseTask,
|
|
workflowStepResults: [{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Legacy Check",
|
|
// phase is undefined → treated as pre-merge
|
|
status: "failed",
|
|
output: "Check failed",
|
|
}],
|
|
});
|
|
expect(result).toContain("pre-merge workflow steps");
|
|
});
|
|
|
|
it("does NOT block merge when only post-merge workflow step has failed", () => {
|
|
const result = getTaskMergeBlocker({
|
|
...baseTask,
|
|
workflowStepResults: [{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Post-merge Notify",
|
|
phase: "post-merge",
|
|
status: "failed",
|
|
output: "Notification failed",
|
|
}],
|
|
});
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("does NOT block merge when pre-merge passed and post-merge failed", () => {
|
|
const result = getTaskMergeBlocker({
|
|
...baseTask,
|
|
workflowStepResults: [
|
|
{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Pre-merge Check",
|
|
phase: "pre-merge",
|
|
status: "passed",
|
|
},
|
|
{
|
|
workflowStepId: "WS-002",
|
|
workflowStepName: "Post-merge Notify",
|
|
phase: "post-merge",
|
|
status: "failed",
|
|
output: "Failed",
|
|
},
|
|
],
|
|
});
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("blocks merge when pre-merge step is still pending", () => {
|
|
const result = getTaskMergeBlocker({
|
|
...baseTask,
|
|
workflowStepResults: [{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Pre-merge Check",
|
|
phase: "pre-merge",
|
|
status: "pending",
|
|
}],
|
|
});
|
|
expect(result).toContain("pre-merge workflow steps");
|
|
});
|
|
|
|
it("does NOT block merge when only post-merge step is pending", () => {
|
|
const result = getTaskMergeBlocker({
|
|
...baseTask,
|
|
workflowStepResults: [{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Post-merge Notify",
|
|
phase: "post-merge",
|
|
status: "pending",
|
|
}],
|
|
});
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("allows merge when all pre-merge steps passed regardless of post-merge status", () => {
|
|
const result = getTaskMergeBlocker({
|
|
...baseTask,
|
|
workflowStepResults: [
|
|
{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Pre-merge Check",
|
|
phase: "pre-merge",
|
|
status: "passed",
|
|
},
|
|
{
|
|
workflowStepId: "WS-002",
|
|
workflowStepName: "Post-merge Verify",
|
|
phase: "post-merge",
|
|
status: "skipped",
|
|
},
|
|
],
|
|
});
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("isTaskReadyForMerge", () => {
|
|
it("returns true for a clean task in review", () => {
|
|
expect(isTaskReadyForMerge(baseTask)).toBe(true);
|
|
});
|
|
|
|
it("returns false when pre-merge step failed", () => {
|
|
expect(isTaskReadyForMerge({
|
|
...baseTask,
|
|
workflowStepResults: [{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Check",
|
|
phase: "pre-merge",
|
|
status: "failed",
|
|
}],
|
|
})).toBe(false);
|
|
});
|
|
|
|
it("returns true when only post-merge step failed", () => {
|
|
expect(isTaskReadyForMerge({
|
|
...baseTask,
|
|
workflowStepResults: [{
|
|
workflowStepId: "WS-001",
|
|
workflowStepName: "Notify",
|
|
phase: "post-merge",
|
|
status: "failed",
|
|
}],
|
|
})).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getTaskCompletionBlocker", () => {
|
|
it("returns undefined for a task with no blockers", async () => {
|
|
await expect(getTaskCompletionBlocker(baseCompletionTask)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("returns a reason when task has blockedBy", async () => {
|
|
await expect(getTaskCompletionBlocker({ ...baseCompletionTask, blockedBy: "FN-123" }))
|
|
.resolves.toBe("task is blocked by FN-123");
|
|
});
|
|
|
|
it("returns a reason when a dependency is unresolved", async () => {
|
|
const resolveTask = async (taskId: string) => {
|
|
if (taskId === "FN-001") {
|
|
return { id: "FN-001", column: "done" as const };
|
|
}
|
|
if (taskId === "FN-002") {
|
|
return { id: "FN-002", column: "in-progress" as const };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-001", "FN-002"],
|
|
}, { resolveTask }))
|
|
.resolves.toBe("task has unresolved dependencies: FN-002");
|
|
});
|
|
|
|
it("returns undefined when all dependencies are resolved", async () => {
|
|
const resolveTask = async (taskId: string) => ({ id: taskId, column: "done" as const });
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-001", "FN-002"],
|
|
}, { resolveTask }))
|
|
.resolves.toBeUndefined();
|
|
});
|
|
|
|
// ── in-review as resolved dependency ───────────────────────────────────
|
|
|
|
it("returns undefined when a dependency is in-review", async () => {
|
|
const resolveTask = async (taskId: string) => {
|
|
if (taskId === "FN-001") {
|
|
return { id: "FN-001", column: "in-review" as const };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-001"],
|
|
}, { resolveTask }))
|
|
.resolves.toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined when dependencies are a mix of done and in-review", async () => {
|
|
const resolveTask = async (taskId: string) => {
|
|
if (taskId === "FN-001") {
|
|
return { id: "FN-001", column: "done" as const };
|
|
}
|
|
if (taskId === "FN-002") {
|
|
return { id: "FN-002", column: "in-review" as const };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-001", "FN-002"],
|
|
}, { resolveTask }))
|
|
.resolves.toBeUndefined();
|
|
});
|
|
|
|
it("returns a reason when a dependency is in-progress", async () => {
|
|
const resolveTask = async (taskId: string) => {
|
|
if (taskId === "FN-001") {
|
|
return { id: "FN-001", column: "in-progress" as const };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-001"],
|
|
}, { resolveTask }))
|
|
.resolves.toBe("task has unresolved dependencies: FN-001");
|
|
});
|
|
|
|
it("returns a reason when a dependency is in triage", async () => {
|
|
const resolveTask = async (taskId: string) => {
|
|
if (taskId === "FN-001") {
|
|
return { id: "FN-001", column: "triage" as const };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-001"],
|
|
}, { resolveTask }))
|
|
.resolves.toBe("task has unresolved dependencies: FN-001");
|
|
});
|
|
|
|
it("returns a reason when a dependency is in todo", async () => {
|
|
const resolveTask = async (taskId: string) => {
|
|
if (taskId === "FN-001") {
|
|
return { id: "FN-001", column: "todo" as const };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-001"],
|
|
}, { resolveTask }))
|
|
.resolves.toBe("task has unresolved dependencies: FN-001");
|
|
});
|
|
|
|
it("returns a reason when a dependency task does not exist", async () => {
|
|
const resolveTask = async (_taskId: string) => null;
|
|
|
|
await expect(getTaskCompletionBlocker({
|
|
...baseCompletionTask,
|
|
dependencies: ["FN-999"],
|
|
}, { resolveTask }))
|
|
.resolves.toBe("task has unresolved dependencies: FN-999");
|
|
});
|
|
});
|