From 300d2e2af0d4c30d4db2a7dde9755056785b32d5 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 24 Jan 2026 16:03:30 +0000 Subject: [PATCH 1/5] fix(issue): handle cross-org project slug collisions in alias generation When multiple organizations have projects with the same slug (e.g., org1:dashboard and org2:dashboard), the alias generation now produces unique aliases by prefixing with an org abbreviation (e.g., o1-d, o2-d). - Add buildOrgAwareAliases() to detect and handle slug collisions - Store both hyphenated (o1-d) and compact (o1d) formats for ease of use - Preserve existing short alias behavior for non-colliding projects Fixes #48 --- src/commands/issue/list.ts | 66 ++++++++-------- src/lib/alias.ts | 157 +++++++++++++++++++++++++++++++++++++ test/lib/alias.test.ts | 134 +++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+), 34 deletions(-) diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index ed0110db8..f0a69de58 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -7,10 +7,7 @@ import { buildCommand, numberParser } from "@stricli/core"; import type { SentryContext } from "../../context.js"; -import { - findCommonWordPrefix, - findShortestUniquePrefixes, -} from "../../lib/alias.js"; +import { buildOrgAwareAliases } from "../../lib/alias.js"; import { listIssues } from "../../lib/api-client.js"; import { clearProjectAliases, setProjectAliases } from "../../lib/config.js"; import { createDsnFingerprint } from "../../lib/dsn/index.js"; @@ -130,44 +127,45 @@ type AliasMapResult = { /** * Build project alias map using shortest unique prefix of project slug. + * Handles cross-org slug collisions by prefixing with org abbreviation. * Strips common word prefix before computing unique prefixes for cleaner aliases. * - * Example: spotlight-electron, spotlight-website, spotlight → e, w, s - * Example: frontend, functions, backend → fr, fu, b + * Single org examples: + * spotlight-electron, spotlight-website, spotlight → e, w, s + * frontend, functions, backend → fr, fu, b + * + * Cross-org collision example: + * org1:dashboard, org2:dashboard → o1-d, o2-d */ function buildProjectAliasMap(results: IssueListResult[]): AliasMapResult { - const aliasMap = new Map(); const entries: Record = {}; - // Get all project slugs - const projectSlugs = results.map((r) => r.target.project); - - // Strip common word prefix for cleaner aliases - const strippedPrefix = findCommonWordPrefix(projectSlugs); - - // Create remainders after stripping common prefix - // If stripping leaves empty string, use the original slug - const slugToRemainder = new Map(); - for (const slug of projectSlugs) { - const remainder = slug.slice(strippedPrefix.length); - slugToRemainder.set(slug, remainder || slug); - } - - // Find shortest unique prefix for each remainder - const remainders = [...slugToRemainder.values()]; - const prefixes = findShortestUniquePrefixes(remainders); + // Build org-aware aliases that handle cross-org collisions + const pairs = results.map((r) => ({ + org: r.target.org, + project: r.target.project, + })); + const { aliasMap, strippedPrefix } = buildOrgAwareAliases(pairs); + // Build entries record for storage for (const result of results) { - const projectSlug = result.target.project; - const remainder = slugToRemainder.get(projectSlug) ?? projectSlug; - const alias = prefixes.get(remainder) ?? remainder.charAt(0).toLowerCase(); - - const key = `${result.target.org}:${projectSlug}`; - aliasMap.set(key, alias); - entries[alias] = { - orgSlug: result.target.org, - projectSlug, - }; + const key = `${result.target.org}:${result.target.project}`; + const alias = aliasMap.get(key); + if (alias) { + const entry: ProjectAliasEntry = { + orgSlug: result.target.org, + projectSlug: result.target.project, + }; + entries[alias] = entry; + + // For org-prefixed aliases (contain hyphen from collision handling), + // also store compact format without the last hyphen for ease of use. + // e.g., "o1-d" also stored as "o1d" + if (alias.includes("-")) { + const compactAlias = alias.replace("-", ""); + entries[compactAlias] = entry; + } + } } return { aliasMap, entries, strippedPrefix }; diff --git a/src/lib/alias.ts b/src/lib/alias.ts index 148e44b5f..236ae19e6 100644 --- a/src/lib/alias.ts +++ b/src/lib/alias.ts @@ -109,3 +109,160 @@ export function findShortestUniquePrefixes( return result; } + +/** Input pair for org-aware alias generation */ +export type OrgProjectPair = { + org: string; + project: string; +}; + +/** Result of org-aware alias generation */ +export type OrgAwareAliasResult = { + /** Map from "org:project" key to alias string */ + aliasMap: Map; + /** Common prefix stripped from project slugs (only for non-colliding projects) */ + strippedPrefix: string; +}; + +/** Internal: Groups pairs by project slug and identifies collisions */ +function groupByProjectSlug(pairs: OrgProjectPair[]): { + projectToOrgs: Map>; + collidingSlugs: Set; + uniqueSlugs: Set; +} { + const projectToOrgs = new Map>(); + for (const { org, project } of pairs) { + const orgs = projectToOrgs.get(project) ?? new Set(); + orgs.add(org); + projectToOrgs.set(project, orgs); + } + + const collidingSlugs = new Set(); + const uniqueSlugs = new Set(); + for (const [project, orgs] of projectToOrgs) { + if (orgs.size > 1) { + collidingSlugs.add(project); + } else { + uniqueSlugs.add(project); + } + } + + return { projectToOrgs, collidingSlugs, uniqueSlugs }; +} + +/** Internal: Processes unique (non-colliding) project slugs */ +function processUniqueSlugs( + pairs: OrgProjectPair[], + uniqueSlugs: Set, + aliasMap: Map +): string { + const uniqueProjects = pairs.filter((p) => uniqueSlugs.has(p.project)); + const uniqueProjectSlugs = [...new Set(uniqueProjects.map((p) => p.project))]; + + if (uniqueProjectSlugs.length === 0) { + return ""; + } + + const strippedPrefix = findCommonWordPrefix(uniqueProjectSlugs); + const slugToRemainder = new Map(); + + for (const slug of uniqueProjectSlugs) { + const remainder = slug.slice(strippedPrefix.length); + slugToRemainder.set(slug, remainder || slug); + } + + const uniqueRemainders = [...slugToRemainder.values()]; + const uniquePrefixes = findShortestUniquePrefixes(uniqueRemainders); + + for (const { org, project } of uniqueProjects) { + const remainder = slugToRemainder.get(project) ?? project; + const alias = + uniquePrefixes.get(remainder) ?? remainder.charAt(0).toLowerCase(); + aliasMap.set(`${org}:${project}`, alias); + } + + return strippedPrefix; +} + +/** Internal: Processes colliding project slugs that need org prefixes */ +function processCollidingSlugs( + projectToOrgs: Map>, + collidingSlugs: Set, + aliasMap: Map +): void { + // Get all orgs involved in collisions + const collidingOrgs = new Set(); + for (const slug of collidingSlugs) { + const orgs = projectToOrgs.get(slug); + if (orgs) { + for (const org of orgs) { + collidingOrgs.add(org); + } + } + } + + const orgPrefixes = findShortestUniquePrefixes([...collidingOrgs]); + + for (const slug of collidingSlugs) { + const orgs = projectToOrgs.get(slug); + if (!orgs) { + continue; + } + + const projectPrefix = slug.charAt(0).toLowerCase(); + + for (const org of orgs) { + const orgPrefix = orgPrefixes.get(org) ?? org.charAt(0).toLowerCase(); + aliasMap.set(`${org}:${slug}`, `${orgPrefix}-${projectPrefix}`); + } + } +} + +/** + * Build aliases for org/project pairs, handling cross-org slug collisions. + * + * - Unique project slugs → shortest unique prefix of project slug + * - Colliding slugs (same project in multiple orgs) → "{orgPrefix}-{projectPrefix}" + * + * Common word prefixes (like "spotlight-" in "spotlight-electron") are stripped + * before computing project prefixes to keep aliases short. + * + * @param pairs - Array of org/project pairs to generate aliases for + * @returns Map from "org:project" key to alias string, plus any stripped prefix + * + * @example + * // No collision - same as existing behavior + * buildOrgAwareAliases([ + * { org: "acme", project: "frontend" }, + * { org: "acme", project: "backend" } + * ]) + * // { aliasMap: Map { "acme:frontend" => "f", "acme:backend" => "b" }, strippedPrefix: "" } + * + * @example + * // Collision: same project slug in different orgs + * buildOrgAwareAliases([ + * { org: "org1", project: "dashboard" }, + * { org: "org2", project: "dashboard" } + * ]) + * // { aliasMap: Map { "org1:dashboard" => "o1-d", "org2:dashboard" => "o2-d" }, strippedPrefix: "" } + */ +export function buildOrgAwareAliases( + pairs: OrgProjectPair[] +): OrgAwareAliasResult { + const aliasMap = new Map(); + + if (pairs.length === 0) { + return { aliasMap, strippedPrefix: "" }; + } + + const { projectToOrgs, collidingSlugs, uniqueSlugs } = + groupByProjectSlug(pairs); + + const strippedPrefix = processUniqueSlugs(pairs, uniqueSlugs, aliasMap); + + if (collidingSlugs.size > 0) { + processCollidingSlugs(projectToOrgs, collidingSlugs, aliasMap); + } + + return { aliasMap, strippedPrefix }; +} diff --git a/test/lib/alias.test.ts b/test/lib/alias.test.ts index 268e3b61f..1c8f69b17 100644 --- a/test/lib/alias.test.ts +++ b/test/lib/alias.test.ts @@ -4,6 +4,7 @@ import { describe, expect, test } from "bun:test"; import { + buildOrgAwareAliases, findCommonWordPrefix, findShortestUniquePrefixes, } from "../../src/lib/alias.js"; @@ -154,3 +155,136 @@ describe("alias generation integration", () => { expect(prefixes.get("worker")).toBe("w"); }); }); + +describe("buildOrgAwareAliases", () => { + test("returns empty map for empty input", () => { + const result = buildOrgAwareAliases([]); + expect(result.aliasMap.size).toBe(0); + expect(result.strippedPrefix).toBe(""); + }); + + test("single org multiple projects - no collision", () => { + const result = buildOrgAwareAliases([ + { org: "acme", project: "frontend" }, + { org: "acme", project: "backend" }, + ]); + expect(result.aliasMap.get("acme:frontend")).toBe("f"); + expect(result.aliasMap.get("acme:backend")).toBe("b"); + expect(result.strippedPrefix).toBe(""); + }); + + test("multiple orgs with unique project slugs - no collision", () => { + const result = buildOrgAwareAliases([ + { org: "org1", project: "frontend" }, + { org: "org2", project: "backend" }, + ]); + expect(result.aliasMap.get("org1:frontend")).toBe("f"); + expect(result.aliasMap.get("org2:backend")).toBe("b"); + expect(result.strippedPrefix).toBe(""); + }); + + test("same project slug in different orgs - collision", () => { + const result = buildOrgAwareAliases([ + { org: "org1", project: "dashboard" }, + { org: "org2", project: "dashboard" }, + ]); + + const alias1 = result.aliasMap.get("org1:dashboard"); + const alias2 = result.aliasMap.get("org2:dashboard"); + + // Both should have org-prefixed format with hyphen + expect(alias1).toContain("-"); + expect(alias2).toContain("-"); + + // Must be different aliases + expect(alias1).not.toBe(alias2); + + // Should follow pattern: orgPrefix-projectPrefix + expect(alias1).toMatch(/^o.*-d$/); + expect(alias2).toMatch(/^o.*-d$/); + }); + + test("collision with distinct org names", () => { + const result = buildOrgAwareAliases([ + { org: "acme-corp", project: "api" }, + { org: "bigco", project: "api" }, + ]); + + const alias1 = result.aliasMap.get("acme-corp:api"); + const alias2 = result.aliasMap.get("bigco:api"); + + // Org prefixes should be unique: "a" vs "b" + expect(alias1).toBe("a-a"); + expect(alias2).toBe("b-a"); + }); + + test("mixed - some colliding, some unique project slugs", () => { + const result = buildOrgAwareAliases([ + { org: "org1", project: "dashboard" }, + { org: "org2", project: "dashboard" }, + { org: "org1", project: "backend" }, + ]); + + // dashboard collides → org-prefixed aliases + const dashAlias1 = result.aliasMap.get("org1:dashboard"); + const dashAlias2 = result.aliasMap.get("org2:dashboard"); + expect(dashAlias1).toContain("-"); + expect(dashAlias2).toContain("-"); + expect(dashAlias1).not.toBe(dashAlias2); + + // backend is unique → simple alias + const backendAlias = result.aliasMap.get("org1:backend"); + expect(backendAlias).toBe("b"); + }); + + test("preserves common word prefix stripping for unique projects", () => { + const result = buildOrgAwareAliases([ + { org: "acme", project: "spotlight-electron" }, + { org: "acme", project: "spotlight-website" }, + ]); + expect(result.aliasMap.get("acme:spotlight-electron")).toBe("e"); + expect(result.aliasMap.get("acme:spotlight-website")).toBe("w"); + expect(result.strippedPrefix).toBe("spotlight-"); + }); + + test("handles single project", () => { + const result = buildOrgAwareAliases([{ org: "acme", project: "frontend" }]); + expect(result.aliasMap.get("acme:frontend")).toBe("f"); + }); + + test("collision with similar org names uses longer prefixes", () => { + const result = buildOrgAwareAliases([ + { org: "organization1", project: "app" }, + { org: "organization2", project: "app" }, + ]); + + const alias1 = result.aliasMap.get("organization1:app"); + const alias2 = result.aliasMap.get("organization2:app"); + + // Both orgs start with "organization", so prefixes need to be longer + expect(alias1).not.toBe(alias2); + // Should include enough of the org to be unique + expect(alias1).toMatch(/-a$/); // ends with project prefix + expect(alias2).toMatch(/-a$/); + }); + + test("multiple collisions across same orgs", () => { + const result = buildOrgAwareAliases([ + { org: "org1", project: "api" }, + { org: "org2", project: "api" }, + { org: "org1", project: "web" }, + { org: "org2", project: "web" }, + ]); + + // All four should have org-prefixed aliases + expect(result.aliasMap.get("org1:api")).toContain("-"); + expect(result.aliasMap.get("org2:api")).toContain("-"); + expect(result.aliasMap.get("org1:web")).toContain("-"); + expect(result.aliasMap.get("org2:web")).toContain("-"); + + // All should be unique + const aliases = [...result.aliasMap.values()]; + const uniqueAliases = new Set(aliases); + expect(uniqueAliases.size).toBe(aliases.length); + }); +}); From 741fb0134a2bb89f668251bf1c56b3c1d19e60c6 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 24 Jan 2026 17:16:16 +0000 Subject: [PATCH 2/5] fix: address PR review comments - Use findShortestUniquePrefixes for colliding project slugs to handle cases like 'api' and 'app' both colliding across orgs (prevents identical aliases like 'o1-a' for both) - Remove compact alias storage (e.g., 'o1d' for 'o1-d') to avoid potential overwrites and edge cases with hyphenated org names - Add test for same-letter colliding project slugs --- src/commands/issue/list.ts | 11 +---------- src/lib/alias.ts | 7 ++++++- test/lib/alias.test.ts | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index f0a69de58..409d925a1 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -152,19 +152,10 @@ function buildProjectAliasMap(results: IssueListResult[]): AliasMapResult { const key = `${result.target.org}:${result.target.project}`; const alias = aliasMap.get(key); if (alias) { - const entry: ProjectAliasEntry = { + entries[alias] = { orgSlug: result.target.org, projectSlug: result.target.project, }; - entries[alias] = entry; - - // For org-prefixed aliases (contain hyphen from collision handling), - // also store compact format without the last hyphen for ease of use. - // e.g., "o1-d" also stored as "o1d" - if (alias.includes("-")) { - const compactAlias = alias.replace("-", ""); - entries[compactAlias] = entry; - } } } diff --git a/src/lib/alias.ts b/src/lib/alias.ts index 236ae19e6..3e2094066 100644 --- a/src/lib/alias.ts +++ b/src/lib/alias.ts @@ -203,13 +203,18 @@ function processCollidingSlugs( const orgPrefixes = findShortestUniquePrefixes([...collidingOrgs]); + // Compute unique prefixes for colliding project slugs to handle + // cases like "api" and "app" both colliding across orgs + const projectPrefixes = findShortestUniquePrefixes([...collidingSlugs]); + for (const slug of collidingSlugs) { const orgs = projectToOrgs.get(slug); if (!orgs) { continue; } - const projectPrefix = slug.charAt(0).toLowerCase(); + const projectPrefix = + projectPrefixes.get(slug) ?? slug.charAt(0).toLowerCase(); for (const org of orgs) { const orgPrefix = orgPrefixes.get(org) ?? org.charAt(0).toLowerCase(); diff --git a/test/lib/alias.test.ts b/test/lib/alias.test.ts index 1c8f69b17..7a298004f 100644 --- a/test/lib/alias.test.ts +++ b/test/lib/alias.test.ts @@ -287,4 +287,29 @@ describe("buildOrgAwareAliases", () => { const uniqueAliases = new Set(aliases); expect(uniqueAliases.size).toBe(aliases.length); }); + + test("collision with same-letter project slugs uses unique project prefixes", () => { + // Both "api" and "app" start with "a" - need unique project prefixes + const result = buildOrgAwareAliases([ + { org: "org1", project: "api" }, + { org: "org2", project: "api" }, + { org: "org1", project: "app" }, + { org: "org2", project: "app" }, + ]); + + // All four should be unique + const aliases = [...result.aliasMap.values()]; + const uniqueAliases = new Set(aliases); + expect(uniqueAliases.size).toBe(4); + + // api and app should have different project prefixes (not both "a") + const org1Api = result.aliasMap.get("org1:api"); + const org1App = result.aliasMap.get("org1:app"); + expect(org1Api).not.toBe(org1App); + + // Project prefixes should distinguish api vs app + // e.g., "o1-api" vs "o1-app" or "o1-api" vs "o1-app" + expect(org1Api).toMatch(/^o.*-api$/); + expect(org1App).toMatch(/^o.*-app$/); + }); }); From faae7149c029e7445ec0dafc69731f9b086a2572 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 24 Jan 2026 17:54:17 +0000 Subject: [PATCH 3/5] fix: use colon separator for colliding aliases to prevent namespace collision Change the separator in org-prefixed aliases from hyphen to colon (e.g., 'o1:d' instead of 'o1-d'). This prevents potential collisions between unique project aliases (which can contain hyphens) and colliding project aliases. Since Sentry slugs cannot contain colons, the two alias namespaces are now completely disjoint. --- src/lib/alias.ts | 2 +- test/lib/alias.test.ts | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lib/alias.ts b/src/lib/alias.ts index 3e2094066..d12b40ea6 100644 --- a/src/lib/alias.ts +++ b/src/lib/alias.ts @@ -218,7 +218,7 @@ function processCollidingSlugs( for (const org of orgs) { const orgPrefix = orgPrefixes.get(org) ?? org.charAt(0).toLowerCase(); - aliasMap.set(`${org}:${slug}`, `${orgPrefix}-${projectPrefix}`); + aliasMap.set(`${org}:${slug}`, `${orgPrefix}:${projectPrefix}`); } } } diff --git a/test/lib/alias.test.ts b/test/lib/alias.test.ts index 7a298004f..97f9d7f1e 100644 --- a/test/lib/alias.test.ts +++ b/test/lib/alias.test.ts @@ -192,16 +192,16 @@ describe("buildOrgAwareAliases", () => { const alias1 = result.aliasMap.get("org1:dashboard"); const alias2 = result.aliasMap.get("org2:dashboard"); - // Both should have org-prefixed format with hyphen - expect(alias1).toContain("-"); - expect(alias2).toContain("-"); + // Both should have org-prefixed format with colon + expect(alias1).toContain(":"); + expect(alias2).toContain(":"); // Must be different aliases expect(alias1).not.toBe(alias2); - // Should follow pattern: orgPrefix-projectPrefix - expect(alias1).toMatch(/^o.*-d$/); - expect(alias2).toMatch(/^o.*-d$/); + // Should follow pattern: orgPrefix:projectPrefix + expect(alias1).toMatch(/^o.*:d$/); + expect(alias2).toMatch(/^o.*:d$/); }); test("collision with distinct org names", () => { @@ -214,8 +214,8 @@ describe("buildOrgAwareAliases", () => { const alias2 = result.aliasMap.get("bigco:api"); // Org prefixes should be unique: "a" vs "b" - expect(alias1).toBe("a-a"); - expect(alias2).toBe("b-a"); + expect(alias1).toBe("a:a"); + expect(alias2).toBe("b:a"); }); test("mixed - some colliding, some unique project slugs", () => { @@ -225,11 +225,11 @@ describe("buildOrgAwareAliases", () => { { org: "org1", project: "backend" }, ]); - // dashboard collides → org-prefixed aliases + // dashboard collides → org-prefixed aliases with colon const dashAlias1 = result.aliasMap.get("org1:dashboard"); const dashAlias2 = result.aliasMap.get("org2:dashboard"); - expect(dashAlias1).toContain("-"); - expect(dashAlias2).toContain("-"); + expect(dashAlias1).toContain(":"); + expect(dashAlias2).toContain(":"); expect(dashAlias1).not.toBe(dashAlias2); // backend is unique → simple alias @@ -264,8 +264,8 @@ describe("buildOrgAwareAliases", () => { // Both orgs start with "organization", so prefixes need to be longer expect(alias1).not.toBe(alias2); // Should include enough of the org to be unique - expect(alias1).toMatch(/-a$/); // ends with project prefix - expect(alias2).toMatch(/-a$/); + expect(alias1).toMatch(/:a$/); // ends with project prefix + expect(alias2).toMatch(/:a$/); }); test("multiple collisions across same orgs", () => { @@ -276,11 +276,11 @@ describe("buildOrgAwareAliases", () => { { org: "org2", project: "web" }, ]); - // All four should have org-prefixed aliases - expect(result.aliasMap.get("org1:api")).toContain("-"); - expect(result.aliasMap.get("org2:api")).toContain("-"); - expect(result.aliasMap.get("org1:web")).toContain("-"); - expect(result.aliasMap.get("org2:web")).toContain("-"); + // All four should have org-prefixed aliases with colon + expect(result.aliasMap.get("org1:api")).toContain(":"); + expect(result.aliasMap.get("org2:api")).toContain(":"); + expect(result.aliasMap.get("org1:web")).toContain(":"); + expect(result.aliasMap.get("org2:web")).toContain(":"); // All should be unique const aliases = [...result.aliasMap.values()]; @@ -308,8 +308,8 @@ describe("buildOrgAwareAliases", () => { expect(org1Api).not.toBe(org1App); // Project prefixes should distinguish api vs app - // e.g., "o1-api" vs "o1-app" or "o1-api" vs "o1-app" - expect(org1Api).toMatch(/^o.*-api$/); - expect(org1App).toMatch(/^o.*-app$/); + // e.g., "o1:api" vs "o1:app" + expect(org1Api).toMatch(/^o.*:api$/); + expect(org1App).toMatch(/^o.*:app$/); }); }); From a862d1cda635f6c4ddcf957800ef5cc626012126 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 24 Jan 2026 18:28:38 +0000 Subject: [PATCH 4/5] feat: add ALIAS column for multi-project issue list Add a dedicated ALIAS column in multi-project mode that shows the typeable shorthand (e.g., 'o1:d-a3') for each issue. This replaces the previous approach of trying to underline parts of the short ID. Changes: - Add COL_ALIAS constant (15 chars) for the new column - Update formatIssueListHeader to conditionally show ALIAS column - Update formatIssueRow to include alias shorthand in multi-project mode - Simplify formatShortId since alias is now in its own column - Add tests for ALIAS column formatting Layout in multi-project mode: LEVEL ALIAS SHORT ID COUNT SEEN TITLE ERROR o1:d-a3 DASHBOARD-A3 42 2h ago Something broke Layout in single-project mode (unchanged): LEVEL SHORT ID COUNT SEEN TITLE ERROR DASHBOARD-A3 42 2h ago Something broke --- src/commands/issue/list.ts | 30 ++++++-- src/lib/formatters/human.ts | 109 +++++++++++++++++++++--------- test/lib/formatters/human.test.ts | 90 +++++++++++++++++++++++- 3 files changed, 189 insertions(+), 40 deletions(-) diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index 409d925a1..4fbbbfb81 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -57,11 +57,19 @@ function parseSort(value: string): SortValue { /** * Write the issue list header with column titles. + * + * @param stdout - Output writer + * @param title - Section title + * @param isMultiProject - Whether to show ALIAS column for multi-project mode */ -function writeListHeader(stdout: Writer, title: string): void { +function writeListHeader( + stdout: Writer, + title: string, + isMultiProject = false +): void { stdout.write(`${title}:\n\n`); - stdout.write(muted(`${formatIssueListHeader()}\n`)); - stdout.write(`${divider(80)}\n`); + stdout.write(muted(`${formatIssueListHeader(isMultiProject)}\n`)); + stdout.write(`${divider(isMultiProject ? 96 : 80)}\n`); } /** Issue with formatting options attached */ @@ -101,7 +109,7 @@ function writeListFooter( break; case "multi": stdout.write( - "\nTip: Use 'sentry issue get ' to view details (e.g., 'f-g').\n" + "\nTip: Use 'sentry issue get ' to view details (see ALIAS column).\n" ); break; default: @@ -164,11 +172,17 @@ function buildProjectAliasMap(results: IssueListResult[]): AliasMapResult { /** * Attach formatting options to each issue based on alias map. + * + * @param results - Issue list results with targets + * @param aliasMap - Map from "org:project" to alias + * @param strippedPrefix - Common prefix stripped from project slugs + * @param isMultiProject - Whether in multi-project mode (shows ALIAS column) */ function attachFormatOptions( results: IssueListResult[], aliasMap: Map, - strippedPrefix: string + strippedPrefix: string, + isMultiProject: boolean ): IssueWithOptions[] { return results.flatMap((result) => result.issues.map((issue) => { @@ -184,6 +198,7 @@ function attachFormatOptions( projectSlug: result.target.project, projectAlias: alias, strippedPrefix: hasPrefix ? strippedPrefix : undefined, + isMultiProject, }, }; }) @@ -370,7 +385,8 @@ export const listCommand = buildCommand({ const issuesWithOptions = attachFormatOptions( validResults, aliasMap, - strippedPrefix + strippedPrefix, + isMultiProject ); // Sort by user preference @@ -399,7 +415,7 @@ export const listCommand = buildCommand({ ? `Issues in ${firstTarget.orgDisplay}/${firstTarget.projectDisplay}` : `Issues from ${validResults.length} projects`; - writeListHeader(stdout, title); + writeListHeader(stdout, title, isMultiProject); const termWidth = process.stdout.columns || 80; writeIssueRows(stdout, issuesWithOptions, termWidth); diff --git a/src/lib/formatters/human.ts b/src/lib/formatters/human.ts index b5257619f..670b811cd 100644 --- a/src/lib/formatters/human.ts +++ b/src/lib/formatters/human.ts @@ -263,19 +263,50 @@ export function formatRelativeTime(dateString: string | undefined): string { /** Column widths for issue list table */ const COL_LEVEL = 7; +const COL_ALIAS = 15; const COL_SHORT_ID = 22; const COL_COUNT = 5; const COL_SEEN = 10; -/** Column where title starts (sum of all previous columns + separators) */ +/** Column where title starts in single-project mode (no ALIAS column) */ const TITLE_START_COL = COL_LEVEL + 1 + COL_SHORT_ID + 1 + COL_COUNT + 2 + COL_SEEN + 2; // = 50 +/** Column where title starts in multi-project mode (with ALIAS column) */ +const TITLE_START_COL_MULTI = + COL_LEVEL + + 1 + + COL_ALIAS + + 1 + + COL_SHORT_ID + + 1 + + COL_COUNT + + 2 + + COL_SEEN + + 2; // = 66 + /** * Format the header row for issue list table. * Uses same column widths as data rows to ensure alignment. + * + * @param isMultiProject - Whether to include ALIAS column for multi-project mode */ -export function formatIssueListHeader(): string { +export function formatIssueListHeader(isMultiProject = false): string { + if (isMultiProject) { + return ( + "LEVEL".padEnd(COL_LEVEL) + + " " + + "ALIAS".padEnd(COL_ALIAS) + + " " + + "SHORT ID".padEnd(COL_SHORT_ID) + + " " + + "COUNT".padStart(COL_COUNT) + + " " + + "SEEN".padEnd(COL_SEEN) + + " " + + "TITLE" + ); + } return ( "LEVEL".padEnd(COL_LEVEL) + " " + @@ -336,18 +367,20 @@ function wrapTitle(text: string, startCol: number, termWidth: number): string { export type FormatShortIdOptions = { /** Project slug to determine the prefix for suffix highlighting */ projectSlug?: string; - /** Project alias (e.g., "e", "w", "s") for multi-project display */ + /** Project alias (e.g., "e", "w", "o1:d") for multi-project display */ projectAlias?: string; /** Common prefix that was stripped to compute the alias (e.g., "spotlight-") */ strippedPrefix?: string; + /** Whether in multi-project mode (shows ALIAS column) */ + isMultiProject?: boolean; }; /** * Format a short ID with the unique suffix highlighted with underline. * * Single project mode: "CRAFT-G" → "CRAFT-_G_" (suffix underlined) - * Multi-project mode: "SPOTLIGHT-WEBSITE-2A" with alias "w" and strippedPrefix "spotlight-" - * → "SPOTLIGHT-_W_EBSITE-_2A_" (alias char in remainder and suffix underlined) + * Multi-project mode: "DASHBOARD-A3" → "DASHBOARD-_A3_" (just suffix underlined, + * alias is shown in separate ALIAS column) * * @param shortId - Full short ID (e.g., "CRAFT-G", "SPOTLIGHT-WEBSITE-A3") * @param options - Formatting options (projectSlug, projectAlias, strippedPrefix) @@ -361,7 +394,7 @@ export function formatShortId( const opts: FormatShortIdOptions = typeof options === "string" ? { projectSlug: options } : (options ?? {}); - const { projectSlug, projectAlias, strippedPrefix } = opts; + const { projectSlug } = opts; // Extract suffix from shortId (the part after PROJECT-) const upperShortId = shortId.toUpperCase(); @@ -373,30 +406,7 @@ export function formatShortId( } } - // Multi-project mode: highlight alias position and suffix - if (projectAlias && projectSlug) { - const upperSlug = projectSlug.toUpperCase(); - const aliasLen = projectAlias.length; - - // Find where the alias corresponds to in the project slug - // If strippedPrefix exists, the alias is from the remainder after stripping - const strippedLen = strippedPrefix?.length ?? 0; - const aliasStartInSlug = Math.min(strippedLen, upperSlug.length); - - // Build the formatted output: PROJECT-SLUG with alias part underlined, then -SUFFIX underlined - // e.g., "SPOTLIGHT-WEBSITE" with alias "w", strippedPrefix "spotlight-" - // → aliasStartInSlug = 10, so we underline chars 10-11 (the "W") - const beforeAlias = upperSlug.slice(0, aliasStartInSlug); - const aliasChars = upperSlug.slice( - aliasStartInSlug, - aliasStartInSlug + aliasLen - ); - const afterAlias = upperSlug.slice(aliasStartInSlug + aliasLen); - - return `${beforeAlias}${boldUnderline(aliasChars)}${afterAlias}-${boldUnderline(suffix.toUpperCase())}`; - } - - // Single project mode: show full shortId with suffix highlighted + // Show full shortId with suffix highlighted if (projectSlug) { const prefix = `${projectSlug.toUpperCase()}-`; if (upperShortId.startsWith(prefix)) { @@ -415,6 +425,22 @@ function getShortIdDisplayLength(shortId: string): number { return shortId.length; } +/** + * Compute the alias shorthand for an issue (e.g., "o1:d-a3", "w-2a"). + * This is what users type to reference the issue. + * + * @param shortId - Full short ID (e.g., "DASHBOARD-A3") + * @param projectAlias - Project alias (e.g., "o1:d", "w") + * @returns Alias shorthand (e.g., "o1:d-a3", "w-2a") or empty string if no alias + */ +function computeAliasShorthand(shortId: string, projectAlias?: string): string { + if (!projectAlias) { + return ""; + } + const suffix = shortId.split("-").pop()?.toLowerCase() ?? ""; + return `${projectAlias}-${suffix}`; +} + /** * Format a single issue for list display. * Wraps long titles with proper indentation. @@ -428,9 +454,17 @@ export function formatIssueRow( termWidth = 80, shortIdOptions?: FormatShortIdOptions | string ): string { + // Handle legacy string parameter (projectSlug only) + const opts: FormatShortIdOptions = + typeof shortIdOptions === "string" + ? { projectSlug: shortIdOptions } + : (shortIdOptions ?? {}); + + const { isMultiProject, projectAlias } = opts; + const levelText = (issue.level ?? "unknown").toUpperCase().padEnd(COL_LEVEL); const level = levelColor(levelText, issue.level); - const formattedShortId = formatShortId(issue.shortId, shortIdOptions); + const formattedShortId = formatShortId(issue.shortId, opts); // Calculate raw display length (without ANSI codes) for padding const rawLen = getShortIdDisplayLength(issue.shortId); @@ -438,8 +472,19 @@ export function formatIssueRow( const shortId = `${formattedShortId}${shortIdPadding}`; const count = `${issue.count}`.padStart(COL_COUNT); const seen = formatRelativeTime(issue.lastSeen); - const title = wrapTitle(issue.title, TITLE_START_COL, termWidth); + // Multi-project mode: include ALIAS column + if (isMultiProject) { + const aliasShorthand = computeAliasShorthand(issue.shortId, projectAlias); + const aliasPadding = " ".repeat( + Math.max(0, COL_ALIAS - aliasShorthand.length) + ); + const alias = `${aliasShorthand}${aliasPadding}`; + const title = wrapTitle(issue.title, TITLE_START_COL_MULTI, termWidth); + return `${level} ${alias} ${shortId} ${count} ${seen} ${title}`; + } + + const title = wrapTitle(issue.title, TITLE_START_COL, termWidth); return `${level} ${shortId} ${count} ${seen} ${title}`; } diff --git a/test/lib/formatters/human.test.ts b/test/lib/formatters/human.test.ts index b4ce0370c..7ef8b4477 100644 --- a/test/lib/formatters/human.test.ts +++ b/test/lib/formatters/human.test.ts @@ -3,7 +3,12 @@ */ import { describe, expect, test } from "bun:test"; -import { formatShortId } from "../../../src/lib/formatters/human.js"; +import { + formatIssueListHeader, + formatIssueRow, + formatShortId, +} from "../../../src/lib/formatters/human.js"; +import type { SentryIssue } from "../../../src/types/index.js"; // Helper to strip ANSI codes for content testing function stripAnsi(str: string): string { @@ -317,3 +322,86 @@ describe("formatShortId", () => { }); }); }); + +describe("formatIssueListHeader", () => { + test("single project mode does not include ALIAS column", () => { + const header = formatIssueListHeader(false); + expect(header).not.toContain("ALIAS"); + expect(header).toContain("LEVEL"); + expect(header).toContain("SHORT ID"); + expect(header).toContain("COUNT"); + expect(header).toContain("SEEN"); + expect(header).toContain("TITLE"); + }); + + test("multi-project mode includes ALIAS column", () => { + const header = formatIssueListHeader(true); + expect(header).toContain("ALIAS"); + expect(header).toContain("LEVEL"); + expect(header).toContain("SHORT ID"); + expect(header).toContain("COUNT"); + expect(header).toContain("SEEN"); + expect(header).toContain("TITLE"); + }); + + test("ALIAS column comes before SHORT ID in multi-project mode", () => { + const header = formatIssueListHeader(true); + const aliasIndex = header.indexOf("ALIAS"); + const shortIdIndex = header.indexOf("SHORT ID"); + expect(aliasIndex).toBeLessThan(shortIdIndex); + }); +}); + +describe("formatIssueRow", () => { + const mockIssue: SentryIssue = { + id: "123", + shortId: "DASHBOARD-A3", + title: "Test issue", + level: "error", + status: "unresolved", + count: "42", + userCount: 10, + firstSeen: "2024-01-01T00:00:00Z", + lastSeen: "2024-01-02T00:00:00Z", + permalink: "https://sentry.io/issues/123", + }; + + test("single project mode does not include alias column", () => { + const row = formatIssueRow(mockIssue, 80, { + projectSlug: "dashboard", + }); + // Should not have alias shorthand format + expect(stripAnsi(row)).not.toContain("o1:d-a3"); + }); + + test("multi-project mode includes alias column", () => { + const row = formatIssueRow(mockIssue, 120, { + projectSlug: "dashboard", + projectAlias: "o1:d", + isMultiProject: true, + }); + // Should contain the alias shorthand + expect(stripAnsi(row)).toContain("o1:d-a3"); + }); + + test("alias shorthand is lowercase", () => { + const row = formatIssueRow(mockIssue, 120, { + projectSlug: "dashboard", + projectAlias: "o1:d", + isMultiProject: true, + }); + // The alias shorthand should be lowercase + expect(stripAnsi(row)).toContain("o1:d-a3"); + expect(stripAnsi(row)).not.toContain("O1:D-A3"); + }); + + test("unique alias format works in multi-project mode", () => { + const row = formatIssueRow(mockIssue, 120, { + projectSlug: "dashboard", + projectAlias: "d", + isMultiProject: true, + }); + // Should contain simple alias shorthand + expect(stripAnsi(row)).toContain("d-a3"); + }); +}); From d6875b993302ff735a0025b3a4fbb4300fd6618a Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 24 Jan 2026 20:46:31 +0000 Subject: [PATCH 5/5] refactor: remove unused strippedPrefix from public API The strippedPrefix field was no longer used after simplifying formatShortId() to show alias in a dedicated ALIAS column. This removes: - strippedPrefix from FormatShortIdOptions type - strippedPrefix from OrgAwareAliasResult type - References in list.ts and related tests The prefix stripping logic is preserved internally in alias.ts for generating short aliases, but is no longer exposed in the return type. --- src/commands/issue/list.ts | 19 ++++--------------- src/lib/alias.ts | 24 +++++++++++------------- src/lib/formatters/human.ts | 4 +--- test/lib/alias.test.ts | 5 +---- test/lib/formatters/human.test.ts | 19 ++++--------------- 5 files changed, 21 insertions(+), 50 deletions(-) diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index 4fbbbfb81..a362cc684 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -129,8 +129,6 @@ type IssueListResult = { type AliasMapResult = { aliasMap: Map; entries: Record; - /** Common prefix that was stripped from project slugs */ - strippedPrefix: string; }; /** @@ -143,7 +141,7 @@ type AliasMapResult = { * frontend, functions, backend → fr, fu, b * * Cross-org collision example: - * org1:dashboard, org2:dashboard → o1-d, o2-d + * org1:dashboard, org2:dashboard → o1:d, o2:d */ function buildProjectAliasMap(results: IssueListResult[]): AliasMapResult { const entries: Record = {}; @@ -153,7 +151,7 @@ function buildProjectAliasMap(results: IssueListResult[]): AliasMapResult { org: r.target.org, project: r.target.project, })); - const { aliasMap, strippedPrefix } = buildOrgAwareAliases(pairs); + const { aliasMap } = buildOrgAwareAliases(pairs); // Build entries record for storage for (const result of results) { @@ -167,7 +165,7 @@ function buildProjectAliasMap(results: IssueListResult[]): AliasMapResult { } } - return { aliasMap, entries, strippedPrefix }; + return { aliasMap, entries }; } /** @@ -175,29 +173,22 @@ function buildProjectAliasMap(results: IssueListResult[]): AliasMapResult { * * @param results - Issue list results with targets * @param aliasMap - Map from "org:project" to alias - * @param strippedPrefix - Common prefix stripped from project slugs * @param isMultiProject - Whether in multi-project mode (shows ALIAS column) */ function attachFormatOptions( results: IssueListResult[], aliasMap: Map, - strippedPrefix: string, isMultiProject: boolean ): IssueWithOptions[] { return results.flatMap((result) => result.issues.map((issue) => { const key = `${result.target.org}:${result.target.project}`; const alias = aliasMap.get(key); - // Only pass strippedPrefix if this project actually has that prefix - // (e.g., "spotlight" doesn't have "spotlight-" prefix, but "spotlight-electron" does) - const hasPrefix = - strippedPrefix && result.target.project.startsWith(strippedPrefix); return { issue, formatOptions: { projectSlug: result.target.project, projectAlias: alias, - strippedPrefix: hasPrefix ? strippedPrefix : undefined, isMultiProject, }, }; @@ -366,12 +357,11 @@ export const listCommand = buildCommand({ const firstTarget = validResults[0]?.target; // Build project alias map and cache it for multi-project mode - const { aliasMap, entries, strippedPrefix } = isMultiProject + const { aliasMap, entries } = isMultiProject ? buildProjectAliasMap(validResults) : { aliasMap: new Map(), entries: {}, - strippedPrefix: "", }; if (isMultiProject) { @@ -385,7 +375,6 @@ export const listCommand = buildCommand({ const issuesWithOptions = attachFormatOptions( validResults, aliasMap, - strippedPrefix, isMultiProject ); diff --git a/src/lib/alias.ts b/src/lib/alias.ts index d12b40ea6..20f74306c 100644 --- a/src/lib/alias.ts +++ b/src/lib/alias.ts @@ -120,8 +120,6 @@ export type OrgProjectPair = { export type OrgAwareAliasResult = { /** Map from "org:project" key to alias string */ aliasMap: Map; - /** Common prefix stripped from project slugs (only for non-colliding projects) */ - strippedPrefix: string; }; /** Internal: Groups pairs by project slug and identifies collisions */ @@ -155,14 +153,16 @@ function processUniqueSlugs( pairs: OrgProjectPair[], uniqueSlugs: Set, aliasMap: Map -): string { +): void { const uniqueProjects = pairs.filter((p) => uniqueSlugs.has(p.project)); const uniqueProjectSlugs = [...new Set(uniqueProjects.map((p) => p.project))]; if (uniqueProjectSlugs.length === 0) { - return ""; + return; } + // Strip common word prefix for cleaner aliases (e.g., "spotlight-" from + // "spotlight-electron", "spotlight-website") const strippedPrefix = findCommonWordPrefix(uniqueProjectSlugs); const slugToRemainder = new Map(); @@ -180,8 +180,6 @@ function processUniqueSlugs( uniquePrefixes.get(remainder) ?? remainder.charAt(0).toLowerCase(); aliasMap.set(`${org}:${project}`, alias); } - - return strippedPrefix; } /** Internal: Processes colliding project slugs that need org prefixes */ @@ -227,13 +225,13 @@ function processCollidingSlugs( * Build aliases for org/project pairs, handling cross-org slug collisions. * * - Unique project slugs → shortest unique prefix of project slug - * - Colliding slugs (same project in multiple orgs) → "{orgPrefix}-{projectPrefix}" + * - Colliding slugs (same project in multiple orgs) → "{orgPrefix}:{projectPrefix}" * * Common word prefixes (like "spotlight-" in "spotlight-electron") are stripped * before computing project prefixes to keep aliases short. * * @param pairs - Array of org/project pairs to generate aliases for - * @returns Map from "org:project" key to alias string, plus any stripped prefix + * @returns Map from "org:project" key to alias string * * @example * // No collision - same as existing behavior @@ -241,7 +239,7 @@ function processCollidingSlugs( * { org: "acme", project: "frontend" }, * { org: "acme", project: "backend" } * ]) - * // { aliasMap: Map { "acme:frontend" => "f", "acme:backend" => "b" }, strippedPrefix: "" } + * // { aliasMap: Map { "acme:frontend" => "f", "acme:backend" => "b" } } * * @example * // Collision: same project slug in different orgs @@ -249,7 +247,7 @@ function processCollidingSlugs( * { org: "org1", project: "dashboard" }, * { org: "org2", project: "dashboard" } * ]) - * // { aliasMap: Map { "org1:dashboard" => "o1-d", "org2:dashboard" => "o2-d" }, strippedPrefix: "" } + * // { aliasMap: Map { "org1:dashboard" => "o1:d", "org2:dashboard" => "o2:d" } } */ export function buildOrgAwareAliases( pairs: OrgProjectPair[] @@ -257,17 +255,17 @@ export function buildOrgAwareAliases( const aliasMap = new Map(); if (pairs.length === 0) { - return { aliasMap, strippedPrefix: "" }; + return { aliasMap }; } const { projectToOrgs, collidingSlugs, uniqueSlugs } = groupByProjectSlug(pairs); - const strippedPrefix = processUniqueSlugs(pairs, uniqueSlugs, aliasMap); + processUniqueSlugs(pairs, uniqueSlugs, aliasMap); if (collidingSlugs.size > 0) { processCollidingSlugs(projectToOrgs, collidingSlugs, aliasMap); } - return { aliasMap, strippedPrefix }; + return { aliasMap }; } diff --git a/src/lib/formatters/human.ts b/src/lib/formatters/human.ts index 670b811cd..910b72b3d 100644 --- a/src/lib/formatters/human.ts +++ b/src/lib/formatters/human.ts @@ -369,8 +369,6 @@ export type FormatShortIdOptions = { projectSlug?: string; /** Project alias (e.g., "e", "w", "o1:d") for multi-project display */ projectAlias?: string; - /** Common prefix that was stripped to compute the alias (e.g., "spotlight-") */ - strippedPrefix?: string; /** Whether in multi-project mode (shows ALIAS column) */ isMultiProject?: boolean; }; @@ -383,7 +381,7 @@ export type FormatShortIdOptions = { * alias is shown in separate ALIAS column) * * @param shortId - Full short ID (e.g., "CRAFT-G", "SPOTLIGHT-WEBSITE-A3") - * @param options - Formatting options (projectSlug, projectAlias, strippedPrefix) + * @param options - Formatting options (projectSlug, projectAlias, isMultiProject) * @returns Formatted short ID with underline highlights */ export function formatShortId( diff --git a/test/lib/alias.test.ts b/test/lib/alias.test.ts index 97f9d7f1e..840ad0145 100644 --- a/test/lib/alias.test.ts +++ b/test/lib/alias.test.ts @@ -160,7 +160,6 @@ describe("buildOrgAwareAliases", () => { test("returns empty map for empty input", () => { const result = buildOrgAwareAliases([]); expect(result.aliasMap.size).toBe(0); - expect(result.strippedPrefix).toBe(""); }); test("single org multiple projects - no collision", () => { @@ -170,7 +169,6 @@ describe("buildOrgAwareAliases", () => { ]); expect(result.aliasMap.get("acme:frontend")).toBe("f"); expect(result.aliasMap.get("acme:backend")).toBe("b"); - expect(result.strippedPrefix).toBe(""); }); test("multiple orgs with unique project slugs - no collision", () => { @@ -180,7 +178,6 @@ describe("buildOrgAwareAliases", () => { ]); expect(result.aliasMap.get("org1:frontend")).toBe("f"); expect(result.aliasMap.get("org2:backend")).toBe("b"); - expect(result.strippedPrefix).toBe(""); }); test("same project slug in different orgs - collision", () => { @@ -242,9 +239,9 @@ describe("buildOrgAwareAliases", () => { { org: "acme", project: "spotlight-electron" }, { org: "acme", project: "spotlight-website" }, ]); + // Common prefix "spotlight-" is stripped internally, resulting in short aliases expect(result.aliasMap.get("acme:spotlight-electron")).toBe("e"); expect(result.aliasMap.get("acme:spotlight-website")).toBe("w"); - expect(result.strippedPrefix).toBe("spotlight-"); }); test("handles single project", () => { diff --git a/test/lib/formatters/human.test.ts b/test/lib/formatters/human.test.ts index 7ef8b4477..09fe3bda1 100644 --- a/test/lib/formatters/human.test.ts +++ b/test/lib/formatters/human.test.ts @@ -83,29 +83,26 @@ describe("formatShortId", () => { expect(stripAnsi(result)).toBe("FRONTEND-G"); }); - test("formats spotlight-website with stripped prefix", () => { + test("formats spotlight-website in multi-project mode", () => { const result = formatShortId("SPOTLIGHT-WEBSITE-2A", { projectSlug: "spotlight-website", projectAlias: "w", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-WEBSITE-2A"); }); - test("formats spotlight-electron with stripped prefix", () => { + test("formats spotlight-electron in multi-project mode", () => { const result = formatShortId("SPOTLIGHT-ELECTRON-4Y", { projectSlug: "spotlight-electron", projectAlias: "e", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-ELECTRON-4Y"); }); - test("formats spotlight (no stripped prefix applies) correctly", () => { + test("formats spotlight in multi-project mode", () => { const result = formatShortId("SPOTLIGHT-73", { projectSlug: "spotlight", projectAlias: "s", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-73"); }); @@ -122,7 +119,6 @@ describe("formatShortId", () => { const result = formatShortId("spotlight-website-2a", { projectSlug: "spotlight-website", projectAlias: "w", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-WEBSITE-2A"); }); @@ -152,7 +148,6 @@ describe("formatShortId", () => { const result = formatShortId("spotlight-website-2a", { projectSlug: "spotlight-website", projectAlias: "w", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-WEBSITE-2A"); }); @@ -196,7 +191,6 @@ describe("formatShortId", () => { const formatted = formatShortId(shortId, { projectSlug: "spotlight-website", projectAlias: "w", - strippedPrefix: "spotlight-", }); expect(stripAnsi(formatted).length).toBe(shortId.length); }); @@ -221,7 +215,6 @@ describe("formatShortId", () => { const formatted = formatShortId(shortId, { projectSlug: "spotlight-electron", projectAlias: "e", - strippedPrefix: "spotlight-", }); expect(stripAnsi(formatted).length).toBe(shortId.length); }); @@ -232,7 +225,6 @@ describe("formatShortId", () => { const result = formatShortId("SPOTLIGHT-ELECTRON-4Y", { projectSlug: "spotlight-electron", projectAlias: "e", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-ELECTRON-4Y"); }); @@ -241,7 +233,6 @@ describe("formatShortId", () => { const result = formatShortId("SPOTLIGHT-WEBSITE-2C", { projectSlug: "spotlight-website", projectAlias: "w", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-WEBSITE-2C"); }); @@ -250,7 +241,6 @@ describe("formatShortId", () => { const result = formatShortId("SPOTLIGHT-73", { projectSlug: "spotlight", projectAlias: "s", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-73"); }); @@ -302,11 +292,10 @@ describe("formatShortId", () => { } }); - test("multi-project mode applies formatting to alias and suffix", () => { + test("multi-project mode applies formatting to suffix", () => { const result = formatShortId("SPOTLIGHT-ELECTRON-4Y", { projectSlug: "spotlight-electron", projectAlias: "e", - strippedPrefix: "spotlight-", }); expect(stripAnsi(result)).toBe("SPOTLIGHT-ELECTRON-4Y"); if (colorsEnabled) {