Bound issue detail text scans

This commit is contained in:
Dotta committed 2026-04-15 20:43:19 -05:00
1 parent 5efedffa0e
commit 9bc1b9dc5b
5 files changed
+108 -12

No files matched your search

@@ -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();
});
@@ -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<typeof createDb>;
let svc!: ReturnType<typeof issueService>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | 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,
]);
});
});
+2
View File
@@ -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(`(?<![A-Za-z0-9._-])${escapeRegExp(userName)}(?![A-Za-z0-9._-])`, "g");
result = result.replace(pattern, maskUserNameForLogs(userName, replacement));
}
+1 -1
View File
@@ -729,7 +729,7 @@ export function issueRoutes(
const [{ project, goal }, ancestors, mentionedProjectIds, documentPayload, relations] = await Promise.all([
resolveIssueProjectAndGoal(issue),
svc.getAncestors(issue.id),
svc.findMentionedProjectIds(issue.id),
svc.findMentionedProjectIds(issue.id, { includeCommentBodies: false }),
documentsSvc.getIssueDocumentPayload(issue),
svc.getRelationSummaries(issue.id),
]);
+19 -11
View File
@@ -2350,7 +2350,10 @@ export function issueService(db: Db) {
return [...resolved];
},
findMentionedProjectIds: async (issueId: string) => {
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<string>();
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