diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 1f482bad..40e84de8 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -93,7 +93,6 @@ jobs: pull-requests: write contents: read env: - GITHUB_TOKEN: ${{ github.token }} OPENAI_KEY: ${{ secrets.AI_REVIEW_API_KEY }} "OPENAI.API_BASE": ${{ vars.AI_REVIEW_BASE_URL }} "config.model": ${{ vars.AI_REVIEW_MODEL }} @@ -102,5 +101,14 @@ jobs: "github_action_config.auto_improve": "false" steps: + - name: Generate App Token + id: pr-agent-app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CCS_REVIEWER_APP_ID }} + private-key: ${{ secrets.CCS_REVIEWER_PRIVATE_KEY }} + - name: Run PR-Agent uses: qodo-ai/pr-agent@v0.34 + env: + GITHUB_TOKEN: ${{ steps.pr-agent-app-token.outputs.token }} diff --git a/.pr_agent.toml b/.pr_agent.toml index f8fd93ea..8ab3f191 100644 --- a/.pr_agent.toml +++ b/.pr_agent.toml @@ -10,23 +10,27 @@ large_patch_policy = "clip" temperature = 0.1 [pr_reviewer] +require_score_review = true require_tests_review = true -require_estimate_effort_to_review = false +require_estimate_effort_to_review = true +require_can_be_split_review = true require_security_review = true -require_ticket_analysis_review = false +require_todo_scan = true +require_ticket_analysis_review = true publish_output_no_suggestions = true persistent_comment = true -num_max_findings = 6 -final_update_message = false +num_max_findings = 10 +final_update_message = true enable_review_labels_security = false enable_review_labels_effort = false -enable_intro_text = false -enable_help_text = false +enable_intro_text = true +enable_help_text = true extra_instructions = """\ Focus on correctness, security, regressions, and missing verification. Read the full diff before reporting findings, and inspect surrounding code before claiming a bug. -Prefer a short list of confirmed issues over speculative commentary. -Do not pad the review with praise, scorecards, effort estimates, or generic best-practice advice. +Be thorough across the enabled review sections, even when no major defects are found. +When a change looks safe, explain why it appears safe and call out any residual assumptions or manual verification gaps. +Prefer substantive analysis over generic praise or filler, and keep every claim evidence-based. CCS-specific checks: - CLI output in src/ must stay ASCII-only: [OK], [!], [X], [i] diff --git a/package.json b/package.json index bcbddadb..9c056233 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.70.1", + "version": "7.70.1-dev.3", "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more", "keywords": [ "cli", diff --git a/tests/unit/scripts/github/ai-review-workflow.test.ts b/tests/unit/scripts/github/ai-review-workflow.test.ts index 6e1c8573..55d3efb3 100644 --- a/tests/unit/scripts/github/ai-review-workflow.test.ts +++ b/tests/unit/scripts/github/ai-review-workflow.test.ts @@ -16,11 +16,12 @@ describe('PR-Agent review lane migration', () => { const workflow = fs.readFileSync(workflowPath, 'utf8'); const config = fs.readFileSync(prAgentConfigPath, 'utf8'); + const appTokenUsages = workflow.match(/uses: actions\/create-github-app-token@v1/g) ?? []; expect(workflow).toContain('name: AI Code Review'); expect(workflow).toContain('runs-on: [self-hosted, cliproxy]'); expect(workflow).toContain('uses: qodo-ai/pr-agent'); - expect(workflow).toContain('uses: actions/create-github-app-token@v1'); + expect(appTokenUsages).toHaveLength(2); expect(workflow).toContain('OPENAI.API_BASE'); expect(workflow).toContain('OPENAI_KEY'); expect(workflow).toContain('vars.AI_REVIEW_BASE_URL'); @@ -34,12 +35,24 @@ describe('PR-Agent review lane migration', () => { expect(workflow).toContain("format('dispatch-{0}', github.run_id)"); expect(workflow).toContain('CCS_REVIEWER_APP_ID'); expect(workflow).toContain('CCS_REVIEWER_PRIVATE_KEY'); + expect(workflow).toContain('id: pr-agent-app-token'); + expect(workflow).toContain('GITHUB_TOKEN: ${{ steps.pr-agent-app-token.outputs.token }}'); + expect(workflow).not.toContain('GITHUB_TOKEN: ${{ github.token }}'); expect(workflow).not.toContain('uses: anthropics/claude-code-action@v1'); expect(config).toContain('[config]'); expect(config).toContain('git_provider = "github"'); expect(config).toContain('fallback_models = ["gpt-5.4-mini"]'); expect(config).toContain('custom_model_max_tokens = 131072'); + expect(config).toContain('require_score_review = true'); + expect(config).toContain('require_estimate_effort_to_review = true'); + expect(config).toContain('require_can_be_split_review = true'); + expect(config).toContain('require_todo_scan = true'); + expect(config).toContain('require_ticket_analysis_review = true'); + expect(config).toContain('num_max_findings = 10'); + expect(config).toContain('final_update_message = true'); + expect(config).toContain('enable_intro_text = true'); + expect(config).toContain('enable_help_text = true'); expect(config).toContain('[pr_reviewer]'); expect(config).not.toContain('auto_review = true'); expect(config).not.toContain('claude-code-action'); diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx index 52291e11..e1ecf92f 100644 --- a/ui/src/components/account/flow-viz/account-card.tsx +++ b/ui/src/components/account/flow-viz/account-card.tsx @@ -13,6 +13,7 @@ import { getProviderResetTime, getQuotaFailureInfo, } from '@/lib/utils'; +import { formatAccountVariantPart } from '@/lib/account-identity'; import { GripVertical, Loader2, Pause, Play } from 'lucide-react'; import { useAccountQuota, @@ -87,29 +88,110 @@ function getCompactQuotaColor(percentage: number) { return 'bg-red-500'; } -function getVariantMarkerLabel(audience: string, fallbackLabel?: string | null) { - if (audience === 'business') return 'Biz'; - if (audience === 'personal') return 'Pers'; +function getCodexPlanDetailLabel(quota: unknown): string | null { + if (!quota || typeof quota !== 'object' || !('planType' in quota)) { + return null; + } - const normalizedFallback = fallbackLabel?.trim(); + const planType = (quota as { planType?: unknown }).planType; + if (typeof planType !== 'string' || planType.trim().length === 0) { + return null; + } + + return formatAccountVariantPart(planType); +} + +function getVariantDetailLabel( + variant: { + audience: string; + detailLabel?: string | null; + compactDetailLabel?: string | null; + }, + quota?: unknown +) { + const codexPlanDetail = getCodexPlanDetailLabel(quota); + return variant.detailLabel ?? variant.compactDetailLabel ?? codexPlanDetail; +} + +function getVariantCompactDetailLabel( + variant: { + audience: string; + compactDetailLabel?: string | null; + detailLabel?: string | null; + }, + quota?: unknown +) { + const codexPlanDetail = getCodexPlanDetailLabel(quota); + return variant.compactDetailLabel ?? variant.detailLabel ?? codexPlanDetail; +} + +function getVariantInlineLabel( + variant: { + audience: string; + audienceLabel?: string | null; + detailLabel?: string | null; + compactDetailLabel?: string | null; + inlineLabel?: string | null; + }, + quota?: unknown +) { + const detailLabel = getVariantDetailLabel(variant, quota); + const composedLabel = [variant.audienceLabel, detailLabel].filter(Boolean).join(' · '); + return composedLabel || variant.inlineLabel || null; +} + +function getVariantMarkerLabel( + variant: { + audience: string; + audienceLabel?: string | null; + detailLabel?: string | null; + compactDetailLabel?: string | null; + }, + audienceCounts: Map, + quota?: unknown +) { + const compactDetailLabel = getVariantCompactDetailLabel(variant, quota); + if (variant.audience === 'business') { + const businessVariantCount = audienceCounts.get('business') ?? 0; + return businessVariantCount > 1 && compactDetailLabel ? compactDetailLabel : 'Biz'; + } + if (variant.audience === 'personal') { + return compactDetailLabel ?? 'Pers'; + } + + const normalizedFallback = + compactDetailLabel?.trim() || variant.audienceLabel?.trim() || variant.detailLabel?.trim(); return normalizedFallback?.[0]?.toUpperCase() ?? '?'; } function getGroupedVariantSummaryLabel( - variants: Array<{ audience: string; audienceLabel?: string | null; detailLabel?: string | null }> + variants: Array<{ + audience: string; + audienceLabel?: string | null; + detailLabel?: string | null; + compactDetailLabel?: string | null; + }>, + quotas: Array, + audienceCounts: Map ) { const audiences = new Set(variants.map((variant) => variant.audience)); + const hasDistinctDetails = variants.some((variant, index) => + Boolean(getVariantCompactDetailLabel(variant, quotas[index])) + ); - if (audiences.size === 2 && audiences.has('business') && audiences.has('personal')) { + if ( + !hasDistinctDetails && + variants.length === 2 && + audiences.size === 2 && + audiences.has('business') && + audiences.has('personal') + ) { return 'B|P'; } if (variants.length === 1) { const [variant] = variants; - return getVariantMarkerLabel( - variant.audience, - variant.audienceLabel ?? variant.detailLabel ?? null - ); + return getVariantMarkerLabel(variant, audienceCounts, quotas[0]); } return null; @@ -153,20 +235,19 @@ export function AccountCard({ })), showQuota && hasGroupedVariants ); - const groupedHeaderVariants = hasGroupedVariants - ? Array.from( - new Map( - (account.variants ?? []) - .slice() - .sort((left, right) => { - const order = { business: 0, personal: 1, unknown: 2 } as const; - return order[left.audience] - order[right.audience]; - }) - .map((variant) => [variant.audienceLabel ?? variant.detailLabel ?? variant.id, variant]) - ).values() - ) - : []; - const groupedVariantSummaryLabel = getGroupedVariantSummaryLabel(groupedHeaderVariants); + const groupedHeaderVariants = hasGroupedVariants ? (account.variants ?? []) : []; + const groupedVariantQuotas = groupedHeaderVariants.map( + (_, index) => variantQuotaQueries[index]?.data + ); + const groupedVariantAudienceCounts = groupedHeaderVariants.reduce((counts, variant) => { + counts.set(variant.audience, (counts.get(variant.audience) ?? 0) + 1); + return counts; + }, new Map()); + const groupedVariantSummaryLabel = getGroupedVariantSummaryLabel( + groupedHeaderVariants, + groupedVariantQuotas, + groupedVariantAudienceCounts + ); const compactMetaBadges = hasGroupedVariants ? ( <> @@ -174,8 +255,9 @@ export function AccountCard({ className="inline-flex shrink-0 items-center overflow-hidden rounded-md border border-border/60 bg-muted/60 shadow-sm shadow-black/5 dark:bg-zinc-900/80" title={groupedHeaderVariants .map( - (variant) => - variant.audienceLabel ?? variant.detailLabel ?? t('accountSurfaceCard.variant') + (variant, index) => + getVariantInlineLabel(variant, groupedVariantQuotas[index]) ?? + t('accountSurfaceCard.variant') ) .join(' • ')} > @@ -198,8 +280,9 @@ export function AccountCard({ )} > {getVariantMarkerLabel( - variant.audience, - variant.audienceLabel ?? variant.detailLabel ?? null + variant, + groupedVariantAudienceCounts, + groupedVariantQuotas[index] )} )) @@ -224,7 +307,7 @@ export function AccountCard({ const quotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null; const quotaValue = quotaLabel !== null ? Number(quotaLabel) : null; const failureInfo = getQuotaFailureInfo(quota); - const label = variant.audienceLabel ?? variant.detailLabel ?? cleanEmail(variant.email); + const label = getVariantInlineLabel(variant, quota) ?? cleanEmail(variant.email); return ( diff --git a/ui/src/components/account/shared/account-surface-card.tsx b/ui/src/components/account/shared/account-surface-card.tsx index 29391d05..aec49882 100644 --- a/ui/src/components/account/shared/account-surface-card.tsx +++ b/ui/src/components/account/shared/account-surface-card.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'; import { Badge } from '@/components/ui/badge'; import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import type { UnifiedQuotaResult } from '@/hooks/use-cliproxy-stats'; -import { getAccountIdentityPresentation } from '@/lib/account-identity'; +import { formatAccountVariantPart, getAccountIdentityPresentation } from '@/lib/account-identity'; import { cn } from '@/lib/utils'; import { Pause, Star, User } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -63,6 +63,18 @@ function getCompactAudienceBadgeLabel( return '?'; } +function getCompactDetailBadgeClass(audience: 'business' | 'personal' | 'unknown') { + if (audience === 'business') { + return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:border-sky-400/30 dark:bg-sky-500/15 dark:text-sky-200'; + } + + if (audience === 'personal') { + return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:border-emerald-400/30 dark:bg-emerald-500/15 dark:text-emerald-200'; + } + + return 'border-border/60 bg-muted/60 text-muted-foreground'; +} + function resolveEffectiveTier( tier: AccountTier | undefined, quota: UnifiedQuotaResult | undefined @@ -76,6 +88,14 @@ function resolveEffectiveTier( return tier; } +function getCodexPlanDetailLabel(quota: UnifiedQuotaResult | undefined): string | null { + if (!quota || !('planType' in quota) || !quota.planType) { + return null; + } + + return formatAccountVariantPart(quota.planType); +} + export function AccountSurfaceCard({ mode, provider, @@ -104,6 +124,10 @@ export function AccountSurfaceCard({ const title = displayEmail || identity.email || accountId; const normalizedProvider = provider.toLowerCase(); const effectiveTier = resolveEffectiveTier(tier, quota); + const codexPlanDetailLabel = + normalizedProvider === 'codex' ? getCodexPlanDetailLabel(quota) : null; + const resolvedDetailLabel = identity.detailLabel ?? codexPlanDetailLabel; + const resolvedCompactDetailLabel = identity.compactDetailLabel ?? codexPlanDetailLabel; const showTierBadge = (normalizedProvider === 'agy' || normalizedProvider === 'antigravity' || @@ -137,6 +161,17 @@ export function AccountSurfaceCard({ {getCompactAudienceBadgeLabel(identity.audience, t)} )} + {resolvedCompactDetailLabel && ( + + {resolvedCompactDetailLabel} + + )} {paused && ( {/* TODO i18n: missing key for compact "Paused" badge */} @@ -203,9 +238,9 @@ export function AccountSurfaceCard({ {identity.audienceLabel} )} - {!isCompact && identity.detailLabel && ( + {!isCompact && resolvedDetailLabel && ( - {identity.detailLabel} + {resolvedDetailLabel} )} {!isCompact && isDefault && ( diff --git a/ui/src/lib/account-identity.ts b/ui/src/lib/account-identity.ts index b001fd02..7b1236a2 100644 --- a/ui/src/lib/account-identity.ts +++ b/ui/src/lib/account-identity.ts @@ -24,7 +24,7 @@ function normalizeVariantTokenPart(value: string): string { .toLowerCase(); } -function formatVariantPart(part: string): string { +export function formatAccountVariantPart(part: string): string { const normalized = part.trim().toLowerCase(); if (!normalized) { return ''; @@ -103,7 +103,7 @@ function formatWorkspaceLabel(parts: string[]): { }; } - const extraLabel = parts.map(formatVariantPart).filter(Boolean).join(' · '); + const extraLabel = parts.map(formatAccountVariantPart).filter(Boolean).join(' · '); return { detailLabel: extraLabel || 'Team', // TODO i18n: missing key for team fallback compactDetailLabel: extraLabel || 'Team', @@ -167,10 +167,13 @@ export function getAccountIdentityPresentation( } if (suffix && PERSONAL_PLAN_PARTS.has(suffix)) { - const detailParts = [formatVariantPart(suffix), ...parts.slice(0, -1).map(formatVariantPart)] + const detailParts = [ + formatAccountVariantPart(suffix), + ...parts.slice(0, -1).map(formatAccountVariantPart), + ] .filter(Boolean) .join(' · '); - const detailLabel = detailParts || formatVariantPart(suffix); + const detailLabel = detailParts || formatAccountVariantPart(suffix); const inlineLabel = ['Personal', detailLabel].filter(Boolean).join(' · '); // TODO i18n: missing key for Personal return { email: resolvedEmail, @@ -182,7 +185,7 @@ export function getAccountIdentityPresentation( }; } - const fallbackLabel = parts.map(formatVariantPart).filter(Boolean).join(' · '); + const fallbackLabel = parts.map(formatAccountVariantPart).filter(Boolean).join(' · '); return { email: resolvedEmail, audience: 'unknown', diff --git a/ui/src/lib/account-visual-groups.ts b/ui/src/lib/account-visual-groups.ts index 7d9a1ec7..259b0634 100644 --- a/ui/src/lib/account-visual-groups.ts +++ b/ui/src/lib/account-visual-groups.ts @@ -16,6 +16,8 @@ export interface AccountVisualVariant { audience: AccountAudience; audienceLabel: string | null; detailLabel: string | null; + compactDetailLabel: string | null; + inlineLabel: string | null; } export interface AccountVisualGroup { @@ -67,6 +69,8 @@ function buildAccountVariant( audience: identity.audience, audienceLabel: identity.audienceLabel, detailLabel: identity.detailLabel, + compactDetailLabel: identity.compactDetailLabel, + inlineLabel: identity.inlineLabel, }; } @@ -77,8 +81,8 @@ function sortAccountVariants(variants: AccountVisualVariant[]): AccountVisualVar return audienceDelta; } - const leftLabel = left.audienceLabel ?? left.detailLabel ?? left.id; - const rightLabel = right.audienceLabel ?? right.detailLabel ?? right.id; + const leftLabel = left.inlineLabel ?? left.audienceLabel ?? left.detailLabel ?? left.id; + const rightLabel = right.inlineLabel ?? right.audienceLabel ?? right.detailLabel ?? right.id; return leftLabel.localeCompare(rightLabel); }); diff --git a/ui/tests/unit/components/account/flow-viz/account-card.test.tsx b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx index 539e48e3..be9c28d4 100644 --- a/ui/tests/unit/components/account/flow-viz/account-card.test.tsx +++ b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx @@ -20,7 +20,7 @@ vi.mock('@/hooks/use-cliproxy-stats', async () => { const mockedUseAccountQuota = vi.mocked(useAccountQuota); const mockedUseAccountQuotas = vi.mocked(useAccountQuotas); -function makeCodexQuota(planType: 'plus' | 'team', fiveHour: number, weekly: number) { +function makeCodexQuota(planType: 'free' | 'plus' | 'team', fiveHour: number, weekly: number) { return { success: true, planType, @@ -76,7 +76,9 @@ const groupedAccount: AccountData = { failureCount: 0, audience: 'business', audienceLabel: 'Business', - detailLabel: null, + detailLabel: 'Workspace 04a0f049', + compactDetailLabel: '04a0f049', + inlineLabel: 'Business · Workspace 04a0f049', }, { id: 'personal@example.com', @@ -87,11 +89,27 @@ const groupedAccount: AccountData = { failureCount: 1, audience: 'personal', audienceLabel: 'Personal', - detailLabel: null, + detailLabel: 'Free', + compactDetailLabel: 'Free', + inlineLabel: 'Personal · Free', }, ], }; +const groupedAccountWithProPersonal: AccountData = { + ...groupedAccount, + variants: groupedAccount.variants?.map((variant) => + variant.audience === 'personal' + ? { + ...variant, + detailLabel: 'Pro', + compactDetailLabel: 'Pro', + inlineLabel: 'Personal · Pro', + } + : variant + ), +}; + describe('AccountCard grouped quota tooltip', () => { beforeEach(() => { mockedUseAccountQuota.mockReturnValue({ @@ -105,13 +123,13 @@ describe('AccountCard grouped quota tooltip', () => { isLoading: false, }, { - data: makeCodexQuota('plus', 64, 42), + data: makeCodexQuota('free', 64, 42), isLoading: false, }, ] as ReturnType); }); - it('shows provider quota tooltip content for each grouped personal/business row on hover', async () => { + it('keeps grouped Codex account labels distinct and shows quota tooltips for each variant', async () => { render( { /> ); - await userEvent.hover(screen.getByText('Business')); + expect( + screen.getByTitle('Business · Workspace 04a0f049 • Personal · Free') + ).toBeInTheDocument(); + expect(screen.getByText('Biz')).toBeInTheDocument(); + + await userEvent.hover(screen.getByText('Business · Workspace 04a0f049')); const businessPlan = (await screen.findAllByText('Plan: team')).find((node) => node.closest('[data-slot="tooltip-content"]') ); @@ -141,11 +164,35 @@ describe('AccountCard grouped quota tooltip', () => { expect(tooltipContent?.className).toContain('text-popover-foreground'); expect(tooltipContent?.className).toContain('max-w-[calc(100vw-2rem)]'); - await userEvent.hover(screen.getByText('Personal')); - const personalPlan = (await screen.findAllByText('Plan: plus')).find((node) => + await userEvent.hover(screen.getByText('Personal · Free')); + const personalPlan = (await screen.findAllByText('Plan: free')).find((node) => node.closest('[data-slot="tooltip-content"]') ); expect(personalPlan).toBeInTheDocument(); expect(screen.getAllByText('Weekly usage limit').length).toBeGreaterThan(0); }); + + it('keeps richer grouped personal detail when quota planType is coarser runtime evidence', () => { + render( + undefined} + onMouseLeave={() => undefined} + onPointerDown={() => undefined} + onPointerMove={() => undefined} + onPointerUp={() => undefined} + /> + ); + + expect(screen.getByTitle('Business · Workspace 04a0f049 • Personal · Pro')).toBeInTheDocument(); + expect(screen.getByText('Personal · Pro')).toBeInTheDocument(); + expect(screen.queryByText('Personal · Free')).not.toBeInTheDocument(); + }); }); diff --git a/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx b/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx index 6a0a823e..56079208 100644 --- a/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx +++ b/ui/tests/unit/ui/components/account/shared/account-surface-card.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@tests/setup/test-utils'; import { describe, expect, it } from 'vitest'; import { AccountSurfaceCard } from '@/components/account/shared/account-surface-card'; -import type { GeminiCliQuotaResult } from '@/lib/api-client'; +import type { CodexQuotaResult, GeminiCliQuotaResult } from '@/lib/api-client'; function createGeminiQuotaResult( overrides: Partial = {} @@ -29,6 +29,16 @@ function createGeminiQuotaResult( }; } +function createCodexQuotaResult(overrides: Partial = {}): CodexQuotaResult { + return { + success: true, + windows: [], + planType: 'free', + lastUpdated: Date.now(), + ...overrides, + }; +} + describe('AccountSurfaceCard', () => { it('prefers live quota entitlement tier over a stale account tier for Gemini badges', () => { render( @@ -46,4 +56,40 @@ describe('AccountSurfaceCard', () => { expect(screen.getByText('pro')).toBeInTheDocument(); }); + + it('shows both personal identity and free-tier detail for compact Codex cards', () => { + render( + + ); + + expect(screen.getByText('Pers')).toBeInTheDocument(); + expect(screen.getByTitle('Personal')).toBeInTheDocument(); + expect(screen.getByText('Free')).toBeInTheDocument(); + }); + + it('keeps richer token-derived Codex personal detail when live quota planType is coarser', () => { + render( + + ); + + expect(screen.getByText('Pro')).toBeInTheDocument(); + expect(screen.queryByText('Free')).not.toBeInTheDocument(); + }); }); diff --git a/ui/tests/unit/ui/lib/account-visual-groups.test.ts b/ui/tests/unit/ui/lib/account-visual-groups.test.ts index 2c654071..cba2b9c2 100644 --- a/ui/tests/unit/ui/lib/account-visual-groups.test.ts +++ b/ui/tests/unit/ui/lib/account-visual-groups.test.ts @@ -16,7 +16,7 @@ function makeAccount(overrides: Partial & Pick { - it('orders grouped codex variants by audience consistently', () => { + it('preserves grouped codex variant identity details while ordering by audience', () => { const groups = buildAccountVisualGroups([ makeAccount({ id: 'kaidu.kd@gmail.com#free', @@ -33,9 +33,36 @@ describe('buildAccountVisualGroups', () => { 'business', 'personal', ]); + expect(groups[0]?.variants?.map((variant) => variant.inlineLabel)).toEqual([ + 'Business · Workspace 04a0f049', + 'Personal · Free', + ]); + expect(groups[0]?.variants?.map((variant) => variant.compactDetailLabel)).toEqual([ + '04a0f049', + 'Free', + ]); expect(groups[0]?.memberIds).toEqual([ 'kaidu.kd@gmail.com#04a0f049-team', 'kaidu.kd@gmail.com#free', ]); }); + + it('keeps multiple personal codex plans distinct inside the same grouped card', () => { + const groups = buildAccountVisualGroups([ + makeAccount({ + id: 'kaidu.kd@gmail.com#plus', + tokenFile: 'codex-kaidu.kd@gmail.com-plus.json', + }), + makeAccount({ + id: 'kaidu.kd@gmail.com#free', + tokenFile: 'codex-kaidu.kd@gmail.com-free.json', + }), + ]); + + expect(groups).toHaveLength(1); + expect(groups[0]?.variants?.map((variant) => variant.inlineLabel)).toEqual([ + 'Personal · Free', + 'Personal · Plus', + ]); + }); });