Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app/api/agents/[agentName]/next-task/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -36,6 +37,7 @@ export async function GET(
repository: { enabled: true },
},
select: {
id: true,
number: true,
title: true,
url: true,
Expand All @@ -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/"));
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions src/app/api/issues/groom/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -16,6 +17,7 @@ vi.mock("@/lib/prisma", () => ({
prisma: {
issue: {
findUnique: mocks.findIssue,
findFirst: mocks.findFirstIssue,
update: mocks.updateIssue,
},
auditLog: {
Expand Down Expand Up @@ -67,6 +69,7 @@ function mockIssue(extra?: Record<string, unknown>) {
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";
});
Expand All @@ -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";
});
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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"] });
Expand Down Expand Up @@ -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"] });
Expand Down Expand Up @@ -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"] });
Expand Down Expand Up @@ -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"] });
Expand Down Expand Up @@ -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"] });
Expand Down Expand Up @@ -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";
});
Expand Down Expand Up @@ -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",
Expand Down
42 changes: 31 additions & 11 deletions src/app/api/issues/groom/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ export async function POST(request: Request) {
agentName,
} = body as Record<string, unknown>;

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 },
);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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() },
});
}
Expand All @@ -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,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/lib/agent-task.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface IssueRef {
id?: string;
repoFullName: string;
number: number;
title: string;
Expand Down