diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 17d1a8ef..44ac15e6 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -150,6 +150,7 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { await db.delete(projectWorkspaces); await db.delete(projects); await db.delete(goals); + await db.delete(heartbeatRuns); await db.delete(agents); await db.delete(instanceSettings); await db.delete(companies); @@ -1159,6 +1160,68 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { expect(comments.map((comment) => comment.id)).toEqual([firstCommentId]); }); + it("lists user comments when derived run attribution scans a timestamp window", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const commentId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Comments issue", + status: "todo", + priority: "medium", + }); + + await db.insert(heartbeatRuns).values({ + id: randomUUID(), + companyId, + agentId, + contextSnapshot: { issueId }, + createdAt: new Date("2026-05-12T22:58:00.000Z"), + startedAt: new Date("2026-05-12T22:58:00.000Z"), + finishedAt: new Date("2026-05-12T23:14:00.000Z"), + }); + + await db.insert(issueComments).values({ + id: commentId, + companyId, + issueId, + authorUserId: "user-1", + body: "Comment should be visible", + createdAt: new Date("2026-05-12T23:00:00.000Z"), + updatedAt: new Date("2026-05-12T23:00:00.000Z"), + }); + + const comments = await svc.listComments(issueId, { + order: "desc", + limit: 50, + }); + + expect(comments.map((comment) => comment.id)).toEqual([commentId]); + expect(comments[0]?.body).toBe("Comment should be visible"); + }); + it("includes blockedBy summaries on list rows in one batched pass", async () => { const companyId = randomUUID(); const blockerId = randomUUID(); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1e8f1f9b..7a708c28 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -1943,6 +1943,11 @@ export function issueService(db: Db) { }, null); if (minCommentCreatedAtMs === null || maxCommentCreatedAtMs === null) return comments; + const minCommentCreatedAt = new Date(minCommentCreatedAtMs).toISOString(); + const maxCommentCreatedAt = new Date( + maxCommentCreatedAtMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS, + ).toISOString(); + const runs = await db .select({ runId: heartbeatRuns.id, @@ -1969,8 +1974,8 @@ export function issueService(db: Db) { and ${activityLog.runId} = ${heartbeatRuns.id} )`, ), - sql`coalesce(${heartbeatRuns.finishedAt}, ${heartbeatRuns.createdAt}) >= ${new Date(minCommentCreatedAtMs)}`, - sql`coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) <= ${new Date(maxCommentCreatedAtMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS)}`, + sql`coalesce(${heartbeatRuns.finishedAt}, ${heartbeatRuns.createdAt}) >= ${minCommentCreatedAt}::timestamptz`, + sql`coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) <= ${maxCommentCreatedAt}::timestamptz`, ), ) .orderBy(desc(heartbeatRuns.createdAt));