diff --git a/src/app/api/agents/[agentName]/next-task/route.ts b/src/app/api/agents/[agentName]/next-task/route.ts index 4a0cb90..78e7107 100644 --- a/src/app/api/agents/[agentName]/next-task/route.ts +++ b/src/app/api/agents/[agentName]/next-task/route.ts @@ -11,7 +11,7 @@ import { createFollowupPrTask, createGroomTask, } from "@/lib/agent-task"; -import { isBacklogLane } from "@/lib/lane-config"; +import { isBacklogLane, isValidLane, getLaneIds } from "@/lib/lane-config"; export async function GET( request: Request, @@ -122,6 +122,16 @@ export async function GET( const issueLane = lane?.toLowerCase(); const prFixLane = lane; + // Validate lane against configured lanes (allow omitting lane for backward compatibility) + if (issueLane && !isValidLane(issueLane)) { + return NextResponse.json( + { + error: `Invalid lane: "${lane}". Must be one of: ${getLaneIds().join(", ")}`, + }, + { status: 400 }, + ); + } + const leasedIssueIds = await findLeasedIssueIds(agentName); const prFixItems = await listQueuedPrFixItems( diff --git a/src/app/api/agents/[agentName]/queue/route.test.ts b/src/app/api/agents/[agentName]/queue/route.test.ts index feadc71..33575ba 100644 --- a/src/app/api/agents/[agentName]/queue/route.test.ts +++ b/src/app/api/agents/[agentName]/queue/route.test.ts @@ -1,4 +1,15 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { resetLaneConfig, setLaneConfig } from "@/lib/lane-config"; + +const mockToken = "test-agent-token"; +process.env.DISPATCH_AGENT_TOKEN = mockToken; + +vi.mock("@/lib/dispatch-env", () => ({ + isAuthorizedAgentToken: vi.fn((token) => token === mockToken), + isAuthorizedBearerToken: vi.fn((token) => token === mockToken), + getAcceptedAgentTokens: vi.fn(() => [mockToken]), + resetCaches: vi.fn(), +})); const { mocks } = vi.hoisted(() => ({ mocks: { @@ -21,15 +32,111 @@ vi.mock("@/lib/lease", () => ({ })); import { GET } from "./route"; +import { resetAuthCaches } from "@/lib/auth"; + +function request(url: string, agentName = "example-agent", includeAuth = true) { + const headers: Record = {}; + if (includeAuth) headers.Authorization = `Bearer ${mockToken}`; + return new Request(`http://localhost${url}`, { headers }); +} describe("GET /api/agents/[agentName]/queue", () => { beforeEach(() => { + delete process.env.DISPATCH_AUTH_MODE; + resetAuthCaches(); vi.clearAllMocks(); mocks.prFixFindMany.mockResolvedValue([]); mocks.issueFindMany.mockResolvedValue([]); mocks.findLeasedIssueIds.mockResolvedValue([]); }); + afterEach(() => { + resetLaneConfig(); + }); + + // ── Auth tests ───────────────────────────────────────────────────── + + it("returns 401 when no auth header is present", async () => { + const res = await GET( + request("/api/agents/example-agent/queue?lane=normal", "example-agent", false), + { params: Promise.resolve({ agentName: "example-agent" }) }, + ); + + expect(res.status).toBe(401); + const body = await res.json(); + expect(body.error).toBe("Unauthorized"); + }); + + it("returns 401 for bad bearer token", async () => { + const headers: Record = { Authorization: "Bearer wrong-token" }; + const req = new Request("http://localhost/api/agents/example-agent/queue?lane=normal", { headers }); + + const res = await GET(req, { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(401); + const body = await res.json(); + expect(body.error).toBe("Unauthorized"); + }); + + it("valid bearer token preserves existing queue behavior", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-auth", + number: 1, + title: "Auth test issue", + url: "https://github.com/org/repo/issues/1", + labels: ["priority/p1", "status/ready"], + currentLane: "normal", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET( + request("/api/agents/example-agent/queue?lane=normal"), + { params: Promise.resolve({ agentName: "example-agent" }) }, + ); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(1); + expect(body[0].number).toBe(1); + }); + + it("unauthorized request does not call findLeasedIssueIds", async () => { + const req = new Request("http://localhost/api/agents/example-agent/queue?lane=normal", { + headers: { Authorization: "Bearer wrong-token" }, + }); + + await GET(req, { params: Promise.resolve({ agentName: "example-agent" }) }); + + expect(mocks.findLeasedIssueIds).not.toHaveBeenCalled(); + }); + + it("unauthorized request does not call PR-fix lookup", async () => { + const req = new Request("http://localhost/api/agents/example-agent/queue?lane=normal", { + headers: { Authorization: "Bearer wrong-token" }, + }); + + await GET(req, { params: Promise.resolve({ agentName: "example-agent" }) }); + + expect(mocks.prFixFindMany).not.toHaveBeenCalled(); + }); + + it("unauthorized request does not call issue lookup", async () => { + const req = new Request("http://localhost/api/agents/example-agent/queue?lane=normal", { + headers: { Authorization: "Bearer wrong-token" }, + }); + + await GET(req, { params: Promise.resolve({ agentName: "example-agent" }) }); + + expect(mocks.issueFindMany).not.toHaveBeenCalled(); + }); + + // ── PR review-fix prioritization ─────────────────────────────────── + it("prioritizes queued PR review-fix items before new issue work", async () => { mocks.prFixFindMany.mockResolvedValue([ { @@ -63,7 +170,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -88,7 +195,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -112,7 +219,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=escalated"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=escalated"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -154,7 +261,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -164,6 +271,8 @@ describe("GET /api/agents/[agentName]/queue", () => { expect(body[1].type).toBe("issue"); }); + // ── Agent assignment tests ───────────────────────────────────────── + it("includes same-agent claimed issues by default and excludes unlabelled orphans", async () => { mocks.issueFindMany.mockResolvedValue([ { @@ -188,13 +297,12 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); expect(res.status).toBe(200); const body = await res.json(); - // Same-agent claimed work is now included (fix #291), unlabelled orphans are excluded expect(body.map((item: { number: number }) => item.number)).toEqual([52]); }); @@ -212,7 +320,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal&includeClaimed=true"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal&includeClaimed=true"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -246,10 +354,9 @@ describe("GET /api/agents/[agentName]/queue", () => { repository: { fullName: "org/repo" }, }, ]); - // issue-leased is leased by another agent mocks.findLeasedIssueIds.mockResolvedValue(["issue-leased"]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -271,10 +378,9 @@ describe("GET /api/agents/[agentName]/queue", () => { repository: { fullName: "org/repo" }, }, ]); - // example-agent holds the lease, so findLeasedIssueIds returns empty for them mocks.findLeasedIssueIds.mockResolvedValue([]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal&includeClaimed=true"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal&includeClaimed=true"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -286,7 +392,7 @@ describe("GET /api/agents/[agentName]/queue", () => { it("calls findLeasedIssueIds with the requesting agent name", async () => { mocks.issueFindMany.mockResolvedValue([]); - await GET(new Request("http://localhost/api/agents/saffron/queue?lane=normal"), { + await GET(request("/api/agents/saffron/queue?lane=normal", "saffron"), { params: Promise.resolve({ agentName: "saffron" }), }); @@ -319,7 +425,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -352,7 +458,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal&includeRenovate=true"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal&includeRenovate=true"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -387,7 +493,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -420,7 +526,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -443,7 +549,7 @@ describe("GET /api/agents/[agentName]/queue", () => { }, ]); - const res = await GET(new Request("http://localhost/api/agents/example-agent/queue?lane=normal"), { + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { params: Promise.resolve({ agentName: "example-agent" }), }); @@ -451,4 +557,245 @@ describe("GET /api/agents/[agentName]/queue", () => { const body = await res.json(); expect(body.map((item: { number: number }) => item.number)).toEqual([20]); }); + + // ── Lane validation tests ───────────────────────────────────────── + + it("returns 400 for unknown lane", async () => { + mocks.issueFindMany.mockResolvedValue([]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=unknown-lane"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toContain("Invalid lane"); + expect(body.error).toContain("unknown-lane"); + }); + + it("returns 200 for default normal lane", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-normal", + number: 1, + title: "Normal issue", + url: "https://github.com/org/repo/issues/1", + labels: ["priority/p1", "status/ready"], + currentLane: "normal", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(1); + expect(body[0].number).toBe(1); + }); + + it("returns 200 for default escalated lane", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-escalated", + number: 2, + title: "Escalated issue", + url: "https://github.com/org/repo/issues/2", + labels: ["priority/p0", "status/ready"], + currentLane: "escalated", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=escalated"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(1); + expect(body[0].number).toBe(2); + }); + + it("returns 200 when lane param is omitted (backward-compatible)", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-no-lane", + number: 3, + title: "No lane filter", + url: "https://github.com/org/repo/issues/3", + labels: ["priority/p1", "status/ready"], + currentLane: "normal", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(1); + }); + + it("returns 200 for custom configured lane", async () => { + setLaneConfig({ + lanes: [ + { id: "fast", title: "Fast Lane", claimable: true }, + { id: "slow", title: "Slow Lane", claimable: true }, + { id: "parked", title: "Parked", claimable: false }, + ], + }); + + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-fast", + number: 10, + title: "Fast issue", + url: "https://github.com/org/repo/issues/10", + labels: ["priority/p0", "status/ready"], + currentLane: "fast", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=fast"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(1); + expect(body[0].number).toBe(10); + }); + + it("returns 400 for default lane when custom config is active", async () => { + setLaneConfig({ + lanes: [ + { id: "fast", title: "Fast Lane", claimable: true }, + { id: "parked", title: "Parked", claimable: false }, + ], + }); + + mocks.issueFindMany.mockResolvedValue([]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=normal"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toContain("Invalid lane"); + }); + + it("includes issue lane value in response", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-lane-val", + number: 5, + title: "Has lane", + url: "https://github.com/org/repo/issues/5", + labels: ["priority/p1", "status/ready"], + currentLane: "escalated", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=escalated"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body[0].lane).toBe("escalated"); + }); + + it("preserves includeClaimed behavior with lane filter", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-claimed-lane", + number: 6, + title: "Claimed in lane", + url: "https://github.com/org/repo/issues/6", + labels: ["agent/example-agent", "status/ready"], + currentLane: "normal", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=normal&includeClaimed=true"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body[0]).toMatchObject({ number: 6, agentMatch: true }); + }); + + it("preserves includeRenovate behavior with lane filter", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-renovate-lane", + number: 7, + title: "Dependency Dashboard", + url: "https://github.com/org/repo/issues/7", + labels: ["priority/p1", "status/ready"], + currentLane: "normal", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=normal&includeRenovate=true"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(1); + expect(body[0].number).toBe(7); + }); + + it("preserves exclude_decomposed behavior with lane filter", async () => { + mocks.issueFindMany.mockResolvedValue([ + { + id: "issue-decomposed", + number: 8, + title: "Decomposed issue", + url: "https://github.com/org/repo/issues/8", + labels: ["priority/p1", "status/ready"], + currentLane: "normal", + decomposed: true, + repository: { fullName: "org/repo" }, + }, + { + id: "issue-not-decomposed", + number: 9, + title: "Not decomposed", + url: "https://github.com/org/repo/issues/9", + labels: ["priority/p1", "status/ready"], + currentLane: "normal", + decomposed: false, + repository: { fullName: "org/repo" }, + }, + ]); + + const res = await GET(request("/api/agents/example-agent/queue?lane=normal&exclude_decomposed=true"), { + params: Promise.resolve({ agentName: "example-agent" }), + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveLength(1); + expect(body[0].number).toBe(9); + }); }); diff --git a/src/app/api/agents/[agentName]/queue/route.ts b/src/app/api/agents/[agentName]/queue/route.ts index 06bd44b..56e0846 100644 --- a/src/app/api/agents/[agentName]/queue/route.ts +++ b/src/app/api/agents/[agentName]/queue/route.ts @@ -1,12 +1,19 @@ import { NextResponse } from "next/server"; +import { authorizeRequest } from "@/lib/auth"; import { prisma, asPrFixQueueClient } from "@/lib/prisma"; import { buildAgentQueue } from "@/lib/agent-queue"; import { listQueuedPrFixItems, toAgentQueuePrFixItem } from "@/lib/pr-fix-queue"; import { findLeasedIssueIds } from "@/lib/lease"; import { parseExcludedLabels } from "@/lib/config"; +import { isValidLane, getLaneIds } from "@/lib/lane-config"; export async function GET(request: Request, { params }: { params: Promise<{ agentName: string }> }) { const { agentName } = await params; + + if (!(await authorizeRequest(request)).authorized) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const lane = searchParams.get("lane"); const excludeDecomposed = searchParams.get("exclude_decomposed"); @@ -42,6 +49,16 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen const issueLane = lane?.toLowerCase(); const prFixLane = lane; + // Validate lane against configured lanes (allow omitting lane for backward compatibility) + if (issueLane && !isValidLane(issueLane)) { + return NextResponse.json( + { + error: `Invalid lane: "${lane}". Must be one of: ${getLaneIds().join(", ")}`, + }, + { status: 400 }, + ); + } + // Find issues that have active leases from OTHER agents — exclude them // so other agents don't overlap on leased work. const leasedIssueIds = await findLeasedIssueIds(agentName); diff --git a/src/lib/agent-queue.test.ts b/src/lib/agent-queue.test.ts index b09e881..066eea6 100644 --- a/src/lib/agent-queue.test.ts +++ b/src/lib/agent-queue.test.ts @@ -623,6 +623,123 @@ describe("buildAgentQueue agent assignment fixes (issue #291)", () => { }); describe("buildAgentQueue excludes non-worker-actionable issues (issue #369)", () => { + describe("configurable lanes", () => { + afterEach(() => { + resetLaneConfig(); + }); + + it("single claimable lane config works", () => { + setLaneConfig({ + lanes: [ + { id: "default", title: "Default", claimable: true }, + { id: "backlog", title: "Backlog", claimable: false }, + ], + }); + + const issues = [ + makeIssue({ number: 1, labels: ["priority/p1", "status/ready"], lane: "default" }), + makeIssue({ number: 2, labels: ["priority/p1", "status/ready"], lane: "backlog" }), + ]; + + // Default queue includes only claimable lane (default), excludes backlog lane + const result = buildAgentQueue(issues, "worker-agent"); + expect(result).toHaveLength(1); + expect(result[0].number).toBe(1); + + // Explicit lane filter works + const defaultOnly = buildAgentQueue(issues, "worker-agent", { lane: "default" }); + expect(defaultOnly).toHaveLength(1); + expect(defaultOnly[0].number).toBe(1); + }); + + it("three claimable lanes config works", () => { + setLaneConfig({ + lanes: [ + { id: "fast", title: "Fast Lane", claimable: true }, + { id: "slow", title: "Slow Lane", claimable: true }, + { id: "critical", title: "Critical", claimable: true }, + { id: "parked", title: "Parked", claimable: false }, + ], + }); + + const issues = [ + makeIssue({ number: 1, labels: ["priority/p0", "status/ready"], lane: "fast" }), + makeIssue({ number: 2, labels: ["priority/p1", "status/ready"], lane: "slow" }), + makeIssue({ number: 3, labels: ["priority/p0", "status/ready"], lane: "critical" }), + makeIssue({ number: 4, labels: ["priority/p1", "status/ready"], lane: "parked" }), + ]; + + // Default queue includes all claimable lanes, excludes parked + const result = buildAgentQueue(issues, "worker-agent"); + expect(result).toHaveLength(3); + expect(result.map((i) => i.number)).toEqual([1, 3, 2]); // p0 fast, p0 critical, p1 slow + + // Each lane filter works independently + const fastOnly = buildAgentQueue(issues, "worker-agent", { lane: "fast" }); + expect(fastOnly).toHaveLength(1); + expect(fastOnly[0].number).toBe(1); + + const criticalOnly = buildAgentQueue(issues, "worker-agent", { lane: "critical" }); + expect(criticalOnly).toHaveLength(1); + expect(criticalOnly[0].number).toBe(3); + }); + + it("non-claimable lane is excluded from default worker queue", () => { + setLaneConfig({ + lanes: [ + { id: "work", title: "Work", claimable: true }, + { id: "triage", title: "Triage", claimable: false }, + ], + }); + + const issues = [ + makeIssue({ number: 1, labels: ["priority/p1", "status/ready"], lane: "work" }), + makeIssue({ number: 2, labels: ["priority/p1", "status/ready"], lane: "triage" }), + ]; + + const result = buildAgentQueue(issues, "worker-agent"); + expect(result).toHaveLength(1); + expect(result[0].number).toBe(1); + + // Non-claimable lane can be included with claimableOnly=false + const all = buildAgentQueue(issues, "worker-agent", { claimableOnly: false }); + expect(all).toHaveLength(2); + }); + + it("non-claimable lane items not accidentally worker-claimable", () => { + setLaneConfig({ + lanes: [ + { id: "work", title: "Work", claimable: true }, + { id: "backlog", title: "Backlog", claimable: false }, + ], + }); + + const issues = [ + makeIssue({ number: 1, labels: ["priority/p0", "status/ready"], lane: "backlog" }), + ]; + + const result = buildAgentQueue(issues, "worker-agent"); + expect(result).toHaveLength(0); + }); + + it("custom claimable lane can be queried successfully", () => { + setLaneConfig({ + lanes: [ + { id: "alpha", title: "Alpha", claimable: true }, + { id: "backlog", title: "Backlog", claimable: false }, + ], + }); + + const issues = [ + makeIssue({ number: 1, labels: ["priority/p1", "status/ready"], lane: "alpha" }), + ]; + + const result = buildAgentQueue(issues, "worker-agent", { lane: "alpha" }); + expect(result).toHaveLength(1); + expect(result[0].number).toBe(1); + }); + }); + it("excludes status/in-review from default worker queue", () => { const issues = [ makeIssue({ number: 1, labels: ["status/in-review", "priority/p0"] }),