diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 5e4df792486..daa556d4231 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -42,6 +42,7 @@ import { instanceSettingsService } from "../services/instance-settings.ts"; import { clampIssueListLimit, deriveIssueCommentRunLogAttribution, + MAX_CHILD_ISSUES_CREATED_BY_HELPER, ISSUE_LIST_MAX_LIMIT, issueService, readIssueCommentRunLogText, @@ -3922,6 +3923,62 @@ describeEmbeddedPostgres("issueService.create workspace inheritance", () => { expect(child.requestDepth).toBe(MAX_ISSUE_REQUEST_DEPTH); }); + + it("counts only non-terminal children toward the helper child limit", async () => { + const companyId = randomUUID(); + const parentIssueId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await db.insert(issues).values({ + id: parentIssueId, + companyId, + title: "Recurring coordinator", + status: "in_progress", + priority: "high", + }); + + const terminalChildren = Array.from( + { length: MAX_CHILD_ISSUES_CREATED_BY_HELPER }, + (_, index) => ({ + id: randomUUID(), + companyId, + parentId: parentIssueId, + title: `Terminal child ${index + 1}`, + status: index % 2 === 0 ? "done" as const : "cancelled" as const, + priority: "medium" as const, + }), + ); + const activeChildren = Array.from( + { length: MAX_CHILD_ISSUES_CREATED_BY_HELPER - 1 }, + (_, index) => ({ + id: randomUUID(), + companyId, + parentId: parentIssueId, + title: `Active child ${index + 1}`, + status: "todo" as const, + priority: "medium" as const, + }), + ); + await db.insert(issues).values([...terminalChildren, ...activeChildren]); + + const { issue: finalActiveChild } = await svc.createChild(parentIssueId, { + title: "Final active child", + status: "todo", + }); + + expect(finalActiveChild.parentId).toBe(parentIssueId); + await expect(svc.createChild(parentIssueId, { + title: "Over active limit", + status: "todo", + })).rejects.toThrow( + `Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} active child issues for this helper`, + ); + }); }); describeEmbeddedPostgres("issueService blockers and dependency wake readiness", () => { diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index c2415b178dc..584c483612f 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -9394,11 +9394,12 @@ export function issueService(db: Db) { and( eq(issues.companyId, parent.companyId), eq(issues.parentId, parent.id), + notInArray(issues.status, ["done", "cancelled"]), ), ); if (childCount >= MAX_CHILD_ISSUES_CREATED_BY_HELPER) { throw unprocessable( - `Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} child issues for this helper`, + `Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} active child issues for this helper`, ); }