feat(FN-4091): fix stale blocker in WorkflowResultsTab

The branch delivers a fix for stale blockers in the WorkflowResultsTab component, with the primary logic change in the component file and comprehensive test coverage added in the companion test suite.

Fusion-Task-Id: FN-4091
This commit is contained in:
Fusion
2026-05-12 17:20:54 -07:00
committed by gsxdsm
parent bddcc3a931
commit ab66e5b60a
12 changed files with 260 additions and 37 deletions

View File

@@ -305,11 +305,38 @@ describe("getTaskCompletionBlocker", () => {
await expect(getTaskCompletionBlocker(baseCompletionTask)).resolves.toBeUndefined();
});
it("returns a reason when task has blockedBy", async () => {
it("returns a reason when task has blockedBy without resolveTask", async () => {
await expect(getTaskCompletionBlocker({ ...baseCompletionTask, blockedBy: "FN-123" }))
.resolves.toBe("task is blocked by FN-123");
});
it("ignores blockedBy when resolveTask reports the blocker missing", async () => {
const resolveTask = async () => null;
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
blockedBy: "FN-4054",
}, { resolveTask })).resolves.toBeUndefined();
});
it.each(["done", "archived"] as const)("ignores blockedBy when resolveTask reports the blocker is %s", async (column) => {
const resolveTask = async () => ({ id: "FN-4054", column });
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
blockedBy: "FN-4054",
}, { resolveTask })).resolves.toBeUndefined();
});
it.each(["todo", "in-progress", "in-review"] as const)("returns a reason when resolveTask reports an active blocker in %s", async (column) => {
const resolveTask = async () => ({ id: "FN-123", column });
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
blockedBy: "FN-123",
}, { resolveTask })).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") {

View File

@@ -123,6 +123,11 @@ export function isTaskReadyForMerge(
}
export interface TaskCompletionBlockerOptions {
/**
* Resolves a task reference so completion gating can distinguish live blockers
* from stale `blockedBy` markers. Missing tasks and blockers already in
* `done`/`archived` are treated as non-blocking.
*/
resolveTask?: (taskId: string) => Promise<Pick<Task, "id" | "column"> | null | undefined>;
}
@@ -138,8 +143,16 @@ export async function getTaskCompletionBlocker(
task: Pick<Task, "blockedBy" | "dependencies">,
options: TaskCompletionBlockerOptions = {},
): Promise<string | undefined> {
if (task.blockedBy?.trim()) {
return `task is blocked by ${task.blockedBy.trim()}`;
const blockedBy = task.blockedBy?.trim();
if (blockedBy) {
if (!options.resolveTask) {
return `task is blocked by ${blockedBy}`;
}
const blocker = await options.resolveTask(blockedBy);
if (blocker && blocker.column !== "done" && blocker.column !== "archived") {
return `task is blocked by ${blockedBy}`;
}
}
const dependencies = task.dependencies ?? [];