From 349caf02b18778d7254943a0fb7f8892cab8d592 Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 4 Jun 2026 15:56:31 +0200 Subject: [PATCH 1/7] fix(deps): bump @sentry/api to 0.180.0 and fix downstream type errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDK 0.180.0 tightened several types that the old version silently accepted: - issue_id is now string (not number) — remove Number() wrappers in events.ts - lastSeen/firstSeen can be null in SDK types — coerce to undefined at call sites - Two new issue statuses: resolvedInNextRelease and muted — add to ISSUE_STATUSES, STATUS_COLORS, STATUS_ICONS, STATUS_LABELS The reverse exhaustiveness check on ISSUE_STATUSES is removed: the SDK response union now includes a variant with status: string (loose), making the check always fail. The satisfies above still catches invalid values in our tuple. dashboard_id is still typed as number in the path (backend spec fix pending). --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/commands/issue/list.ts | 6 +++--- src/lib/api/events.ts | 4 ++-- src/lib/formatters/colors.ts | 2 ++ src/lib/formatters/human.ts | 8 ++++++-- src/types/sentry.ts | 19 ++++++------------- 7 files changed, 25 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index ede0434c2..185b0cf0f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@clack/prompts": "0.11.0", "@hono/node-server": "^2.0.0", "@mastra/client-js": "^1.4.0", - "@sentry/api": "^0.141.0", + "@sentry/api": "^0.180.0", "@sentry/core": "10.50.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61a7b5f73..fc1aa7599 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,8 @@ importers: specifier: ^1.4.0 version: 1.21.1(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(express@5.2.1)(openapi-types@12.1.3)(zod@3.25.76) '@sentry/api': - specifier: ^0.141.0 - version: 0.141.0(zod@3.25.76) + specifier: ^0.180.0 + version: 0.180.0(zod@3.25.76) '@sentry/core': specifier: 10.50.0 version: 10.50.0(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) @@ -995,8 +995,8 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sentry/api@0.141.0': - resolution: {integrity: sha512-6DAEAhHnE/bkiUsCIGY4V9fPWVg2sc0Wn0ualQ4xEEurKQgtbafhJyZuuwCfTwT/nYldHosjGfLoWFNdXj9tWA==} + '@sentry/api@0.180.0': + resolution: {integrity: sha512-Qv0bJnRgpnlKDFM/V86AR/CdJNOU4JO2tno1BnZjR+ACM9gcyt/Y6xow7YeD6vHLzvguW0SiwqnX/lt4DSmFUQ==} engines: {node: '>=22'} peerDependencies: zod: ^3.24.0 @@ -3882,7 +3882,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@sentry/api@0.141.0(zod@3.25.76)': + '@sentry/api@0.180.0(zod@3.25.76)': optionalDependencies: zod: 3.25.76 diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index f97fccb1b..1b37cdafc 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -369,9 +369,9 @@ function getComparator( ): (a: SentryIssue, b: SentryIssue) => number { switch (sort) { case "date": - return (a, b) => compareDates(a.lastSeen, b.lastSeen); + return (a, b) => compareDates(a.lastSeen ?? undefined, b.lastSeen ?? undefined); case "new": - return (a, b) => compareDates(a.firstSeen, b.firstSeen); + return (a, b) => compareDates(a.firstSeen ?? undefined, b.firstSeen ?? undefined); case "freq": return (a, b) => Number.parseInt(b.count ?? "0", 10) - @@ -379,7 +379,7 @@ function getComparator( case "user": return (a, b) => (b.userCount ?? 0) - (a.userCount ?? 0); default: - return (a, b) => compareDates(a.lastSeen, b.lastSeen); + return (a, b) => compareDates(a.lastSeen ?? undefined, b.lastSeen ?? undefined); } } diff --git a/src/lib/api/events.ts b/src/lib/api/events.ts index ecf08f830..8daf9a56b 100644 --- a/src/lib/api/events.ts +++ b/src/lib/api/events.ts @@ -44,7 +44,7 @@ export async function getLatestEvent( ...config, path: { organization_id_or_slug: orgSlug, - issue_id: Number(issueId), + issue_id: issueId, event_id: "latest", }, }); @@ -237,7 +237,7 @@ export async function listIssueEvents( ...config, path: { organization_id_or_slug: orgSlug, - issue_id: Number(issueId), + issue_id: issueId, }, query: { query: query || undefined, diff --git a/src/lib/formatters/colors.ts b/src/lib/formatters/colors.ts index 4a5dd839c..7bb81285f 100644 --- a/src/lib/formatters/colors.ts +++ b/src/lib/formatters/colors.ts @@ -88,8 +88,10 @@ export const header = (text: string): string => muted(text); const STATUS_COLORS: Record string> = { resolved: green, + resolvedInNextRelease: green, unresolved: yellow, ignored: muted, + muted: muted, }; /** diff --git a/src/lib/formatters/human.ts b/src/lib/formatters/human.ts index 64d0a88b9..d725da501 100644 --- a/src/lib/formatters/human.ts +++ b/src/lib/formatters/human.ts @@ -62,14 +62,18 @@ const FIXABILITY_TAGS: Record[0]> = const STATUS_ICONS: Record = { resolved: colorTag("green", "✓"), + resolvedInNextRelease: colorTag("green", "✓"), unresolved: colorTag("yellow", "●"), ignored: colorTag("muted", "−"), + muted: colorTag("muted", "−"), }; const STATUS_LABELS: Record = { resolved: `${colorTag("green", "✓")} Resolved`, + resolvedInNextRelease: `${colorTag("green", "✓")} Resolved in Next Release`, unresolved: `${colorTag("yellow", "●")} Unresolved`, ignored: `${colorTag("muted", "−")} Ignored`, + muted: `${colorTag("muted", "−")} Muted`, }; /** Maximum features to display before truncating with "... and N more" */ @@ -628,12 +632,12 @@ export function writeIssueTable( // SEEN — lastSeen { header: "SEEN", - value: ({ issue }) => formatRelativeTime(issue.lastSeen), + value: ({ issue }) => formatRelativeTime(issue.lastSeen ?? undefined), }, // AGE — firstSeen { header: "AGE", - value: ({ issue }) => formatRelativeTime(issue.firstSeen), + value: ({ issue }) => formatRelativeTime(issue.firstSeen ?? undefined), }, ]; diff --git a/src/types/sentry.ts b/src/types/sentry.ts index bd3600889..49956ff77 100644 --- a/src/types/sentry.ts +++ b/src/types/sentry.ts @@ -110,24 +110,17 @@ export type SentryProject = Partial & { */ export const ISSUE_STATUSES = [ "resolved", + "resolvedInNextRelease", "unresolved", "ignored", + "muted", ] as const satisfies readonly NonNullable[]; export type IssueStatus = (typeof ISSUE_STATUSES)[number]; -/** - * Compile-time exhaustiveness check for `ISSUE_STATUSES`. - * If the SDK ever adds a status that isn't in the tuple, this resolves to - * `never` and the assignment fails to typecheck. The tuple-wrapping - * (`[X] extends [Y]`) prevents distributive inference so the check fires - * on the union as a whole. - */ -type _IssueStatusParity = [NonNullable] extends [ - IssueStatus, -] - ? true - : never; -const _ISSUE_STATUS_PARITY: _IssueStatusParity = true; +// Note: a reverse exhaustiveness check (SDK → ISSUE_STATUSES) is not possible here +// because RetrieveAnIssueResponses is a union of all HTTP response types, one of which +// has `status: string` (loose), making SdkIssueDetail["status"] resolve to `string`. +// The `satisfies` above catches the forward direction (invalid values in our tuple). export const ISSUE_LEVELS = [ "fatal", From 76c7f6f4c76d43c3d1266b9b4861d84ae7b2b5aa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Jun 2026 14:13:37 +0000 Subject: [PATCH 2/7] chore: regenerate docs --- .../sentry-cli/skills/sentry-cli/references/issue.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 05b6d2b51..3a1c15ef6 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -31,11 +31,11 @@ List issues in a project | `id` | string | Numeric issue ID | | `shortId` | string | Human-readable short ID (e.g. PROJ-ABC) | | `title` | string | Issue title | -| `culprit` | string | Culprit string | +| `culprit` | string \| null | Culprit string | | `count` | string | Total event count | | `userCount` | number | Number of affected users | -| `firstSeen` | string | First occurrence (ISO 8601) | -| `lastSeen` | string | Most recent occurrence (ISO 8601) | +| `firstSeen` | string \| null | First occurrence (ISO 8601) | +| `lastSeen` | string \| null | Most recent occurrence (ISO 8601) | | `level` | string | Severity level | | `status` | string | Issue status | | `permalink` | string | URL to the issue in Sentry | @@ -190,11 +190,11 @@ View details of a specific issue | `id` | string | Numeric issue ID | | `shortId` | string | Human-readable short ID (e.g. PROJ-ABC) | | `title` | string | Issue title | -| `culprit` | string | Culprit string | +| `culprit` | string \| null | Culprit string | | `count` | string | Total event count | | `userCount` | number | Number of affected users | -| `firstSeen` | string | First occurrence (ISO 8601) | -| `lastSeen` | string | Most recent occurrence (ISO 8601) | +| `firstSeen` | string \| null | First occurrence (ISO 8601) | +| `lastSeen` | string \| null | Most recent occurrence (ISO 8601) | | `level` | string | Severity level | | `status` | string | Issue status | | `permalink` | string | URL to the issue in Sentry | From d9dbb50850a5136c454596bf4084672939f51933 Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 4 Jun 2026 16:21:30 +0200 Subject: [PATCH 3/7] fix(formatters): handle camelCase status in statusColor lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit statusColor was calling .toLowerCase() before the STATUS_COLORS lookup, converting resolvedInNextRelease to resolvedinnextrelease which never matched — always falling back to yellow instead of green. Try exact match first, then lowercase fallback for unexpected casing from older Sentry instances. --- src/lib/formatters/colors.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/formatters/colors.ts b/src/lib/formatters/colors.ts index 7bb81285f..bcfe17b24 100644 --- a/src/lib/formatters/colors.ts +++ b/src/lib/formatters/colors.ts @@ -98,8 +98,12 @@ const STATUS_COLORS: Record string> = { * Color text based on issue status (case-insensitive) */ export function statusColor(text: string, status: string | undefined): string { - const normalizedStatus = status?.toLowerCase() as IssueStatus; - const colorFn = STATUS_COLORS[normalizedStatus] ?? STATUS_COLORS.unresolved; + // Try exact match first (handles camelCase like resolvedInNextRelease), + // then fall back to lowercase (handles unexpected uppercase from older instances). + const colorFn = + STATUS_COLORS[status as IssueStatus] ?? + STATUS_COLORS[status?.toLowerCase() as IssueStatus] ?? + STATUS_COLORS.unresolved; return colorFn(text); } From f5c8b31c7f7550bd1828120e052b691de9e9bebf Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 4 Jun 2026 16:25:40 +0200 Subject: [PATCH 4/7] style: fix biome formatting and shorthand property lint errors --- src/commands/issue/list.ts | 9 ++++++--- src/lib/formatters/colors.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index 1b37cdafc..476321ee6 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -369,9 +369,11 @@ function getComparator( ): (a: SentryIssue, b: SentryIssue) => number { switch (sort) { case "date": - return (a, b) => compareDates(a.lastSeen ?? undefined, b.lastSeen ?? undefined); + return (a, b) => + compareDates(a.lastSeen ?? undefined, b.lastSeen ?? undefined); case "new": - return (a, b) => compareDates(a.firstSeen ?? undefined, b.firstSeen ?? undefined); + return (a, b) => + compareDates(a.firstSeen ?? undefined, b.firstSeen ?? undefined); case "freq": return (a, b) => Number.parseInt(b.count ?? "0", 10) - @@ -379,7 +381,8 @@ function getComparator( case "user": return (a, b) => (b.userCount ?? 0) - (a.userCount ?? 0); default: - return (a, b) => compareDates(a.lastSeen ?? undefined, b.lastSeen ?? undefined); + return (a, b) => + compareDates(a.lastSeen ?? undefined, b.lastSeen ?? undefined); } } diff --git a/src/lib/formatters/colors.ts b/src/lib/formatters/colors.ts index bcfe17b24..f364d054f 100644 --- a/src/lib/formatters/colors.ts +++ b/src/lib/formatters/colors.ts @@ -91,7 +91,7 @@ const STATUS_COLORS: Record string> = { resolvedInNextRelease: green, unresolved: yellow, ignored: muted, - muted: muted, + muted, }; /** From acf6a15ac3d29df0ba022da1f5fe8dad6c87e0c2 Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 4 Jun 2026 16:43:04 +0200 Subject: [PATCH 5/7] test(formatters): add coverage for resolvedInNextRelease and muted statuses Add statusColor tests for the two new statuses and the unknown-status fallback path. Add formatStatusIcon and formatStatusLabel tests covering all new entries (resolvedInNextRelease, muted) to reach the 80% patch coverage threshold. --- test/lib/formatters/colors.test.ts | 17 +++++++++++ test/lib/formatters/human.test.ts | 48 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/test/lib/formatters/colors.test.ts b/test/lib/formatters/colors.test.ts index 310f54f7c..549618aae 100644 --- a/test/lib/formatters/colors.test.ts +++ b/test/lib/formatters/colors.test.ts @@ -53,6 +53,23 @@ describe("statusColor", () => { expect(result).toContain("\x1b["); expect(stripAnsi(result)).toBe("text"); }); + + test("resolvedInNextRelease → green-styled text", () => { + const result = statusColor("text", "resolvedInNextRelease"); + expect(result).toContain("\x1b["); + expect(stripAnsi(result)).toBe("text"); + }); + + test("muted → muted-styled text", () => { + const result = statusColor("text", "muted"); + expect(result).toContain("\x1b["); + expect(stripAnsi(result)).toBe("text"); + }); + + test("unknown status defaults to unresolved styling", () => { + const result = statusColor("text", "somethingNew"); + expect(stripAnsi(result)).toBe("text"); + }); }); describe("levelColor", () => { diff --git a/test/lib/formatters/human.test.ts b/test/lib/formatters/human.test.ts index 201a08d90..4db2ef4c1 100644 --- a/test/lib/formatters/human.test.ts +++ b/test/lib/formatters/human.test.ts @@ -13,6 +13,8 @@ import { formatIssueSubtitle, formatProjectCreated, formatShortId, + formatStatusIcon, + formatStatusLabel, formatUpgradeResult, formatUserIdentity, type IssueTableRow, @@ -848,3 +850,49 @@ describe("formatUpgradeResult", () => { }); }); }); + +describe("formatStatusIcon", () => { + test("resolved shows green icon", () => { + expect(stripFormatting(formatStatusIcon("resolved"))).toContain("✓"); + }); + + test("unresolved shows yellow icon", () => { + expect(stripFormatting(formatStatusIcon("unresolved"))).toContain("●"); + }); + + test("resolvedInNextRelease shows green icon", () => { + expect(stripFormatting(formatStatusIcon("resolvedInNextRelease"))).toContain( + "✓" + ); + }); + + test("muted shows muted icon", () => { + expect(stripFormatting(formatStatusIcon("muted"))).toContain("−"); + }); + + test("unknown status falls back to yellow icon", () => { + expect(stripFormatting(formatStatusIcon("unknown"))).toContain("●"); + }); +}); + +describe("formatStatusLabel", () => { + test("resolved → Resolved label", () => { + expect(stripFormatting(formatStatusLabel("resolved"))).toContain( + "Resolved" + ); + }); + + test("resolvedInNextRelease → Resolved in Next Release label", () => { + expect( + stripFormatting(formatStatusLabel("resolvedInNextRelease")) + ).toContain("Resolved in Next Release"); + }); + + test("muted → Muted label", () => { + expect(stripFormatting(formatStatusLabel("muted"))).toContain("Muted"); + }); + + test("unknown status falls back to Unknown label", () => { + expect(stripFormatting(formatStatusLabel("unknown"))).toContain("Unknown"); + }); +}); From 5018f0d4c6213d880cac48ac860c2e52a6b351ce Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 4 Jun 2026 16:51:31 +0200 Subject: [PATCH 6/7] test: cover getComparator null-date paths and new status entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add getComparator tests for sort=new and the null lastSeen/firstSeen paths — these were the 2 uncovered patch lines keeping coverage at 77.78%. Also fixes biome formatting in human.test.ts. --- test/commands/issue/list.test.ts | 51 +++++++++++++++++++++++++++++++ test/lib/formatters/human.test.ts | 6 ++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/test/commands/issue/list.test.ts b/test/commands/issue/list.test.ts index 0da97000e..c7f4a9ecb 100644 --- a/test/commands/issue/list.test.ts +++ b/test/commands/issue/list.test.ts @@ -1319,6 +1319,57 @@ describe("issue list: collapse parameter optimization", () => { }); }); +// --------------------------------------------------------------------------- +// getComparator — sort comparator with null-safe date coercion +// --------------------------------------------------------------------------- + +import { __testing } from "../../../src/commands/issue/list.js"; + +const { getComparator } = __testing; + +import type { SentryIssue } from "../../../src/types/index.js"; + +function makeIssue(overrides: Partial = {}): SentryIssue { + return { + id: "1", + shortId: "TEST-1", + title: "Test", + status: "unresolved", + level: "error", + count: "10", + userCount: 1, + firstSeen: "2024-01-01T00:00:00Z", + lastSeen: "2024-01-02T00:00:00Z", + permalink: "https://sentry.io/issues/1", + ...overrides, + }; +} + +describe("getComparator", () => { + test("sort=new compares by firstSeen with null safety", () => { + const cmp = getComparator("new"); + const older = makeIssue({ firstSeen: "2024-01-01T00:00:00Z" }); + const newer = makeIssue({ firstSeen: "2024-01-02T00:00:00Z" }); + expect(cmp(newer, older)).toBeLessThan(0); + expect(cmp(older, newer)).toBeGreaterThan(0); + }); + + test("sort=new handles null firstSeen", () => { + const cmp = getComparator("new"); + const nullDate = makeIssue({ firstSeen: null as unknown as string }); + const withDate = makeIssue({ firstSeen: "2024-01-01T00:00:00Z" }); + // should not throw + expect(() => cmp(nullDate, withDate)).not.toThrow(); + }); + + test("sort=date (default) handles null lastSeen", () => { + const cmp = getComparator("date"); + const nullDate = makeIssue({ lastSeen: null as unknown as string }); + const withDate = makeIssue({ lastSeen: "2024-01-01T00:00:00Z" }); + expect(() => cmp(nullDate, withDate)).not.toThrow(); + }); +}); + // --------------------------------------------------------------------------- // sanitizeQuery — tests moved to test/lib/search-query.test.ts // --------------------------------------------------------------------------- diff --git a/test/lib/formatters/human.test.ts b/test/lib/formatters/human.test.ts index 4db2ef4c1..3da72756f 100644 --- a/test/lib/formatters/human.test.ts +++ b/test/lib/formatters/human.test.ts @@ -861,9 +861,9 @@ describe("formatStatusIcon", () => { }); test("resolvedInNextRelease shows green icon", () => { - expect(stripFormatting(formatStatusIcon("resolvedInNextRelease"))).toContain( - "✓" - ); + expect( + stripFormatting(formatStatusIcon("resolvedInNextRelease")) + ).toContain("✓"); }); test("muted shows muted icon", () => { From b6d06cbd5be54d4b52d11db1a9722e40a6b02372 Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 4 Jun 2026 17:10:34 +0200 Subject: [PATCH 7/7] test: cover default sort branch and null date ?? branches - Add getComparator test for unknown sort value to hit the default: case - Add getComparator test for sort=new with null firstSeen to cover the null ?? branch - Fix the sort=date null test to call comparator (not just check no-throw) - Add writeIssueTable test with null lastSeen/firstSeen to cover human.ts ?? null branches These were the 2 missing lines and 2 partials causing 77.78% patch coverage. --- test/commands/issue/list.test.ts | 19 +++++++++++++++++-- test/lib/formatters/human.test.ts | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/test/commands/issue/list.test.ts b/test/commands/issue/list.test.ts index c7f4a9ecb..d090dba28 100644 --- a/test/commands/issue/list.test.ts +++ b/test/commands/issue/list.test.ts @@ -1362,11 +1362,26 @@ describe("getComparator", () => { expect(() => cmp(nullDate, withDate)).not.toThrow(); }); - test("sort=date (default) handles null lastSeen", () => { + test("sort=date handles null lastSeen (covers ?? null branch)", () => { const cmp = getComparator("date"); const nullDate = makeIssue({ lastSeen: null as unknown as string }); const withDate = makeIssue({ lastSeen: "2024-01-01T00:00:00Z" }); - expect(() => cmp(nullDate, withDate)).not.toThrow(); + expect(cmp(nullDate, withDate)).not.toBe(undefined); + }); + + test("sort=new handles null firstSeen (covers ?? null branch)", () => { + const cmp = getComparator("new"); + const nullDate = makeIssue({ firstSeen: null as unknown as string }); + const withDate = makeIssue({ firstSeen: "2024-01-01T00:00:00Z" }); + expect(cmp(nullDate, withDate)).not.toBe(undefined); + }); + + test("unknown sort value hits default branch (falls back to lastSeen)", () => { + // A value not in the switch exercises the default: compareDates(lastSeen) case + const cmp = getComparator("unknown_sort" as unknown as "date"); + const older = makeIssue({ lastSeen: "2024-01-01T00:00:00Z" }); + const newer = makeIssue({ lastSeen: "2024-01-02T00:00:00Z" }); + expect(cmp(newer, older)).toBeLessThan(0); }); }); diff --git a/test/lib/formatters/human.test.ts b/test/lib/formatters/human.test.ts index 3da72756f..6ac42f73c 100644 --- a/test/lib/formatters/human.test.ts +++ b/test/lib/formatters/human.test.ts @@ -400,6 +400,24 @@ describe("writeIssueTable", () => { // default impact (0.5)*0.6 + 0.75*0.4 = 0.30+0.30 = 0.60 → 60% expect(text).toContain("60%"); }); + + test("renders table with null lastSeen and firstSeen (covers ?? null branches)", () => { + const { writer, output } = capture(); + const rows: IssueTableRow[] = [ + { + issue: { + ...mockIssue, + lastSeen: null as unknown as string, + firstSeen: null as unknown as string, + }, + orgSlug: "test-org", + formatOptions: { projectSlug: "dashboard" }, + }, + ]; + writeIssueTable(writer, rows); + // Should render without throwing; SEEN/AGE columns receive undefined + expect(stripAnsi(output())).toContain("Test issue"); + }); }); describe("substatusLabel", () => {