Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions src/output/quota-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,29 @@ const COMPACT_BAR_WIDTH = 10;
// at 200% to leave headroom and keep the bar/text readable.
const MAX_DISPLAY_PCT = 200;

// Weekly quota is unlimited when the server reports `current_weekly_status: 3`
// (per the status enum: 1=normal, 2=exhausted, 3=unlimited).
function isUnweekly(status: number | undefined | null): boolean {
return status === 3;
// Server-side status enum: 1=normal (limited), 2=exhausted, 3=unlimited.
//
// Caveat: when `total_count === 0 && status === 3` the model is not in the
// user's plan at all (the API conflates "no bucket" with "unlimited").
// Callers must check `isNotInPlan` first; only treat status=3 as truly
// unlimited when there is a real allowance bucket (total > 0).
Comment on lines +83 to +86
export function isUnweekly(
status: number | undefined | null,
totalCount: number,
): boolean {
return status === 3 && totalCount > 0;
}
Comment on lines +87 to +92

// Detect models whose status=3 actually means "not in your plan" rather
// than unlimited. The signal is `total_count === 0 && status === 3`:
// the server has no allowance bucket (count is zero) but still reports
// status 3, which would otherwise render as "100% unlimited" — the very
// inconsistency reported in #173.
export function isNotInPlan(
totalCount: number,
status: number | undefined | null,
): boolean {
return totalCount === 0 && status === 3;
}
Comment on lines +99 to 104

function clampPct(value: number): number {
Expand Down Expand Up @@ -124,6 +143,8 @@ function renderBar(remainingPct: number, color: boolean, barWidth: number = BAR_
const UNLIMITED_SYMBOL = '∞';
const UNLIMITED_LABEL_CN = '无限';
const UNLIMITED_LABEL_EN = 'unlimited';
const NOT_IN_PLAN_LABEL_CN = '不在套餐中';
const NOT_IN_PLAN_LABEL_EN = 'not in plan';

function renderMetric(
label: string,
Expand All @@ -134,7 +155,18 @@ function renderMetric(
boostPermille?: number | null,
unlimited?: boolean,
unlimitedLabel?: string,
notInPlan?: boolean,
notInPlanLabel?: string,
): string {
Comment on lines 149 to 160
if (notInPlan) {
const nip = notInPlanLabel ?? NOT_IN_PLAN_LABEL_EN;
if (color) {
const bar = `${BG_EMPTY}${' '.repeat(COMPACT_BAR_WIDTH)}${R}`;
return `${D}${label}${R} ${bar} ${D}${nip}${R}`;
}
const bar = `[${'.'.repeat(COMPACT_BAR_WIDTH)}]`;
return `${label} ${bar} ${nip}`;
}
if (unlimited) {
const ul = unlimitedLabel ?? UNLIMITED_SYMBOL;
const ulStr = ul.padStart(4);
Expand Down Expand Up @@ -169,12 +201,20 @@ export function renderQuotaTable(models: QuotaModelRemain[], config: Config): vo

const rows = models.map((m) => {
const displayName = displayModelName(m.model_name, config.region);
const notInPlanLabel = config.region === 'cn' ? NOT_IN_PLAN_LABEL_CN : NOT_IN_PLAN_LABEL_EN;
const intervalNotInPlan = isNotInPlan(m.current_interval_total_count, m.current_interval_status);
const weeklyNotInPlan = isNotInPlan(m.current_weekly_total_count, m.current_weekly_status);
const current = renderMetric(
L.current,
m.current_interval_usage_count,
m.current_interval_total_count,
m.current_interval_remaining_percent,
useColor,
undefined,
false,
undefined,
intervalNotInPlan,
notInPlanLabel,
);
const weekly = renderMetric(
L.weekly,
Expand All @@ -183,8 +223,10 @@ export function renderQuotaTable(models: QuotaModelRemain[], config: Config): vo
m.current_weekly_remaining_percent,
useColor,
m.weekly_boost_permille,
isUnweekly(m.current_weekly_status),
isUnweekly(m.current_weekly_status, m.current_weekly_total_count),
config.region === 'cn' ? UNLIMITED_LABEL_CN : UNLIMITED_LABEL_EN,
weeklyNotInPlan,
notInPlanLabel,
);
const reset = `${L.resetsIn} ${formatDuration(m.remains_time, L.now)}`;
return { displayName, current, weekly, reset };
Expand Down
128 changes: 122 additions & 6 deletions test/output/quota-table.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, it, expect } from 'bun:test';
import { renderQuotaTable } from '../../src/output/quota-table';
import {
renderQuotaTable,
isNotInPlan,
isUnweekly,
} from '../../src/output/quota-table';
import type { Config } from '../../src/config/schema';
import type { QuotaModelRemain } from '../../src/types/api';

Expand Down Expand Up @@ -189,7 +193,11 @@ describe('renderQuotaTable', () => {
expect(output).not.toContain('300%');
});

it('renders "无限" for weekly when status=3 (CN region)', () => {
// Behavior change for #173: when status=3 but total_count=0, the API is
// signalling "not in your plan" (a 0/0 bucket), not "unlimited". The CLI
// must surface this so users do not see "100% remaining" for a model
// they cannot actually use.
it('renders "不在套餐中" for weekly when status=3 + total_count=0 (CN region)', () => {
const lines: string[] = [];
const originalLog = console.log;

Expand All @@ -216,13 +224,13 @@ describe('renderQuotaTable', () => {
}

const output = lines.join('\n');
expect(output).toContain('[██████████]');
expect(output).toContain('周剩余');
expect(output).toContain('无限');
expect(output).toContain('不在套餐中');
expect(output).not.toContain('无限');
expect(output).not.toContain('150%');
});

it('renders "unlimited" for weekly when status=3 (global region)', () => {
it('renders "not in plan" for weekly when status=3 + total_count=0 (global region)', () => {
const lines: string[] = [];
const originalLog = console.log;

Expand All @@ -248,9 +256,117 @@ describe('renderQuotaTable', () => {
}

const output = lines.join('\n');
expect(output).toContain('[██████████]');
expect(output).toContain('Wk left');
expect(output).toContain('not in plan');
expect(output).not.toContain('unlimited');
expect(output).not.toContain('100%');
});

it('still renders "unlimited" when status=3 + total_count > 0 (genuine unlimited)', () => {
const lines: string[] = [];
const originalLog = console.log;

console.log = (message?: unknown) => {
lines.push(String(message ?? ''));
};

try {
renderQuotaTable(
[
{
...createModel(),
current_weekly_total_count: 5000,
current_weekly_usage_count: 100,
current_weekly_remaining_percent: 98,
current_weekly_status: 3,
},
],
{ ...createConfig(), noColor: true },
);
} finally {
console.log = originalLog;
}

const output = lines.join('\n');
expect(output).toContain('Wk left');
expect(output).toContain('unlimited');
expect(output).not.toContain('not in plan');
});

it('renders "not in plan" for interval (current) row when status=3 + total_count=0', () => {
const lines: string[] = [];
const originalLog = console.log;

console.log = (message?: unknown) => {
lines.push(String(message ?? ''));
};

try {
renderQuotaTable(
[
{
...createModel(),
model_name: 'video',
current_interval_total_count: 0,
current_interval_usage_count: 0,
current_interval_remaining_percent: 100,
current_interval_status: 3,
current_weekly_total_count: 0,
current_weekly_usage_count: 0,
current_weekly_remaining_percent: 100,
current_weekly_status: 3,
},
],
{ ...createConfig(), noColor: true },
);
} finally {
console.log = originalLog;
}

const output = lines.join('\n');
expect(output).toContain('Left');
expect(output).toContain('not in plan');
// The bug: previously this rendered "100%" via current_interval_remaining_percent.
expect(output).not.toContain('100%');
});
});

describe('isNotInPlan', () => {
it('returns true when total_count is 0 and status is 3', () => {
expect(isNotInPlan(0, 3)).toBe(true);
});

it('returns false when total_count is positive (real unlimited)', () => {
expect(isNotInPlan(100, 3)).toBe(false);
});

it('returns false when status is not 3 (plain zero quota)', () => {
expect(isNotInPlan(0, 1)).toBe(false);
expect(isNotInPlan(0, 2)).toBe(false);
});

it('returns false when status is undefined or null', () => {
expect(isNotInPlan(0, undefined)).toBe(false);
expect(isNotInPlan(0, null)).toBe(false);
});
});

describe('isUnweekly', () => {
it('returns true only when status is 3 and total_count is positive', () => {
expect(isUnweekly(3, 100)).toBe(true);
});

it('returns false when total_count is 0 (would be not-in-plan)', () => {
expect(isUnweekly(3, 0)).toBe(false);
});

it('returns false when status is not 3', () => {
expect(isUnweekly(1, 100)).toBe(false);
expect(isUnweekly(2, 100)).toBe(false);
});

it('returns false when status is undefined or null', () => {
expect(isUnweekly(undefined, 100)).toBe(false);
expect(isUnweekly(null, 100)).toBe(false);
});
});