Merge pull request #1015 from kaitranntt/dev

chore: promote dev to main
This commit is contained in:
Kai (Tam Nhu) Tran
2026-04-14 18:17:50 -04:00
committed by GitHub
11 changed files with 330 additions and 60 deletions
+9 -1
View File
@@ -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 }}
+12 -8
View File
@@ -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]
+1 -1
View File
@@ -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",
@@ -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');
@@ -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<string, number>,
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<unknown>,
audienceCounts: Map<string, number>
) {
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<string, number>());
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]
)}
</span>
))
@@ -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 (
<Tooltip key={variant.id}>
@@ -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)}
</span>
)}
{resolvedCompactDetailLabel && (
<span
title={resolvedDetailLabel ?? resolvedCompactDetailLabel}
className={cn(
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0',
getCompactDetailBadgeClass(identity.audience)
)}
>
{resolvedCompactDetailLabel}
</span>
)}
{paused && (
<span className="text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0 bg-amber-500/15 text-amber-700 dark:bg-amber-500/25 dark:text-amber-300">
{/* TODO i18n: missing key for compact "Paused" badge */}
@@ -203,9 +238,9 @@ export function AccountSurfaceCard({
{identity.audienceLabel}
</Badge>
)}
{!isCompact && identity.detailLabel && (
{!isCompact && resolvedDetailLabel && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
{identity.detailLabel}
{resolvedDetailLabel}
</Badge>
)}
{!isCompact && isDefault && (
+8 -5
View File
@@ -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',
+6 -2
View File
@@ -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);
});
@@ -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<typeof useAccountQuotas>);
});
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(
<AccountCard
account={groupedAccount}
@@ -130,7 +148,12 @@ describe('AccountCard grouped quota tooltip', () => {
/>
);
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(
<AccountCard
account={groupedAccountWithProPersonal}
zone="left"
originalIndex={0}
isHovered={false}
isDragging={false}
offset={{ x: 0, y: 0 }}
showDetails={false}
privacyMode={false}
onMouseEnter={() => 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();
});
});
@@ -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<GeminiCliQuotaResult> = {}
@@ -29,6 +29,16 @@ function createGeminiQuotaResult(
};
}
function createCodexQuotaResult(overrides: Partial<CodexQuotaResult> = {}): 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(
<AccountSurfaceCard
mode="compact"
provider="codex"
accountId="user@example.com#free"
email="user@example.com"
displayEmail="user@example.com"
tokenFile="codex-user@example.com-free.json"
showQuota={false}
/>
);
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(
<AccountSurfaceCard
mode="compact"
provider="codex"
accountId="user@example.com#pro"
email="user@example.com"
displayEmail="user@example.com"
tokenFile="codex-user@example.com-pro.json"
quota={createCodexQuotaResult({ planType: 'free' })}
showQuota={false}
/>
);
expect(screen.getByText('Pro')).toBeInTheDocument();
expect(screen.queryByText('Free')).not.toBeInTheDocument();
});
});
@@ -16,7 +16,7 @@ function makeAccount(overrides: Partial<OAuthAccount> & Pick<OAuthAccount, 'id'
}
describe('buildAccountVisualGroups', () => {
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',
]);
});
});