From 9bc1b9dc5be9074fd796b0d34c7cf66c5495783f Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 15 Apr 2026 10:04:42 -0500 Subject: [PATCH] Bound issue detail text scans --- .../issues-goal-context-routes.test.ts | 4 + server/src/__tests__/issues-service.test.ts | 82 +++++++++++++++++++ server/src/log-redaction.ts | 2 + server/src/routes/issues.ts | 2 +- server/src/services/issues.ts | 30 ++++--- 5 files changed, 108 insertions(+), 12 deletions(-) diff --git a/server/src/__tests__/issues-goal-context-routes.test.ts b/server/src/__tests__/issues-goal-context-routes.test.ts index f7382e8b9a3..c20040840d0 100644 --- a/server/src/__tests__/issues-goal-context-routes.test.ts +++ b/server/src/__tests__/issues-goal-context-routes.test.ts @@ -190,6 +190,10 @@ describe("issue goal context routes", () => { title: projectGoal.title, }), ); + expect(mockIssueService.findMentionedProjectIds).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + { includeCommentBodies: false }, + ); expect(mockGoalService.getDefaultCompanyGoal).not.toHaveBeenCalled(); }); diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 0629686a154..a7e05245760 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -22,6 +22,7 @@ import { } from "./helpers/embedded-postgres.js"; import { instanceSettingsService } from "../services/instance-settings.ts"; import { issueService } from "../services/issues.ts"; +import { buildProjectMentionHref } from "@paperclipai/shared"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; @@ -1182,3 +1183,84 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness", }); }); }); + +describeEmbeddedPostgres("issueService.findMentionedProjectIds", () => { + let db!: ReturnType; + let svc!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issues-mentioned-projects-"); + db = createDb(tempDb.connectionString); + svc = issueService(db); + await ensureIssueRelationsTable(db); + }, 20_000); + + afterEach(async () => { + await db.delete(issueComments); + await db.delete(issueRelations); + await db.delete(issueInboxArchives); + await db.delete(activityLog); + await db.delete(issues); + await db.delete(executionWorkspaces); + await db.delete(projectWorkspaces); + await db.delete(projects); + await db.delete(agents); + await db.delete(instanceSettings); + await db.delete(companies); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + it("can skip comment-body scans for bounded issue detail reads", async () => { + const companyId = randomUUID(); + const issueId = randomUUID(); + const titleProjectId = randomUUID(); + const commentProjectId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(projects).values([ + { + id: titleProjectId, + companyId, + name: "Title project", + status: "in_progress", + }, + { + id: commentProjectId, + companyId, + name: "Comment project", + status: "in_progress", + }, + ]); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: `Link [Title](${buildProjectMentionHref(titleProjectId)})`, + description: null, + status: "todo", + priority: "medium", + }); + + await db.insert(issueComments).values({ + companyId, + issueId, + body: `Comment link [Comment](${buildProjectMentionHref(commentProjectId)})`, + }); + + expect(await svc.findMentionedProjectIds(issueId, { includeCommentBodies: false })).toEqual([titleProjectId]); + expect(await svc.findMentionedProjectIds(issueId)).toEqual([ + titleProjectId, + commentProjectId, + ]); + }); +}); diff --git a/server/src/log-redaction.ts b/server/src/log-redaction.ts index ab59b3e414a..952d1553ad1 100644 --- a/server/src/log-redaction.ts +++ b/server/src/log-redaction.ts @@ -112,6 +112,7 @@ export function redactCurrentUserText(input: string, opts?: CurrentUserRedaction let result = input; for (const homeDir of [...homeDirs].sort((a, b) => b.length - a.length)) { + if (!result.includes(homeDir)) continue; const lastSegment = splitPathSegments(homeDir).pop() ?? ""; const replacementDir = lastSegment ? replaceLastPathSegment(homeDir, maskUserNameForLogs(lastSegment, replacement)) @@ -120,6 +121,7 @@ export function redactCurrentUserText(input: string, opts?: CurrentUserRedaction } for (const userName of [...userNames].sort((a, b) => b.length - a.length)) { + if (!result.includes(userName)) continue; const pattern = new RegExp(`(? { + findMentionedProjectIds: async ( + issueId: string, + opts?: { includeCommentBodies?: boolean }, + ) => { const issue = await db .select({ companyId: issues.companyId, @@ -2362,21 +2365,26 @@ export function issueService(db: Db) { .then((rows) => rows[0] ?? null); if (!issue) return []; - const comments = await db - .select({ body: issueComments.body }) - .from(issueComments) - .where(eq(issueComments.issueId, issueId)); - const mentionedIds = new Set(); - for (const source of [ - issue.title, - issue.description ?? "", - ...comments.map((comment) => comment.body), - ]) { + for (const source of [issue.title, issue.description ?? ""]) { for (const projectId of extractProjectMentionIds(source)) { mentionedIds.add(projectId); } } + + if (opts?.includeCommentBodies !== false) { + const comments = await db + .select({ body: issueComments.body }) + .from(issueComments) + .where(eq(issueComments.issueId, issueId)); + + for (const comment of comments) { + for (const projectId of extractProjectMentionIds(comment.body)) { + mentionedIds.add(projectId); + } + } + } + if (mentionedIds.size === 0) return []; const rows = await db