mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-21 00:20:47 +00:00
Merge pull request #1015 from kaitranntt/dev
chore: promote dev to main
This commit is contained in:
@@ -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
@@ -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
@@ -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 && (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user