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
151 changes: 151 additions & 0 deletions src/app/api/agents/[agentName]/active-work/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ describe("GET /api/agents/:agentName/active-work", () => {
issue: {
number: 42,
repository: { fullName: "org/repo" },
state: "status/in-progress",
labels: ["agent/test-agent", "priority/p1"],
currentLane: "normal",
},
});
mocks.issueFindUnique.mockResolvedValue({ id: "issue-abc" });
Expand All @@ -68,6 +71,9 @@ describe("GET /api/agents/:agentName/active-work", () => {
expect(body.context.issueNumber).toBe(42);
expect(body.context.branch).toBe("feat/my-feature");
expect(body.context.leaseId).toBe("l-1");
expect(body.context.lane).toBe("normal");
expect(body.context.status).toBe("status/in-progress");
expect(body.context.labels).toEqual(["agent/test-agent", "priority/p1"]);
});

it("returns hasActiveWork: false when no active lease exists", async () => {
Expand All @@ -92,6 +98,9 @@ describe("GET /api/agents/:agentName/active-work", () => {
issue: {
number: 42,
repository: { fullName: "org/repo" },
state: "status/in-progress",
labels: [],
currentLane: "normal",
},
});

Expand All @@ -114,6 +123,9 @@ describe("GET /api/agents/:agentName/active-work", () => {
issue: {
number: 42,
repository: { fullName: "org/repo" },
state: "status/in-progress",
labels: [],
currentLane: "normal",
},
});

Expand All @@ -136,6 +148,145 @@ describe("GET /api/agents/:agentName/active-work", () => {
issue: {
number: 999,
repository: { fullName: "org/repo" },
state: "status/in-progress",
labels: [],
currentLane: "normal",
},
});

mocks.issueFindUnique.mockResolvedValueOnce(null);

const res = await makeActiveWorkRequest("test-agent");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.hasActiveWork).toBe(false);
});

it("returns escalated lane metadata when issue is in escalated lane", async () => {
mocks.leaseFindFirst.mockResolvedValueOnce({
id: "l-2",
agentName: "escalated-agent",
issueId: "issue-xyz",
checkpoint: "changes_made",
branch: "feat/esc-fix",
prUrl: null,
expiredAt: new Date(Date.now() + 60000),
renewedAt: new Date(),
issue: {
number: 55,
repository: { fullName: "org/repo" },
state: "status/in-progress",
labels: ["agent/escalated-agent", "priority/p1"],
currentLane: "escalated",
},
});

const res = await makeActiveWorkRequest("escalated-agent");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.hasActiveWork).toBe(true);
expect(body.context.lane).toBe("escalated");
expect(body.context.status).toBe("status/in-progress");
expect(body.context.labels).toEqual(["agent/escalated-agent", "priority/p1"]);
});

it("returns backlog lane metadata when issue is in backlog lane", async () => {
mocks.leaseFindFirst.mockResolvedValueOnce({
id: "l-3",
agentName: "test-agent",
issueId: "issue-backlog",
checkpoint: "issue_claimed",
branch: null,
prUrl: null,
expiredAt: new Date(Date.now() + 60000),
renewedAt: new Date(),
issue: {
number: 77,
repository: { fullName: "org/repo" },
state: "status/backlog",
labels: ["agent/test-agent"],
currentLane: "backlog",
},
});

const res = await makeActiveWorkRequest("test-agent");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.hasActiveWork).toBe(true);
expect(body.context.lane).toBe("backlog");
expect(body.context.status).toBe("status/backlog");
});

it("defaults lane to normal when currentLane is null", async () => {
mocks.leaseFindFirst.mockResolvedValueOnce({
id: "l-4",
agentName: "test-agent",
issueId: "issue-null-lane",
checkpoint: "issue_claimed",
branch: null,
prUrl: null,
expiredAt: new Date(Date.now() + 60000),
renewedAt: new Date(),
issue: {
number: 88,
repository: { fullName: "org/repo" },
state: "status/ready",
labels: [],
currentLane: null,
},
});

const res = await makeActiveWorkRequest("test-agent");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.hasActiveWork).toBe(true);
expect(body.context.lane).toBe("normal");
});

it("returns lane/status metadata for closed issue (stale lease still resolvable)", async () => {
mocks.leaseFindFirst.mockResolvedValueOnce({
id: "l-5",
agentName: "test-agent",
issueId: "issue-closed",
checkpoint: "changes_made",
branch: "feat/closed-issue-fix",
prUrl: null,
expiredAt: new Date(Date.now() + 60000),
renewedAt: new Date(),
issue: {
number: 200,
repository: { fullName: "org/repo" },
state: "status/done",
labels: ["type/bug", "priority/p1"],
currentLane: "normal",
},
});

const res = await makeActiveWorkRequest("test-agent");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.hasActiveWork).toBe(true);
expect(body.context.lane).toBe("normal");
expect(body.context.status).toBe("status/done");
expect(body.context.labels).toEqual(["type/bug", "priority/p1"]);
});

it("returns hasActiveWork: false when referenced issue is missing (orphaned lease)", async () => {
mocks.leaseFindFirst.mockResolvedValueOnce({
id: "l-1",
agentName: "test-agent",
issueId: "issue-missing",
checkpoint: "issue_claimed",
branch: null,
prUrl: null,
expiredAt: new Date(Date.now() + 60000),
renewedAt: new Date(),
issue: {
number: 999,
repository: { fullName: "org/repo" },
state: "status/in-progress",
labels: [],
currentLane: "normal",
},
});

Expand Down
3 changes: 3 additions & 0 deletions src/lib/lease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ export async function resolveActiveWork(agentName: string): Promise<ResumeContex
prUrl: lease.prUrl ?? undefined,
}),
leaseId: lease.id,
lane: lease.issue.currentLane ?? "normal",
status: lease.issue.state,
labels: lease.issue.labels,
};
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/next-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export interface ResumeContext {

export interface ResumeContextWithLease extends ResumeContext {
leaseId: string;
lane: string;
status: string;
labels: string[];
}

// ─── Resume Context Builder ─────────────────────────────────────────────────
Expand Down