diff --git a/apps/web/app/api/agent/projects/route.ts b/apps/web/app/api/agent/projects/route.ts index 6658c43..4a16bfd 100644 --- a/apps/web/app/api/agent/projects/route.ts +++ b/apps/web/app/api/agent/projects/route.ts @@ -112,13 +112,13 @@ export async function GET(request: NextRequest) { archivedAt: true, blockedByDependencies: { select: { - dependencyTask: { select: { id: true, name: true, status: true } }, + dependencyTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, unblocksDependencies: { select: { - blockedTask: { select: { id: true, name: true, status: true } }, + blockedTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, diff --git a/apps/web/app/api/agent/tasks/[taskId]/route.ts b/apps/web/app/api/agent/tasks/[taskId]/route.ts index aae15de..f7b6291 100644 --- a/apps/web/app/api/agent/tasks/[taskId]/route.ts +++ b/apps/web/app/api/agent/tasks/[taskId]/route.ts @@ -100,13 +100,13 @@ export async function GET(request: NextRequest, { params }: RouteContext) { archivedAt: true, blockedByDependencies: { select: { - dependencyTask: { select: { id: true, name: true, status: true } }, + dependencyTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, unblocksDependencies: { select: { - blockedTask: { select: { id: true, name: true, status: true } }, + blockedTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, @@ -389,13 +389,13 @@ export async function PATCH(request: NextRequest, { params }: RouteContext) { archivedAt: true, blockedByDependencies: { select: { - dependencyTask: { select: { id: true, name: true, status: true } }, + dependencyTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, unblocksDependencies: { select: { - blockedTask: { select: { id: true, name: true, status: true } }, + blockedTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, diff --git a/apps/web/app/api/agent/tasks/route.ts b/apps/web/app/api/agent/tasks/route.ts index 8b23b33..9cec361 100644 --- a/apps/web/app/api/agent/tasks/route.ts +++ b/apps/web/app/api/agent/tasks/route.ts @@ -113,13 +113,13 @@ export async function GET(request: NextRequest) { archivedAt: true, blockedByDependencies: { select: { - dependencyTask: { select: { id: true, name: true, status: true } }, + dependencyTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, unblocksDependencies: { select: { - blockedTask: { select: { id: true, name: true, status: true } }, + blockedTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, @@ -346,13 +346,13 @@ export async function POST(request: NextRequest) { archivedAt: true, blockedByDependencies: { select: { - dependencyTask: { select: { id: true, name: true, status: true } }, + dependencyTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, unblocksDependencies: { select: { - blockedTask: { select: { id: true, name: true, status: true } }, + blockedTask: { select: { id: true, name: true, status: true, archivedAt: true } }, }, orderBy: { createdAt: "asc" }, }, diff --git a/apps/web/lib/api/task-dependencies.ts b/apps/web/lib/api/task-dependencies.ts index 5eee08a..0e31340 100644 --- a/apps/web/lib/api/task-dependencies.ts +++ b/apps/web/lib/api/task-dependencies.ts @@ -4,8 +4,11 @@ type DependencyTask = { id: string name: string status: Status + archivedAt?: Date | null } +type SerializedDependencyTask = Omit + type DependencyEdge = { dependencyTask: DependencyTask } @@ -21,8 +24,14 @@ export type TaskDependencyPayload = { export function serializeTaskDependencies(task: T) { const { blockedByDependencies, unblocksDependencies, ...rest } = task - const dependencies = blockedByDependencies?.map((dependency) => dependency.dependencyTask) ?? [] - const unblocks = unblocksDependencies?.map((dependency) => dependency.blockedTask) ?? [] + const dependencies = + blockedByDependencies + ?.map((dependency) => serializeDependencyTask(dependency.dependencyTask)) + .filter((dependency): dependency is SerializedDependencyTask => dependency !== null) ?? [] + const unblocks = + unblocksDependencies + ?.map((dependency) => serializeDependencyTask(dependency.blockedTask)) + .filter((dependency): dependency is SerializedDependencyTask => dependency !== null) ?? [] return { ...rest, @@ -33,6 +42,18 @@ export function serializeTaskDependencies(task: } } +function serializeDependencyTask(task: DependencyTask): SerializedDependencyTask | null { + if (task.archivedAt) { + return null + } + + return { + id: task.id, + name: task.name, + status: task.status, + } +} + export function hasDependencyCycle( edges: Array<{ blockedTaskId: string; dependencyTaskId: string }>, blockedTaskId: string, diff --git a/apps/web/lib/api/task-visibility.test.ts b/apps/web/lib/api/task-visibility.test.ts new file mode 100644 index 0000000..a53d674 --- /dev/null +++ b/apps/web/lib/api/task-visibility.test.ts @@ -0,0 +1,112 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" + +import { Status } from "@/generated/prisma/enums" + +import { serializeTaskDependencies } from "./task-dependencies" + +describe("Agent API task visibility", () => { + it("omits archived dependency and unblocks tasks from serialized task payloads", () => { + const activeDependency = { + id: "dependency-active", + name: "Active dependency", + status: Status.done, + archivedAt: null, + } + const archivedDependency = { + id: "dependency-archived", + name: "Archived dependency", + status: Status.done, + archivedAt: new Date("2026-05-16T07:00:00.000Z"), + } + const activeBlockedTask = { + id: "blocked-active", + name: "Active blocked task", + status: Status.todo, + archivedAt: null, + } + const archivedBlockedTask = { + id: "blocked-archived", + name: "Archived blocked task", + status: Status.todo, + archivedAt: new Date("2026-05-16T07:00:00.000Z"), + } + + const result = serializeTaskDependencies({ + id: "task-1", + blockedByDependencies: [ + { dependencyTask: activeDependency }, + { dependencyTask: archivedDependency }, + ], + unblocksDependencies: [ + { blockedTask: activeBlockedTask }, + { blockedTask: archivedBlockedTask }, + ], + }) + + assert.deepEqual(result.dependencies, [ + { id: activeDependency.id, name: activeDependency.name, status: activeDependency.status }, + ]) + assert.deepEqual(result.dependencyIds, [activeDependency.id]) + assert.deepEqual(result.unblocks, [ + { id: activeBlockedTask.id, name: activeBlockedTask.name, status: activeBlockedTask.status }, + ]) + assert.equal(result.isDependencyReady, true) + }) + + it("omits archivedAt from serialized active dependency and unblocks tasks", () => { + const result = serializeTaskDependencies({ + id: "task-1", + blockedByDependencies: [ + { + dependencyTask: { + id: "dependency-active", + name: "Active dependency", + status: Status.done, + archivedAt: null, + }, + }, + ], + unblocksDependencies: [ + { + blockedTask: { + id: "blocked-active", + name: "Active blocked task", + status: Status.todo, + archivedAt: null, + }, + }, + ], + }) + + assert.deepEqual(result.dependencies, [ + { id: "dependency-active", name: "Active dependency", status: Status.done }, + ]) + assert.deepEqual(result.unblocks, [ + { id: "blocked-active", name: "Active blocked task", status: Status.todo }, + ]) + assert.equal("archivedAt" in result.dependencies[0], false) + assert.equal("archivedAt" in result.unblocks[0], false) + }) + + it("does not mark dependency readiness from archived dependencies", () => { + const result = serializeTaskDependencies({ + id: "task-1", + blockedByDependencies: [ + { + dependencyTask: { + id: "dependency-archived", + name: "Archived dependency", + status: Status.done, + archivedAt: new Date("2026-05-16T07:00:00.000Z"), + }, + }, + ], + unblocksDependencies: [], + }) + + assert.deepEqual(result.dependencies, []) + assert.deepEqual(result.dependencyIds, []) + assert.equal(result.isDependencyReady, false) + }) +})