From bb9dfe2087eb127f5240e5676e70cd79fa87109c Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Sun, 9 Aug 2026 03:11:41 +0530 Subject: [PATCH] fix(formatters): truncate must never exceed maxLen for small limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit truncate('hello', 0) previously sliced from the end (slice(0, -1)) and appended the ellipsis, producing 'hell…' — longer than the requested limit and longer than the input itself. Negative limits behaved the same way. Limits of 1 or less now render the ellipsis alone (empty input stays empty), honoring the documented contract that the output length includes the ellipsis. --- README.md | 1 - src/lib/api/github.ts | 62 +++++++++++++++----------------- src/lib/utils/formatters.test.ts | 11 ++++++ src/lib/utils/formatters.ts | 4 +++ 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 07c81d5..adfb2c6 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ These instructions have been tested on a clean machine to ensure a reliable setu > **Note:** If the repository already includes `.env.example`, you only need to copy it to `.env`. Creating a new `.env.example` is only necessary if the file is missing. - 5. **Start the development server:** ```bash bun dev diff --git a/src/lib/api/github.ts b/src/lib/api/github.ts index 3816c83..d1cb00c 100644 --- a/src/lib/api/github.ts +++ b/src/lib/api/github.ts @@ -137,21 +137,19 @@ export async function searchIssues( const issues = data.items .filter((item) => !item.pull_request) - .map( - (item): Issue => ({ - number: item.number, - title: item.title, - body: item.body, - user: item.user, - labels: item.labels, - assignees: item.assignees, - comments_count: item.comments, - created_at: item.created_at, - updated_at: item.updated_at, - html_url: item.html_url, - state: item.state, - }), - ); + .map((item): Issue => ({ + number: item.number, + title: item.title, + body: item.body, + user: item.user, + labels: item.labels, + assignees: item.assignees, + comments_count: item.comments, + created_at: item.created_at, + updated_at: item.updated_at, + html_url: item.html_url, + state: item.state, + })); allIssues.push(...issues); @@ -190,15 +188,13 @@ export async function fetchIssueComments( const data = await response.json(); - return data.map( - (c: Record): Comment => ({ - id: c.id as number, - user: c.user as Comment['user'], - body: c.body as string, - created_at: c.created_at as string, - updated_at: c.updated_at as string, - }), - ); + return data.map((c: Record): Comment => ({ + id: c.id as number, + user: c.user as Comment['user'], + body: c.body as string, + created_at: c.created_at as string, + updated_at: c.updated_at as string, + })); } export async function fetchIssueTimeline( @@ -217,16 +213,14 @@ export async function fetchIssueTimeline( const data: Record[] = await response.json(); - return data.map( - (e): TimelineEvent => ({ - event: e.event as string, - created_at: e.created_at as string, - actor: e.actor as TimelineEvent['actor'], - source: e.source as TimelineEvent['source'], - commit_id: e.commit_id as string | undefined, - label: e.label as TimelineEvent['label'], - }), - ); + return data.map((e): TimelineEvent => ({ + event: e.event as string, + created_at: e.created_at as string, + actor: e.actor as TimelineEvent['actor'], + source: e.source as TimelineEvent['source'], + commit_id: e.commit_id as string | undefined, + label: e.label as TimelineEvent['label'], + })); } catch { return []; } diff --git a/src/lib/utils/formatters.test.ts b/src/lib/utils/formatters.test.ts index 56f7cc2..bc25442 100644 --- a/src/lib/utils/formatters.test.ts +++ b/src/lib/utils/formatters.test.ts @@ -48,6 +48,17 @@ describe('formatters utilities', () => { expect(truncate('hello world', 6)).toBe('hello…'); }); + it('never exceeds maxLen when the limit is 1', () => { + expect(truncate('hello', 1)).toBe('…'); + }); + + it('handles zero or negative maxLen without producing longer output', () => { + // Previously truncate('hello', 0) returned 'hell…' — longer than the input + expect(truncate('hello', 0)).toBe('…'); + expect(truncate('hello', -3)).toBe('…'); + expect(truncate('', 0)).toBe(''); + }); + // ── complexityLabel ───────────────────────────── it('returns correct complexity label', () => { expect(complexityLabel(2)).toBe('Simple'); diff --git a/src/lib/utils/formatters.ts b/src/lib/utils/formatters.ts index 42afbaa..5720555 100644 --- a/src/lib/utils/formatters.ts +++ b/src/lib/utils/formatters.ts @@ -48,6 +48,10 @@ export function formatTimeAgo(ts: string | number): string { * @returns The truncated string with an ellipsis, or the original string if it is short enough. */ export function truncate(str: string, maxLen: number): string { + if (str.length === 0) return str; + // The ellipsis alone is one character, so anything at or below 1 can only + // render the ellipsis itself + if (maxLen <= 1) return '…'; if (str.length <= maxLen) return str; return str.slice(0, maxLen - 1) + '…'; }