From 92dbeb41a05d54abf16b7d8fcfe089a4e787800e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 12:13:08 +0000 Subject: [PATCH 1/3] fix(test): complete env isolation in terminalLink tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The withTTY helper only saved/restored SENTRY_PLAIN_OUTPUT and isTTY, but did not clear NO_COLOR or FORCE_COLOR. In environments where NO_COLOR is set (e.g., CI), isPlainOutput() returns true before reaching the TTY check, causing terminalLink to return plain text and two tests to fail. Save/restore NO_COLOR and FORCE_COLOR alongside SENTRY_PLAIN_OUTPUT so the tests exercise the actual OSC 8 code path regardless of the outer environment. Co-authored-by: Miguel Betegón --- test/lib/formatters/colors.test.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/lib/formatters/colors.test.ts b/test/lib/formatters/colors.test.ts index c4d7705c7..e3130ee7f 100644 --- a/test/lib/formatters/colors.test.ts +++ b/test/lib/formatters/colors.test.ts @@ -114,21 +114,31 @@ describe("fixabilityColor", () => { }); describe("terminalLink", () => { - /** Save/restore isTTY and env around TTY-specific tests */ + /** Save/restore isTTY and all plain-output env vars around TTY-specific tests */ function withTTY(fn: () => void): void { const savedTTY = process.stdout.isTTY; const savedPlain = process.env.SENTRY_PLAIN_OUTPUT; + const savedNoColor = process.env.NO_COLOR; + const savedForceColor = process.env.FORCE_COLOR; process.stdout.isTTY = true; delete process.env.SENTRY_PLAIN_OUTPUT; + delete process.env.NO_COLOR; + delete process.env.FORCE_COLOR; try { fn(); } finally { process.stdout.isTTY = savedTTY; - if (savedPlain !== undefined) { - process.env.SENTRY_PLAIN_OUTPUT = savedPlain; - } else { - delete process.env.SENTRY_PLAIN_OUTPUT; - } + restoreEnv("SENTRY_PLAIN_OUTPUT", savedPlain); + restoreEnv("NO_COLOR", savedNoColor); + restoreEnv("FORCE_COLOR", savedForceColor); + } + } + + function restoreEnv(key: string, saved: string | undefined): void { + if (saved !== undefined) { + process.env[key] = saved; + } else { + delete process.env[key]; } } From ac2c0e333e6a5a234005505fa0e0af1be32b6130 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 12:13:57 +0000 Subject: [PATCH 2/3] fix(events): drop nextCursor when trimming listIssueEvents results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When auto-paginating issue events and the accumulated results exceed the requested limit, the function trimmed the array but preserved nextCursor. That cursor points to the next API page, skipping the untrimmed tail of the current batch — causing events to be lost during cursor-based pagination. Drop nextCursor when trimming, matching the existing behavior of listIssuesAllPages in issues.ts which correctly returns no cursor when results are trimmed. Co-authored-by: Miguel Betegón --- src/lib/api/events.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/api/events.ts b/src/lib/api/events.ts index 22e2f5c91..3f253ffcc 100644 --- a/src/lib/api/events.ts +++ b/src/lib/api/events.ts @@ -252,10 +252,10 @@ export async function listIssueEvents( // Trim to exact limit. Unlike listIssuesAllPages (which controls per_page), // the issue events endpoint has no per-page parameter, so the API may return - // more items than requested. We preserve nextCursor so the command-level - // cursor stack can navigate to subsequent pages. - const trimmed = - allEvents.length > limit ? allEvents.slice(0, limit) : allEvents; - - return { data: trimmed, nextCursor }; + // more items than requested. When trimming, drop nextCursor — it points past + // the trimmed items and would cause the cursor stack to skip events. + if (allEvents.length > limit) { + return { data: allEvents.slice(0, limit) }; + } + return { data: allEvents, nextCursor }; } From 84da84cd0a0aceb286372f9ce7dbe9eea3e314aa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 12:14:57 +0000 Subject: [PATCH 3/3] fix(pagination): guard JSON.parse of cursor_stack against corrupt data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getPaginationState() calls JSON.parse on cursor_stack from SQLite without a try/catch. If the stored JSON is corrupt (partial write, manual edit, incompatible migration), the CLI crashes with an unhandled SyntaxError instead of degrading gracefully. Wrap JSON.parse in try/catch. On parse failure, log a debug message, delete the corrupt row, and return undefined (treating it as no saved state). This lets the user continue navigating from a fresh first page rather than being stuck with a hard crash. Co-authored-by: Miguel Betegón --- src/lib/db/pagination.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/lib/db/pagination.ts b/src/lib/db/pagination.ts index c702918d9..3a1773428 100644 --- a/src/lib/db/pagination.ts +++ b/src/lib/db/pagination.ts @@ -14,10 +14,13 @@ import { ValidationError } from "../errors.js"; import { CURSOR_KEYWORDS } from "../list-command.js"; +import { logger } from "../logger.js"; import { getApiBaseUrl } from "../sentry-client.js"; import { getDatabase } from "./index.js"; import { runUpsert } from "./utils.js"; +const log = logger.withTag("pagination"); + /** Default TTL for stored cursors: 5 minutes */ const CURSOR_TTL_MS = 5 * 60 * 1000; @@ -84,7 +87,16 @@ export function getPaginationState( return; } - const stack = JSON.parse(row.cursor_stack) as string[]; + let stack: string[]; + try { + stack = JSON.parse(row.cursor_stack) as string[]; + } catch { + log.debug("Corrupt cursor_stack in pagination DB, clearing state"); + db.query( + "DELETE FROM pagination_cursors WHERE command_key = ? AND context = ?" + ).run(commandKey, contextKey); + return; + } return { stack, index: row.page_index }; }