From 5a7d5e98a85bcc6600ec12b6924087984ff2575e Mon Sep 17 00:00:00 2001 From: cryppadotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:58:49 +0000 Subject: [PATCH] fix: include REST creation recipients in composer assignment history --- ...e-comment-attribution-audit-routes.test.ts | 22 +++++++++++++++++++ server/src/routes/issues.ts | 4 +++- tests/e2e/board-attachment-receipts.spec.ts | 6 +++-- ui/src/lib/assignees.test.ts | 12 ++++++++++ ui/src/lib/assignees.ts | 2 +- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/server/src/__tests__/issue-comment-attribution-audit-routes.test.ts b/server/src/__tests__/issue-comment-attribution-audit-routes.test.ts index 36d17da5c5..860af32ab4 100644 --- a/server/src/__tests__/issue-comment-attribution-audit-routes.test.ts +++ b/server/src/__tests__/issue-comment-attribution-audit-routes.test.ts @@ -78,6 +78,7 @@ describeEmbeddedPostgres("issue comment attribution and patch audit routes", () async function seed() { const company = await db.insert(companies).values({ name: "Attribution Company", + issueCounter: 2, issuePrefix: `AT${randomUUID().replaceAll("-", "").slice(0, 6).toUpperCase()}`, }).returning().then((rows) => rows[0]!); const [actorAgent, targetAgent] = await db.insert(agents).values(["Actor", "Target"].map((name) => ({ @@ -162,6 +163,27 @@ describeEmbeddedPostgres("issue comment attribution and patch audit routes", () }; } + it("records the human recipient for REST task and child creation", async () => { + const fixture = await seed(); + const app = await createApp(db, agentActor(fixture)); + for (const [path, action] of [ + [`/api/companies/${fixture.company.id}/issues`, "issue.created"], + [`/api/issues/${fixture.issue.id}/children`, "issue.child_created"], + ]) { + const response = await request(app).post(path!).send({ + title: "Task for the human", status: "todo", assigneeUserId: fixture.responsibleUserId, + }); + expect(response.status, JSON.stringify(response.body)).toBe(201); + const events = await db.select().from(activityLog).where(and( + eq(activityLog.entityId, response.body.id), eq(activityLog.action, action!), + )); + expect(events).toEqual([expect.objectContaining({ + actorType: "agent", actorId: fixture.actorAgent.id, + details: expect.objectContaining({ assigneeAgentId: null, assigneeUserId: fixture.responsibleUserId }), + })]); + } + }); + it("attributes foreign-issue agent comments and emits complete agent and human PATCH receipts", async () => { const fixture = await seed(); const agentApp = await createApp(db, agentActor(fixture)); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index f738e91aba..b4270a1227 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -653,7 +653,7 @@ const attachmentArtifactMetadataInputSchema = z .passthrough(); function buildCreateIssueActivityStatusDetails( - issue: { assigneeAgentId: string | null; status: string }, + issue: { assigneeAgentId: string | null; assigneeUserId: string | null; status: string }, res: Response, ) { const statusDefault = res.locals.createIssueStatusDefault as @@ -661,6 +661,8 @@ function buildCreateIssueActivityStatusDetails( const assignmentWakeSkipped = !issue.assigneeAgentId || issue.status === "backlog"; return { + assigneeAgentId: issue.assigneeAgentId, + assigneeUserId: issue.assigneeUserId, status: issue.status, statusDefaulted: statusDefault?.defaulted ?? false, statusDefaultReason: statusDefault?.reason ?? "explicit", diff --git a/tests/e2e/board-attachment-receipts.spec.ts b/tests/e2e/board-attachment-receipts.spec.ts index 649758e26e..912cde9ad8 100644 --- a/tests/e2e/board-attachment-receipts.spec.ts +++ b/tests/e2e/board-attachment-receipts.spec.ts @@ -9,7 +9,7 @@ import { // Real local Board UI, upload storage, comment HTTP routes, and disposable DB. // Only the interface flag and explicit transport/upload rejection faults are mocked. -// Tasks belong to the Board user: no agent, runner, or provider is contacted. +// Tasks have no recipient: no agent, runner, or provider is contacted. type Attachment = { id: string; issueCommentId: string | null; @@ -44,7 +44,9 @@ async function setup(page: Page, request: APIRequestContext, classic: boolean) { data: { title: "Inspect the newly uploaded Board files", status: "todo", - assigneeUserId: "local-board", + // Keep attachment sends on the comment route. A task assigned to the + // current human now suggests an empty recipient and sends a reassignment. + assigneeUserId: null, }, }), ); diff --git a/ui/src/lib/assignees.test.ts b/ui/src/lib/assignees.test.ts index 4628ea6c59..36f912816e 100644 --- a/ui/src/lib/assignees.test.ts +++ b/ui/src/lib/assignees.test.ts @@ -154,3 +154,15 @@ it("prefers a newer native reassignment over a REST assignment and ignores nativ ]; expect(suggestedCommentAssigneeValue({ assigneeUserId: "me" }, [], "me", undefined, history)).toBe("agent:beta"); }); + + +it.each(["issue.created", "issue.child_created"])("uses a REST %s receipt assigned to the current human", (action) => { + const event: ActivityEvent = { + id: "creation", companyId: "company", entityType: "issue", entityId: "task", + agentId: "creator", runId: null, actorType: "agent", actorId: "creator", action, + createdAt: new Date("2026-02-01"), + details: { title: "Task", identifier: "PAP-1", status: "todo", assigneeAgentId: null, assigneeUserId: "me" }, + }; + expect(suggestedCommentAssigneeValue({ assigneeUserId: "me" }, [{ authorAgentId: "commenter" }], "me", undefined, [event])).toBe("agent:creator"); + expect(suggestedCommentAssigneeValue({ assigneeUserId: "other" }, [], "other", undefined, [event])).toBe(""); +}); diff --git a/ui/src/lib/assignees.ts b/ui/src/lib/assignees.ts index d49878ff03..1992442cc9 100644 --- a/ui/src/lib/assignees.ts +++ b/ui/src/lib/assignees.ts @@ -41,7 +41,7 @@ export function suggestedCommentAssigneeValue( if (event.actorType !== "agent" || !event.actorId || event.actorId === currentAgentId) return false; const details = event.details; if (!details || details.assigneeUserId !== currentUserId) return false; - if (event.action === "issue.created") return true; + if (event.action === "issue.created" || event.action === "issue.child_created") return true; if (event.action === "issue.reassigned") { return details.changed === true && Object.hasOwn(details, "previousAssigneeUserId")