fix(server): flag truncated issue descriptions (#4771)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The issue list API is one of the surfaces API consumers use to synchronize issue metadata. > - The list endpoint intentionally returns a bounded `description` preview so large descriptions do not bloat list responses. > - Before this change, that preview looked like a complete field value because the response did not say whether it had been shortened. > - That made round-trip clients vulnerable to accidentally PATCHing a preview back over the full description. > - This pull request keeps the existing preview behavior but adds an explicit `descriptionTruncated` flag. > - The benefit is backwards-compatible visibility into truncated issue descriptions, so clients can avoid data-loss workflows. ## Linked Issues or Issue Description Fixes #4758. Related PR: #4792 also targets #4758, but it includes unrelated logger changes and currently has separate review/security concerns. This PR keeps the fix scoped to the issue-list description truncation API behavior. ## What Changed - Added `descriptionTruncated` to the issue list projection when `description` exceeds the existing 1200-character preview limit. - Exposed `descriptionTruncated?: boolean` on the shared `Issue` type. - Added service tests for truncated descriptions, exact-limit descriptions, null descriptions, and multibyte-safe preview truncation. ## Verification June 18, 2026 refresh after rebasing onto current `origin/master`: - `pnpm install --frozen-lockfile` - `pnpm exec vitest run server/src/__tests__/issues-service.test.ts` - `pnpm --filter @paperclipai/server typecheck` - `pnpm typecheck` - `git diff --check origin/master...HEAD` - GitHub PR checks are green on head `12e828e6`. Earlier pre-review verification also included `pnpm test`. ## Risks - Low risk. This is an additive API response field; existing clients can ignore it. - The list endpoint still returns the same bounded `description` preview. Clients that need full text should continue fetching the issue detail, but can now detect when that is necessary. - No database migration or UI behavior change. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex based on GPT-5, via Codex desktop on April 29, June 15, and June 18, 2026. Used tool-assisted repository inspection, code editing, local test execution, GitHub CLI workflows, and PR review follow-up. Exact context window size is not surfaced by the tool. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots (N/A: no UI change) - [x] I have updated relevant documentation to reflect my changes (N/A: additive API field covered by tests) - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Sami Rusani <sr@samirusani>
This commit is contained in:
parent
c1c46f1e4e
commit
14fd8aee36
|
|
@ -793,6 +793,7 @@ export interface Issue {
|
|||
ancestors?: IssueAncestor[];
|
||||
title: string;
|
||||
description: string | null;
|
||||
descriptionTruncated?: boolean;
|
||||
status: IssueStatus;
|
||||
workMode: IssueWorkMode;
|
||||
priority: IssuePriority;
|
||||
|
|
|
|||
|
|
@ -2590,11 +2590,65 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
|||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result?.description).toHaveLength(1200);
|
||||
expect(result?.descriptionTruncated).toBe(true);
|
||||
expect(result?.executionPolicy).toBeNull();
|
||||
expect(result?.executionState).toBeNull();
|
||||
expect(result?.executionWorkspaceSettings).toBeNull();
|
||||
});
|
||||
|
||||
it("marks list descriptions as not truncated when they fit the preview limit", async () => {
|
||||
const companyId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
const description = "x".repeat(1200);
|
||||
|
||||
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: issueId,
|
||||
companyId,
|
||||
title: "Exact preview issue",
|
||||
description,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
const [result] = await svc.list(companyId);
|
||||
|
||||
expect(result?.description).toHaveLength(1200);
|
||||
expect(result?.descriptionTruncated).toBe(false);
|
||||
});
|
||||
|
||||
it("marks null list descriptions as not truncated", async () => {
|
||||
const companyId = randomUUID();
|
||||
const issueId = 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: issueId,
|
||||
companyId,
|
||||
title: "Null description issue",
|
||||
description: null,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
const [result] = await svc.list(companyId);
|
||||
|
||||
expect(result?.description).toBeNull();
|
||||
expect(result?.descriptionTruncated).toBe(false);
|
||||
});
|
||||
|
||||
it("does not let description preview truncation split multibyte characters", async () => {
|
||||
const companyId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
|
@ -2620,6 +2674,7 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
|||
|
||||
expect(result?.description).toHaveLength(1200);
|
||||
expect(result?.description?.endsWith("—")).toBe(true);
|
||||
expect(result?.descriptionTruncated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3145,6 +3145,12 @@ const issueListSelect = {
|
|||
)
|
||||
END
|
||||
`,
|
||||
descriptionTruncated: sql<boolean>`
|
||||
CASE
|
||||
WHEN ${issues.description} IS NULL THEN false
|
||||
ELSE length(${issues.description}) > ${ISSUE_LIST_DESCRIPTION_MAX_CHARS}
|
||||
END
|
||||
`,
|
||||
status: issues.status,
|
||||
workMode: issues.workMode,
|
||||
harnessKind: issues.harnessKind,
|
||||
|
|
|
|||
Loading…
Reference in New Issue