diff --git a/src/app/api/agents/[agentName]/next-task/route.ts b/src/app/api/agents/[agentName]/next-task/route.ts index 8660a0e..0ed99fa 100644 --- a/src/app/api/agents/[agentName]/next-task/route.ts +++ b/src/app/api/agents/[agentName]/next-task/route.ts @@ -8,6 +8,7 @@ import { createGroomTask, } from "@/lib/agent-task"; import { isBacklogLane, getBacklogLane } from "@/lib/lane-config"; +import { isRenovateIssue } from "@/lib/agent-queue"; import { fetchAgentQueueData } from "@/lib/agent-queue-fetch"; export async function GET( @@ -36,6 +37,7 @@ export async function GET( repository: { enabled: true }, }, select: { + id: true, number: true, title: true, url: true, @@ -47,6 +49,7 @@ export async function GET( }); const candidates = issues + .filter((issue) => !isRenovateIssue(issue)) .map((issue) => { const hasStatus = issue.labels.some((l) => l.startsWith("status/")); const hasPriority = issue.labels.some((l) => l.startsWith("priority/")); @@ -83,6 +86,7 @@ export async function GET( agentName, lane: best.currentLane ?? getBacklogLane()?.id ?? "backlog", issue: { + id: best.id, repoFullName: best.repository.fullName, number: best.number, title: best.title, diff --git a/src/app/api/issues/groom/route.test.ts b/src/app/api/issues/groom/route.test.ts index 8931f9c..c5dcb06 100644 --- a/src/app/api/issues/groom/route.test.ts +++ b/src/app/api/issues/groom/route.test.ts @@ -4,6 +4,7 @@ import { resetAuthCaches } from "@/lib/auth"; const { mocks } = vi.hoisted(() => ({ mocks: { findIssue: vi.fn().mockResolvedValue(null), + findFirstIssue: vi.fn().mockResolvedValue(null), updateIssue: vi.fn().mockResolvedValue(undefined), createAuditLog: vi.fn().mockResolvedValue({ id: "log-1" }), removeIssueLabel: vi.fn().mockResolvedValue(undefined), @@ -16,6 +17,7 @@ vi.mock("@/lib/prisma", () => ({ prisma: { issue: { findUnique: mocks.findIssue, + findFirst: mocks.findFirstIssue, update: mocks.updateIssue, }, auditLog: { @@ -67,6 +69,7 @@ function mockIssue(extra?: Record) { describe("POST /api/issues/groom — auth", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; }); @@ -88,6 +91,7 @@ describe("POST /api/issues/groom — auth", () => { describe("POST /api/issues/groom — validation", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; }); @@ -149,6 +153,7 @@ describe("POST /api/issues/groom — validation", () => { describe("POST /api/issues/groom — actor resolution", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; mockIssue(); @@ -228,6 +233,7 @@ describe("POST /api/issues/groom — actor resolution", () => { describe("POST /api/issues/groom — promote_to_ready", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; mockIssue({ labels: ["status/backlog", "priority/p2"] }); @@ -338,6 +344,7 @@ describe("POST /api/issues/groom — promote_to_ready", () => { describe("POST /api/issues/groom — escalate", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; mockIssue({ labels: ["status/in-progress"] }); @@ -392,6 +399,7 @@ describe("POST /api/issues/groom — escalate", () => { describe("POST /api/issues/groom — mark_not_ready", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; mockIssue({ labels: ["status/backlog"] }); @@ -466,6 +474,7 @@ describe("POST /api/issues/groom — mark_not_ready", () => { describe("POST /api/issues/groom — mark_needs_info", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; mockIssue({ labels: ["status/backlog"] }); @@ -526,6 +535,7 @@ describe("POST /api/issues/groom — mark_needs_info", () => { describe("POST /api/issues/groom — mark_blocked", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; mockIssue({ labels: ["status/backlog"] }); @@ -586,6 +596,7 @@ describe("POST /api/issues/groom — mark_blocked", () => { describe("POST /api/issues/groom — error handling", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); process.env.DISPATCH_AGENT_TOKEN = "test-token"; process.env.DISPATCH_AUTH_MODE = "disabled"; }); @@ -617,6 +628,7 @@ describe("POST /api/issues/groom — error handling", () => { describe("POST /api/issues/groom — auth modes", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.findFirstIssue.mockResolvedValue(null); resetAuthCaches(); mocks.findIssue.mockResolvedValue({ id: "issue-1", diff --git a/src/app/api/issues/groom/route.ts b/src/app/api/issues/groom/route.ts index 123e338..df1ad9e 100644 --- a/src/app/api/issues/groom/route.ts +++ b/src/app/api/issues/groom/route.ts @@ -64,9 +64,9 @@ export async function POST(request: Request) { agentName, } = body as Record; - if (!issueId || !repoFullName || typeof issueNumber !== "number" || typeof action !== "string") { + if (!repoFullName || typeof issueNumber !== "number" || typeof action !== "string") { return NextResponse.json( - { error: "Missing required fields: issueId, repoFullName, issueNumber, action" }, + { error: "Missing required fields: repoFullName, issueNumber, action" }, { status: 400 }, ); } @@ -99,17 +99,37 @@ export async function POST(request: Request) { } try { - const issue = await prisma.issue.findUnique({ - where: { id: issueId as string }, - include: { repository: true }, - }); + // Look up the issue: try by DB id first, fall back to repoFullName + issueNumber + let issue = null; + + if (issueId && typeof issueId === "string") { + issue = await prisma.issue.findUnique({ + where: { id: issueId }, + include: { repository: true }, + }); + } + + if (!issue) { + // Fallback: look up by repoFullName + issueNumber + issue = await prisma.issue.findFirst({ + where: { + number: issueNumber, + repository: { fullName: repoFullName as string }, + }, + include: { repository: true }, + }); + } if (!issue) { - return NextResponse.json({ error: "Issue not found in local cache" }, { status: 404 }); + return NextResponse.json( + { error: "Issue not found in local cache" }, + { status: 404 }, + ); } const effectiveRepo = (issue.repository?.fullName ?? repoFullName) as string; const effectiveNumber = issue.number; + const effectiveIssueId = issue.id; const beforeLabels = [...issue.labels]; let afterLabels = [...issue.labels]; const groomedAt = new Date(); @@ -197,12 +217,12 @@ export async function POST(request: Request) { // Update GitHub labels if status changed; always refresh lastSyncedAt if (action === "promote_to_ready") { await prisma.issue.update({ - where: { id: issueId as string }, + where: { id: effectiveIssueId }, data: { ...groomingData, labels: afterLabels, lastSyncedAt: new Date() }, }); } else { await prisma.issue.update({ - where: { id: issueId as string }, + where: { id: effectiveIssueId }, data: { ...groomingData, lastSyncedAt: new Date() }, }); } @@ -222,7 +242,7 @@ export async function POST(request: Request) { action: actionLabels[action], repoFullName: effectiveRepo, issueNumber: effectiveNumber, - issueId: issueId as string, + issueId: effectiveIssueId, beforeLabels, afterLabels: action === "promote_to_ready" ? afterLabels : beforeLabels, success: true, @@ -245,7 +265,7 @@ export async function POST(request: Request) { action: `issue_groomed_${action}`, repoFullName: repoFullName as string, issueNumber: issueNumber as number, - issueId: issueId as string, + issueId: (issueId as string) ?? null, beforeLabels: [], afterLabels: [], success: false, diff --git a/src/lib/agent-task.ts b/src/lib/agent-task.ts index 7148436..203d77d 100644 --- a/src/lib/agent-task.ts +++ b/src/lib/agent-task.ts @@ -1,4 +1,5 @@ export interface IssueRef { + id?: string; repoFullName: string; number: number; title: string;